Table of Contents
- Key Highlights
- Introduction
- From CSV to scatter plot: what the dataset looks like
- Drawing the best line: the intuition behind ordinary least squares
- Calculating the regression line: by hand and in Python
- How well does distance explain calories? Interpreting R-squared and standard error
- Residuals: where the line gets it wrong and what they reveal
- Hypothesis testing and uncertainty: t-statistics and confidence intervals
- Checking assumptions: linearity, independence, homoscedasticity, normality
- Multiple regression: adding elevation and time—and why it barely helps
- Diagnosing multicollinearity: VIF and practical remedies
- Practical fixes and alternative modeling choices
- How to build a more predictive calorie estimator
- Influence diagnostics: who moves the line?
- Interpreting coefficients in multiple regression: the danger of "holding everything else constant"
- Practical guidance for runners, coaches, and analysts
- Limitations, biases, and ethical considerations
- When a simple regression is enough—and when it isn’t
- Actionable takeaways from the Strava exercise
- FAQ
Key Highlights
- A simple linear regression on 102 Strava runs shows distance explains about 96% of variation in reported calorie burn: roughly 68 calories per kilometer for this dataset.
- Residuals, multicollinearity, and model assumptions reveal where a one-variable model breaks down and which practical steps (feature selection, regularization, mixed models) improve real-world prediction.
Introduction
Textbook derivations of linear regression can feel abstract: Greek letters, summations, and toy datasets that never match the mess of lived experience. Teaching the mechanics is one thing; applying them to your own data is another. Using a six-month Strava export—102 runs, distances from 0.02 km to a half marathon, recorded calories and elevation—I fitted regression models from first principles, inspected diagnostics, and pushed beyond basic fitting until the limits of distance-based prediction became clear.
This exercise produces more than numbers. It clarifies when a simple model suffices, when additional variables genuinely help, and when statistical artifacts—like nearly perfect correlation between distance and time—lead to misleading interpretations. The lessons apply to sports analytics, wearable-device data, and any single-subject dataset where repeated measures and context matter.
Below: a step-by-step reconstruction of the analysis, extended explanations of diagnostics and remedies, practical code snippets, and guidance for runners, coaches, and data learners who want models that are both reliable and interpretable.
From CSV to scatter plot: what the dataset looks like
The data began as a Strava export: 415 activities over six months spanning runs, walks, cycling, gym sessions, and yoga. Filtering to runs produced 102 observations with fields such as:
- distance_km
- moving_time_min (or total time)
- total_calories
- elevation_gain_m
- activity_name / timestamp (useful for context)
Summary statistics for the runs:
- mean(distance) ≈ 5.41 km
- mean(calories) ≈ 378.45 kcal
- min(distance) = 0.02 km (likely a spurious short activity)
- max(distance) = 21.1 km (half marathon, 1,581 kcal)
Plotting distance on the x-axis and calories on the y-axis reveals a tight, roughly linear upward trend. Most points cluster near 5 km runs, with a handful of long runs and tiny excursions. That cluster pattern alone hints that a linear model may capture the dominant relationship. But the scatter plot also invites questions: where are the systematic deviations? Which runs lie far from the trend? Answering those questions requires fitting the line and analyzing residuals.
Drawing the best line: the intuition behind ordinary least squares
Imagine scattering the 102 points on a whiteboard. A straight line that “best” represents them can be drawn with a ruler, but statistics formalizes “best” as the line minimizing the sum of squared vertical distances between each point and the line. Squaring residuals penalizes large errors more than small ones. This is Ordinary Least Squares (OLS).
A straight line has the form: calories = β0 + β1 × distance
β0 (intercept) anchors where the line crosses the y-axis. β1 (slope) measures the change in predicted calories per one additional kilometer. OLS determines β0 and β1 so that the sum of squared residuals Σ(y_i − (β0 + β1 x_i))^2 is minimized.
Interpreting β1 in practice: it gives an average effect across all runs. If β1 ≈ 68 kcal per km (as found here), that doesn’t imply every runner or every run will follow that exact rate. It says that within this dataset, the expected increase in calories for each additional kilometer is about 68 calories.
Calculating the regression line: by hand and in Python
Computationally, the slope β1 and intercept β0 can be derived from sums of squares:
- x̄ = mean(distance)
- ȳ = mean(calories)
- SS_xx = Σ(x_i − x̄)^2
- SS_xy = Σ(x_i − x̄)(y_i − ȳ)
Then: β1 = SS_xy / SS_xx β0 = ȳ − β1 × x̄
For this dataset:
- x̄ ≈ 5.41 km
- ȳ ≈ 378.45 kcal
- SS_xx ≈ 849.75
- SS_xy ≈ 57,754.12
- β1 ≈ 57,754.12 / 849.75 ≈ 67.97 kcal/km
- β0 ≈ 378.45 − 67.97 × 5.41 ≈ 10.52 kcal
So the fitted line is: calories = 10.52 + 67.97 × distance_km
That matches a widely-cited physiological approximation: running burns roughly 1 kcal per kilogram per kilometer. For a 68 kg person, that rules-of-thumb produces ≈ 68 kcal/km. The close numeric agreement is not coincidence: energy cost per km per kg is a robust physiological finding. It does make clear, however, that mass (body weight) is a major missing variable when using only distance.
Python code (concise, reproducible):
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
df = pd.read_csv('strava_workout_history.csv')
runs = df[df['sport_type'] == 'Run'].copy()
x = runs['distance_km'].values
y = runs['total_calories'].values
# Manual computation
x_mean = x.mean()
y_mean = y.mean()
ss_xx = ((x - x_mean)**2).sum()
ss_xy = ((x - x_mean) * (y - y_mean)).sum()
beta1 = ss_xy / ss_xx
beta0 = y_mean - beta1 * x_mean
print(f"calories = {beta0:.2f} + {beta1:.2f} * distance_km")
# Using statsmodels
X = sm.add_constant(x)
model = sm.OLS(y, X).fit()
print(model.summary())
# Scatter plot with regression line
plt.scatter(x, y, alpha=0.6, edgecolors='k', linewidth=0.5)
xs = np.linspace(x.min(), x.max(), 100)
plt.plot(xs, beta0 + beta1 * xs, color='red', linewidth=2)
plt.xlabel('Distance (km)')
plt.ylabel('Calories')
plt.title('Calories vs Distance: 102 Runs')
plt.show()
The statsmodels summary provides coefficients, standard errors, t-statistics, p-values, and R-squared. Each diagnostic reveals different aspects of fit and uncertainty.
How well does distance explain calories? Interpreting R-squared and standard error
R-squared (R²) measures the fraction of variance in calories explained by the model: R² = 1 − SSE / SST where SSE = Σ(y_i − ŷ_i)^2 and SST = Σ(y_i − ȳ)^2.
Using the SS quantities: R² = (SS_xy)^2 / (SS_xx × SS_yy)
For this dataset:
- SS_yy ≈ 4,106,945.25
- R² ≈ 0.956 → roughly 96%
A 96% R² indicates distance alone accounts for almost all the variability in Strava’s calorie estimates across these runs. That level of explanatory power is unusually high for observational human data, but it reflects the strong physical relationship between distance and energy expenditure, combined with Strava’s internal estimation approach.
Standard Error of the Estimate (SEE) provides a measure of typical prediction error: SEE = sqrt(SSE / (n − 2)) ≈ 42.62 kcal
Expressed relative to mean calorie burn (~378 kcal), that’s an average prediction error of about 11%. For practical purposes—quick estimates of caloric expenditure per run—that’s a small error using a single variable.
Caveat: high R² does not imply causal understanding nor guarantee good out-of-sample prediction, particularly when the dataset is narrow (single runner) or when predictors are proxies for each other.
Residuals: where the line gets it wrong and what they reveal
Residuals are the differences between actual and predicted calories:
residual_i = y_i − ŷ_i
They indicate where the model under- or over-predicts. Residual analysis uncovers patterns, outliers, and non-linearities.
Examples from the dataset:
- Half marathon (21.1 km): actual 1,581 kcal, predicted ≈ 1,445 kcal → residual ≈ +136 kcal. The model underpredicted for the long run.
- A 10K evening run: actual 514 kcal, predicted ≈ 694 kcal → residual ≈ −180 kcal. The model overpredicted.
Why these misses? Possible reasons:
- Intensity and pace: the 10K evening run appears to have been at a relaxed pace, reducing calories per km. The model assumes an average intensity across runs.
- Physiological costs for long durations: longer runs involve greater cumulative metabolic costs beyond linear distance (thermoregulation, glycogen depletion), perhaps making calories per km slightly higher on ultra-long efforts.
- Device estimation quirks: Strava’s calorie estimate may incorporate heart rate data, if available, or user weight. Differences in wearable sensors or their algorithms create noise.
Residual plots (predicted vs residuals) and distributions (histogram, Q-Q plot) are essential. For this dataset residuals are roughly symmetric with mild skew and show no strong heteroscedastic pattern—consistent with the OLS assumptions used here.
Hypothesis testing and uncertainty: t-statistics and confidence intervals
Finding a slope β1 = 67.97 begs the question: could this have arisen by chance? Hypothesis testing addresses that. The null hypothesis: the true slope is zero (no relationship). The t-statistic compares the estimated coefficient to its standard error:
t = β1 / SE(β1)
Standard error of the slope: SE(β1) = sqrt(SSE / ((n − 2) × SS_xx))
For this dataset:
- SSE ≈ 184,446.36
- SE(β1) ≈ 1.47
- t ≈ 67.97 / 1.47 ≈ 46.2
A t-statistic of 46 is enormous. The p-value is effectively zero. The slope is statistically significant beyond any reasonable doubt.
Confidence interval (95%): β1 ± t_critical × SE(β1) ≈ 67.97 ± 1.98 × 1.47 ≈ [65.06, 70.88]
That interval is narrow and informative: each kilometer burned between 65 and 71 calories, with 95% confidence based on the sample and OLS assumptions.
Statistical significance is not the same as practical importance. Here both apply: physically meaningful rate per km and statistically precise estimate.
Checking assumptions: linearity, independence, homoscedasticity, normality
OLS inference depends on four main assumptions. Ignoring them risks incorrect conclusions.
-
Linearity: The expected value of y given x is linear. Check with scatter plot and residual-vs-predicted plot. For this dataset, the scatter is linear across the bulk of observations.
-
Independence: Observations should be independent. In time-ordered runs from a single person, strict independence is unlikely. Sessions close in time may be correlated (same energy levels, fatigue, weather). Mild dependence typically biases standard errors downward or upward depending on pattern. When dependence is suspected, use clustering, time-series techniques, or mixed-effects models to account for within-subject correlation.
-
Homoscedasticity: Residual variance should be roughly constant across x. Residual plots here show no strong fanning; variance looks stable. If residual spread increased with distance, weighted least squares or robust standard errors would be needed.
-
Normality of residuals: Critical for small-sample inference. Q-Q plots and histograms in this dataset show only mild skew, acceptable for the sample size (n=102). Extreme deviations would call for transformations or non-parametric inference.
Checking these assumptions is as important as fitting the coefficients. Diagnostics identify when p-values and confidence intervals can be trusted.
Multiple regression: adding elevation and time—and why it barely helps
A natural thought: calories should depend on more than just distance. Elevation gain, duration/time, pace, and heart rate should alter energy cost. Adding elevation_gain_m and moving_time_min produces a multiple regression:
calories = β0 + β1 × distance + β2 × elevation + β3 × time
Fitting this with statsmodels returned coefficients approximately: calories = 1.25 + 44.73 × distance + 0.20 × elevation + 3.34 × time with R² still ≈ 0.96 and SEE ≈ 41.92
Why did R² barely change? Multicollinearity. Distance and moving time are correlated at r ≈ 0.99. They provide nearly identical information: longer runs take longer. When predictors are nearly collinear:
- Coefficients become unstable and sensitive to small changes in data.
- Standard errors inflate for correlated predictors.
- Interpretation of “holding other variables constant” becomes meaningless because those conditions are rare or impossible in practice.
In this model the distance coefficient dropped from ~68 to ~45 because the model attempted to partition shared variance between distance and time. Since time and distance move together almost perfectly, each absorbed a portion of the shared effect arbitrarily.
Correlation matrix highlights:
- corr(distance, time) ≈ 0.99
- corr(distance, elevation) ≈ 0.21
- corr(time, elevation) ≈ 0.14
Elevation contributes new information. It increases predicted calories slightly, with ≈0.2 kcal per meter of elevation gain. That equates to ~20 kcal for a 100 m climb—reasonable and interpretable.
Best practice: avoid including strongly collinear predictors simultaneously. Choose the one with clearer physical meaning (distance), or create composite variables that capture distinct aspects (pace = distance/time).
Diagnosing multicollinearity: VIF and practical remedies
Variance Inflation Factor (VIF) quantifies multicollinearity for each predictor:
VIF_j = 1 / (1 − R_j^2)
where R_j^2 is the R² from regressing the j-th predictor on all others.
Rules of thumb:
- VIF > 5 indicates moderate collinearity.
- VIF > 10 suggests severe multicollinearity.
In this dataset, VIF for distance or time would be enormous because of their near-perfect correlation.
Remedies:
- Drop redundant variables (prefer the simpler, more interpretable one).
- Combine predictors into a single composite (e.g., average pace).
- Use dimensionality reduction (PCA) if many predictors are correlated.
- Use regularized regression (Ridge) that shrinks coefficients and stabilizes estimates.
- Reframe the question to avoid “holding correlated variables constant” interpretations that are physically impossible.
Code to compute VIF with statsmodels:
from statsmodels.stats.outliers_influence import variance_inflation_factor
X = runs[['distance_km', 'elevation_gain_m', 'moving_time_min']]
X = sm.add_constant(X)
vif_data = pd.DataFrame()
vif_data['feature'] = X.columns
vif_data['VIF'] = [variance_inflation_factor(X.values, i)
for i in range(X.shape[1])]
print(vif_data)
Expect large VIFs for distance and time.
Practical fixes and alternative modeling choices
When simple OLS fails to capture realities or violates assumptions, several practical methods improve robustness and predictive performance.
-
Feature engineering
- Pace (min/km) = time/distance captures intensity and separates effects of distance and speed.
- Energy-per-km per kg uses body weight: kcal_per_km = calories / distance; normalized by body mass yields physiology-aligned metric.
- Temperature, surface type (trail vs road), and wind direction may improve predictions for outdoor runs.
-
Outlier handling
- Remove or down-weight obvious data errors (0.02 km activity).
- Use robust regression (RANSAC, HuberRegressor) or M-estimators to mitigate influence of outliers.
-
Regularization
- Ridge regression (L2) shrinks coefficients and stabilizes correlated predictors.
- Lasso (L1) provides automatic feature selection by driving small coefficients to zero.
- ElasticNet blends both.
Example with scikit-learn:
from sklearn.linear_model import RidgeCV
from sklearn.model_selection import cross_val_score
X = runs[['distance_km', 'elevation_gain_m', 'moving_time_min']].values
y = runs['total_calories'].values
alphas = np.logspace(-3, 3, 50)
ridge = RidgeCV(alphas=alphas, cv=5).fit(X, y)
print(ridge.alpha_, ridge.coef_)
scores = cross_val_score(ridge, X, y, cv=5, scoring='neg_mean_squared_error')
print(np.sqrt(-scores).mean())
-
Cross-validation and out-of-sample testing
- Split data into train/test or use k-fold CV to estimate predictive generalization.
- Leave-one-out CV may be appropriate for small samples but is computationally intensive.
-
Mixed-effects models (hierarchical models)
- If analyzing multiple runners, include random intercepts and slopes per runner to account for individual differences.
- For single-runner repeated measures, time effects or clustering by week-day can be modeled as random effects to handle dependence.
Example with statsmodels MixedLM:
import statsmodels.formula.api as smf
# Suppose runs DataFrame has columns: calories, distance_km, week
md = smf.mixedlm("total_calories ~ distance_km + elevation_gain_m", runs, groups=runs["week"])
mdf = md.fit()
print(mdf.summary())
Mixed models are particularly useful when observations cluster (e.g., runs by season, by training block).
- Non-linear models
- If residual plots show curvature, consider polynomial terms or transformations (log distance).
- Generalized additive models (GAMs) allow for flexible, smooth nonlinear relationships while maintaining interpretability.
How to build a more predictive calorie estimator
A Strava-like calorie estimator should incorporate:
- Body weight and age (metabolic cost scales with mass)
- Pace or heart rate (intensity)
- Elevation gain and terrain (hills, trail vs road)
- Duration and cumulative load (fatigue effects)
- Device-specific parameters (sensor-derived heart rate, power for treadmill/track)
A practical recipe:
- Collect training data with ground-truth calories if possible (indirect calorimetry, metabolic cart, or heart rate-based lab calibration).
- Engineer features: distance, time, pace, average HR, HR zones, elevation gain, temperature, surface coding.
- Split data into training and test sets; use k-fold CV for model selection.
- Start with a parsimonious baseline (distance + weight) and evaluate improvements from added features using adjusted R², RMSE, and cross-validated error.
- Use regularization to handle many correlated features and avoid overfitting.
- Validate on held-out sessions collected under different conditions (e.g., long runs, interval sessions).
Real-world example: weight-adjusted estimate Energy cost of running is often approximated by: kcal ≈ mass_kg × distance_km × cost_per_kg_per_km
Typical cost_per_kg_per_km ≈ 1.0 kcal/(kg·km). For a 70 kg runner: kcal ≈ 70 × distance_km × 1 = 70 kcal/km
Compare this to the estimated slope ≈ 68 kcal/km: the dataset owner was likely close to 68 kg or Strava’s internal weighting produced similar scaling.
Influence diagnostics: who moves the line?
Not every point contributes equally. High leverage points (extreme x values like the half marathon) can pull the regression line. Cook’s distance quantifies how much a single observation influences fitted values. Leverage (hat matrix diagonal) identifies points with extreme predictor values.
Compute Cook’s distance:
import statsmodels.api as sm
influence = model.get_influence()
cooks_d = influence.cooks_distance[0]
high_cooks = runs[cooks_d > 4 / len(runs)]
print(high_cooks)
If a few runs drive the line substantially, consider:
- Verifying data quality for those activities.
- Fitting models with and without them to assess robustness.
- Using robust estimators that reduce the influence of outliers.
In the Strava dataset, the half marathon is high-leverage but not necessarily an error; it reflects a valid long-run behavior. The question becomes: does one want a model that approximates typical runs or one that captures extremes accurately? The answer determines whether to include or weight such events.
Interpreting coefficients in multiple regression: the danger of "holding everything else constant"
Coefficients in a multivariable model represent marginal effects "holding other variables constant." This is mathematically precise but can be practically meaningless when covariates cannot vary independently. The distance coefficient dropping from ~68 to ~45 after adding time illustrates this: asking how calories change by increasing distance while holding time constant is physically unrealistic—if distance increases by 1 km but time stays the same, pace must increase drastically.
Interpretation strategies:
- Use predictors that represent orthogonal aspects (distance and elevation, or pace rather than both distance/time).
- Report marginal effects for realistic scenarios: e.g., "adding 1 km at constant pace increases calories by X" rather than "holding time constant."
- Present predicted values for joint changes: compute expected calories for typical run profiles (5K easy, 10K tempo, half marathon).
Practical guidance for runners, coaches, and analysts
- Quick estimate: use distance × 1 kcal/kg/km as a first approximation. For a 70 kg runner, multiply distance (km) by ~70 kcal.
- If heart rate is available, incorporate it: HR captures intensity and improves per-km estimates, particularly for intervals and tempo runs.
- Beware device estimates: different wearables use different algorithms. Comparing across devices requires harmonization.
- For training load tracking, consistency matters more than absolute accuracy. Use the same model/device over time to detect trends.
- For caloric budgeting (nutrition planning), use conservative margins—predictive error of ~10–15% is typical with simple models.
Limitations, biases, and ethical considerations
- Single-subject data limits generalizability. Estimates derived from one runner reflect that runner’s weight, running economy, and activity profile.
- Device algorithms (Strava, wearables) often incorporate proprietary methods that are not transparent; using their outputs as "ground truth" embeds those algorithmic biases into any derived model.
- Missing variables: weight, gender, age, fitness level, and heart rate substantially affect calorie burn. Their absence limits the model’s physiological relevance.
- Non-independence: repeated measures from one individual violate the independence assumption. Mixed models or clustered standard errors address this.
- Privacy: sharing GPS and biometric data requires care. De-identify data and avoid exposing precise routes or timestamps when publishing.
When a simple regression is enough—and when it isn’t
A one-variable model yields intuition and often strong explanatory power when the predictor is tightly linked to the outcome. For running, distance has a physically based relationship with energy cost; that makes single-variable models surprisingly effective for many applications.
Use simple regression when:
- The goal is quick, interpretable estimates for the same individual.
- You have limited features and limited data.
- You prioritize transparency over marginal gains in predictive accuracy.
Avoid or augment simple regression when:
- Making individualized nutrition prescriptions across diverse athletes.
- Trying to capture intensity-driven calorie differences (intervals vs easy runs).
- You need robust out-of-sample performance across conditions (hot/humid, trail vs road).
Actionable takeaways from the Strava exercise
- Distance explains most of the variance in Strava’s calorie estimates for a single runner: ~68 kcal/km and R² ≈ 0.96.
- Residuals point to intensity and long-duration effects; inspecting them identifies opportunities for feature engineering.
- Multicollinearity (distance vs time) undermines naive inclusion of multiple correlated predictors. Use VIF, drop redundant variables, or choose more informative constructs like pace.
- Always check OLS assumptions: residual plots, Q-Q plots, leverage and Cook’s distance, and tests for heteroscedasticity (Breusch-Pagan).
- For predictive tasks, combine domain knowledge with cross-validated models and consider regularization or mixed-effects frameworks when appropriate.
FAQ
Q: Why does the intercept (≈10.52 kcal) have no physical meaning? A: The intercept is the model’s predicted calorie burn at zero kilometers. Physically, a zero-kilometer run should burn essentially zero calories attributable to running. The intercept exists to mathematically anchor the line. With predictors centered or models constrained through the origin, intercepts can be adjusted, but doing so imposes extra assumptions that often worsen fit.
Q: Can I use this model for other runners? A: Not directly. This model reflects one runner’s physiology and the specifics of Strava’s calorie estimates. To generalize, include body weight and demographic information, train on a diverse sample, and validate on independent runners.
Q: Why didn’t adding elevation and time increase R² much? A: Distance and time are nearly perfectly correlated in this dataset; they represent nearly the same information. R² increases only if a new predictor explains variance that existing predictors do not. Elevation had some independent information; time did not.
Q: How should I handle the 0.02 km/15 kcal “Night Run”? A: Verify whether the activity is a recording error. If it reflects a real short run, keep it but consider robust regression or down-weighting. If it’s a data artifact, remove it before fitting.
Q: What does a high R² of 0.96 mean in practice? A: It means 96% of the variance in observed calories is explained by distance in this sample. It indicates a strong linear relationship but does not prove causality and may not hold out-of-sample without similar conditions.
Q: Should I include pace in a model? A: Yes—pace captures intensity, which affects calories per km. However, because pace = time/distance, including pace alongside distance and time can reintroduce multicollinearity. Choose variables with distinct physical meaning and check correlations.
Q: How do I account for repeated measures over time (dependence)? A: Use mixed-effects models with random intercepts or slopes to model within-subject correlation, cluster-robust standard errors, or time-series techniques if runs are serially dependent.
Q: What performance metric should I use? A: Root Mean Squared Error (RMSE) and Mean Absolute Error (MAE) are common for continuous outcomes. Cross-validated RMSE gives a realistic estimate of out-of-sample performance. Use adjusted R² when comparing models with different numbers of predictors.
Q: If I want to build a better calorie estimator, what’s the priority list of additional variables? A: Body weight and heart rate are top priorities, followed by pace, elevation gain, terrain type, temperature, and wind. Each adds physiological or energetic information that reduces residual variance.
Q: Are there quick rules of thumb for estimating calories from running? A: Multiply distance (km) by body mass (kg) and by ~1 kcal/kg/km. For many runners, this yields ~60–90 kcal/km depending on mass and running economy.
This analysis shows how a single-line model can both illuminate the dominant drivers of an outcome and expose the gaps where reality is richer than a straight line. Working with personal Strava data turns abstract formulas into tangible insights—residuals become stories of pace and terrain, coefficients reflect physiology, and diagnostics guide the next iteration of improvement.