2026-05-18 12:53:24 -04:00
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
2026-06-12 06:25:45 -04:00
"# 03 — 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",
2026-06-12 06:25:45 -04:00
"execution_count": 1,
2026-05-18 12:53:24 -04:00
"metadata": {},
2026-06-12 06:25:45 -04:00
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1497 days, 2022-04-08 → 2026-05-17\n"
]
}
],
2026-05-18 12:53:24 -04:00
"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-06-12 06:25:45 -04:00
"print(f'{len(w)} days, {w.index.min().date()} → {w.index.max().date()}')"
2026-05-18 12:53:24 -04:00
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
2026-06-12 06:25:45 -04:00
"## Recent 30 days — at a glance"
2026-05-18 12:53:24 -04:00
]
},
{
"cell_type": "code",
2026-06-12 06:25:45 -04:00
"execution_count": 2,
2026-05-18 12:53:24 -04:00
"metadata": {},
2026-06-12 06:25:45 -04:00
"outputs": [
{
"ename": "TypeError",
"evalue": "no numeric data to plot",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mTypeError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m recent = w.tail(\u001b[32m30\u001b[39m)\n\u001b[32m 2\u001b[39m fig, axes = plt.subplots(\u001b[32m5\u001b[39m, \u001b[32m1\u001b[39m, figsize=(\u001b[32m13\u001b[39m, \u001b[32m10\u001b[39m), sharex=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m recent[\u001b[33m'sleep_score'\u001b[39m].plot(ax=axes[\u001b[32m0\u001b[39m], marker=\u001b[33m'o'\u001b[39m, color=\u001b[33m'C0'\u001b[39m)\n\u001b[32m 4\u001b[39m axes[\u001b[32m0\u001b[39m].set_title(\u001b[33m'Sleep score'\u001b[39m); axes[\u001b[32m0\u001b[39m].grid(alpha=\u001b[32m0.3\u001b[39m)\n\u001b[32m 5\u001b[39m recent[\u001b[33m'resting_hr'\u001b[39m].plot(ax=axes[\u001b[32m1\u001b[39m], marker=\u001b[33m'o'\u001b[39m, color=\u001b[33m'C1'\u001b[39m)\n\u001b[32m 6\u001b[39m axes[\u001b[32m1\u001b[39m].set_title(\u001b[33m'Resting HR (bpm)'\u001b[39m); axes[\u001b[32m1\u001b[39m].grid(alpha=\u001b[32m0.3\u001b[39m)\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/obsidian/RunningLog/garmin/.venv/lib/python3.13/site-packages/pandas/plotting/_core.py:1185\u001b[39m, in \u001b[36mPlotAccessor.__call__\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 1182\u001b[39m label_name = label_kw \u001b[38;5;129;01mor\u001b[39;00m data.columns\n\u001b[32m 1183\u001b[39m data.columns = label_name\n\u001b[32m-> \u001b[39m\u001b[32m1185\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mplot_backend\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mplot\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mdata\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mkind\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mkind\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/obsidian/RunningLog/garmin/.venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/__init__.py:71\u001b[39m, in \u001b[36mplot\u001b[39m\u001b[34m(data, kind, **kwargs)\u001b[39m\n\u001b[32m 69\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33max\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[38;5;28mgetattr\u001b[39m(ax, \u001b[33m\"\u001b[39m\u001b[33mleft_ax\u001b[39m\u001b[33m\"\u001b[39m, ax)\n\u001b[32m 70\u001b[39m plot_obj = PLOT_CLASSES[kind](data, **kwargs)\n\u001b[32m---> \u001b[39m\u001b[32m71\u001b[39m \u001b[30;43mplot_obj\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mgenerate\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 72\u001b[39m plt.draw_if_interactive()\n\u001b[32m 73\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m plot_obj.result\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/obsidian/RunningLog/garmin/.venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/core.py:516\u001b[39m, in \u001b[36mMPLPlot.generate\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 514\u001b[39m \u001b[38;5;129m@final\u001b[39m\n\u001b[32m 515\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mgenerate\u001b[39m(\u001b[38;5;28mself\u001b[39m) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m516\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_compute_plot_data\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 517\u001b[39m fig = \u001b[38;5;28mself\u001b[39m.fig\n\u001b[32m 518\u001b[39m \u001b[38;5;28mself\u001b[39m._make_plot(fig)\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/Documents/obsidian/RunningLog/garmin/.venv/lib/python3.13/site-packages/pandas/plotting/_matplotlib/core.py:716\u001b[39m, in \u001b[36mMPLPlot._compute_plot_data\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 714\u001b[39m \u001b[38;5;66;03m# no non-numeric frames or series allowed\u001b[39;00m\n\u001b[32m 715\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m is_empty:\n\u001b[32m--> \u001b[39m\u001b[32m716\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[33m\"\u001b[39m\u001b[33mno numeric data to plot\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 718\u001b[39m \u001b[38;5;28mself\u001b[39m.data = numeric_data.apply(\u001b[38;5;28mtype\u001b[39m(\u001b[38;5;28mself\u001b[39m)._convert_to_ndarray)\n",
"\u001b[31mTypeError\u001b[39m: no numeric data to plot"
]
},
{
"data": {
"image/png": "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Hb17987byAIAAAC02BobY8eOjd27d0d9fX1eMHTAgAGxYsWK8oKiW7ZsyTulHO+1116L3/72t/H000+f0F+a2vLyyy/noGTfvn3Ro0ePGDFiRNx33315ygkAAADAydSUSqVSFFxaPDTtjrJ//37rbQAAAEAVfX+veCoKAAAAwLlCsAEAAAAUlmADAAAAKCzBBgAAAFBYgg0AAACgsAQbAAAAQGEJNgAAAIDCEmwAAAAAhSXYAAAAAApLsAEAAAAUlmADAAAAKCzBBgAAAFBYgg0AAACgsAQbAAAAQGEJNgAAAIDCEmwAAAAAhSXYAAAAAApLsAEAAAAUlmADAAAAKCzBBgAAAFBYgg0AAACgsAQbAAAAQHUFG/Pnz4+6urqora2NoUOHxtq1a0/advHixVFTU9PkSNcdr1QqRX19fXTv3j3atWsXw4cPj9dff/1Mbg0AAACoIhUHG8uWLYtp06ZFQ0NDrF+/Pvr37x8jR46MXbt2nfSaDh06xPbt28vHH//4xybn58yZE4888kgsWLAgXnjhhWjfvn3u85133jmzpwIAAACqQsXBxsMPPxy33357TJw4Ma6++uocRlx00UWxaNGik16TRml069atfHTt2rXJaI25c+fGPffcE6NHj45+/frFo48+Gn/+859j+fLlZ/5kAAAAwHmvomDjyJEjsW7dujxVpNxBq1a5vGbNmpNed/Dgwbj88sujZ8+eObz4/e9/Xz63efPm2LFjR5M+O3bsmKe4nKpPAAAAgIqCjT179sTRo0ebjLhIUjmFE83p06dPHs3xq1/9Kn7605/GsWPH4tprr41t27bl843XVdLn4cOH48CBA00OAAAAoPqc9V1Rhg0bFuPHj48BAwbE9ddfH48//nh8/OMfjx/84Adn3Ofs2bPzqI7GI40EAQAAAKpPRcFGly5donXr1rFz584m9amc1s44HRdeeGF8+tOfjk2bNuVy43WV9Dlz5szYv39/+di6dWsljwEAAABUY7DRpk2bGDhwYKxatapcl6aWpHIamXE60lSWV155JW/tmvTq1SsHGMf3maaWpN1RTtZn27Zt804rxx8AAABA9bmg0gvSVq8TJkyIQYMGxZAhQ/KOJocOHcq7pCRp2smll16ap4sk3/rWt+Kaa66J3r17x759++Lb3/523u510qRJ5R1Tpk6dGvfff39cddVVOeiYNWtW9OjRI8aMGdPSzwsAAABUc7AxduzY2L17d9TX1+fFPdPaGStWrCgv/rlly5a8U0qjt956K28Pm9p26tQpj/h47rnn8laxjaZPn57DkTvuuCOHH9ddd13us7a2tqWeEwAAADgP1ZRKpVIUXJq6khYRTettmJYCAAAA1fP9/azvigIAAABwtgg2AAAAgMISbAAAAACFJdgAAAAACkuwAQAAABSWYAMAAAAoLMEGAAAAUFiCDQAAAKCwBBsAAABAYQk2AAAAgMISbAAAAACFJdgAAAAACkuwAQAAABSWYAMAAAAoLMEGAAAAUFiCDQAAAKCwBBsAAABAYQk2AAAAgMISbAAAAACFJdgAAAAACkuwAQAAABSWYAMAAAAoLMEGAAAAUF3Bxvz586Ouri5qa2tj6NChsXbt2pO2XbhwYXz2s5+NTp065WP48OEntL/11lujpqamyXHTTTedya0BAAAAVaT
"text/plain": [
"<Figure size 1300x1000 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
2026-05-18 12:53:24 -04:00
"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",
2026-06-12 06:25:45 -04:00
"execution_count": 3,
2026-05-18 12:53:24 -04:00
"metadata": {},
2026-06-12 06:25:45 -04:00
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1300x400 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
2026-05-18 12:53:24 -04:00
"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-06-12 06:25:45 -04:00
"ax.set_title('Sleep stages — 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",
2026-06-12 06:25:45 -04:00
"execution_count": 4,
2026-05-18 12:53:24 -04:00
"metadata": {},
2026-06-12 06:25:45 -04:00
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>n_days</th>\n",
" <th>rhr_mean</th>\n",
" <th>hrv_mean</th>\n",
" <th>sleep_score_mean</th>\n",
" <th>avg_stress</th>\n",
" </tr>\n",
" <tr>\n",
" <th>load_bucket_prev</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>rest</th>\n",
" <td>285</td>\n",
" <td>48.1</td>\n",
" <td>68.2</td>\n",
" <td>NaN</td>\n",
" <td>32.7</td>\n",
" </tr>\n",
" <tr>\n",
" <th>light</th>\n",
" <td>7</td>\n",
" <td>50.3</td>\n",
" <td>61.3</td>\n",
" <td>NaN</td>\n",
" <td>40.1</td>\n",
" </tr>\n",
" <tr>\n",
" <th>moderate</th>\n",
" <td>35</td>\n",
" <td>48.9</td>\n",
" <td>65.3</td>\n",
" <td>NaN</td>\n",
" <td>31.5</td>\n",
" </tr>\n",
" <tr>\n",
" <th>hard</th>\n",
" <td>28</td>\n",
" <td>48.6</td>\n",
" <td>67.1</td>\n",
" <td>NaN</td>\n",
" <td>31.6</td>\n",
" </tr>\n",
" <tr>\n",
" <th>very hard</th>\n",
" <td>8</td>\n",
" <td>48.6</td>\n",
" <td>61.6</td>\n",
" <td>NaN</td>\n",
" <td>32.8</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" n_days rhr_mean hrv_mean sleep_score_mean avg_stress\n",
"load_bucket_prev \n",
"rest 285 48.1 68.2 NaN 32.7\n",
"light 7 50.3 61.3 NaN 40.1\n",
"moderate 35 48.9 65.3 NaN 31.5\n",
"hard 28 48.6 67.1 NaN 31.6\n",
"very hard 8 48.6 61.6 NaN 32.8"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
2026-05-18 12:53:24 -04:00
"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",
2026-06-12 06:25:45 -04:00
"execution_count": 5,
2026-05-18 12:53:24 -04:00
"metadata": {},
2026-06-12 06:25:45 -04:00
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA6oAAAMWCAYAAAD4dHsQAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3QV4FFcXBuAvCcUTXGIQrHiCewhOhOAELRCkRQKBoG2hAm1xhxaK0+IWJAR3d3eNIAm0eIEC+Z9zl12yMaA/ZDab732eaTM7l925O2tnzrl3LKKioqJAREREREREZCIstd4BIiIiIiIiougYqBIREREREZFJYaBKREREREREJoWBKhEREREREZkUBqpERERERERkUhioEhERERERkUlhoEpEREREREQmhYEqERERERERmRQGqkRERERERGRSGKgSERERERGRSWGgSkREREREZCZ27twJb29v2NnZwcLCAoGBge/8N9u3b0epUqWQKlUq5M+fH3PmzInVZsqUKXByckLq1KlRvnx5HDx4EJ8SA1UiIiIiIiIz8eTJE7i4uKjA8n1cu3YNXl5eqF69Oo4fP45evXqhU6dO2LBhg6HN4sWLERAQgO+//x5Hjx5V91+3bl1ERER8sn5YREVFRX2yeyciIiIiIiJNWFhYYOXKlWjYsGG8bQYMGICgoCCcPn3acFuLFi1w//59rF+/Xq1LBrVs2bKYPHmyWn/9+jUcHR3Ro0cPDBw48JPsOzOq9FHJeY+HDx+q/xMRERER8Tfj/+/58+fqN3b0RW77GPbt24datWoZ3SbZUrldvHjxAkeOHDFqY2lpqdb1bT6FFJ/snilZkjdNxowZERoaChsbG613h4iIiIhM9DejZOQka5chQwaYsmfPnqlgTUsjR47Ezz//bHSblOH+8MMP//d93759Gzly5DC6TdblGP3zzz/4+++/8erVqzjbnD9/Hp8KA1X6qB49eqT+Lx88RERERETv+u1oyoGqBKlZ0qTHU7zSdD9y5syJO3fuqImM9GTiI3PGQJU+Kmtra/X/1rBHSjOrLE9raQFz9cOVzTBHUVafwVylypBZ6134JJ4/+AvmKsrKSutdoA/017/mecyyWGqbGfqUrB6Ewxy9Tp8d5ubRo8fI61zK8NvRVEkmVYJULX/bvsBrzL8droLUT1GxqA+Co5N1eaw0adLAyspK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
"text/plain": [
"<Figure size 1000x800 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
2026-05-18 12:53:24 -04:00
"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",
2026-06-12 06:25:45 -04:00
"execution_count": 6,
2026-05-18 12:53:24 -04:00
"metadata": {},
2026-06-12 06:25:45 -04:00
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>distance_km</th>\n",
" <th>training_load</th>\n",
" <th>sleep_score</th>\n",
" <th>sleep_hours</th>\n",
" <th>resting_hr</th>\n",
" <th>hrv_last_night</th>\n",
" <th>avg_stress</th>\n",
" </tr>\n",
" <tr>\n",
" <th>calendar_date</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2026-03-02</th>\n",
" <td>5.3</td>\n",
" <td>58.1</td>\n",
" <td>NaN</td>\n",
" <td>7.5</td>\n",
" <td>54.9</td>\n",
" <td>60.1</td>\n",
" <td>39.4</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-03-09</th>\n",
" <td>12.1</td>\n",
" <td>127.0</td>\n",
" <td>NaN</td>\n",
" <td>6.7</td>\n",
" <td>53.5</td>\n",
" <td>64.2</td>\n",
" <td>38.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-03-16</th>\n",
" <td>2.3</td>\n",
" <td>23.2</td>\n",
" <td>NaN</td>\n",
" <td>6.7</td>\n",
" <td>53.0</td>\n",
" <td>60.0</td>\n",
" <td>32.3</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-03-23</th>\n",
" <td>18.9</td>\n",
" <td>158.7</td>\n",
" <td>NaN</td>\n",
" <td>6.8</td>\n",
" <td>53.1</td>\n",
" <td>58.2</td>\n",
" <td>34.7</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-03-30</th>\n",
" <td>20.0</td>\n",
" <td>139.7</td>\n",
" <td>NaN</td>\n",
" <td>6.9</td>\n",
" <td>52.3</td>\n",
" <td>60.3</td>\n",
" <td>31.9</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-04-06</th>\n",
" <td>9.2</td>\n",
" <td>99.3</td>\n",
" <td>NaN</td>\n",
" <td>7.6</td>\n",
" <td>47.9</td>\n",
" <td>71.3</td>\n",
" <td>23.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-04-13</th>\n",
" <td>24.8</td>\n",
" <td>240.3</td>\n",
" <td>NaN</td>\n",
" <td>6.4</td>\n",
" <td>50.7</td>\n",
" <td>63.5</td>\n",
" <td>32.9</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-04-20</th>\n",
" <td>0.0</td>\n",
" <td>0.0</td>\n",
" <td>NaN</td>\n",
" <td>7.3</td>\n",
" <td>49.0</td>\n",
" <td>62.9</td>\n",
" <td>27.1</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-04-27</th>\n",
" <td>29.7</td>\n",
" <td>281.7</td>\n",
" <td>NaN</td>\n",
" <td>7.1</td>\n",
" <td>50.4</td>\n",
" <td>62.0</td>\n",
" <td>36.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-04</th>\n",
" <td>26.6</td>\n",
" <td>243.2</td>\n",
" <td>NaN</td>\n",
" <td>6.8</td>\n",
" <td>51.1</td>\n",
" <td>62.4</td>\n",
" <td>33.3</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-11</th>\n",
" <td>20.8</td>\n",
" <td>190.0</td>\n",
" <td>NaN</td>\n",
" <td>7.1</td>\n",
" <td>51.1</td>\n",
" <td>66.0</td>\n",
" <td>29.9</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-18</th>\n",
" <td>0.0</td>\n",
" <td>0.0</td>\n",
" <td>NaN</td>\n",
" <td>6.9</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" distance_km training_load sleep_score sleep_hours \\\n",
"calendar_date \n",
"2026-03-02 5.3 58.1 NaN 7.5 \n",
"2026-03-09 12.1 127.0 NaN 6.7 \n",
"2026-03-16 2.3 23.2 NaN 6.7 \n",
"2026-03-23 18.9 158.7 NaN 6.8 \n",
"2026-03-30 20.0 139.7 NaN 6.9 \n",
"2026-04-06 9.2 99.3 NaN 7.6 \n",
"2026-04-13 24.8 240.3 NaN 6.4 \n",
"2026-04-20 0.0 0.0 NaN 7.3 \n",
"2026-04-27 29.7 281.7 NaN 7.1 \n",
"2026-05-04 26.6 243.2 NaN 6.8 \n",
"2026-05-11 20.8 190.0 NaN 7.1 \n",
"2026-05-18 0.0 0.0 NaN 6.9 \n",
"\n",
" resting_hr hrv_last_night avg_stress \n",
"calendar_date \n",
"2026-03-02 54.9 60.1 39.4 \n",
"2026-03-09 53.5 64.2 38.0 \n",
"2026-03-16 53.0 60.0 32.3 \n",
"2026-03-23 53.1 58.2 34.7 \n",
"2026-03-30 52.3 60.3 31.9 \n",
"2026-04-06 47.9 71.3 23.0 \n",
"2026-04-13 50.7 63.5 32.9 \n",
"2026-04-20 49.0 62.9 27.1 \n",
"2026-04-27 50.4 62.0 36.0 \n",
"2026-05-04 51.1 62.4 33.3 \n",
"2026-05-11 51.1 66.0 29.9 \n",
"2026-05-18 NaN NaN NaN "
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
2026-05-18 12:53:24 -04:00
"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": {
2026-06-12 06:25:45 -04:00
"display_name": "garmin (3.13.13.final.0)",
2026-05-19 08:34:22 -04:00
"language": "python",
"name": "python3"
},
"language_info": {
2026-06-12 06:25:45 -04:00
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.13"
2026-05-19 08:34:22 -04:00
}
2026-05-18 12:53:24 -04:00
},
"nbformat": 4,
"nbformat_minor": 5
2026-06-12 06:25:45 -04:00
}