Composition corporelle × Performance¶

Objectif : analyser les corrélations entre évolution de la composition corporelle (Withings) et performances sportives (Strava).

  1. Évolution temporelle poids / masse grasse / masse musculaire
  2. Évolution des performances (allure, efficacité cardio)
  3. Matrice de corrélations Spearman (composition × performance)
  4. Corrélations glissantes sur 12 semaines
  5. 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.