1.x updates
This commit is contained in:
@@ -4,30 +4,21 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# 02 — Running\n",
|
||||
"# 02 \u2014 Running\n",
|
||||
"\n",
|
||||
"Volume, pace, HR efficiency, training load."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"331 runs from 2022-04-09 to 2026-05-10\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import sys\n",
|
||||
"sys.path.insert(0, '..')\n",
|
||||
"import pandas as pd\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"from analysis import open_conn, load_activities\n",
|
||||
"from openrun import open_conn, load_activities\n",
|
||||
"\n",
|
||||
"conn = open_conn()\n",
|
||||
"runs = load_activities(conn, type='running')\n",
|
||||
@@ -103,7 +94,7 @@
|
||||
" ax.scatter(g['avg_hr'], g['pace_min_per_km'], s=g['distance_km'] * 6, alpha=0.5, label=str(yr))\n",
|
||||
"ax.invert_yaxis() # faster pace = smaller number, want top\n",
|
||||
"ax.set_xlabel('Avg HR (bpm)')\n",
|
||||
"ax.set_ylabel('Pace (min/km, faster ↑)')\n",
|
||||
"ax.set_ylabel('Pace (min/km, faster \u2191)')\n",
|
||||
"ax.set_title('Pace vs. HR by run, sized by distance')\n",
|
||||
"ax.legend(title='Year')\n",
|
||||
"ax.grid(alpha=0.3)\n",
|
||||
@@ -147,10 +138,10 @@
|
||||
"monthly_easy = easy.set_index('start_time_local')['pace_min_per_km'].resample('ME').median()\n",
|
||||
"\n",
|
||||
"fig, ax = plt.subplots(figsize=(13, 4))\n",
|
||||
"ax.scatter(easy['start_time_local'], easy['pace_min_per_km'], alpha=0.3, s=easy['distance_km']*5, label='runs (HR<150, ≥3km)')\n",
|
||||
"ax.scatter(easy['start_time_local'], easy['pace_min_per_km'], alpha=0.3, s=easy['distance_km']*5, label='runs (HR<150, \u22653km)')\n",
|
||||
"monthly_easy.plot(ax=ax, color='C3', lw=2, marker='o', label='Monthly median')\n",
|
||||
"ax.invert_yaxis()\n",
|
||||
"ax.set_ylabel('Pace (min/km, faster ↑)')\n",
|
||||
"ax.set_ylabel('Pace (min/km, faster \u2191)')\n",
|
||||
"ax.set_title('Easy-pace trend (proxy for aerobic fitness)')\n",
|
||||
"ax.legend()\n",
|
||||
"ax.grid(alpha=0.3)\n",
|
||||
@@ -160,14 +151,49 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": "## Training load — Banister CTL / ATL / TSB\n\nGarmin reports a per-activity training load. The standard endurance-training lens (TrainingPeaks \"Performance Management Chart\") tracks three derived numbers:\n\n- **CTL** *Chronic Training Load* — EWMA of daily load with τ = 42 days. **Fitness.**\n- **ATL** *Acute Training Load* — same EWMA with τ = 7 days. **Fatigue.**\n- **TSB** *Training Stress Balance* — yesterday's CTL minus yesterday's ATL. **Form.**\n\nTSB interpretation:\n\n| TSB | meaning |\n|---|---|\n| < −30 | severely fatigued (injury risk) |\n| −10 to −30 | productive overload — heart of a build |\n| −10 to 0 | balanced building |\n| 0 to +10 | sharpening |\n| **+10 to +25** | **fresh / peaked — race-day target** |\n| > +25 | detrained (taper too long) |\n\nThis replaces the older 7/28-day rolling ACWR plot — same data, EWMAs are smoother and TSB gives you race-day-readiness directly."
|
||||
"source": "## Training load \u2014 Banister CTL / ATL / TSB\n\nGarmin reports a per-activity training load. The standard endurance-training lens (TrainingPeaks \"Performance Management Chart\") tracks three derived numbers:\n\n- **CTL** *Chronic Training Load* \u2014 EWMA of daily load with \u03c4 = 42 days. **Fitness.**\n- **ATL** *Acute Training Load* \u2014 same EWMA with \u03c4 = 7 days. **Fatigue.**\n- **TSB** *Training Stress Balance* \u2014 yesterday's CTL minus yesterday's ATL. **Form.**\n\nTSB interpretation:\n\n| TSB | meaning |\n|---|---|\n| < \u221230 | severely fatigued (injury risk) |\n| \u221210 to \u221230 | productive overload \u2014 heart of a build |\n| \u221210 to 0 | balanced building |\n| 0 to +10 | sharpening |\n| **+10 to +25** | **fresh / peaked \u2014 race-day target** |\n| > +25 | detrained (taper too long) |\n\nThis replaces the older 7/28-day rolling ACWR plot \u2014 same data, EWMAs are smoother and TSB gives you race-day-readiness directly."
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "from analysis import banister, daily_training_load_series\n\ntl = daily_training_load_series(conn)\npmc = banister(tl)\n\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(13, 7), sharex=True)\n\n# top panel: fitness + fatigue\nax1.plot(pmc.index, pmc['CTL'], color='#2a9d8f', lw=2, label='CTL (fitness, 42d)')\nax1.plot(pmc.index, pmc['ATL'], color='#e76f51', lw=1.2, alpha=0.85, label='ATL (fatigue, 7d)')\nax1.set_ylabel('training load')\nax1.legend(loc='upper left')\nax1.grid(alpha=0.3)\nax1.set_title('Performance Management Chart — CTL / ATL / TSB')\n\n# bottom panel: form\nax2.plot(pmc.index, pmc['TSB'], color='#264653', lw=1.5)\nax2.axhspan(10, 25, color='#2a9d8f', alpha=0.12, label='race-ready (+10 to +25)')\nax2.axhspan(-30, -10, color='#e9c46a', alpha=0.12, label='productive overload (−30 to −10)')\nax2.axhline(0, color='gray', lw=0.6)\nax2.axhline(-30, color='#e76f51', ls='--', lw=0.8, label='injury-risk floor (−30)')\nax2.set_ylabel('TSB (form)')\nax2.legend(loc='lower left', fontsize=9)\nax2.grid(alpha=0.3)\n\n# annotate prior race days\nrace_dates = pd.to_datetime(['2023-09-23', '2024-09-21', '2025-09-06', '2025-09-20'])\nfor rd in race_dates:\n if rd in pmc.index:\n ax2.axvline(rd, color='#d62828', alpha=0.4, lw=1)\n ax2.annotate(f\"{rd.strftime('%Y-%m-%d')}\\nTSB={pmc.loc[rd, 'TSB']:+.0f}\",\n xy=(rd, pmc.loc[rd, 'TSB']), xytext=(5, 8),\n textcoords='offset points', fontsize=8, color='#d62828')\nplt.tight_layout()"
|
||||
"source": [
|
||||
"from openrun import banister, daily_training_load_series\n",
|
||||
"\n",
|
||||
"tl = daily_training_load_series(conn)\n",
|
||||
"pmc = banister(tl)\n",
|
||||
"\n",
|
||||
"fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(13, 7), sharex=True)\n",
|
||||
"\n",
|
||||
"# top panel: fitness + fatigue\n",
|
||||
"ax1.plot(pmc.index, pmc['CTL'], color='#2a9d8f', lw=2, label='CTL (fitness, 42d)')\n",
|
||||
"ax1.plot(pmc.index, pmc['ATL'], color='#e76f51', lw=1.2, alpha=0.85, label='ATL (fatigue, 7d)')\n",
|
||||
"ax1.set_ylabel('training load')\n",
|
||||
"ax1.legend(loc='upper left')\n",
|
||||
"ax1.grid(alpha=0.3)\n",
|
||||
"ax1.set_title('Performance Management Chart \u2014 CTL / ATL / TSB')\n",
|
||||
"\n",
|
||||
"# bottom panel: form\n",
|
||||
"ax2.plot(pmc.index, pmc['TSB'], color='#264653', lw=1.5)\n",
|
||||
"ax2.axhspan(10, 25, color='#2a9d8f', alpha=0.12, label='race-ready (+10 to +25)')\n",
|
||||
"ax2.axhspan(-30, -10, color='#e9c46a', alpha=0.12, label='productive overload (\u221230 to \u221210)')\n",
|
||||
"ax2.axhline(0, color='gray', lw=0.6)\n",
|
||||
"ax2.axhline(-30, color='#e76f51', ls='--', lw=0.8, label='injury-risk floor (\u221230)')\n",
|
||||
"ax2.set_ylabel('TSB (form)')\n",
|
||||
"ax2.legend(loc='lower left', fontsize=9)\n",
|
||||
"ax2.grid(alpha=0.3)\n",
|
||||
"\n",
|
||||
"# annotate prior race days\n",
|
||||
"race_dates = pd.to_datetime(['2023-09-23', '2024-09-21', '2025-09-06', '2025-09-20'])\n",
|
||||
"for rd in race_dates:\n",
|
||||
" if rd in pmc.index:\n",
|
||||
" ax2.axvline(rd, color='#d62828', alpha=0.4, lw=1)\n",
|
||||
" ax2.annotate(f\"{rd.strftime('%Y-%m-%d')}\\nTSB={pmc.loc[rd, 'TSB']:+.0f}\",\n",
|
||||
" xy=(rd, pmc.loc[rd, 'TSB']), xytext=(5, 8),\n",
|
||||
" textcoords='offset points', fontsize=8, color='#d62828')\n",
|
||||
"plt.tight_layout()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
|
||||
Reference in New Issue
Block a user