Files
openrun/examples/notebooks/03_recovery.ipynb

198 lines
6.1 KiB
Plaintext
Raw Normal View History

2026-05-18 12:53:24 -04:00
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
2026-05-19 08:34:22 -04:00
"# 03 \u2014 Recovery & wellness\n",
2026-05-18 12:53:24 -04:00
"\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",
2026-05-19 08:34:22 -04:00
"from openrun import open_conn, load_wellness, joined\n",
2026-05-18 12:53:24 -04:00
"\n",
"conn = open_conn()\n",
"w = load_wellness(conn)\n",
"j = joined(conn)\n",
2026-05-19 08:34:22 -04:00
"print(f'{len(w)} days, {w.index.min().date()} \u2192 {w.index.max().date()}')"
2026-05-18 12:53:24 -04:00
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
2026-05-19 08:34:22 -04:00
"## Recent 30 days \u2014 at a glance"
2026-05-18 12:53:24 -04:00
]
},
{
"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",
2026-05-19 08:34:22 -04:00
"ax.set_title('Sleep stages \u2014 last 60 nights')\n",
2026-05-18 12:53:24 -04:00
"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": {
2026-05-19 08:34:22 -04:00
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
2026-05-18 12:53:24 -04:00
},
"nbformat": 4,
"nbformat_minor": 5
2026-05-19 08:34:22 -04:00
}