Files
openrun/examples/notebooks/exploration.ipynb

624 lines
144 KiB
Plaintext
Raw Normal View History

2026-05-18 12:53:24 -04:00
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Garmin data exploration\n",
"\n",
"Run `uv run auth.py` and `uv run sync.py --full` once before using this notebook.\n",
"\n",
"In VSCode, pick the `.venv` Python interpreter at the top right of this notebook as the kernel."
]
},
{
"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": [
"activities 378\n",
"activity_fit_files 349\n",
"activity_splits 2285\n",
"activity_time_in_zone 345\n",
"daily_body_battery 365\n",
"daily_hrv 363\n",
"daily_intensity_minutes 365\n",
"daily_resting_hr 363\n",
"daily_sleep 1462\n",
"daily_steps 1497\n",
"daily_stress 365\n",
"manual_activities 0\n",
"race_plan 0\n",
"sqlite_sequence 0\n",
"sync_state 3\n"
]
}
],
2026-05-18 12:53:24 -04:00
"source": [
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
2026-05-19 08:34:22 -04:00
"from openrun import open_conn\n",
2026-05-18 12:53:24 -04:00
"\n",
2026-05-19 08:34:22 -04:00
"conn = open_conn()\n",
2026-05-18 12:53:24 -04:00
"\n",
"# What tables do we have, and how many rows in each?\n",
"tables = pd.read_sql(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\", conn)\n",
"for t in tables['name']:\n",
" n = conn.execute(f'SELECT COUNT(*) FROM {t}').fetchone()[0]\n",
" print(f'{t:30s} {n:>6}')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
2026-06-12 06:25:45 -04:00
"## Activities — load into pandas"
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": [
{
"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>activity_id</th>\n",
" <th>start_time_local</th>\n",
" <th>activity_type</th>\n",
" <th>activity_name</th>\n",
" <th>distance_m</th>\n",
" <th>duration_s</th>\n",
" <th>avg_hr</th>\n",
" <th>max_hr</th>\n",
" <th>calories</th>\n",
" <th>elevation_gain_m</th>\n",
" <th>training_load</th>\n",
" <th>aerobic_te</th>\n",
" <th>anaerobic_te</th>\n",
" <th>distance_km</th>\n",
" <th>duration_min</th>\n",
" <th>pace_min_per_km</th>\n",
" <th>week</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>22836022213</td>\n",
" <td>2026-05-10 10:43:58</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>17198.130859</td>\n",
" <td>8742.279297</td>\n",
" <td>156.0</td>\n",
" <td>178.0</td>\n",
" <td>1482.0</td>\n",
" <td>231.0</td>\n",
" <td>126.186203</td>\n",
" <td>3.7</td>\n",
" <td>0.9</td>\n",
" <td>17.198131</td>\n",
" <td>145.704655</td>\n",
" <td>8.472122</td>\n",
" <td>2026-05-04</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>22803352843</td>\n",
" <td>2026-05-07 20:17:15</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>3647.889893</td>\n",
" <td>1754.900024</td>\n",
" <td>170.0</td>\n",
" <td>190.0</td>\n",
" <td>367.0</td>\n",
" <td>36.0</td>\n",
" <td>63.818512</td>\n",
" <td>3.0</td>\n",
" <td>0.0</td>\n",
" <td>3.647890</td>\n",
" <td>29.248334</td>\n",
" <td>8.017877</td>\n",
" <td>2026-05-04</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>22755455510</td>\n",
" <td>2026-05-03 17:03:51</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>9317.000000</td>\n",
" <td>4746.172852</td>\n",
" <td>155.0</td>\n",
" <td>191.0</td>\n",
" <td>825.0</td>\n",
" <td>139.0</td>\n",
" <td>81.749390</td>\n",
" <td>3.1</td>\n",
" <td>0.7</td>\n",
" <td>9.317000</td>\n",
" <td>79.102881</td>\n",
" <td>8.490166</td>\n",
" <td>2026-04-27</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>22755454604</td>\n",
" <td>2026-05-02 14:10:35</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>12422.639648</td>\n",
" <td>5750.320801</td>\n",
" <td>148.0</td>\n",
" <td>177.0</td>\n",
" <td>1047.0</td>\n",
" <td>151.0</td>\n",
" <td>102.831665</td>\n",
" <td>3.5</td>\n",
" <td>0.0</td>\n",
" <td>12.422640</td>\n",
" <td>95.838680</td>\n",
" <td>7.714840</td>\n",
" <td>2026-04-27</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>22755453569</td>\n",
" <td>2026-04-30 19:41:35</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>4834.930176</td>\n",
" <td>2435.958984</td>\n",
" <td>158.0</td>\n",
" <td>190.0</td>\n",
" <td>479.0</td>\n",
" <td>82.0</td>\n",
" <td>58.665405</td>\n",
" <td>2.9</td>\n",
" <td>0.0</td>\n",
" <td>4.834930</td>\n",
" <td>40.599316</td>\n",
" <td>8.397084</td>\n",
" <td>2026-04-27</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" activity_id start_time_local activity_type activity_name \\\n",
"0 22836022213 2026-05-10 10:43:58 running Milford Running \n",
"1 22803352843 2026-05-07 20:17:15 running Milford Running \n",
"2 22755455510 2026-05-03 17:03:51 running Milford Running \n",
"3 22755454604 2026-05-02 14:10:35 running Milford Running \n",
"4 22755453569 2026-04-30 19:41:35 running Milford Running \n",
"\n",
" distance_m duration_s avg_hr max_hr calories elevation_gain_m \\\n",
"0 17198.130859 8742.279297 156.0 178.0 1482.0 231.0 \n",
"1 3647.889893 1754.900024 170.0 190.0 367.0 36.0 \n",
"2 9317.000000 4746.172852 155.0 191.0 825.0 139.0 \n",
"3 12422.639648 5750.320801 148.0 177.0 1047.0 151.0 \n",
"4 4834.930176 2435.958984 158.0 190.0 479.0 82.0 \n",
"\n",
" training_load aerobic_te anaerobic_te distance_km duration_min \\\n",
"0 126.186203 3.7 0.9 17.198131 145.704655 \n",
"1 63.818512 3.0 0.0 3.647890 29.248334 \n",
"2 81.749390 3.1 0.7 9.317000 79.102881 \n",
"3 102.831665 3.5 0.0 12.422640 95.838680 \n",
"4 58.665405 2.9 0.0 4.834930 40.599316 \n",
"\n",
" pace_min_per_km week \n",
"0 8.472122 2026-05-04 \n",
"1 8.017877 2026-05-04 \n",
"2 8.490166 2026-04-27 \n",
"3 7.714840 2026-04-27 \n",
"4 8.397084 2026-04-27 "
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
2026-05-18 12:53:24 -04:00
"source": [
"activities = pd.read_sql(\n",
" \"SELECT activity_id, start_time_local, activity_type, activity_name, \"\n",
" \"distance_m, duration_s, avg_hr, max_hr, calories, elevation_gain_m, \"\n",
" \"training_load, aerobic_te, anaerobic_te \"\n",
" \"FROM activities ORDER BY start_time_local DESC\",\n",
" conn,\n",
" parse_dates=['start_time_local'],\n",
")\n",
"activities['distance_km'] = activities['distance_m'] / 1000\n",
"activities['duration_min'] = activities['duration_s'] / 60\n",
"activities['pace_min_per_km'] = activities['duration_min'] / activities['distance_km']\n",
"activities['week'] = activities['start_time_local'].dt.to_period('W').dt.start_time\n",
"activities.head()"
]
},
{
"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": {
"text/plain": [
"activity_type\n",
"running 335\n",
"trail_running 15\n",
"cycling 9\n",
"transition_v2 7\n",
"open_water_swimming 6\n",
"multi_sport 4\n",
"mountain_biking 1\n",
"assistance 1\n",
"Name: count, dtype: int64"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
2026-05-18 12:53:24 -04:00
"source": [
"activities['activity_type'].value_counts()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Weekly running mileage"
]
},
{
"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": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAGGCAYAAACqvTJ0AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsvQe4FEXav12gAkZUVMxizjliDihmXVlXXbOs7rrqGlfFnDFnzArmnHMWE6jrmnNCUQQjYAJZOf/rrvd75qvTdPdM1+kzZw787usaj0xPpaeeUF1VXd2hqampyQkhhBBCCCGEEEIIUUc61rMwIYQQQgghhBBCCCFAk1JCCCGEEEIIIYQQou5oUkoIIYQQQgghhBBC1B1NSgkhhBBCCCGEEEKIuqNJKSGEEEIIIYQQQghRdzQpJYQQQgghhBBCCCHqjialhBBCCCGEEEIIIUTd0aSUEEIIIYQQQgghhKg7mpQSQgghhBBCCCGEEHVHk1JCCCGEEBmccMIJrkOHDu67777LldEee+zhevToMcXIoz0yaNAgX/dhw4ZVvlt//fX9RwghhBBtgyalhBBCCNEQ3HbbbX7S4O67757k2vLLL++vPf3005Ncm3/++d2aa65Zp1oKIYQQQoiy0KSUEEIIIRqCtdde2/99/vnnm30/duxY9/bbb7upp57avfDCC82uDR8+3H8srWhdjjnmGPfbb7+1SzHvuuuuvu4LLLBAW1dFCCGEEP8fU9v/CCGEEEK0JXPPPbdbcMEFJ5mUGjJkiGtqanLbb7/9JNfs35P7pNTEiRPd77//7rp06dKm9WBikE97ZKqppvIfIYQQQjQO2iklhBBCiIaByaXXXnut2W4cdkctvfTSbrPNNnNDhw71EzThNR7rW2uttSrf3XDDDW7llVd20047rZt11lndjjvu6HdTJXnppZfcpptu6rp27eqmm246t956602yEyuNzz//3C2yyCJumWWWcaNGjZrkOhNonC+1zTbbTHJt3Lhxvry///3vuWXQpv3339/deOONvu2dO3d2jzzyiHvmmWf8Nf6GcE4S33NuUnjO1QwzzOC++uort+222/r/n3322d1hhx3m/vjjj0nSnn322e6KK65wCy+8sC9v1VVXda+88krVM6Wsrvfcc4+XCWmpM/VNQr1XWWUVP7lGOZdffnnN51Rx9hP5v/nmm76v6DP64Y477vDXBw8e7FZffXXf74svvrh74oknqp4plcb48ePd8ccf7/OmLfPNN587/PDD/fchAwcOdBtuuKGbY445/O+WWmopd+mll06SH/pKG5l0pc4bbLCBe/fdd72O0Echo0ePdgcddJAvkzypwxlnnNFM54UQQojJCU1KCSGEEKKhJqUmTJjgJ4wMJoo4M4rPmDFj/KN84bUllljCdevWzf/71FNPdbvttptbdNFF3bnnnutv8J988km37rrr+ht+46mnnvLf8WggExCnnXaav84kw8svv5xZv08++cSnm3HGGf0ES/fu3Sf5DRMfu+yyi3v44YfdDz/80Oza/fff78vkejWo48EHH+x22GEHd8EFF0QdpM7kU+/evb18mHRiMuecc87xk09JbrrpJnfWWWf5CbNTTjnFT95st912vj+qwY61f/7zn34C8Mwzz/STb3369HHff/995TdMNjIJyHcnnnii69u3rzvppJP8ZFat/Pjjj27LLbf0k0+Uw8QNZd56663+7+abb+5OP/1098svv7g///nP7qeffiogrf+bQNp66629rLbaait30UUX+Qm98847z/dDCBNQPAp41FFHeZkykYQMBgwY0Ox3/fr18+1lMg75opv0CXUM+fXXX33/MKmKDl944YV+spX0hxxySKF2CCGEEO2GJiGEEEKIBuGdd95pYnhy8skn+39PmDChafrpp2+69tpr/b+7d+/eNGDAAP//Y8eObZpqqqma9t57b//vYcOG+X+feuqpzfJ86623mqaeeurK9xMnTmxadNFFm3r37u3/3/j111+bFlxwwaaNN9648t3xxx/v6/Ptt982vffee01zzz1306qrrtr0ww8/NCtj9913b1pggQUq//7ggw98uksvvbTZ77beeuumHj16NCs3DdJ27NjRyyPk6aef9tf4G/LZZ5/57wcOHNisTnx30kknNfvtiiuu2LTyyitPkrZbt27N2nXvvff67++///5J5JGsa6dOnZo+/vjjyndvvPGG//6iiy6qfLfVVls1TTfddE1fffVV5buPPvrI900tQ9L11lvP/+6mm26qfPf+++9XZDV06NDK948++ugk8uD/+Y72hnnyMa6//nqf13PPPdes7Msuu8ynfeGFF5rpSxJ0aqGFFqr8e+TIkb592267bbPfnXDCCT4/+shA59H1Dz/8sNlvjzzySK/XX3zxRVUZCSGEEO0N7ZQSQgghRMOw5JJL+l09dlbUG2+84XeU2Nv1+GuP2HHWFDuB7Dypu+66y+90+ctf/uK+++67ymfOOef0u1PszX2vv/66++ijj9xf//pXv2vHfkc5G220kXv22WcneVyK3VnsYmG3Eo+FzTLLLLntWGyxxfxuHh6/M9g1xe6pnXfeuabH1SiPR8Jayj/+8Y9m/15nnXXcp59+Osnv2AkUtovfQdpvk/Tq1cs/jmcst9xybqaZZqqkpZ+QG7uOeIzN4PE0HsusFR5BZEeUwWN6M888s9cb5G3Y/9dS95Dbb7/d58Xuu1CH2EEH4dsfeUzQYAcfv6PPKJN/A7v0/ve///kdVCEHHHBAatnInD4Iy0a2yA+9FEIIISY32udJlUIIIYSYLGGyhoknmxhiAooze5i8AK5dfPHF/v9tcsompZhoYuMOE1BpTDPNNJXfwe67755ZDyYVwgkaHuXiUb1HH33UT4zUAo9gcdYSZ1DxmBeTDjwKx1vgaoFD31sKZzdxjlQI7eIxuCTzzz//JL+DtN9WS5ss55tvvvHnhFk/hqR9l8W88847yYQeZ3Tx6Fzyu1rrHoJuvPfee5PIzKAdBvrHo59MjvLoXVJ/qAN9n9ZGzjpLTmxSNudl1VK2EEIIMbmgSSkhhBBCNBRMMnH20ltvvVU5T8rg///973/7w7vZTcWum4UWWshfYxKLCQt2I6W9Zc0mk2wXFOf7rLDCCql1SE48cT7Stdde63c+VTuk3GBHD2dCkYZzhzgriHOF2N1TC+FOHCNrh1V4cHlIkbfNZf32/57Qa720Rcgqp6zy0Y1ll13Wn0eWhk1+cbYYu+rYUcVv+b5Tp07uoYce8udPxRxMTpqNN97YH6qetftOCCGEmNzQpJQQQgghGgrb+cSkE5NSHFZu8FY9DrfmkHEOQ+dga4PHx5iEYIdR3g28PWbG42U8GlULTGBNPfXU/jEsDjnn0b9qsBtmiy228JNSPLJHW84//3zXEmx3TXhoO9iOnEaF3W7s2vr4448nuZb2XVuBbvDIKBNOeY9YMmnK2/juu+++ZrvEwsf7gB1y1sZw5xuPjSZ3cVH2zz//XLNOCiGEEJMDOlNKCCGEEA0Fu4mYwGAyhx1R4U4pJqRWWmkl/4YzzoCyCSzgTXHsmOFNZ8kdMvzb3gTHxBYTALxhjUmAJN9+++0k3zFBwRvreKMbj/0xGVELPKr37rvv+t1d1C08DykGJjnIJ3m+0CWXXOIaGerMZAtv2hsxYkTleyZr2NnWKHAeGTp35ZVXTnKNxw/tjXm2MyvUMx7ZGzhwYLM0TG4xmcmb+kLsEdRk2TwKyCOiSZiE5GwqIYQQYnJDO6WEEEII0VDwGNSqq67qnnvuOT8JxSRSCJNU55xzjv//cFKKiaZTTjnF9evXzw0bNswfqs2ups8++8zdfffdbp999nGHHXaY69ixo7vqqqv8AdtLL72023PPPd0888zjJyPY6cIOKnbCJCEdj+CRLxMIPKplB2BnwU4pDm7nPCnKY8dQS+Ccou23395ddNFFfqKMNj/wwAPt4ryhE044wT322GNurbXWcvvuu69/5JDJmWWWWcYfPt8IMIl42223+cPh0QXqSj3ff/99/z0TRkyabrLJJl5POWuMxzmZ3GQii/7
"text/plain": [
"<Figure size 1200x400 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
2026-05-18 12:53:24 -04:00
"source": [
"runs = activities[activities['activity_type'].str.contains('running', case=False, na=False)]\n",
"weekly = runs.groupby('week')['distance_km'].sum()\n",
"\n",
"fig, ax = plt.subplots(figsize=(12, 4))\n",
"weekly.plot(kind='bar', ax=ax)\n",
"ax.set_ylabel('km')\n",
"ax.set_title('Weekly running mileage')\n",
"ax.set_xlabel('')\n",
"plt.xticks(rotation=45, ha='right')\n",
"plt.tight_layout()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
2026-06-12 06:25:45 -04:00
"## Sleep, stress, HRV — daily timeline"
2026-05-18 12:53:24 -04:00
]
},
{
"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": {
"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>total_steps</th>\n",
" <th>sleep_score</th>\n",
" <th>avg_stress</th>\n",
" <th>hrv</th>\n",
" <th>resting_hr</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",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2026-05-04</th>\n",
" <td>4134</td>\n",
" <td>None</td>\n",
" <td>30.0</td>\n",
" <td>71.0</td>\n",
" <td>52.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-05</th>\n",
" <td>5588</td>\n",
" <td>None</td>\n",
" <td>32.0</td>\n",
" <td>70.0</td>\n",
" <td>50.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-06</th>\n",
" <td>6791</td>\n",
" <td>None</td>\n",
" <td>23.0</td>\n",
" <td>65.0</td>\n",
" <td>52.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-07</th>\n",
" <td>8062</td>\n",
" <td>None</td>\n",
" <td>34.0</td>\n",
" <td>75.0</td>\n",
" <td>52.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-08</th>\n",
" <td>2501</td>\n",
" <td>None</td>\n",
" <td>27.0</td>\n",
" <td>47.0</td>\n",
" <td>51.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-09</th>\n",
" <td>7450</td>\n",
" <td>None</td>\n",
" <td>25.0</td>\n",
" <td>73.0</td>\n",
" <td>49.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-10</th>\n",
" <td>30430</td>\n",
" <td>None</td>\n",
" <td>37.0</td>\n",
" <td>70.0</td>\n",
" <td>51.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-11</th>\n",
" <td>5712</td>\n",
" <td>None</td>\n",
" <td>31.0</td>\n",
" <td>62.0</td>\n",
" <td>53.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-12</th>\n",
" <td>3709</td>\n",
" <td>None</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-13</th>\n",
" <td>4219</td>\n",
" <td>None</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-14</th>\n",
" <td>5936</td>\n",
" <td>None</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-15</th>\n",
" <td>4634</td>\n",
" <td>None</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-16</th>\n",
" <td>7592</td>\n",
" <td>None</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2026-05-17</th>\n",
" <td>20264</td>\n",
" <td>None</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" total_steps sleep_score avg_stress hrv resting_hr\n",
"calendar_date \n",
"2026-05-04 4134 None 30.0 71.0 52.0\n",
"2026-05-05 5588 None 32.0 70.0 50.0\n",
"2026-05-06 6791 None 23.0 65.0 52.0\n",
"2026-05-07 8062 None 34.0 75.0 52.0\n",
"2026-05-08 2501 None 27.0 47.0 51.0\n",
"2026-05-09 7450 None 25.0 73.0 49.0\n",
"2026-05-10 30430 None 37.0 70.0 51.0\n",
"2026-05-11 5712 None 31.0 62.0 53.0\n",
"2026-05-12 3709 None NaN NaN NaN\n",
"2026-05-13 4219 None NaN NaN NaN\n",
"2026-05-14 5936 None NaN NaN NaN\n",
"2026-05-15 4634 None NaN NaN NaN\n",
"2026-05-16 7592 None NaN NaN NaN\n",
"2026-05-17 20264 None NaN NaN NaN"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
2026-05-18 12:53:24 -04:00
"source": [
"wellness = pd.read_sql(\n",
" \"\"\"\n",
" SELECT s.calendar_date,\n",
" s.total_steps,\n",
" sl.sleep_score,\n",
" st.avg_stress,\n",
" h.last_night_avg AS hrv,\n",
" rh.resting_hr\n",
" FROM daily_steps s\n",
" LEFT JOIN daily_sleep sl ON sl.calendar_date = s.calendar_date\n",
" LEFT JOIN daily_stress st ON st.calendar_date = s.calendar_date\n",
" LEFT JOIN daily_hrv h ON h.calendar_date = s.calendar_date\n",
" LEFT JOIN daily_resting_hr rh ON rh.calendar_date = s.calendar_date\n",
" ORDER BY s.calendar_date\n",
" \"\"\",\n",
" conn,\n",
" parse_dates=['calendar_date'],\n",
").set_index('calendar_date')\n",
"wellness.tail(14)"
]
},
{
"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": [
{
"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[6]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m 1\u001b[39m fig, axes = plt.subplots(\u001b[32m4\u001b[39m, \u001b[32m1\u001b[39m, figsize=(\u001b[32m12\u001b[39m, \u001b[32m8\u001b[39m), sharex=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m wellness[\u001b[33m'sleep_score'\u001b[39m].plot(ax=axes[\u001b[32m0\u001b[39m], title=\u001b[33m'Sleep score'\u001b[39m)\n\u001b[32m 3\u001b[39m wellness[\u001b[33m'resting_hr'\u001b[39m].plot(ax=axes[\u001b[32m1\u001b[39m], title=\u001b[33m'Resting HR'\u001b[39m)\n\u001b[32m 4\u001b[39m wellness[\u001b[33m'hrv'\u001b[39m].plot(ax=axes[\u001b[32m2\u001b[39m], title=\u001b[33m'HRV (last night avg)'\u001b[39m)\n\u001b[32m 5\u001b[39m wellness[\u001b[33m'avg_stress'\u001b[39m].plot(ax=axes[\u001b[32m3\u001b[39m], title=\u001b[33m'Avg stress'\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": "iVBORw0KGgoAAAANSUhEUgAAA+kAAAKZCAYAAADEaGoKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQAATNlJREFUeJzt3Q2QV/W9H/7PgrKLTKAgkQeDguIlteEh4Um8ZoyVipZJYe4kBdtckBFtuDdMCIkIVpYYbVFiHGIhISEh4GQSiHeUdKJDpFTMpKKMYEZJwUrEAAnLU2RXSAQv/P7z/f5nf5eVRVjC6tnd12vmBM453/P9nbM5Au/f96miVCqVAgAAAPjQtfuwbwAAAAD4/wnpAAAAUBBCOgAAABSEkA4AAAAFIaQDAABAQQjpAAAAUBBCOgAAABSEkA4AAAAFIaQDAABAQQjpAAAA0FJD+q9+9av47Gc/G717946KiopYvXr1Ga9Zv359fOpTn4rKysro379/LF++/JQyixcvjr59+0ZVVVWMHDkyNm7c2NRbAwAAgLYV0o8cORKDBw/Oofps7NixI8aOHRs33HBD/OY3v4kZM2bE1KlT45e//GW5zKpVq2LmzJkxb9682Lx5c65/zJgxsW/fvqbeHgAAALRYFaVSqXTOF1dUxJNPPhnjx48/bZm77747nnrqqdiyZUv52MSJE+PQoUOxZs2avJ9azocPHx6LFi3K+ydOnIg+ffrE9OnTY/bs2ed6ewAAANCiXNDcH7Bhw4YYPXp0g2OplTy1qCfHjh2LTZs2xZw5c8rn27Vrl69J1zbm6NGjeauXQv2f/vSnuPjii/MXBwAAANCcUnv322+/nYeCpwzbYkJ6TU1N9OjRo8GxtF9XVxd/+ctf4q233orjx483Wmbbtm2N1jl//vy47777mvW+AQAA4Ex27doVH/vYx6LFhPTmkFrd0xj2erW1tXHZZZflH07nzp0/1HsDAACg9aurq8vDtD/ykY+c13qbPaT37Nkz9u7d2+BY2k9humPHjtG+ffu8NVYmXduYNEt82t4r1SmkAwAA8EE530Oum32d9FGjRsW6desaHFu7dm0+nnTo0CGGDh3aoEwaY57268sAAABAW9DkkH748OG8lFra6pdYS7/fuXNnuSv6pEmTyuW/+MUvxhtvvBGzZs3KY8y/853vxM9+9rP4yle+Ui6Tuq4vXbo0VqxYEVu3bo1p06blpd6mTJlyfp4SAAAAWoAmd3d/6aWX8prn9erHhk+ePDmWL18ee/bsKQf2pF+/fnkJthTKv/3tb+cB9T/4wQ/yDO/1JkyYEPv374/q6uo80dyQIUPy8mzvnUwOAAAAWrO/ap30Ig3Y79KlS55Azph0AAAAWmoObfYx6QAAAMDZEdIBAACgIIR0AAAAKAghHQAAAApCSAcAAICCENIBAACgIIR0AAAAKAghHQAAAApCSAcAAICCENIBAACgIIR0AAAAKAghHQAAAApCSAcAAICCENIBAACgIIR0AAAAKAghHQAAAApCSAcAAICCENIBAACgIIR0AAAAKAghHQAAAApCSAcAAICCENIBAACgJYf0xYsXR9++faOqqipGjhwZGzduPG3Zz3zmM1FRUXHKNnbs2HKZ22677ZTzN99887k9EQAAALRQFzT1glWrVsXMmTNjyZIlOaAvXLgwxowZE6+99lpccsklp5R/4okn4tixY+X9gwcPxuDBg+Pzn/98g3IplP/oRz8q71dWVjb9aQAAAKAttaQ/8sgjcccdd8SUKVPi6quvzmH9oosuimXLljVavlu3btGzZ8/ytnbt2lz+vSE9hfKTy3Xt2vXcnwoAAABae0hPLeKbNm2K0aNH/0sF7drl/Q0bNpxVHT/84Q9j4sSJ0alTpwbH169fn1viBwwYENOmTcst7gAAANCWNKm7+4EDB+L48ePRo0ePBsfT/rZt2854fRq7vmXLlhzU39vV/e/+7u+iX79+8bvf/S7uueeeuOWWW3Lwb9++/Sn1HD16NG/16urqmvIYAAAA0DrGpP81UjgfOHBgjBgxosHx1LJeL50fNGhQXHnllbl1/cYbbzylnvnz58d99933gdwzAAAAFLK7e/fu3XPL9t69exscT/tpHPn7OXLkSKxcuTJuv/32M37OFVdckT9r+/btjZ6fM2dO1NbWlrddu3Y15TEAAACg5Yf0Dh06xNChQ2PdunXlYydOnMj7o0aNet9rH3/88dxF/Qtf+MIZP2f37t15THqvXr0aPZ8mmevcuXODDQAAANrc7O5p+bWlS5fGihUrYuvWrXmSt9RKnmZ7TyZNmpRbuhvr6j5+/Pi4+OKLGxw/fPhw3HXXXfHCCy/Em2++mQP/uHHjon///nlpNwAAAGgrmjwmfcKECbF///6orq6OmpqaGDJkSKxZs6Y8mdzOnTvzjO8nS2uo//rXv45nnnnmlPpS9/lXXnklh/5Dhw5F796946abbor777/fWukAAAC0KRWlUqkULVya3b1Lly55fLqu7wAAALTUHNrk7u4AAABA8xDSAQAAoCCEdAAAACgIIR0AAAAKQkgHAACAghDSAQAAoCCEdAAAACgIIR0AAAAKQkgHAACAghDSAQAAoCCEdAAAACgIIR0AAAAKQkgHAACAghDSAQAAoCCEdAAAACgIIR0AAAAKQkgHAACAghDSAQAAoCCEdAAAACgIIR0AAAAKQkgHAACAghDSAQAAoCCEdAAAAGjJIX3x4sXRt2/fqKqqipEjR8bGjRtPW3b58uVRUVHRYEvXnaxUKkV1dXX06tUrOnbsGKNHj47XX3/9XG4NAAAA2k5IX7VqVcycOTPmzZsXmzdvjsGDB8eYMWNi3759p72mc+fOsWfPnvL2+9//vsH5BQsWxKOPPhpLliyJF198MTp16pTrfOedd87tqQAAAKAthPRHHnkk7rjjjpgyZUpcffXVOVhfdNFFsWzZstNek1rPe/bsWd569OjRoBV94cKFce+998a4ceNi0KBB8dhjj8Uf//jHWL169bk/GQAAALTmkH7s2LHYtGlT7o5erqBdu7y/YcOG0153+PDhuPzyy6NPnz45iP/2t78tn9uxY0fU1NQ0qLNLly65G/3p6jx69GjU1dU12AAAAKBNhfQDBw7E8ePHG7SEJ2k/Be3GDBgwILey//znP48f//jHceLEibj22mtj9+7d+Xz9dU2pc/78+TnI128p/AMAAEBL1+yzu48aNSomTZoUQ4YMieuvvz6eeOKJ+OhHPxrf+973zrnOOXPmRG1tbXnbtWvXeb1nAAAAKHxI7969e7Rv3z727t3b4HjaT2PNz8aFF14Yn/zkJ2P79u15v/66ptRZWVmZJ6M7eQMAAIA2FdI7dOgQQ4cOjXXr1pWPpe7raT+1mJ+N1F3+1VdfzcutJf369cth/OQ60xjzNMv72dYJAAAArcEFTb0gLb82efLkGDZsWIwYMSLPzH7kyJE823uSurZfeumledx48o1vfCOuueaa6N+/fxw6dCi++c1v5iXYpk6dWp75fcaMGfHAAw/EVVddlUP73Llzo3fv3jF+/Pjz/bwAAADQekL6hAkTYv/+/VFdXZ0ndktjzdesWVOe+G3nzp15xvd6b731Vl6yLZXt2rVrbol//vnn8/Jt9WbNmpWD/p133pmD/HXXXZfrrKqqOl/PCQAAAIVXUUoLlbdwqXt8muU9TSJnfDoAAAAtNYc2++zuAAAAwNkR0gEAAKAghHQAAAAoCCEdAAAACkJIBwAAgIIQ0gEAAKAghHQAAAAoCCEdAAAACkJIBwAAgIIQ0gEAAKAghHQAAAAoCCEdAAAACkJIBwAAgIIQ0gEAAKAghHQAAAAoCCEdAAAACkJIBwAAgIIQ0gEAAKAghHQAAAAoCCEdAAAACkJIBwAAgIIQ0gEAAKAlh/TFixdH3759o6qqKkaOHBkbN248bdmlS5fGpz/96ejatWveRo8efUr52267LSoqKhpsN99887ncGgAAALSdkL5q1aqYOXNmzJs3LzZv3hyDBw+OMWPGxL59+xotv379+rj11lvj2WefjQ0bNkSfPn3ippt
"text/plain": [
"<Figure size 1200x800 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
2026-05-18 12:53:24 -04:00
"source": [
"fig, axes = plt.subplots(4, 1, figsize=(12, 8), sharex=True)\n",
"wellness['sleep_score'].plot(ax=axes[0], title='Sleep score')\n",
"wellness['resting_hr'].plot(ax=axes[1], title='Resting HR')\n",
"wellness['hrv'].plot(ax=axes[2], title='HRV (last night avg)')\n",
"wellness['avg_stress'].plot(ax=axes[3], title='Avg stress')\n",
"plt.tight_layout()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Querying the raw JSON\n",
"\n",
"Every table has a `raw` column with the full Garmin response. Use SQLite's JSON1 functions, or load and `pd.json_normalize`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import json\n",
"raw_rows = pd.read_sql('SELECT activity_id, raw FROM activities LIMIT 5', conn)\n",
"expanded = pd.json_normalize([json.loads(r) for r in raw_rows['raw']])\n",
"print(f'{len(expanded.columns)} columns in the raw activity payload')\n",
"expanded.columns.tolist()[:30]"
]
}
],
"metadata": {
"kernelspec": {
2026-06-12 06:25:45 -04:00
"display_name": "garmin (3.13.13.final.0)",
2026-05-18 12:53:24 -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-18 12:53:24 -04:00
}
},
"nbformat": 4,
"nbformat_minor": 5
2026-06-12 06:25:45 -04:00
}