Files
openrun/notebooks/01_overview.ipynb

632 lines
131 KiB
Plaintext
Raw Normal View History

2026-05-18 12:53:24 -04:00
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 01 — Overview\n",
"\n",
"Sanity-check the synced data: what's there, how complete it is, and what you've been up to recently.\n",
"\n",
"**Kernel:** pick the `.venv` from `garmin/` (top-right of this notebook)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"sys.path.insert(0, '..') # so we can import analysis.py from the project root\n",
"\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"from analysis import open_conn, load_activities, load_wellness\n",
"\n",
"pd.options.display.float_format = '{:.2f}'.format\n",
"conn = open_conn()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Data coverage"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"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>table</th>\n",
" <th>rows</th>\n",
" <th>from</th>\n",
" <th>to</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>activities</td>\n",
" <td>360</td>\n",
" <td>2022-04-09 08:30:18</td>\n",
" <td>2026-05-10 10:43:58</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>activity_fit_files</td>\n",
" <td>0</td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>activity_splits</td>\n",
" <td>2285</td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>daily_body_battery</td>\n",
" <td>365</td>\n",
" <td>2025-05-13</td>\n",
" <td>2026-05-12</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>daily_hrv</td>\n",
" <td>363</td>\n",
" <td>2025-05-13</td>\n",
" <td>2026-05-12</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>daily_intensity_minutes</td>\n",
" <td>365</td>\n",
" <td>2025-05-13</td>\n",
" <td>2026-05-12</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>daily_resting_hr</td>\n",
" <td>363</td>\n",
" <td>2025-05-13</td>\n",
" <td>2026-05-11</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>daily_sleep</td>\n",
" <td>365</td>\n",
" <td>2025-05-13</td>\n",
" <td>2026-05-12</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>daily_steps</td>\n",
" <td>364</td>\n",
" <td>2025-05-13</td>\n",
" <td>2026-05-11</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>daily_stress</td>\n",
" <td>365</td>\n",
" <td>2025-05-13</td>\n",
" <td>2026-05-12</td>\n",
" </tr>\n",
" <tr>\n",
" <th>10</th>\n",
" <td>sync_state</td>\n",
" <td>1</td>\n",
" <td></td>\n",
" <td></td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" table rows from to\n",
"0 activities 360 2022-04-09 08:30:18 2026-05-10 10:43:58\n",
"1 activity_fit_files 0 \n",
"2 activity_splits 2285 \n",
"3 daily_body_battery 365 2025-05-13 2026-05-12\n",
"4 daily_hrv 363 2025-05-13 2026-05-12\n",
"5 daily_intensity_minutes 365 2025-05-13 2026-05-12\n",
"6 daily_resting_hr 363 2025-05-13 2026-05-11\n",
"7 daily_sleep 365 2025-05-13 2026-05-12\n",
"8 daily_steps 364 2025-05-13 2026-05-11\n",
"9 daily_stress 365 2025-05-13 2026-05-12\n",
"10 sync_state 1 "
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"summary = []\n",
"for (table,) in conn.execute(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\"):\n",
" n = conn.execute(f'SELECT COUNT(*) FROM \"{table}\"').fetchone()[0]\n",
" date_col = 'start_time_local' if table == 'activities' else ('calendar_date' if table.startswith('daily_') else None)\n",
" if date_col and n:\n",
" mn, mx = conn.execute(f'SELECT MIN({date_col}), MAX({date_col}) FROM {table}').fetchone()\n",
" else:\n",
" mn = mx = ''\n",
" summary.append({'table': table, 'rows': n, 'from': mn, 'to': mx})\n",
"pd.DataFrame(summary)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Activity types"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"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>count</th>\n",
" </tr>\n",
" <tr>\n",
" <th>activity_type</th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>running</th>\n",
" <td>331</td>\n",
" </tr>\n",
" <tr>\n",
" <th>trail_running</th>\n",
" <td>15</td>\n",
" </tr>\n",
" <tr>\n",
" <th>cycling</th>\n",
" <td>6</td>\n",
" </tr>\n",
" <tr>\n",
" <th>multi_sport</th>\n",
" <td>4</td>\n",
" </tr>\n",
" <tr>\n",
" <th>open_water_swimming</th>\n",
" <td>2</td>\n",
" </tr>\n",
" <tr>\n",
" <th>assistance</th>\n",
" <td>1</td>\n",
" </tr>\n",
" <tr>\n",
" <th>mountain_biking</th>\n",
" <td>1</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" count\n",
"activity_type \n",
"running 331\n",
"trail_running 15\n",
"cycling 6\n",
"multi_sport 4\n",
"open_water_swimming 2\n",
"assistance 1\n",
"mountain_biking 1"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"activities = load_activities(conn)\n",
"activities['activity_type'].value_counts().to_frame('count')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Last 10 activities"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"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>start_time_local</th>\n",
" <th>activity_type</th>\n",
" <th>activity_name</th>\n",
" <th>distance_km</th>\n",
" <th>duration_min</th>\n",
" <th>pace_min_per_km</th>\n",
" <th>avg_hr</th>\n",
" <th>training_load</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>2026-05-10 10:43:58</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>17.20</td>\n",
" <td>145.70</td>\n",
" <td>8.41</td>\n",
" <td>156.00</td>\n",
" <td>126.19</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>2026-05-07 20:17:15</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>3.65</td>\n",
" <td>29.25</td>\n",
" <td>7.71</td>\n",
" <td>170.00</td>\n",
" <td>63.82</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>2026-05-03 17:03:51</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>9.32</td>\n",
" <td>79.10</td>\n",
" <td>8.33</td>\n",
" <td>155.00</td>\n",
" <td>81.75</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>2026-05-02 14:10:35</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>12.42</td>\n",
" <td>95.84</td>\n",
" <td>7.67</td>\n",
" <td>148.00</td>\n",
" <td>102.83</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>2026-04-30 19:41:35</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>4.83</td>\n",
" <td>40.60</td>\n",
" <td>8.31</td>\n",
" <td>158.00</td>\n",
" <td>58.67</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>2026-04-25 08:21:12</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>15.94</td>\n",
" <td>130.58</td>\n",
" <td>8.02</td>\n",
" <td>149.00</td>\n",
" <td>102.70</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>2026-04-23 18:34:03</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>4.49</td>\n",
" <td>36.66</td>\n",
" <td>7.96</td>\n",
" <td>148.00</td>\n",
" <td>51.16</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>2026-04-22 17:25:33</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>5.55</td>\n",
" <td>45.77</td>\n",
" <td>8.23</td>\n",
" <td>167.00</td>\n",
" <td>75.78</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>2026-04-22 16:48:43</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>3.75</td>\n",
" <td>29.48</td>\n",
" <td>7.69</td>\n",
" <td>156.00</td>\n",
" <td>52.06</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>2026-04-12 13:25:00</td>\n",
" <td>running</td>\n",
" <td>Milford Running</td>\n",
" <td>7.19</td>\n",
" <td>57.90</td>\n",
" <td>7.91</td>\n",
" <td>163.00</td>\n",
" <td>81.90</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" start_time_local activity_type activity_name distance_km \\\n",
"0 2026-05-10 10:43:58 running Milford Running 17.20 \n",
"1 2026-05-07 20:17:15 running Milford Running 3.65 \n",
"2 2026-05-03 17:03:51 running Milford Running 9.32 \n",
"3 2026-05-02 14:10:35 running Milford Running 12.42 \n",
"4 2026-04-30 19:41:35 running Milford Running 4.83 \n",
"5 2026-04-25 08:21:12 running Milford Running 15.94 \n",
"6 2026-04-23 18:34:03 running Milford Running 4.49 \n",
"7 2026-04-22 17:25:33 running Milford Running 5.55 \n",
"8 2026-04-22 16:48:43 running Milford Running 3.75 \n",
"9 2026-04-12 13:25:00 running Milford Running 7.19 \n",
"\n",
" duration_min pace_min_per_km avg_hr training_load \n",
"0 145.70 8.41 156.00 126.19 \n",
"1 29.25 7.71 170.00 63.82 \n",
"2 79.10 8.33 155.00 81.75 \n",
"3 95.84 7.67 148.00 102.83 \n",
"4 40.60 8.31 158.00 58.67 \n",
"5 130.58 8.02 149.00 102.70 \n",
"6 36.66 7.96 148.00 51.16 \n",
"7 45.77 8.23 167.00 75.78 \n",
"8 29.48 7.69 156.00 52.06 \n",
"9 57.90 7.91 163.00 81.90 "
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"cols = ['start_time_local', 'activity_type', 'activity_name', 'distance_km', 'duration_min', 'pace_min_per_km', 'avg_hr', 'training_load']\n",
"activities[cols].tail(10).iloc[::-1].reset_index(drop=True)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "20e72ec0",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Monthly running mileage, year-over-year"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1100x400 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"runs = activities[activities['activity_type'].str.contains('running', case=False, na=False)].copy()\n",
"monthly = (runs.assign(month_no=runs['start_time_local'].dt.month)\n",
" .groupby(['year', 'month_no'])['distance_km'].sum()\n",
" .unstack('year'))\n",
"\n",
"fig, ax = plt.subplots(figsize=(11, 4))\n",
"monthly.plot(ax=ax, marker='o')\n",
"ax.set_xticks(range(1, 13))\n",
"ax.set_xticklabels(['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'])\n",
"ax.set_ylabel('km')\n",
"ax.set_xlabel('')\n",
"ax.set_title('Monthly running km — year over year')\n",
"ax.legend(title='Year')\n",
"ax.grid(alpha=0.3)\n",
"plt.tight_layout()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Wellness completeness (last 90 days)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"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>% missing</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>total_steps</th>\n",
" <td>0.00</td>\n",
" </tr>\n",
" <tr>\n",
" <th>sleep_score</th>\n",
" <td>7.78</td>\n",
" </tr>\n",
" <tr>\n",
" <th>avg_stress</th>\n",
" <td>0.00</td>\n",
" </tr>\n",
" <tr>\n",
" <th>hrv_last_night</th>\n",
" <td>7.78</td>\n",
" </tr>\n",
" <tr>\n",
" <th>resting_hr</th>\n",
" <td>1.11</td>\n",
" </tr>\n",
" <tr>\n",
" <th>bb_charged</th>\n",
" <td>1.11</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" % missing\n",
"total_steps 0.00\n",
"sleep_score 7.78\n",
"avg_stress 0.00\n",
"hrv_last_night 7.78\n",
"resting_hr 1.11\n",
"bb_charged 1.11"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"w = load_wellness(conn).tail(90)\n",
"missing = w[['total_steps','sleep_score','avg_stress','hrv_last_night','resting_hr','bb_charged']].isna().mean().to_frame('% missing') * 100\n",
"missing"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "garmin (.venv)",
"language": "python",
"name": "garmin"
},
"language_info": {
"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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}