198 lines
6.1 KiB
Plaintext
198 lines
6.1 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# 03 \u2014 Recovery & wellness\n",
|
|
"\n",
|
|
"Sleep, HRV, RHR, body battery, and how they relate to training load."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import sys\n",
|
|
"import numpy as np\n",
|
|
"import pandas as pd\n",
|
|
"import matplotlib.pyplot as plt\n",
|
|
"from openrun import open_conn, load_wellness, joined\n",
|
|
"\n",
|
|
"conn = open_conn()\n",
|
|
"w = load_wellness(conn)\n",
|
|
"j = joined(conn)\n",
|
|
"print(f'{len(w)} days, {w.index.min().date()} \u2192 {w.index.max().date()}')"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Recent 30 days \u2014 at a glance"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"recent = w.tail(30)\n",
|
|
"fig, axes = plt.subplots(5, 1, figsize=(13, 10), sharex=True)\n",
|
|
"recent['sleep_score'].plot(ax=axes[0], marker='o', color='C0')\n",
|
|
"axes[0].set_title('Sleep score'); axes[0].grid(alpha=0.3)\n",
|
|
"recent['resting_hr'].plot(ax=axes[1], marker='o', color='C1')\n",
|
|
"axes[1].set_title('Resting HR (bpm)'); axes[1].grid(alpha=0.3)\n",
|
|
"recent['hrv_last_night'].plot(ax=axes[2], marker='o', color='C2')\n",
|
|
"axes[2].set_title('HRV (last night avg, ms)'); axes[2].grid(alpha=0.3)\n",
|
|
"recent['avg_stress'].plot(ax=axes[3], marker='o', color='C3')\n",
|
|
"axes[3].set_title('Avg stress'); axes[3].grid(alpha=0.3)\n",
|
|
"recent[['bb_highest','bb_lowest']].plot(ax=axes[4], marker='o')\n",
|
|
"axes[4].set_title('Body Battery (high/low)'); axes[4].grid(alpha=0.3)\n",
|
|
"plt.tight_layout()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Sleep composition over time"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"stages = w[['deep_s','light_s','rem_s','awake_s']].fillna(0) / 3600 # hours\n",
|
|
"stages.columns = ['Deep','Light','REM','Awake']\n",
|
|
"fig, ax = plt.subplots(figsize=(13, 4))\n",
|
|
"stages.tail(60).plot.area(ax=ax, alpha=0.7, color=['#1f3a93','#6dd5fa','#9b59b6','#e74c3c'])\n",
|
|
"ax.set_ylabel('Hours')\n",
|
|
"ax.set_title('Sleep stages \u2014 last 60 nights')\n",
|
|
"ax.grid(alpha=0.3)\n",
|
|
"plt.tight_layout()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Does yesterday's training affect today's HRV / RHR?\n",
|
|
"\n",
|
|
"Compare days *after* a run vs days *after* rest, restricted to days where you actually have HRV/RHR readings."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"by_prev = j.copy()\n",
|
|
"by_prev['ran_yesterday'] = by_prev['distance_km_prev'].gt(0)\n",
|
|
"by_prev['load_bucket_prev'] = pd.cut(by_prev['training_load_prev'].fillna(0),\n",
|
|
" bins=[-0.1, 0, 50, 100, 200, 1e6],\n",
|
|
" labels=['rest','light','moderate','hard','very hard'])\n",
|
|
"summary = (by_prev.groupby('load_bucket_prev', observed=True)\n",
|
|
" .agg(n_days=('resting_hr','count'),\n",
|
|
" rhr_mean=('resting_hr','mean'),\n",
|
|
" hrv_mean=('hrv_last_night','mean'),\n",
|
|
" sleep_score_mean=('sleep_score','mean'),\n",
|
|
" avg_stress=('avg_stress','mean')))\n",
|
|
"summary.round(1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"fig, axes = plt.subplots(1, 3, figsize=(13, 4), sharex=True)\n",
|
|
"for ax, col, title in zip(axes,\n",
|
|
" ['rhr_mean','hrv_mean','sleep_score_mean'],\n",
|
|
" ['Resting HR (next day)','HRV (next night)','Sleep score']):\n",
|
|
" summary[col].plot(kind='bar', ax=ax, color='C0')\n",
|
|
" ax.set_title(title)\n",
|
|
" ax.set_xlabel('Yesterday training load')\n",
|
|
" ax.grid(alpha=0.3, axis='y')\n",
|
|
"plt.tight_layout()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Correlations\n",
|
|
"Pairwise correlations between training and recovery signals (Spearman, robust to outliers)."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"cols = ['training_load','distance_km','training_load_prev','distance_km_prev',\n",
|
|
" 'sleep_score','sleep_hours','deep_pct','rem_pct',\n",
|
|
" 'resting_hr','hrv_last_night','avg_stress','bb_highest']\n",
|
|
"corr = j[cols].corr(method='spearman')\n",
|
|
"\n",
|
|
"fig, ax = plt.subplots(figsize=(10, 8))\n",
|
|
"im = ax.imshow(corr, vmin=-1, vmax=1, cmap='RdBu_r')\n",
|
|
"ax.set_xticks(range(len(cols))); ax.set_xticklabels(cols, rotation=45, ha='right')\n",
|
|
"ax.set_yticks(range(len(cols))); ax.set_yticklabels(cols)\n",
|
|
"for i in range(len(cols)):\n",
|
|
" for k in range(len(cols)):\n",
|
|
" v = corr.iloc[i, k]\n",
|
|
" if pd.notna(v):\n",
|
|
" ax.text(k, i, f'{v:.2f}', ha='center', va='center',\n",
|
|
" color='white' if abs(v) > 0.5 else 'black', fontsize=8)\n",
|
|
"plt.colorbar(im, ax=ax)\n",
|
|
"plt.tight_layout()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Weekly summary: km vs. recovery indicators"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"weekly = j.resample('W-MON').agg({\n",
|
|
" 'distance_km': 'sum',\n",
|
|
" 'training_load': 'sum',\n",
|
|
" 'sleep_score': 'mean',\n",
|
|
" 'sleep_hours': 'mean',\n",
|
|
" 'resting_hr': 'mean',\n",
|
|
" 'hrv_last_night': 'mean',\n",
|
|
" 'avg_stress': 'mean',\n",
|
|
"})\n",
|
|
"weekly.tail(12).round(1)"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": ".venv",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"name": "python"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
} |