Composition corporelle × Performance¶
Objectif : analyser les corrélations entre évolution de la composition corporelle (Withings) et performances sportives (Strava).
- Évolution temporelle poids / masse grasse / masse musculaire
- Évolution des performances (allure, efficacité cardio)
- Matrice de corrélations Spearman (composition × performance)
- Corrélations glissantes sur 12 semaines
- Profil corporel des meilleures performances vs le reste
In [1]:
import sys
sys.path.append('..')
import pandas as pd
import plotly.io as pio
pio.renderers.default = 'notebook'
from src.data.loader import load_activities, load_competitions, load_weight_measurements
from src.features.training_load import compute_tss
from src.features.body_features import (
weekly_body, weekly_performance, build_combined,
rolling_correlation, peak_form_profile,
)
from src.viz.correlation_charts import (
chart_body_trends, chart_performance_trends, chart_correlation_matrix,
chart_rolling_correlation, chart_scatter_body_vs_pace, chart_peak_vs_normal,
)
1. Chargement des données¶
In [2]:
USER_ID = 1
activities = load_activities(user_id=USER_ID)
activities = compute_tss(activities)
competitions = load_competitions(user_id=USER_ID)
weights = load_weight_measurements(user_id=USER_ID)
print(f"Activités : {len(activities)}")
print(f"Pesées : {len(weights)}")
print(f"Compétitions : {len(competitions)}")
print()
print("Période des pesées :", weights['measured_at'].min().date(), "->", weights['measured_at'].max().date())
print("Plage de poids : {:.1f} – {:.1f} kg".format(weights['weight_kg'].min(), weights['weight_kg'].max()))
print("Plage masse grasse : {:.1f} – {:.1f} %".format(weights['fat_ratio'].min(), weights['fat_ratio'].max()))
Activités : 2969 Pesées : 618 Compétitions : 21 Période des pesées : 2022-10-24 -> 2026-06-22 Plage de poids : 83.0 – 102.8 kg Plage masse grasse : 22.0 – 35.1 %
2. Séries temporelles hebdomadaires¶
In [3]:
wb = weekly_body(weights)
combined = build_combined(weights, activities)
print(f"Semaines avec pesées : {len(wb)}")
print(f"Semaines communes : {len(combined)}")
print(f"Période : {combined.index.min().date()} -> {combined.index.max().date()}")
print()
print(combined.describe().round(2))
Semaines avec pesées : 183
Semaines communes : 176
Période : 2022-10-31 -> 2026-06-22
weight_kg fat_ratio muscle_mass_kg fat_mass_kg fat_free_mass_kg \
count 176.00 169.00 169.00 169.00 169.00
mean 93.25 28.21 63.39 26.33 66.73
std 4.44 2.09 1.57 3.08 1.65
min 83.68 22.00 59.45 18.55 62.60
25% 90.12 26.84 62.00 24.20 65.26
50% 94.22 28.46 63.69 26.75 67.04
75% 96.00 29.54 64.63 28.28 68.03
max 101.98 33.26 66.84 33.73 70.33
pace_min_km hr_efficiency avg_tss
count 159.00 176.00 176.00
mean 6.96 0.11 59.93
std 0.36 0.21 28.37
min 6.15 0.03 13.24
25% 6.69 0.08 42.80
50% 6.91 0.10 54.23
75% 7.16 0.12 72.28
max 8.06 2.85 190.93
3. Évolution composition corporelle¶
In [4]:
fig = chart_body_trends(wb, competitions)
fig.show()
4. Évolution des performances¶
In [5]:
fig = chart_performance_trends(combined)
fig.show()
5. Matrice de corrélations Spearman¶
Rappel : l'allure est en min/km — une corrélation positive avec le poids signifie que quand le poids augmente, l'allure augmente (= on court moins vite). Une corrélation négative avec la masse musculaire signifie que plus de muscles → allure plus basse → plus rapide.
In [6]:
fig = chart_correlation_matrix(combined)
fig.show()
# Print top correlations with pace
perf_col = 'pace_min_km'
if perf_col in combined.columns:
body_cols = ['weight_kg', 'fat_ratio', 'muscle_mass_kg', 'fat_free_mass_kg']
corrs = combined[[perf_col] + [c for c in body_cols if c in combined.columns]].dropna().corr(method='spearman')[perf_col].drop(perf_col).sort_values()
print("Corrélations Spearman avec allure course (pace_min_km) :")
for col, val in corrs.items():
arrow = '+' if val > 0 else '-'
print(f" {col:22s}: {val:+.3f} {'(cours moins vite quand augmente)' if val > 0 else '(cours plus vite quand augmente)'}")
Corrélations Spearman avec allure course (pace_min_km) : muscle_mass_kg : -0.040 (cours plus vite quand augmente) fat_free_mass_kg : -0.038 (cours plus vite quand augmente) weight_kg : +0.148 (cours moins vite quand augmente) fat_ratio : +0.235 (cours moins vite quand augmente)
6. Corrélations glissantes 12 semaines¶
In [7]:
pairs = [
('fat_ratio', 'pace_min_km', 'Masse grasse %', 'Allure (min/km)'),
('weight_kg', 'pace_min_km', 'Poids (kg)', 'Allure (min/km)'),
('muscle_mass_kg','pace_min_km','Masse musculaire', 'Allure (min/km)'),
]
for x_col, y_col, x_label, y_label in pairs:
if x_col in combined.columns and y_col in combined.columns:
roll = rolling_correlation(combined, x_col, y_col, window_weeks=12)
if len(roll) > 4:
fig = chart_rolling_correlation(roll, x_label, y_label, window_weeks=12)
fig.show()
7. Profil corporel des meilleures performances¶
In [8]:
profile = peak_form_profile(competitions, combined, window_weeks=4, top_n=5)
if not profile.empty:
print(f"Courses analysées : {len(profile)}")
print()
print("Profil corporel moyen — 4 semaines avant la course :")
print(profile.groupby('is_peak')[['weight_kg','fat_ratio','muscle_mass_kg']].mean().round(2).rename(index={True: 'Top 5 perfs', False: 'Autres'}))
else:
print("[!] Pas assez de données pour le profil.")
Courses analysées : 16
Profil corporel moyen — 4 semaines avant la course :
weight_kg fat_ratio muscle_mass_kg
is_peak
Autres 94.40 28.35 64.10
Top 5 perfs 93.42 28.17 63.77
In [9]:
if not profile.empty:
fig = chart_peak_vs_normal(profile)
fig.show()
8. Scatter : composition corporelle vs allure de course¶
In [10]:
if not profile.empty:
for col, label in [('weight_kg', 'Poids (kg)'), ('fat_ratio', 'Masse grasse (%)'), ('muscle_mass_kg', 'Masse musculaire (kg)')]:
if col in profile.columns and profile[col].notna().sum() >= 3:
fig = chart_scatter_body_vs_pace(profile, col, label)
fig.show()
9. Synthèse¶
Résumé des corrélations significatives trouvées et interprétation.
In [11]:
from scipy.stats import spearmanr
print("Corrélations Spearman significatives (p < 0.05) :")
print()
if 'pace_min_km' in combined.columns:
for col in ['weight_kg', 'fat_ratio', 'muscle_mass_kg', 'fat_free_mass_kg']:
if col not in combined.columns:
continue
valid = combined[[col, 'pace_min_km']].dropna()
if len(valid) < 10:
continue
r, p = spearmanr(valid[col], valid['pace_min_km'])
sig = '***' if p < 0.001 else ('**' if p < 0.01 else ('*' if p < 0.05 else 'n.s.'))
label = {'weight_kg': 'Poids', 'fat_ratio': 'Masse grasse %', 'muscle_mass_kg': 'Masse musculaire', 'fat_free_mass_kg': 'Masse maigre'}[col]
print(f" {label:22s} vs allure : r={r:+.3f} p={p:.4f} {sig}")
Corrélations Spearman significatives (p < 0.05) : Poids vs allure : r=+0.179 p=0.0240 * Masse grasse % vs allure : r=+0.235 p=0.0035 ** Masse musculaire vs allure : r=-0.040 p=0.6219 n.s. Masse maigre vs allure : r=-0.038 p=0.6433 n.s.