{ "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 }