2025-11-08 19:49:57 +01:00
{
"cells": [
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 1,
2025-11-08 19:49:57 +01:00
"id": "63f43af5",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import seaborn as sns\n",
"import matplotlib.pyplot as plt\n",
2025-11-17 17:15:44 +01:00
"import numpy as np\n",
"import os"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 2,
2025-11-17 17:15:44 +01:00
"id": "97da3d1c",
"metadata": {},
"outputs": [],
"source": [
"base_dir = os.path.dirname(os.path.abspath('.'))"
2025-11-08 19:49:57 +01:00
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 3,
2025-11-08 19:49:57 +01:00
"id": "b0ee2af1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Smoothed columns created:\n",
"['VO2(ml/min)_smoothed', 'VCO2(ml/min)_smoothed', 'HR(bpm)_smoothed', 'VT(l)_smoothed', 'BF(bpm)_smoothed', 'VE(l/min)_smoothed', 'VO2 Pulse_smoothed', 'VO2 Breath_smoothed', 'CHO_smoothed', 'FAT_smoothed']\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
2025-11-24 17:52:56 +01:00
"/tmp/ipykernel_680949/3076306744.py:3: FutureWarning: errors='ignore' is deprecated and will raise in a future version. Use to_numeric without passing `errors` and catch exceptions explicitly instead\n",
2025-11-08 19:49:57 +01:00
" df = df.apply(pd.to_numeric, errors='ignore')\n"
]
}
],
"source": [
2025-11-17 17:15:44 +01:00
"df = pd.read_csv(f'{base_dir}/data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
2025-11-08 19:49:57 +01:00
"# Convert all columns to numeric where possible, coercing errors to NaN\n",
"df = df.apply(pd.to_numeric, errors='ignore')\n",
"df['VO2 Pulse'] = df['VO2(ml/min)'] / df['HR(bpm)'] # VO2 Pulse in mL/beat\n",
"df['VO2 Breath'] = df['VO2(ml/min)'] / df['BF(bpm)'] # VO2 per Breath in mL/breath\n",
"df['CHO'] = df['EE(kcal/min)'] * df['CARBS(%)']/100\n",
"df['FAT'] = df['EE(kcal/min)'] * df['FAT(%)']/100\n",
"# Smooth key columns using rolling window\n",
"window_size = 10\n",
"\n",
"# List of columns to smooth\n",
"columns_to_smooth = ['VO2(ml/min)', 'VCO2(ml/min)', 'HR(bpm)', 'VT(l)', 'BF(bpm)', 'VE(l/min)', 'VO2 Pulse', 'VO2 Breath', 'CHO', 'FAT']\n",
"\n",
"# Apply smoothing to each column\n",
"for col in columns_to_smooth:\n",
" if col in df.columns:\n",
" df[f'{col}_smoothed'] = df[col].rolling(window=window_size, min_periods=1).mean()\n",
"\n",
"print(\"Smoothed columns created:\")\n",
"print([col for col in df.columns if '_smoothed' in col])"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 4,
2025-11-17 17:15:44 +01:00
"id": "99116a35",
"metadata": {},
"outputs": [],
"source": [
"df_2 = pd.read_excel(f'{base_dir}/data/SECA body comp for all patients.xlsx')\n",
"spirometry_data = pd.read_csv(f'{base_dir}/data/spirometry_data.csv')\n",
2025-11-24 17:52:56 +01:00
"oxygenation = pd.read_csv(f'{base_dir}/data/Keirstyn Train Red NIRS Muscle Oxygen.csv')\n",
"oxygenation_2 = pd.read_csv(f'{base_dir}/data/muscle_oxygenation.csv', skiprows=445)"
2025-11-17 17:15:44 +01:00
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 5,
2025-11-08 19:49:57 +01:00
"id": "fbd292c3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"22.369999999999997\n"
]
}
],
"source": [
"print(df['VO2 Pulse'].max())"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 6,
2025-11-17 17:15:44 +01:00
"id": "4c439b2c",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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0e+BgoaIiIhEqnpNnfriiy/Yv39/ldsnT54cbLLu3r07I0eOpEOHDqSlpVFSUsK6det47733KC0tZcaMGXzwwQdcfPHFnHnmmTRr1oznnnuOzZs3AzB06FAmT54cfO7mzZvXqsaUlBSuvPJKnnnmGRYvXsyiRYvo3r0777zzDgDjxo1j8ODBfPTRR9U+PjY2lpEjR3LiiSfSokULmjVrRk5ODjNnzmTJkiUYY7j99tuZPHky8fHxHDp0iNdffz34+AsvvJCTTz6ZnJwctm3bxpw5cyo9/6uvvkp+fj4AHTt25PLLLycxMZGdO3eyevVqFi1aVOPvdd68ecGPa7sXyrp164IfJyUl0bdv32Pe/4cffghOHwK49dZbK01LO56FCxcGP965cyd//OMfg9cLCgr4+OOPmTZtGm+88QYXX3xxjZ+33LRp05gwYQJjx45lwYIFzJw5E4DNmzdz991389JLLwHO9K1yw4YN4/zzz6e0tJQdO3bwzTffsHbt2mqff9iwYSxbtgxwfu71XQBBRERExGvqFTTeeeed4AF7RUOHDqVTp04kJiayadMm9u3bx6JFi9i1axf5+fmcfPLJrFq1itWrVwPwv//9j4svvpjRo0czevRoPv3002DQ6N+/P3fccUd9yuTXv/41zz77LMYYnnrqKfr27RtsiL7llltYuXLlUR973XXXcd1117Fy5UpWrVpFZmYmUVFRTJo0iSVLlgBw4MABli5dytixYykpKQkuZZqcnMybb75Z6QDcGFNpZabCwsLgxzfffDP33HNPpa9f3v9QE5s2bQp+3KlTpxo/bt68ebz44ovB67fffjuJiYnHfMzjjz8eXNkqOTmZG264ocZfD2DPnj2VrsfGxnLdddcRHx/Piy++SE5ODqWlpfziF79g4sSJtGjRolbPf+aZZ/LFF19gWRbGGM4+++zgClZvvPEGTz/9NAkJCZV+/k899RQjR46s9Dxbt26tNkB17Ngx+HHFn7uIiIiIOBp11amCggJuuukm/vOf/wQPSqvT2Cs/9enTh7PPPpvPP/+c999/n9TUVAC6dOnCpEmTjhk0li9fzhVXXMGaNWuO+TXKv4e0tDT69+/PmjVrOHjwIN26dWPYsGH06tWLgQMHMnHiRLp16xZ83NixY4NN6b///e+ZOnUqffv2pU+fPowYMYKxY8fWeAPFjIyM4Mc1HfmZOnUql156KSUlJYDT6PyHP/zhmI/Zt28f//nPf4LXr7/+epKTk2v09coVFxdXuv7YY4/x61//GnB+Jj/+8Y8ByM3NZerUqVx99dW1ev7LL788uAKWZVlcdtllwaBRXFzMqlWrgj/f8v//M844g1GjRtGrVy9OOOEETj31VAYOHFjt81cMPhV/7iIiIiLiqFfQePnll485ZeS3v/0tr7zyynGfp7rlVhvaLbfcwueff05JSUnwwPDmm28+5kF8QUEB559/fpWz79Wp+D28+eabXHLJJXz//ffs3r2bjz/+OPg5n8/HrbfeyhNPPAHARRddxB133ME//vEPioqK+Prrr/n666+D9+/SpQvTpk2jf//+tf6ej+fJJ5/kjjvuCIbAa665hhdffDG42tTRPP3008GRgOjoaH7zm9/U+muXh71yFZfTPXJp3bqMGLRu3brS9TZt2lS6np2dDTirZm3evJn
2025-11-08 19:49:57 +01:00
"text/plain": [
2025-11-17 17:15:44 +01:00
"<Figure size 800x800 with 1 Axes>"
2025-11-08 19:49:57 +01:00
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
2025-11-17 17:15:44 +01:00
"# Filter df_2 for Keirstyn Moran\n",
"keirstyn_data = df_2[df_2['LastName'].str.contains('Moran', case=False, na=False)]\n",
"# Get the fat mass percentage for Keirstyn\n",
"fat_percentage = keirstyn_data['Adult_FMP'].iloc[0]\n",
"weight_kg = keirstyn_data['Weight'].iloc[0]\n",
"age = keirstyn_data['Age'].iloc[0]\n",
"gender = keirstyn_data['Gender'].iloc[0]\n",
"lean_percentage = 100 - fat_percentage\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Create donut chart\n",
"fat_mass_lbs = 27.6\n",
"lean_mass_lbs = 95.4\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Calculate percentages from the provided weights\n",
"total_weight = fat_mass_lbs + lean_mass_lbs\n",
"fat_percentage = (fat_mass_lbs / total_weight) * 100\n",
"lean_percentage = (lean_mass_lbs / total_weight) * 100\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Data for the chart\n",
"sizes = [fat_percentage, lean_percentage]\n",
"colors = ['#fde3ac', '#ff9966'] # Light yellow/tan and orange from the image\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"plt.figure(figsize=(8, 8))\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Create the donut chart without labels first\n",
"wedges, texts, autotexts = plt.pie(sizes,\n",
" autopct='', # Remove auto percentages\n",
" startangle=90,\n",
" wedgeprops=dict(width=0.5, edgecolor='w'),\n",
" colors=colors,\n",
" labels=['', '']) # Remove default labels\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Add custom text annotations positioned manually\n",
"plt.text(-1, 1, 'Fat Mass (27.6lbs)\\n22.4%', \n",
" fontsize=14, fontweight='bold', ha='center', va='center',\n",
" bbox=dict(boxstyle=\"round,pad=0.3\", facecolor='white', alpha=0.8))\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"plt.text(1, -1, 'Lean Mass (95.4lbs)\\n77.6%', \n",
" fontsize=14, fontweight='bold', ha='center', va='center',\n",
" bbox=dict(boxstyle=\"round,pad=0.3\", facecolor='white', alpha=0.8))\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Set the title\n",
"plt.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle\n",
"plt.savefig(f'{base_dir}/graphs/body_composition_chart.png', bbox_inches='tight', dpi=600)\n",
2025-11-08 19:49:57 +01:00
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 7,
2025-11-17 17:15:44 +01:00
"id": "a565f1b3",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
2025-11-08 19:49:57 +01:00
"text/plain": [
2025-11-17 17:15:44 +01:00
"<Figure size 1000x200 with 1 Axes>"
2025-11-08 19:49:57 +01:00
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
2025-11-17 17:15:44 +01:00
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Set a common style\n",
"sns.set_theme(style=\"whitegrid\")\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Define the segments with muted colors\n",
"segments = [\n",
" ('#F8A8A8', 0, 15), # Muted Red/Salmon: 0% to 15%\n",
" ('#FFEECC', 15, 5), # Pale Yellow/Cream: 15% to 20%\n",
" ('#D0F0C0', 20, 15), # Pale Green/Mint: 20% to 35%\n",
" ('#FFEECC', 35, 5), # Pale Yellow/Cream: 35% to 40%\n",
" ('#F8A8A8', 40, 10) # Muted Red/Salmon: 40% to 50%\n",
"]\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"target_value = 22.4\n",
"demographic = \"20-39\\n(F)\"\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"fig, ax = plt.subplots(figsize=(10, 2))\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Create the Segmented Bar\n",
"for color, start, length in segments:\n",
" ax.barh(y=0, width=length, left=start, height=1, color=color, edgecolor='black', linewidth=0.5)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Add the Indicator (Triangle)\n",
"ax.plot(target_value, 1.05, marker='v', color='black', markersize=10, clip_on=False, transform=ax.get_xaxis_transform())\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Set Axis Properties and Labels\n",
"ax.set_xlim(0, 50)\n",
"ax.set_xticks(range(0, 51, 5))\n",
"ax.set_yticks([])\n",
"ax.text(-0.05, 0, demographic, transform=ax.get_yaxis_transform(), va='center', ha='right', fontsize=12)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"ax.set_xlim(0, 50)\n",
"ticks = range(0, 51, 5)\n",
"ax.set_xticks(ticks)\n",
"labels = [f\"{t}%\" for t in ticks]\n",
"ax.set_xticklabels(labels)\n",
"# Clean up spines and add small ticks\n",
"ax.spines['right'].set_visible(False)\n",
"ax.spines['top'].set_visible(False)\n",
"ax.spines['left'].set_visible(False)\n",
"ax.spines['bottom'].set_visible(True)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"for x in range(0, 51, 5):\n",
" ax.plot([x, x], [-0.05, -0.01], color='black', transform=ax.get_xaxis_transform(), clip_on=False)\n",
2025-11-08 19:49:57 +01:00
"\n",
"plt.tight_layout()\n",
2025-11-17 17:15:44 +01:00
"plt.savefig(f'{base_dir}/graphs/body_fat_percent_chart.png', bbox_inches='tight', dpi=300)\n",
"plt.show() # This is where the file is saved and displayed above."
2025-11-08 19:49:57 +01:00
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 8,
2025-11-17 17:15:44 +01:00
"id": "470e871e",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
2025-11-17 17:15:44 +01:00
{
"name": "stdout",
"output_type": "stream",
"text": [
2025-11-21 09:23:13 +01:00
"Displaying Metabolism Chart...\n",
"Estimated RMR from data: 1385 kcal/day\n"
2025-11-17 17:15:44 +01:00
]
},
2025-11-08 19:49:57 +01:00
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
2025-11-08 19:49:57 +01:00
"text/plain": [
2025-11-17 17:15:44 +01:00
"<Figure size 1000x250 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
2025-11-21 09:23:13 +01:00
"Displaying Fuel Source Chart...\n",
"Resting phase fuel mix: Fats 32.9%, Carbs 67.1%\n"
2025-11-17 17:15:44 +01:00
]
},
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
2025-11-17 17:15:44 +01:00
"text/plain": [
"<Figure size 1000x250 with 1 Axes>"
2025-11-08 19:49:57 +01:00
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
2025-11-17 17:15:44 +01:00
"import matplotlib.pyplot as plt\n",
"import matplotlib.patches as patches\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"def plot_metabolism_chart():\n",
" \"\"\"\n",
" Generates and displays the 'Slow vs Fast Metabolism' chart.\n",
" \"\"\"\n",
" fig, ax = plt.subplots(figsize=(10, 2.5))\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Chart data and positions ---\n",
" categories = ['Very Slow', 'Slow', 'Average', 'Fast', 'Very Fast']\n",
" positions = [1500, 3000, 4500, 6000, 7500]\n",
2025-11-18 16:57:39 +01:00
" # Step 1: Filter resting phase (usually lowest VO2 or MET values)\n",
" rest_phase = df[df['MET'] <= 1.1] # assuming <1.1 MET means rest\n",
"\n",
" # Step 2: Compute resting metabolic rate\n",
" rmr = rest_phase['EE(kcal/day)'].mean()\n",
"\n",
" print(f\"Estimated RMR from data: {rmr:.0f} kcal/day\")\n",
" kcal_value = rmr\n",
2025-11-17 17:15:44 +01:00
" # Position the indicator and highlight based on the kcal value\n",
" # For this example, we'll place it in the 'Very Slow' section.\n",
" indicator_pos = kcal_value\n",
" highlight_end = kcal_value\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Main Bar (Background) ---\n",
" # Create a rounded rectangle for the main bar\n",
" main_bar = patches.FancyBboxPatch(\n",
" (0, 0.4), 9000, 0.2,\n",
" boxstyle=\"round,pad=0,rounding_size=0.1\",\n",
" ec=\"none\", fc=\"#E0E0E0\",\n",
" )\n",
" ax.add_patch(main_bar)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Highlighted Bar ---\n",
" # Create a rounded rectangle for the highlighted section\n",
" highlight_bar = patches.FancyBboxPatch(\n",
" (0, 0.4), highlight_end, 0.2,\n",
" boxstyle=\"round,pad=0,rounding_size=0.1\",\n",
" ec=\"none\", fc=\"#B2FFC8\",\n",
" )\n",
" ax.add_patch(highlight_bar)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Text and Labels ---\n",
" # Add the kcal text inside the highlighted bar\n",
" ax.text(highlight_end / 2, 0.5, f'{kcal_value}kCals', \n",
" ha='center', va='center', color='#006400', fontsize=14, weight='bold')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Indicator Triangle ---\n",
" ax.plot(indicator_pos, 0.65, 'v', markersize=15, color='#606060', clip_on=False)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Ticks and Labels ---\n",
" # Add category labels and tick marks below the bar\n",
" for pos, label in zip(positions, categories):\n",
" ax.text(pos, 0.15, label, ha='center', va='center', fontsize=12, color='#333333')\n",
" ax.plot([pos, pos], [0.35, 0.39], color='grey', lw=5)\n",
2025-11-08 19:49:57 +01:00
"\n",
"\n",
2025-11-17 17:15:44 +01:00
" # --- Chart Styling ---\n",
" ax.set_title('Slow vs Fast Metabolism', fontsize=18, weight='bold', loc='left')\n",
" ax.set_xlim(0, 9000) # Add padding before and after\n",
" ax.set_ylim(0, 1)\n",
2025-11-08 19:49:57 +01:00
" \n",
2025-11-17 17:15:44 +01:00
" # Hide axes and spines for a cleaner look\n",
" ax.axis('off')\n",
2025-11-08 19:49:57 +01:00
" \n",
" plt.tight_layout()\n",
2025-11-17 17:15:44 +01:00
" plt.savefig(f'{base_dir}/graphs/metabolism_chart.png', bbox_inches='tight', dpi=300)\n",
2025-11-08 19:49:57 +01:00
" plt.show()\n",
"\n",
"\n",
2025-11-17 17:15:44 +01:00
"def plot_fuel_source_chart():\n",
" \"\"\"\n",
" Generates and displays the 'Fuel Source' chart.\n",
" \"\"\"\n",
" fig, ax = plt.subplots(figsize=(10, 2.5))\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Chart data and positions ---\n",
2025-11-18 16:57:39 +01:00
" rest_phase = df[df['RER'] == 0.9] # filter rest data\n",
" fat_rest = rest_phase['FAT(%)'].mean()\n",
" carb_rest = rest_phase['CARBS(%)'].mean()\n",
"\n",
" print(f\"Resting phase fuel mix: Fats {fat_rest:.1f}%, Carbs {carb_rest:.1f}%\")\n",
"\n",
" fat_percentage = fat_rest\n",
2025-11-17 17:15:44 +01:00
" carb_percentage = 100 - fat_percentage\n",
" optimal_point = 75\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Main Bars (Fats and Carbs) ---\n",
" # Fats bar (yellow)\n",
" fats_bar = patches.FancyBboxPatch(\n",
" (0, 0.4), fat_percentage, 0.2,\n",
" boxstyle=\"round,pad=0,rounding_size=0.1\",\n",
" ec=\"none\", fc=\"#FEEAAB\",\n",
" )\n",
" ax.add_patch(fats_bar)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # Carbs bar (blue) - starts where the fats bar ends\n",
" carbs_bar = patches.FancyBboxPatch(\n",
" (fat_percentage, 0.4), carb_percentage, 0.2,\n",
" boxstyle=\"round,pad=0,rounding_size=0.1\",\n",
" ec=\"none\", fc=\"#A7F5FF\",\n",
" )\n",
" ax.add_patch(carbs_bar)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Text and Labels ---\n",
" # Add percentage labels inside the bars\n",
" ax.text(fat_percentage / 2, 0.5, f'Fats\\n{fat_percentage}%', \n",
" ha='center', va='center', color='#333333', fontsize=12, weight='bold')\n",
" ax.text(fat_percentage + carb_percentage / 2, 0.5, f'Carbs\\n{100-fat_percentage}%', \n",
" ha='center', va='center', color='#333333', fontsize=12, weight='bold')\n",
" \n",
" # Add 'Optimal' label\n",
" ax.text(optimal_point, 0.75, 'Optimal', ha='center', va='center', fontsize=12)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Indicator Triangle ---\n",
" ax.plot(fat_percentage, 0.65, 'v', markersize=15, color='#606060', clip_on=False)\n",
" \n",
" # --- Ticks and Labels ---\n",
" positions = [0, 25, 50, 75, 100]\n",
" for pos in positions:\n",
" ax.text(pos, 0.15, str(pos), ha='center', va='center', fontsize=12, color='#333333')\n",
" ax.plot([pos, pos], [0.35, 0.39], color='grey', lw=5)\n",
" \n",
" # Add a special tick for the 'Optimal' point\n",
" ax.plot([optimal_point, optimal_point], [0.6, 0.7], color='black', lw=2)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # --- Chart Styling ---\n",
" ax.set_title('Fuel Source', fontsize=18, weight='bold', loc='left')\n",
" # ax.set_xlim(-5, 105) # Add padding\n",
" ax.set_ylim(0, 1)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" # Hide axes and spines\n",
" ax.axis('off')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
" plt.tight_layout()\n",
" plt.savefig(f'{base_dir}/graphs/fuel_source_chart.png', bbox_inches='tight', dpi=300)\n",
" plt.show()\n",
2025-11-08 19:49:57 +01:00
"\n",
"\n",
2025-11-17 17:15:44 +01:00
"if __name__ == '__main__':\n",
" # Call the functions to display each plot\n",
" print(\"Displaying Metabolism Chart...\")\n",
" plot_metabolism_chart()\n",
" \n",
" print(\"\\nDisplaying Fuel Source Chart...\")\n",
" plot_fuel_source_chart()\n"
2025-11-08 19:49:57 +01:00
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 9,
2025-11-17 17:15:44 +01:00
"id": "0ab6f0b0",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA/YAAAFdCAYAAACpXPZQAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAeJJJREFUeJzt3Xd8FVX+//HXbek9oYQiRREioEDoSFXBglJWVqwgCtiwgKi4lrWLArKiKLiKICAiIk2FlS9NEOklKsUFQTqEhCQ39d6b+f2RX2YJBEi/Ke/n45EHk5kzcz735N6Qz5wz51gMwzAQERERERERkQrJ6u0ARERERERERKTolNiLiIiIiIiIVGBK7EVEREREREQqMCX2IiIiIiIiIhWYEnsRERERERGRCkyJvYiIiIiIiEgFpsReREREREREpAJTYi8iIiIiIiJSgSmxFxEREREREanAlNiLiIiIiIiIVGBK7EVEREREREQqMCX2IiIiIiIiIhWYEnsRERERERGRCkyJvYiIiJd8/vnn3Hvvveb3LVu2ZM+ePWVS9/Lly+nRo0eZ1CUiIiKlS4m9iFQZ9957L59//nmB9+cei4mJYffu3ea+5ORkGjduzOHDh0spUilP7r33Xpo1a0bLli1p27Yt9957L7/++mup1LVt2zYaN258yXKTJk3ikUceKZUYREREpOJRYi8icgkhISFMmDDB22GIFz399NNs27aNn376iZiYmHyTapfL5YXIRERERMDu7QBERMq7u+66iy+++IJNmzbRpk0bb4cjXuTr68vtt9/O9OnTeeihh4iIiCA1NZWffvqJp556ioEDBzJ58mQWL15MSkoKLVu25JVXXqFGjRoA/PHHH/zjH//gjz/+oFmzZjRv3jzP9Rs3bsyCBQuIiYkBYMmSJUydOpXDhw8TGhrKiBEjCAkJYcqUKWRnZ9OyZUsgp6ffMAy++OILZs+eTXx8PDExMfzzn//k8ssvB+D48eM8//zzbN++nfr169OzZ88ybLmyl5CQgNPpLPL5wcHBhIeHl2BEIiIipUeJvYjIJYSGhjJ06FDGjx/PnDlzvB2OeFF6ejpff/01tWvXJiwsjO+++44PPviA9957j8zMTN577z1+++03Zs+eTVhYGO+99x4jR45k1qxZuN1uHn74YW655RZmzpzJb7/9xvDhwy849H7FihW89tprTJw4kXbt2pGYmMiJEye46qqrGD58OLt27WLy5Mlm+dmzZzNv3jw+/vhj6tSpw+zZs3nooYf47rvv8PHxYdSoUdSpU4d169Zx9OhRhg4dWlbNVuZcLhdjx44lJSWlyNcICQnhtddew+FwlGBkIiIipUND8UVECmDQoEEcOXKE5cuXezsU8YIJEybQunVrrr/+evbv328m1J06daJz585YrVb8/Pz48ssvGTNmDNWrV8fHx4cnn3ySrVu3cuzYMbZv305iYiKPPfYYPj4+tGzZkptuuumCdc6ePZt7772XDh06YLVaiYyM5Kqrrrpo+ccff5z69etjt9u57777yMjIYOfOnRw7dozNmzfzzDPP4O/vz+WXX87AgQNLvJ3KC7vdTkREBBaLpUjnWywWwsPDsdvV/yEiIhWD/scSESkAPz8/HnvsMSZMmMCsWbO8HY6UsZEjRzJ48ODz9teqVcvcTkxMJC0tjbvvvjtPQulwODh27BgnT56kevXqeXqAa9euzf79+/Ot8+jRo/Tt27fAMR45coTRo0djs9nMfS6Xi+PHj+NwOPD19SUyMjJP3ZWVxWLh1ltv5YMPPijS+YZhcOuttxb5xoCIiEhZU2IvIlJAt99+O9OmTWPBggXeDkXKibMTv7CwMPz9/Zk7d675XPvZNm/ezMmTJ3G5XGZyf/To0Qteu1atWhw8ePCS9eaqWbMmzz//PF26dDnv2LFjx8jMzOT06dNmcn+xuiuDmJgY6tWrx19//YVhGAU+z2KxcNlll5nzHIiIiFQEGoovIlWKx+MhMzPT/MrKyrro/rPZbDaeeuopPv7447IOWyoAq9XKwIEDGTt2LMeOHQNyevG///57AK655hpCQ0OZPHkyWVlZ7Nixgx9++OGC1xs4cCAzZsxg48aNZGdnc/r0aX7//XcAoqKiOHr0KG632yx/99138/7775sjAJxOJ8uXL8fpdBIdHU2rVq0YN24cGRkZ7N+/n6+++qq0mqJcyO21L0xSD+qtFxGRikmJvYhUKe+88w5XX321+XXjjTdedP+5evXqRb169coyZKlARo4cSYsWLRg0aBAtW7bkb3/7G2vXrgVyhuR/9NFHrF27lnbt2jFu3Dj69+9/wWtdf/31jBkzhldffZXY2Fhuv/129u7dC8CNN95IUFAQHTp0oHXr1gDcc8899OvXjxEjRtCqVStuuukmlixZYl5v/PjxHD9+nA4dOvD000/zt7/9rRRbonzI7bUvaJJusVioV6+eeutFRKTCsRiFvZWdj+eee45vv/0WgBkzZtCuXbtiB1YW5s+fz5gxYwAYMWIEjz32WJ7jbrebTp06cebMGcLDw1m7dm2BJtI5fPgw1113HQCPPfYYI0aMKPngRURE5JJ+//33Qj1r/9hjj110kkIREZHyqEr32F9//fXmc47Lli077/j69es5c+YMAD179tTsuCIiIhVMQXvt1VsvIiIVWZVO7ENCQujcuTMAe/fuZd++fXmOL1261Ny+5ZZbyjQ2ERERKb6CPmuvZ+tFRKQiK7PE/vDhwzRu3JjGjRszadIkc//8+fPN/Rs2bABgw4YN5r4vv/ySt99+m44dO9KmTRueeOIJEhMT81z7l19+oW/fvjRv3pxbb72V1atXc++999K4cWN69Ohx0bjOTtjPnsTI7Xab61VXr16dNm3aALB//35GjRpFp06daNasGZ07d2bMmDEFml049zU999xz5r6zX+v8+fPPa6uJEycyYcIE2rVrR9u2bXn33XfxeDx8//339OrVi1atWjFkyBAOHz6cpy6n08m7775Lr169aNasGW3atOGhhx7it99+u2ScIiIilcmleu3VWy8iIhVduR9bPn78eFJSUszvly5dit1uZ/z48QD8+eefDB061JzBeu/evTzyyCOEhIQU6Prdu3fH39+f9PR0li1bZj5n/8svv5jD8G+88UasViu7d+/mzjvvJC0tzTz/5MmTzJ8/n1WrVvH1119Tp06dknjZpi+//NKMA+Df//43+/fvZ+XKlWbvw7p163j66aeZM2cOAKmpqdx5553mJEuQs5bxypUrWbduHdOmTTMnWxIREansLrWuvXrrRUSkoiv3Q/GtViuzZ89m3bp1XHnllUDO8/DZ2dkA5rJBAE8//TRbtmzhmWeeISEhoUDXDwwMpGvXrkDe4fj5DcN/8803zaT+nXfeYcuWLTz77LMAJCQk8N577xX35Z4nKyuL2bNns2LFCgIDAwFYsWIFt99+O5s2bTJn7t62bRsnTpwAYPr06ezduxebzcaHH35IXFwcy5Yto169emRlZfHWW2+VeJwiIiLl2YV67dVbLyIilUG5T+xvv/12YmNjiYqKokuXLkBO73N8fDyQk9BCzpq+DzzwAEFBQQwaNIjo6OgC13HucHy3282PP/4IQO3atWnRogXp6els3rwZgGbNmtGnTx+CgoK4//77qVmzJoC5pFFJuu6664iNjaV27dpcfvnl5v7hw4cTEhJCx44dzX25jwOsWbMGyFmX+9FHH6V58+b06tWLgwcPAvDrr7/idDpLPFYREZHy6kLP2qu3XkREKgOvD8X3eDwXPV6/fn1z29fX19zO7aU/efIkkPMcvNX6v/sUNWrU4NixYwWKoWvXrgQGBpKamsqyZcto2bKlOfz95ptvBiA5OdmM9eybBhaLhZo1a3L8+HHOnDlzyddzrtyRBxdSu3Ztc9vPz8/czo0hd1Z/+F+bFGS0QlJSEkFBQYWKVUREpCLL7bX/66+/MAwDi8XCZZddpt56kXIsOzsbj8dDdnb2JSfBFCmPLBYLVqsVm82WJ18taWWW2Pv4+JjbmZmZ5va5k76d6+wl5vK7m169enUOHTrEqVOnzP+
2025-11-08 19:49:57 +01:00
"text/plain": [
"<Figure size 1150x360 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import os\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib.pyplot as plt\n",
"import matplotlib.transforms as mtransforms\n",
"from matplotlib.patches import FancyBboxPatch\n",
"# Ensure data is loaded\n",
"try:\n",
" spirometry_df = spirometry_data.copy()\n",
"except NameError:\n",
" spirometry_df = pd.read_csv('data/spirometry_data.csv')\n",
"\n",
"# Coerce numeric columns\n",
"for col in ['Best', 'LLN', 'Pred.', '%Pred.', 'ZScore']:\n",
" if col in spirometry_df.columns:\n",
" spirometry_df[col] = pd.to_numeric(spirometry_df[col], errors='coerce')\n",
"\n",
"# Select rows of interest and prepare display values\n",
"rows_map = {\n",
" 'Lung Volume': 'FVC',\n",
" 'Lung Power': 'FEV1',\n",
" 'Power/Volume': 'FEV1/FVC%'\n",
"}\n",
"\n",
"records = []\n",
"for label, param in rows_map.items():\n",
" row = spirometry_df.loc[spirometry_df['Parameters'].str.strip() == param]\n",
" if row.empty:\n",
" continue\n",
" row = row.iloc[0]\n",
" records.append({\n",
" 'label': label,\n",
" 'param': param,\n",
" 'best': row['Best'],\n",
" 'pct': row['%Pred.'],\n",
" 'z': row['ZScore']\n",
" })\n",
"\n",
"# Figure setup\n",
"os.makedirs('graphs', exist_ok=True)\n",
"fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(11.5, 3.6), sharex=True,\n",
" gridspec_kw={'hspace': 0.65})\n",
"\n",
"x_min, x_max = -5, 3\n",
"# Segment colors: red -> orange -> yellow -> green\n",
"segments = [\n",
" (-5, -4, '#f4a7a7'), # red-ish\n",
" (-4, -3, '#f7c49a'), # orange-ish\n",
" (-3, -1.7, '#f6e3a3'), # yellow-ish\n",
" (-1.7, 3, '#c9f0cc'), # green-ish\n",
"]\n",
"\n",
"ticks = np.arange(x_min, x_max + 1, 1)\n",
"labels = [str(i) for i in ticks]\n",
"\n",
"# Plot each row\n",
"for ax, rec in zip(axes, records):\n",
" # Background segments\n",
" for a, b, color in segments:\n",
" ax.barh(0, width=b-a, left=a, height=0.6, color=color, edgecolor='none')\n",
"\n",
" # LLN (-1) and Predicted (0) markers\n",
" # ax.axvline(-1, color='black', lw=1)\n",
" ax.axvline(0, color='black', lw=1)\n",
"\n",
" # Z-score pointer (downward triangle) at top of each panel\n",
" if pd.notna(rec['z']):\n",
" trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes)\n",
" ax.plot(float(rec['z']), 1.2, marker='v', markersize=12, color='dimgray',\n",
" transform=trans, clip_on=False)\n",
"\n",
2025-11-17 17:15:44 +01:00
" # Labels, ticks, and styling\n",
" ax.set_title(rec['label'], loc='left', fontsize=11, fontweight='bold', pad=2)\n",
" ax.set_xlim(x_min, x_max)\n",
" ax.set_yticks([])\n",
" ax.set_xticks(ticks)\n",
" ax.set_xticklabels(labels, fontsize=8)\n",
" ax.set_xlabel('')\n",
"\n",
"# Add x-axis label to the bottom axis\n",
"# axes[-1].set_xlabel('Z-score', fontsize=10)\n",
"\n",
"# Top annotations\n",
"axes[0].text(-1.7, 0.45, 'LLN', ha='center', va='bottom', fontsize=9)\n",
"axes[0].text(0, 0.45, 'Predicted', ha='center', va='bottom', fontsize=9)\n",
"\n",
"# Right-side summary boxes\n",
"fig.subplots_adjust(right=0.78)\n",
"box_ax = fig.add_axes([0.805, 0.06, 0.18, 0.90]) # [left, bottom, width, height]\n",
"box_ax.axis('off')\n",
"\n",
"# Helper to draw a pill-shaped text box\n",
"\n",
"def pill(ax, xy, text):\n",
" x, y = xy\n",
" # Draw rounded rectangle background\n",
" bbox = FancyBboxPatch((x-0.48, y-0.09), 0.96, 0.18,\n",
" boxstyle='round,pad=0.02,rounding_size=0.08',\n",
" ec='#dddddd', fc='#f3f3f3', linewidth=1.0)\n",
" ax.add_patch(bbox)\n",
" ax.text(x, y+0.025, text, ha='center', va='center', fontsize=11, fontweight='bold')\n",
" ax.text(x, y-0.055, 'of predicted', ha='center', va='center', fontsize=9, color='#555555')\n",
"\n",
"box_ax.set_xlim(0, 1)\n",
"box_ax.set_ylim(0, 1)\n",
"\n",
"# Prepare display strings and positions (top to bottom)\n",
"right_items = []\n",
"for rec in records:\n",
" name = 'FVC' if rec['param'] == 'FVC' else ('FEV1' if rec['param'] == 'FEV1' else 'FEV1/FVC')\n",
" unit = 'L' if rec['param'] in ('FVC', 'FEV1') else '%'\n",
" value_fmt = f\"{rec['best']:.2f}{unit}\"\n",
" pct_fmt = f\"{rec['pct']:.1f}%\"\n",
" right_items.append((name, value_fmt, pct_fmt))\n",
"\n",
"# Sort to match image order on the right (FVC, FEV1, FEV1/FVC)\n",
"order = ['FVC', 'FEV1', 'FEV1/FVC']\n",
"right_items_sorted = [next(item for item in right_items if item[0] == k) for k in order]\n",
"\n",
"ys = [0.82, 0.48, 0.15]\n",
"for (name, value_fmt, pct_fmt), y in zip(right_items_sorted, ys):\n",
" main_line = f\"{name}\\n{value_fmt} → {pct_fmt}\"\n",
" pill(box_ax, (0.5, y), main_line)\n",
"\n",
"plt.savefig(f'{base_dir}/graphs/spirometry_chart.png', dpi=300, bbox_inches='tight')\n",
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 10,
2025-11-17 17:15:44 +01:00
"id": "ef8bc7ac",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
2025-11-17 17:15:44 +01:00
"text/plain": [
"<Figure size 1800x500 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
"phase_times = first_unique_phase['T(sec)'].tolist()\n",
"\n",
"plt.figure(figsize=(18, 5))\n",
"ax1 = plt.subplot()\n",
"\n",
"# Plot VT with step-like appearance\n",
"line1 = sns.lineplot(data=df, x='T(sec)', y='VT(l)_smoothed', label='VT (L)')\n",
"ax1.set_xlabel('Time (sec)')\n",
"ax1.set_ylabel('VT (L)')\n",
"ax1.grid(True, alpha=0.1)\n",
"ax1.set_ylim(0, min(8, df['VT(l)_smoothed'].max()))\n",
"\n",
"# Set x-axis limits to remove padding\n",
"ax1.set_xlim(0, df['T(sec)'].max())\n",
"\n",
"# Plot speed as step function on secondary y-axis\n",
"ax2 = ax1.twinx()\n",
"ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
"line2 = sns.lineplot(data=df, x='T(sec)', y='Speed', color='green', ax=ax2, \n",
" drawstyle='steps-post', linewidth=2, label='Speed')\n",
"ax2.set_ylabel('Speed')\n",
"ax2.set_ylim(0, min(30, df['Speed'].max()) + 1)\n",
"\n",
"# Remove default legends first\n",
"ax1.get_legend().remove()\n",
"ax2.get_legend().remove()\n",
"\n",
"# Combine legends from both axes in the top left\n",
"lines1, labels1 = ax1.get_legend_handles_labels()\n",
"lines2, labels2 = ax2.get_legend_handles_labels()\n",
"ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')\n",
"\n",
"# Add colored background regions if you have phase information\n",
"ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
"ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
"ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
"ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
"\n",
"plt.savefig(f'{base_dir}/graphs/respiratory.png', dpi=300, bbox_inches='tight')\n",
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 11,
2025-11-17 17:15:44 +01:00
"id": "06244aa2",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAABdEAAAMfCAYAAAA5Z570AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd4U9X/B/B3uvekA0rpADrYyChLKbJBNggypIIgSxFEvg6mICCIIojgzwEIyJSlKEvZm8oeZXSwSinQ0kF37u+PY1aTdKRJB7xfz5Mnyb3nnnNucnPTfnLu58gkSZJARERERERERERERERazMq6A0RERERERERERERE5RWD6EREREREREREREREejCITkRERERERERERESkB4PoRERERERERERERER6MIhORERERERERERERKQHg+hERERERERERERERHowiE5EREREREREREREpAeD6EREREREREREREREejCITkRERERERERERESkB4PoRERUYv7+/pDJZJDJZJgxY0ZZd0dp5cqVyn7JZDKNdeHh4crlERERyuWxsbEa2xw4cKB0O10E+vpOVNGU13NHeXPq1Cl07twZ7u7uMDMzU75mycnJZd21CnHOpKIr6HuzNLYvz2bMmKHcL39//7LuTomV5/MKERFRecQgOhFRBXDgwAGNf0r13SpSQDX/P9q6Ai/qweL8/7A+r/+oPy8BcvXgqPrNzs4OgYGBGDBgAPbv31/W3TSp/J/b2NjYUms7IiKizD8fJQ2QmzLArv76hIeHG7VuY3vw4AE6d+6MXbt24cmTJ5AkqVjbP3nyBFOmTEHDhg3h6OgIKysreHp6IjQ0FL169cLMmTNx584dE/WeFIryXa1+zJfH4/J5/d41NX1/s9nY2MDPzw8DBgzAoUOHSrVPJT2vEBERvYgsyroDREREptKkSRMsWLCgWNu4ublpbFO9enVjd6vERo8ejddeew0AUKdOnTLuTfFkZGQgJiYGMTEx2LBhA77//nuMHDmyrLtFZeTTTz/F06dPAQAtWrQo496UT7t378aTJ08AiGDc2LFj4efnBwCwtbUtcNu4uDi0atUKd+/e1ViemJiIxMREXLt2Ddu2bUP9+vXh6+trUP8qwjmTqDzKysrC7du3cfv2bWzYsAGzZ8/Gp59+Wiptl+S8QkRE9KJiEJ2IqALq378/Gjdu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
2025-11-17 17:15:44 +01:00
"text/plain": [
"<Figure size 1500x800 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"\n",
"# Group by speed and calculate mean for numeric columns only\n",
"speed_groups = df.groupby('Speed').mean(numeric_only=True).round(1)\n",
"\n",
"# Drop the first and last row from speed_groups\n",
"speed_groups = speed_groups.iloc[1:-1]\n",
"\n",
"# Filter data to only include speeds in the desired range\n",
"filtered_data = speed_groups[(speed_groups.index >= 3.5) & (speed_groups.index <= 7.5)]\n",
"\n",
"# Create figure with specific size\n",
"plt.figure(figsize=(15, 8))\n",
"plt.style.use('default')\n",
"\n",
"# Create stage labels and positions\n",
"stage_labels = [f'Stage {i}' for i in range(1, len(filtered_data) + 1)]\n",
"x_positions = np.arange(len(filtered_data))\n",
"\n",
"# Calculate fat and carbs energy expenditure from percentages\n",
"fat_ee = filtered_data['EE(kcal/min)'] * filtered_data['FAT(%)'] / 100\n",
"carbs_ee = filtered_data['EE(kcal/min)'] * filtered_data['CARBS(%)'] / 100\n",
"\n",
"# Create the main axis for the stacked bars\n",
"ax1 = plt.gca()\n",
"\n",
"# Create stacked bar chart with colors\n",
"bars_fat = ax1.bar(x_positions, fat_ee, color='#1f77b4', alpha=0.8, width=0.6, label='Fat')\n",
"bars_carbs = ax1.bar(x_positions, carbs_ee, bottom=fat_ee, color='#ff7f0e', alpha=0.8, width=0.6, label='Carbs')\n",
"\n",
"# Set labels and formatting for primary axis\n",
"ax1.set_xlabel('', fontsize=12)\n",
"ax1.set_ylabel('Fuel (kcal/min)', fontsize=12)\n",
"ax1.set_ylim(0, 20)\n",
"\n",
"# Add individual values on each bar segment\n",
"for i, (fat_val, carb_val, total_val) in enumerate(zip(fat_ee, carbs_ee, filtered_data['EE(kcal/min)'])):\n",
" if fat_val > 0.3: # Fat value\n",
" ax1.text(i, fat_val/2, f'{fat_val:.1f}', ha='center', va='center',\n",
" fontsize=9, fontweight='bold', color='white')\n",
" if carb_val > 0.3: # Carbs value\n",
" ax1.text(i, fat_val + carb_val/2, f'{carb_val:.1f}', ha='center', va='center',\n",
" fontsize=9, fontweight='bold', color='white')\n",
" # Total EE\n",
" ax1.text(i, total_val + 0.5, f'{total_val:.1f} kcal', ha='center', va='bottom',\n",
" fontsize=10, fontweight='bold', color='black')\n",
"\n",
"# Add speed labels below x-axis\n",
"for i, speed in enumerate(filtered_data.index):\n",
" ax1.text(i, -1.5, f'{speed:.1f} mph', ha='center', va='top', fontsize=9)\n",
" ax1.text(i, -2.8, f'{speed*1.609:.1f} min/km', ha='center', va='top', fontsize=8, color='gray')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Create secondary y-axis for heart rate\n",
"ax2 = ax1.twinx()\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Plot heart rate line (no manual offset)\n",
"hr_line = ax2.plot(x_positions, filtered_data['HR(bpm)'],\n",
" marker='o', linewidth=3, markersize=8, color='red', label='Heart Rate')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Set heart rate axis formatting\n",
"ax2.set_ylabel('Heart Rate (bpm)', fontsize=12, color='red')\n",
"ax2.tick_params(axis='y', labelcolor='red')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Dynamically adjust HR axis to float above bars\n",
"max_bar_height = max(filtered_data['EE(kcal/min)'])\n",
"ax2.set_ylim(0, 220) # ensures HR line is above bars\n",
2025-11-08 19:49:57 +01:00
"\n",
"\n",
2025-11-17 17:15:44 +01:00
"# Add HR values above the points\n",
"for i, hr in enumerate(filtered_data['HR(bpm)']):\n",
" ax2.text(i, hr + 10, f'{int(hr)}bpm', ha='center', va='bottom',\n",
" fontsize=10, fontweight='bold', color='red')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Set x-axis formatting\n",
"ax1.set_xticks(x_positions)\n",
"ax1.set_xticklabels(stage_labels, fontsize=11)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Add title\n",
"plt.suptitle('Fuel Utilization Report - Institute of Science, Health and Performance',\n",
" fontsize=14, fontweight='bold', y=0.95)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Create legend\n",
"lines1, labels1 = ax1.get_legend_handles_labels()\n",
"lines2, labels2 = ax2.get_legend_handles_labels()\n",
"ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left',\n",
" frameon=True, fancybox=True, shadow=True)\n",
"\n",
"# Add grid\n",
"ax1.grid(True, alpha=0.3, linestyle='-', linewidth=0.5)\n",
"ax1.set_axisbelow(True)\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Adjust layout\n",
"plt.tight_layout()\n",
"plt.subplots_adjust(bottom=0.1, top=0.9)\n",
"plt.savefig(f'{base_dir}/graphs/fuel_utilization_chart.png', dpi=300)\n",
2025-11-08 19:49:57 +01:00
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 12,
2025-11-17 17:15:44 +01:00
"id": "8a1878a0",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
{
2025-11-17 17:15:44 +01:00
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
2025-11-17 17:15:44 +01:00
"text/plain": [
"<Figure size 1800x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
2025-11-08 19:49:57 +01:00
}
],
"source": [
2025-11-17 17:15:44 +01:00
"first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
"phase_times = first_unique_phase['T(sec)'].tolist()\n",
"\n",
"plt.figure(figsize=(18, 5))\n",
"ax1 = plt.subplot()\n",
"\n",
"# Plot VO2 Pulse\n",
"#\n",
"line1 = sns.lineplot(data=df, x='T(sec)', y='VO2 Pulse_smoothed', label='VO2 Pulse (mL/beat)', color='blue')\n",
"ax1.set_xlabel('Time (sec)')\n",
"ax1.set_ylabel('VO2 Pulse (mL/beat)')\n",
"# ax1.set_title('VO2 Pulse, Heart Rate, and Speed Over Time')\n",
"ax1.set_ylim(0, df['VO2 Pulse_smoothed'].max())\n",
"ax1.grid(True, alpha=0.1)\n",
"# Set x-axis limits to remove padding\n",
"ax1.set_xlim(0, df['T(sec)'].max())\n",
"# Create second y-axis for heart rate\n",
"#\n",
"ax2 = ax1.twinx()\n",
"line2 = sns.lineplot(data=df, x='T(sec)', y='HR(bpm)_smoothed', color='red', ax=ax2, \n",
" linewidth=2, label='Heart Rate (bpm)')\n",
"ax2.set_ylabel('Heart Rate (bpm)', color='red')\n",
"ax2.tick_params(axis='y', labelcolor='red')\n",
"ax2.set_ylim(60, df['HR(bpm)_smoothed'].max() + 1)\n",
"\n",
"# Create third y-axis for speed\n",
"ax3 = ax1.twinx()\n",
"ax3.spines['right'].set_position(('outward', 60))\n",
"\n",
"line3 = sns.lineplot(data=df, x='T(sec)', y='Speed', color='green', ax=ax3, \n",
" drawstyle='steps-post', linewidth=2, label='Speed')\n",
"ax3.set_ylabel('Speed', color='green')\n",
"ax3.tick_params(axis='y', labelcolor='green')\n",
"ax3.set_ylim(0, df['Speed'].max() + 1)\n",
"\n",
"ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
"\n",
"# Remove default legends first\n",
"if ax1.get_legend():\n",
" ax1.get_legend().remove()\n",
"if ax2.get_legend():\n",
" ax2.get_legend().remove()\n",
"if ax3.get_legend():\n",
" ax3.get_legend().remove()\n",
"\n",
"# Combine legends from all axes in the top left\n",
"lines1, labels1 = ax1.get_legend_handles_labels()\n",
"lines2, labels2 = ax2.get_legend_handles_labels()\n",
"lines3, labels3 = ax3.get_legend_handles_labels()\n",
"ax1.legend(lines1 + lines2 + lines3, labels1 + labels2 + labels3, loc='upper left')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"# Add colored background regions if you have phase information\n",
"ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
"ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
"ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
"ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"plt.savefig(f'{base_dir}/graphs/vo2_pulse_chart.png', bbox_inches='tight', dpi=300)\n",
"plt.show()"
2025-11-08 19:49:57 +01:00
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 13,
2025-11-17 17:15:44 +01:00
"id": "7361fb05",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
{
2025-11-17 17:15:44 +01:00
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
2025-11-17 17:15:44 +01:00
"text/plain": [
"<Figure size 1800x500 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
2025-11-08 19:49:57 +01:00
}
],
"source": [
2025-11-17 17:15:44 +01:00
"first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
"phase_times = first_unique_phase['T(sec)'].tolist()\n",
"\n",
"plt.figure(figsize=(18, 5))\n",
"ax1 = plt.subplot()\n",
"\n",
"# Plot VT with step-like appearance\n",
"line1 = sns.lineplot(data=df, x='T(sec)', y='VO2 Breath_smoothed', label='VO2 per Breath (mL/breath)')\n",
"ax1.set_xlabel('Time (sec)')\n",
"ax1.set_ylabel('VO2 per Breath (mL/breath)')\n",
"# ax1.set_title('VO2 per Breath and Speed Over Time')\n",
"ax1.set_ylim(0, df['VO2 Breath_smoothed'].max() + 1)\n",
"ax1.grid(True, alpha=0.1)\n",
"# Set x-axis limits to remove padding\n",
"ax1.set_xlim(0, df['T(sec)'].max())\n",
"\n",
"# Plot speed as step function on secondary y-axis\n",
"ax2 = ax1.twinx()\n",
"ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
"line2 = sns.lineplot(data=df, x='T(sec)', y='Speed', color='green', ax=ax2, \n",
" drawstyle='steps-post', linewidth=2, label='Speed')\n",
"ax2.set_ylim(0, df['Speed'].max() + 1)\n",
"ax2.set_ylabel('Speed')\n",
"\n",
"# Remove default legends first\n",
"ax1.get_legend().remove()\n",
"ax2.get_legend().remove()\n",
"\n",
"# Combine legends from both axes in the top left\n",
"lines1, labels1 = ax1.get_legend_handles_labels()\n",
"lines2, labels2 = ax2.get_legend_handles_labels()\n",
"ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')\n",
"\n",
"# Add colored background regions if you have phase information\n",
"ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
"ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
"ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
"ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"plt.savefig(f'{base_dir}/graphs/vo2_breath_chart.png', bbox_inches='tight', dpi=300)\n",
"plt.show()"
2025-11-08 19:49:57 +01:00
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 14,
2025-11-17 17:15:44 +01:00
"id": "c89478ff",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
2025-11-08 19:49:57 +01:00
"text/plain": [
2025-11-17 17:15:44 +01:00
"<Figure size 1800x500 with 2 Axes>"
2025-11-08 19:49:57 +01:00
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
2025-11-17 17:15:44 +01:00
"first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
"phase_times = first_unique_phase['T(sec)'].tolist()\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"plt.figure(figsize=(18, 5))\n",
"ax1 = plt.subplot()\n",
2025-11-08 19:49:57 +01:00
"\n",
2025-11-17 17:15:44 +01:00
"df['CHO']\n",
"# Plot VT with step-like appearance\n",
"line1 = sns.lineplot(data=df, x='T(sec)', y='CHO_smoothed', label='CHO (kcal/min)')\n",
"ax1.set_xlabel('Time (sec)')\n",
"ax1.set_ylabel('CHO (g/min)')\n",
"# ax1.set_title('CHO and Speed Over Time')\n",
"ax1.grid(True, alpha=0.1)\n",
"# Set x-axis limits to remove padding\n",
"ax1.set_xlim(0, df['T(sec)'].max())\n",
"# Plot speed as step function on secondary y-axis\n",
"ax2 = ax1.twinx()\n",
"ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
"line2 = sns.lineplot(data=df, x='T(sec)', y='FAT_smoothed', color='green', ax=ax2, label='FAT (kcal/min)')\n",
"ax2.set_ylabel('FAT (kcal/min)')\n",
"\n",
"ax2.set_ylim(0, 15) # ensures HR line is above bars\n",
"\n",
"# Remove default legends first\n",
"ax1.get_legend().remove()\n",
"ax2.get_legend().remove()\n",
"\n",
"# Combine legends from both axes in the top left\n",
"lines1, labels1 = ax1.get_legend_handles_labels()\n",
"lines2, labels2 = ax2.get_legend_handles_labels()\n",
"ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')\n",
"\n",
"# Add colored background regions if you have phase information\n",
"ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
"ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
"ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
"ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
"\n",
"plt.savefig(f'{base_dir}/graphs/fat_metabolism_chart.png', bbox_inches='tight', dpi=300)\n",
"plt.show()"
2025-11-08 19:49:57 +01:00
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 15,
2025-11-17 17:15:44 +01:00
"id": "1db16040",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
2025-11-08 19:49:57 +01:00
"text/plain": [
2025-11-17 17:15:44 +01:00
"<Figure size 1800x500 with 3 Axes>"
2025-11-08 19:49:57 +01:00
]
},
"metadata": {},
2025-11-17 17:15:44 +01:00
"output_type": "display_data"
2025-11-08 19:49:57 +01:00
}
],
"source": [
2025-11-17 17:15:44 +01:00
"first_unique_phase = df.drop_duplicates(subset='PHASE')\n",
"phase_times = first_unique_phase['T(sec)'].tolist()\n",
"\n",
"plt.figure(figsize=(18, 5))\n",
"ax1 = plt.subplot()\n",
"\n",
"# Plot VO2 Pulse\n",
"line1 = sns.lineplot(data=df, x='T(sec)', y='VCO2(ml/min)_smoothed', label='VCO2 (ml/min)', color='blue')\n",
"ax1.set_xlabel('Time (sec)')\n",
"ax1.set_ylabel('VO2 Pulse (mL/beat)')\n",
"# ax1.set_title('VO2 Pulse, Heart Rate, and Speed Over Time')\n",
"ax1.set_ylim(0, df['VCO2(ml/min)'].max())\n",
"ax1.grid(True, alpha=0.1)\n",
"# Set x-axis limits to remove padding\n",
"ax1.set_xlim(0, df['T(sec)'].max())\n",
"\n",
"# Create second y-axis for heart rate\n",
"ax2 = ax1.twinx()\n",
"line2 = sns.lineplot(data=df, x='T(sec)', y='HR(bpm)_smoothed', color='red', ax=ax2, \n",
" linewidth=2, label='Heart Rate (bpm)')\n",
"ax2.set_ylabel('Heart Rate (bpm)', color='red')\n",
"ax2.set_ylim(df['HR(bpm)_smoothed'].min() - 1, df['HR(bpm)_smoothed'].max() + 1)\n",
"ax2.tick_params(axis='y', labelcolor='red')\n",
"\n",
"# Create third y-axis for speed\n",
"ax3 = ax1.twinx()\n",
"ax3.spines['right'].set_position(('outward', 60))\n",
"line3 = sns.lineplot(data=df, x='T(sec)', y='BF(bpm)_smoothed', color='green', ax=ax3, linewidth=2, label='BF (bpm)')\n",
"ax3.set_ylabel('BF (bpm)', color='green')\n",
"ax3.tick_params(axis='y', labelcolor='green')\n",
"ax3.set_ylim(0, df['BF(bpm)_smoothed'].max() + 1)\n",
"ax1.set_xticks(np.arange(0, df['T(sec)'].max(), 200))\n",
"\n",
"# Remove default legends first\n",
"if ax1.get_legend():\n",
" ax1.get_legend().remove()\n",
"if ax2.get_legend():\n",
" ax2.get_legend().remove()\n",
"if ax3.get_legend():\n",
" ax3.get_legend().remove()\n",
"\n",
"# Combine legends from all axes in the top left\n",
"lines1, labels1 = ax1.get_legend_handles_labels()\n",
"lines2, labels2 = ax2.get_legend_handles_labels()\n",
"lines3, labels3 = ax3.get_legend_handles_labels()\n",
"ax1.legend(lines1 + lines2 + lines3, labels1 + labels2 + labels3, loc='upper left')\n",
"\n",
"# Add colored background regions if you have phase information\n",
"ax1.axvspan(0, phase_times[1], alpha=0.2, color='lightblue')\n",
"ax1.axvspan(phase_times[1], phase_times[2], alpha=0.2, color='purple')\n",
"ax1.axvspan(phase_times[2], phase_times[3], alpha=0.2, color='lightgreen')\n",
"ax1.axvspan(phase_times[3], df['T(sec)'].max(), alpha=0.2, color='blue')\n",
"\n",
"plt.savefig(f'{base_dir}/graphs/recovery_chart.png', bbox_inches='tight', dpi=300)\n",
"plt.show()"
2025-11-08 19:49:57 +01:00
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 16,
2025-11-17 17:15:44 +01:00
"id": "c8ad6076",
2025-11-08 19:49:57 +01:00
"metadata": {},
"outputs": [
{
"data": {
2025-11-24 17:52:56 +01:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAIcCAYAAAAnqB3MAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAA7y9JREFUeJzs3Xd4FNX6wPHvbE+ym56QhIQkBELvoEhXmogUFRD12vtVUe/Pq/deG3jF3hvWK1evKKAIdgWkIyJVpLdQQgLpZTdbZ35/TLKwJEBAMALv53nm2d3ZMzNnZs9O2Jdz3qNomqYhhBBCCCGEEEIIIcQfyNDQFRBCCCGEEEIIIYQQZx8JSgkhhBBCCCGEEEKIP5wEpYQQQgghhBBCCCHEH06CUkIIIYQQQgghhBDiDydBKSGEEEIIIYQQQgjxh5OglBBCCCGEEEIIIYT4w0lQSgghhBBCCCGEEEL84SQoJYQQQgghhBBCCCH+cBKUEkIIIYQQQgghhBB/OAlKCSGEOOn++te/MnDgwFO2f7/fz/33309aWhoGg4GRI0cetXz37t25//77T1l9TtT48eNRFKWhq3FSLV++HIvFwq5du05o+5ycHBRFYfLkySe87XPPPXdCxwYoKioiIiKCb7755oT3UR/XXXcdGRkZ9S5rt9tPaX1qTJ48GUVRyMnJOSX7/6PORVEUxo8ff8qPc6qdKechhBBCHIkEpYQQ4gyiKEq9lvnz5wNQUFDA3XffTcuWLQkLCyMxMZFzzjmHBx54gMrKyuB+j+eH5M6dO3n33Xf517/+FVx3MoIFh/rPf/7Ds88+y6hRo/jvf//Lvffey4YNGxg/fnydP6YfeOABXn/9dfLz80/K8f8o8+fPr/dnWmPdunWMGjWK9PR0bDYbjRs3ZuDAgbz66qsh+87IyODiiy8Ovi4qKuLZZ5+lT58+JCQkEB0dTffu3Zk6depx1fnBBx/kiiuuID09PbiuX79+IXUNCwujffv2vPTSS6iqeoJX5/f55ptv6vyxHxcXx0033cTDDz/8h9bH5XIxfvz44HfzZDr8+lssFjIzM7nlllvYs2fPST/eqbR48WKGDBlC48aNsdlsNGnShGHDhjFlypSGrtpxqfluf/rppw1dldOaqqp88MEHnHvuucTGxuJwOMjOzuaaa65h2bJlwXJH+/sghBCiYZkaugJCCCFOng8//DDk9QcffMDs2bNrrW/VqhXFxcV07dqV8vJybrjhBlq2bElRURG//vorkyZN4vbbbz+hHg0vv/wymZmZnH/++b/rXI7mxx9/pHHjxrz44ovBdZ9++ikTJkygX79+tXqgjBgxgsjISN544w0ee+yxU1avk61Vq1a1Prt//vOf2O12HnzwwVrlly5dyvnnn0+TJk24+eabSUpKYs+ePSxbtoyXX36Zu+6664jH+umnn3jwwQe56KKLeOihhzCZTHz22WeMHTuWDRs2MGHChGPWd82aNcyZM4elS5fWei81NZUnn3wSgMLCQqZMmcK9995LQUEBEydODJZLT0+nqqoKs9l8zOP9Ht988w2vv/56nYGp2267jVdeeYUff/yRCy644JQc/5133gkJyLlcruA17tev30k/3qHX3+v1smHDBt58802+//57Nm7cSHh4+Ek/5sk2ffp0Lr/8cjp27Mjdd99NTEwMO3fuZOHChbzzzjtceeWVDV3Fk66qqgqTSf65fiTjxo3j9ddfZ8SIEVx11VWYTCY2b97Mt99+S9OmTenevTtA8B5W198HIYQQDUv+ygkhxBnkL3/5S8jrZcuWMXv27FrrAZ599ll2797NkiVL6NGjR8h75eXlWCyW4z6+z+fjo48+4rbbbjvubY/HgQMHiI6Ornd5g8HAqFGj+OCDD5gwYcJpM2SuUaNGtT67p556ivj4+Do/04kTJxIVFcUvv/xS6/ocOHDgqMdq06YNW7duDenh9Ne//pUBAwbw9NNPc//99xMREXHUfbz//vs0adIk+EPwUFFRUSF1vu2222jZsiWvvvoqjz32GEajEdB7+9lstqMe51Rr1aoVbdu2ZfLkyacsKHWqg26HO/z6A2RmZnLnnXeyZMmSUzrc9mQZP348rVu3ZtmyZbXuT8dq36erhv4uNDRVVfF6vXVeh/379/PGG29w88038/bbb4e899JLL1FQUPBHVVMIIcTvIMP3hBDiLLV9+3aMRmOdAYTIyMgT+jG0ePFiCgsLGTBgwAnVyePx8Oijj9KsWTOsVitpaWncf//9eDwe4OAwwHnz5rF+/frgcKTJkyczevRoAM4///xawxQBBg4cyK5du1izZs0x6/Hcc8/Ro0cP4uLiCAsLo0uXLnUOs1EUhTvvvJOZM2fStm1brFYrbdq04bvvvqvz2nTr1g2bzUZWVhZvvfXWCV2jo9m+fTtt2rSpM2CXmJh41G0zMzNDAlKgn9/IkSPxeDzs2LHjmMefOXMmF1xwQb2CfjabjW7dulFRURESUDhSTqnp06fTunVrbDYbbdu25fPPPz9qXqa3336brKwsrFYr3bp145dffgm+d9111/H6668Hz/HwIZCgt5cvv/wSTdOOeA6lpaUYjUZeeeWV4LrCwkIMBgNxcXEh295+++0kJSWF1KGm7jk5OSQkJAAEg6Z15RLKzc1l5MiR2O12EhISuO+++wgEAkes37HU1OdYPXFmzZrF0KFDSUlJwWq1kpWVxb///e86j/3zzz9z0UUXERMTQ0REBO3bt+fll18+6v7XrFlDQkIC/fr1Cxk2fLjt27fTrVu3OgPmx2rfAKtXr2bIkCFERkZit9vp379/yBAvOJhTa+HChdx6663ExcURGRnJNddcQ0lJSa19fvvtt/Tu3ZuIiAgcDgdDhw5l/fr1x6xLfR3eDmry0G3bto3rrruO6OhooqKiuP7663G5XLW2/9///keXLl0ICwsjNjaWsWPH1hqyuXXrVi677DKSkpKw2WykpqYyduxYysrKjlq3fv360bZtW1auXEmPHj0ICwsjMzOTN998s1bZY93bDz3fO++8k48++og2bdpgtVrrvJ+CPlRc0zR69uxZ53WraRPH+vtwPO379ddfp2nTpoSFhXHOOeewaNEi+vXrV6t3Y33PVwghhPSUEkKIs1Z6ejqBQIAPP/yQa6+99qTsc+nSpSiKQqdOnY57W1VVGT58OIsXL+aWW26hVatWrFu3jhdffJEtW7Ywc+ZMEhIS+PDDD5k4cSKVlZXB4UjNmzdn3LhxvPLKK/zrX/+iVatWAMFHgC5dugCwZMmSY9bv5ZdfZvjw4Vx11VV4vV4++eQTRo8ezVdffcXQoUNDyi5evJgZM2bw17/+FYfDwSuvvMJll13G7t27iYuLA/Q8T4MGDSIhIYHx48fj9/t59NFHadSo0XFfp6NJT0/np59+4rfffqNt27YnZZ81ebji4+OPWi43N5fdu3fTuXPneu+7JgB1rF5vX3/9NZdffjnt2rXjySefpKSkhBtvvJHGjRvXWX7KlClUVFRw6623oigKzzzzDJdeeik7duzAbDZz6623sm/fvjqHttbo0qULL774IuvXrz/itYyOjqZt27YsXLiQcePGAXp7UBSF4uJiNmzYQJs2bQBYtGgRvXv3rnM/CQkJwSGzl1xyCZdeeikA7du3D5YJBAIMHjyYc889l+eee445c+bw/PPPk5WVxe23337U61ezfWFhIaD3aNy4cWPwR3NdP+oPNXnyZOx2O3/729+w2+38+OOPPPLII5SXl/Pss88Gy82ePZuLL76Y5ORk7r77bpKSkti4cSNfffUVd999d537/uWXXxg8eDBdu3Zl1qxZhIWFHbEe6enpzJ07l71795KamnrMcz7U+vXr6d27N5GRkdx///2YzWbeeust+vXrx4IFCzj33HNDyt95551ER0czfvx4Nm/ezKRJk9i1a1cwFxQQvHcOHjyYp59+GpfLxaRJk+jVqxerV68+pcPExowZQ2ZmJk8++SSrVq3i3XffJTExkaeffjpYZuLEiTz88MOMGTOGm266iYKCAl599VX69OnD6tWriY6Oxuv1MnjwYDweD3f
2025-11-08 19:49:57 +01:00
"text/plain": [
2025-11-17 17:15:44 +01:00
"<Figure size 1200x550 with 1 Axes>"
2025-11-08 19:49:57 +01:00
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
2025-11-17 17:15:44 +01:00
"# Plot TSI (Left) and TSI2 (Right) with Black Slope (Trend) Lines per Stage\n",
"from numpy.polynomial.polynomial import Polynomial\n",
"\n",
"plt.figure(figsize=(12, 5.5))\n",
"\n",
"# Plot TSI (Left Leg)\n",
"plt.plot(\n",
" oxygenation['Time'], oxygenation['TSI'], \n",
" label='TSI (Left Leg)', color='steelblue', linewidth=2\n",
")\n",
"\n",
"# Plot TSI2 (Right Leg)\n",
"plt.plot(\n",
" oxygenation['Time'], oxygenation['TSI-second'],\n",
" label='TSI2 (Right Leg)', color='orange', linewidth=2\n",
")\n",
"\n",
"# Define time intervals for stages (adjust these based on your test protocol)\n",
"# Looking at the data range, we'll create intervals\n",
"max_time = oxygenation['Time'].max()\n",
"intervals = [\n",
" (0, 250),\n",
" (250, 500),\n",
" (500, 750),\n",
" (750, 1000),\n",
" (1000, 1250),\n",
" (1250, 1500),\n",
" (1500, max_time)\n",
"]\n",
"\n",
"# Calculate and plot trend lines for each interval\n",
"for start_time, end_time in intervals:\n",
" # Filter data for this interval\n",
" mask_interval = (oxygenation['Time'] >= start_time) & (oxygenation['Time'] <= end_time)\n",
" \n",
" # TSI (Left Leg) trend for this interval\n",
" mask_left = mask_interval & ~oxygenation['TSI'].isna()\n",
" if mask_left.sum() > 1: # Need at least 2 points for a line\n",
" x_left = oxygenation.loc[mask_left, 'Time']\n",
" y_left = oxygenation.loc[mask_left, 'TSI']\n",
" coefs_left = Polynomial.fit(x_left, y_left, 1).convert().coef\n",
" trend_left = coefs_left[0] + coefs_left[1] * x_left\n",
" plt.plot(x_left, trend_left, color='black', linestyle='--', linewidth=2, alpha=0.8)\n",
" \n",
" # TSI-second (Right Leg) trend for this interval\n",
" mask_right = mask_interval & ~oxygenation['TSI-second'].isna()\n",
" if mask_right.sum() > 1: # Need at least 2 points for a line\n",
" x_right = oxygenation.loc[mask_right, 'Time']\n",
" y_right = oxygenation.loc[mask_right, 'TSI-second']\n",
" coefs_right = Polynomial.fit(x_right, y_right, 1).convert().coef\n",
" trend_right = coefs_right[0] + coefs_right[1] * x_right\n",
" plt.plot(x_right, trend_right, color='black', linestyle='--', linewidth=2, alpha=0.8)\n",
"\n",
"plt.xlabel('Time (s)')\n",
"plt.ylabel('TSI (%)')\n",
"plt.title('TSI (Left) and TSI2 (Right) with Black Slope Lines per Stage')\n",
"plt.legend(fontsize=10, loc='upper right')\n",
2025-11-08 19:49:57 +01:00
"plt.tight_layout()\n",
2025-11-17 17:15:44 +01:00
"plt.grid(alpha=0.25)\n",
"# plt.savefig('graphs/tsi_comparison_with_trends.png', bbox_inches='tight', dpi=160)\n",
2025-11-08 19:49:57 +01:00
"plt.show()"
]
},
2025-11-21 09:23:13 +01:00
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 17,
2025-11-21 09:23:13 +01:00
"id": "25327cc1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Age: 45, Gender: male, VO2 Max: 35.5\n",
"Age Range: 40-49, Category: Poor\n"
]
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1400x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"import matplotlib.patches as patches\n",
"from matplotlib.table import Table\n",
"import numpy as np\n",
"\n",
"# VO2 Max Master Chart Data\n",
"VO2_MASTER_CHART = {\n",
" 'male': {\n",
" '20-29': {\n",
" 'Very Poor': (29.0, 38.1),\n",
" 'Poor': (38.1, 44.9),\n",
" 'Fair': (44.9, 50.2),\n",
" 'Good': (50.2, 61.8),\n",
" 'Excellent': (57.1, 66.3),\n",
" 'Superior': (66.3, None) # None means open-ended\n",
" },\n",
" '30-39': {\n",
" 'Very Poor': (27.2, 34.1),\n",
" 'Poor': (34.1, 39.6),\n",
" 'Fair': (39.6, 45.2),\n",
" 'Good': (45.2, 51.6),\n",
" 'Excellent': (51.6, 59.8),\n",
" 'Superior': (59.8, None)\n",
" },\n",
" '40-49': {\n",
" 'Very Poor': (24.2, 30.5),\n",
" 'Poor': (30.5, 35.7),\n",
" 'Fair': (35.7, 40.3),\n",
" 'Good': (40.3, 46.7),\n",
" 'Excellent': (46.7, 55.6),\n",
" 'Superior': (55.6, None)\n",
" },\n",
" '50-59': {\n",
" 'Very Poor': (20.9, 26.1),\n",
" 'Poor': (26.1, 30.7),\n",
" 'Fair': (30.7, 35.1),\n",
" 'Good': (35.1, 41.2),\n",
" 'Excellent': (41.2, 50.7),\n",
" 'Superior': (50.7, None)\n",
" },\n",
" '60-69': {\n",
" 'Very Poor': (17.4, 22.4),\n",
" 'Poor': (22.4, 26.6),\n",
" 'Fair': (26.6, 30.5),\n",
" 'Good': (30.5, 36.1),\n",
" 'Excellent': (36.1, 43.0),\n",
" 'Superior': (43.0, None)\n",
" }\n",
" },\n",
" 'female': {\n",
" '20-29': {\n",
" 'Very Poor': (21.7, 28.6),\n",
" 'Poor': (28.6, 34.6),\n",
" 'Fair': (34.6, 40.6),\n",
" 'Good': (40.6, 46.5),\n",
" 'Excellent': (46.5, 56.0),\n",
" 'Superior': (56.0, None)\n",
" },\n",
" '30-39': {\n",
" 'Very Poor': (19.0, 24.1),\n",
" 'Poor': (24.1, 28.2),\n",
" 'Fair': (28.2, 32.2),\n",
" 'Good': (32.2, 35.7),\n",
" 'Excellent': (35.7, 45.8),\n",
" 'Superior': (45.8, None)\n",
" },\n",
" '40-49': {\n",
" 'Very Poor': (17.0, 21.3),\n",
" 'Poor': (21.3, 24.9),\n",
" 'Fair': (24.9, 28.7),\n",
" 'Good': (28.7, 34.0),\n",
" 'Excellent': (34.0, 41.7),\n",
" 'Superior': (41.7, None)\n",
" },\n",
" '50-59': {\n",
" 'Very Poor': (16.0, 19.1),\n",
" 'Poor': (19.1, 24.4),\n",
" 'Fair': (21.8, 27.6),\n",
" 'Good': (25.2, 28.6),\n",
" 'Excellent': (28.6, 35.9),\n",
" 'Superior': (35.9, None)\n",
" },\n",
" '60-69': {\n",
" 'Very Poor': (13.4, 16.5),\n",
" 'Poor': (16.5, 18.9),\n",
" 'Fair': (18.9, 21.2),\n",
" 'Good': (21.2, 24.6),\n",
" 'Excellent': (24.6, 29.4),\n",
" 'Superior': (29.4, None)\n",
" }\n",
" }\n",
"}\n",
"\n",
"def get_age_range(age):\n",
" \"\"\"Determine age range from age in years.\"\"\"\n",
" if 20 <= age <= 29:\n",
" return '20-29'\n",
" elif 30 <= age <= 39:\n",
" return '30-39'\n",
" elif 40 <= age <= 49:\n",
" return '40-49'\n",
" elif 50 <= age <= 59:\n",
" return '50-59'\n",
" elif 60 <= age <= 69:\n",
" return '60-69'\n",
" else:\n",
" # Default to closest range\n",
" if age < 20:\n",
" return '20-29'\n",
" elif age >= 70:\n",
" return '60-69'\n",
" else:\n",
" return '30-39' # fallback\n",
"\n",
"def determine_category(vo2_max, age_range, gender):\n",
" \"\"\"Determine VO2 max category based on value, age, and gender.\"\"\"\n",
" gender_key = 'male' if gender.lower().startswith('m') else 'female'\n",
" ranges = VO2_MASTER_CHART[gender_key][age_range]\n",
" \n",
" categories = ['Very Poor', 'Poor', 'Fair', 'Good', 'Excellent', 'Superior']\n",
" \n",
" # Check Superior category first (open-ended)\n",
" min_val, max_val = ranges['Superior']\n",
" if max_val is None and vo2_max >= min_val:\n",
" print(f\"It is always superior\")\n",
" return 'Superior'\n",
" \n",
" # Check other categories from Excellent down to Very Poor\n",
" # Ranges are typically [min, max) - inclusive of min, exclusive of max\n",
" # But when value equals max, it belongs to the next category\n",
" for category in reversed(categories[:-1]): # Exclude Superior as we already checked it\n",
" min_val, max_val = ranges[category]\n",
" # Check if value falls in this range (inclusive of min, exclusive of max)\n",
" if min_val <= vo2_max < max_val:\n",
" return category\n",
" \n",
" # If value is below all ranges, return Very Poor\n",
" # This handles the case where vo2_max < min of Very Poor\n",
" return 'Very Poor'\n",
"\n",
"def format_range(min_val, max_val):\n",
" \"\"\"Format range as string.\"\"\"\n",
" if max_val is None:\n",
" return f\"{min_val}+\"\n",
" else:\n",
" return f\"{min_val}-{max_val}\"\n",
"\n",
"def generate_vo2_max_table(age, gender, vo2_max_value, save_path=None):\n",
" \"\"\"\n",
" Generate VO2 Max table with indicator arrow.\n",
" \n",
" Args:\n",
" age: Patient age in years\n",
" gender: 'male' or 'female' (or 'm'/'f')\n",
" vo2_max_value: Patient's VO2 max value\n",
" save_path: Optional path to save the figure\n",
" \n",
" Returns:\n",
" fig, ax: matplotlib figure and axes\n",
" \"\"\"\n",
" # Determine age range and category\n",
" age_range = get_age_range(age)\n",
" category = determine_category(vo2_max_value, age_range, gender)\n",
" \n",
" # Debug: print the determined category\n",
" print(f\"Age: {age}, Gender: {gender}, VO2 Max: {vo2_max_value}\")\n",
" print(f\"Age Range: {age_range}, Category: {category}\")\n",
" \n",
" # Get the appropriate data\n",
" gender_key = 'male' if gender.lower().startswith('m') else 'female'\n",
" ranges = VO2_MASTER_CHART[gender_key][age_range]\n",
" \n",
" # Prepare table data\n",
" headers = ['Age', 'Very Poor', 'Poor', 'Fair', 'Good', 'Excellent', 'Superior']\n",
" age_label = f\"{age_range} ({gender[0].upper()})\"\n",
" \n",
" row_data = [age_label]\n",
" for cat in ['Very Poor', 'Poor', 'Fair', 'Good', 'Excellent', 'Superior']:\n",
" min_val, max_val = ranges[cat]\n",
" row_data.append(format_range(min_val, max_val))\n",
" \n",
" # Create figure\n",
" fig, ax = plt.subplots(figsize=(14, 3))\n",
" ax.axis('off')\n",
" \n",
" # Create table\n",
" table_data = [headers, row_data]\n",
" table = ax.table(cellText=table_data,\n",
" cellLoc='center',\n",
" loc='center',\n",
" bbox=[0, 0, 1, 1])\n",
" \n",
" # Style the table\n",
" table.auto_set_font_size(False)\n",
" table.set_fontsize(11)\n",
" table.scale(1, 2.5)\n",
" \n",
" # Header row styling (cyan background)\n",
" for i in range(len(headers)):\n",
" cell = table[(0, i)]\n",
" cell.set_facecolor('#7dd3fc') # cyan-300 equivalent\n",
" cell.set_text_props(weight='bold', color='black')\n",
" cell.set_edgecolor('#9ca3af') # gray-400\n",
" cell.set_linewidth(1)\n",
" \n",
" # Find the column index for the category (before styling)\n",
" category_index = headers.index(category)\n",
" \n",
" # Data row styling\n",
" for i in range(len(row_data)):\n",
" cell = table[(1, i)]\n",
" if i == 0: # Age column\n",
" cell.set_facecolor('#a5f3fc') # cyan-200\n",
" cell.set_text_props(weight='semibold', color='black')\n",
" else:\n",
" cell.set_facecolor('#f3f4f6') # gray-100\n",
" cell.set_text_props(color='black')\n",
" # Bold the cell that corresponds to the patient's category\n",
" if i == category_index:\n",
" cell.set_text_props(weight='bold', color='black')\n",
" cell.set_edgecolor('#9ca3af') # gray-400\n",
" cell.set_linewidth(1)\n",
" \n",
" # Add arrow indicator below the category column\n",
" # Calculate position\n",
" cell_width = 1.0 / len(headers)\n",
" arrow_x = (category_index + 0.5) * cell_width\n",
" \n",
" # Draw arrow pointing up\n",
" arrow = patches.FancyArrowPatch(\n",
" (arrow_x, -0.15), (arrow_x, -0.05),\n",
" arrowstyle='->', mutation_scale=20, \n",
" linewidth=2, color='black',\n",
" transform=ax.transAxes\n",
" )\n",
" ax.add_patch(arrow)\n",
" \n",
" # Add triangle at the top\n",
" triangle = patches.RegularPolygon(\n",
" (arrow_x, -0.05), 3, radius=0.02,\n",
" orientation=np.pi/2, color='black',\n",
" transform=ax.transAxes\n",
" )\n",
" ax.add_patch(triangle)\n",
" \n",
" # Set title - calculate approximate percentile\n",
" # For Superior category, use 100th percentile as shown in template\n",
" if category == 'Superior':\n",
" percentile = '100th percentile'\n",
" else:\n",
" percentile_map = {\n",
" 'Very Poor': '1st-10th percentile',\n",
" 'Poor': '10th-20th percentile',\n",
" 'Fair': '20th-40th percentile',\n",
" 'Good': '40th-60th percentile',\n",
" 'Excellent': '60th-80th percentile'\n",
" }\n",
" percentile = percentile_map.get(category, 'N/A')\n",
" \n",
" title = f\"VO2 Max - {vo2_max_value:.1f} ({percentile})\"\n",
" ax.set_title(title, fontsize=14, fontweight='bold', pad=20)\n",
" \n",
" plt.tight_layout()\n",
" \n",
" if save_path:\n",
" plt.savefig(save_path, dpi=300, bbox_inches='tight')\n",
" \n",
" return fig, ax\n",
"\n",
"# Test the function\n",
"# Example: 30-year-old female with VO2 max of 49.5\n",
"fig, ax = generate_vo2_max_table(\n",
" age=45,\n",
" gender='male',\n",
" vo2_max_value=35.5,\n",
" save_path=f'{base_dir}/graphs/vo2_max_table.png'\n",
")\n",
"plt.show()\n"
]
},
2025-11-08 19:49:57 +01:00
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 18,
2025-11-21 11:22:13 +01:00
"id": "c46b53f0",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Age: 50, Gender: male, Resting Heart Rate: 76\n",
"Age Range: 46-55, Category: Average\n"
]
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1600x300 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Resting Heart Rate Master Chart Data\n",
"RHR_MASTER_CHART = {\n",
" 'male': {\n",
" '18-25': {\n",
" 'Poor': (85, None), # 85bpm+\n",
" 'Below Average': (79, 85),\n",
" 'Average': (74, 79),\n",
" 'Above Average': (70, 74),\n",
" 'Good': (66, 70),\n",
" 'Excellent': (61, 66),\n",
" 'Athlete': (40, 61)\n",
" },\n",
" '26-35': {\n",
" 'Poor': (83, None), # 83bpm+\n",
" 'Below Average': (77, 83),\n",
" 'Average': (73, 77),\n",
" 'Above Average': (69, 73),\n",
" 'Good': (65, 69),\n",
" 'Excellent': (60, 65),\n",
" 'Athlete': (42, 60)\n",
" },\n",
" '36-45': {\n",
" 'Poor': (85, None), # 85bpm+\n",
" 'Below Average': (79, 85),\n",
" 'Average': (74, 79),\n",
" 'Above Average': (70, 74),\n",
" 'Good': (65, 70),\n",
" 'Excellent': (60, 65),\n",
" 'Athlete': (45, 60)\n",
" },\n",
" '46-55': {\n",
" 'Poor': (84, None), # 84bpm+\n",
" 'Below Average': (78, 84),\n",
" 'Average': (74, 78),\n",
" 'Above Average': (70, 74),\n",
" 'Good': (66, 70),\n",
" 'Excellent': (61, 66),\n",
" 'Athlete': (48, 61)\n",
" },\n",
" '56-65': {\n",
" 'Poor': (84, None), # 84bpm+\n",
" 'Below Average': (78, 84),\n",
" 'Average': (74, 78),\n",
" 'Above Average': (70, 74),\n",
" 'Good': (65, 70),\n",
" 'Excellent': (60, 65),\n",
" 'Athlete': (50, 60)\n",
" },\n",
" '65+': {\n",
" 'Poor': (84, None), # 84bpm+\n",
" 'Below Average': (77, 84),\n",
" 'Average': (73, 77),\n",
" 'Above Average': (70, 73),\n",
" 'Good': (65, 70),\n",
" 'Excellent': (60, 65),\n",
" 'Athlete': (52, 60)\n",
" }\n",
" },\n",
" 'female': {\n",
" '18-25': {\n",
" 'Poor': (82, None), # 82bpm+\n",
" 'Below Average': (74, 82),\n",
" 'Average': (70, 74),\n",
" 'Above Average': (66, 70),\n",
" 'Good': (62, 66),\n",
" 'Excellent': (56, 62),\n",
" 'Athlete': (40, 56)\n",
" },\n",
" '26-35': {\n",
" 'Poor': (82, None), # 82bpm+\n",
" 'Below Average': (75, 82),\n",
" 'Average': (71, 75),\n",
" 'Above Average': (66, 71),\n",
" 'Good': (62, 66),\n",
" 'Excellent': (55, 62),\n",
" 'Athlete': (44, 55)\n",
" },\n",
" '36-45': {\n",
" 'Poor': (83, None), # 83bpm+\n",
" 'Below Average': (76, 83),\n",
" 'Average': (71, 76),\n",
" 'Above Average': (67, 71),\n",
" 'Good': (63, 67),\n",
" 'Excellent': (57, 63),\n",
" 'Athlete': (47, 57)\n",
" },\n",
" '46-55': {\n",
" 'Poor': (84, None), # 84bpm+\n",
" 'Below Average': (77, 84),\n",
" 'Average': (72, 77),\n",
" 'Above Average': (68, 72),\n",
" 'Good': (64, 68),\n",
" 'Excellent': (58, 64),\n",
" 'Athlete': (49, 58)\n",
" },\n",
" '56-65': {\n",
" 'Poor': (82, None), # 82bpm+\n",
" 'Below Average': (76, 82),\n",
" 'Average': (72, 76),\n",
" 'Above Average': (68, 72),\n",
" 'Good': (62, 68),\n",
" 'Excellent': (57, 62),\n",
" 'Athlete': (51, 57)\n",
" },\n",
" '65+': {\n",
" 'Poor': (80, None), # 80bpm+\n",
" 'Below Average': (74, 80),\n",
" 'Average': (70, 74),\n",
" 'Above Average': (66, 70),\n",
" 'Good': (62, 66),\n",
" 'Excellent': (56, 62),\n",
" 'Athlete': (52, 56)\n",
" }\n",
" }\n",
"}\n",
"\n",
"def get_rhr_age_range(age):\n",
" \"\"\"Determine age range from age in years for resting heart rate.\"\"\"\n",
" if 18 <= age <= 25:\n",
" return '18-25'\n",
" elif 26 <= age <= 35:\n",
" return '26-35'\n",
" elif 36 <= age <= 45:\n",
" return '36-45'\n",
" elif 46 <= age <= 55:\n",
" return '46-55'\n",
" elif 56 <= age <= 65:\n",
" return '56-65'\n",
" else:\n",
" # Default to closest range\n",
" if age < 18:\n",
" return '18-25'\n",
" elif age > 65:\n",
" return '65+'\n",
" else:\n",
" return '26-35' # fallback\n",
"\n",
"def determine_rhr_category(rhr, age_range, gender):\n",
" \"\"\"Determine resting heart rate category based on value, age, and gender.\"\"\"\n",
" gender_key = 'male' if gender.lower().startswith('m') else 'female'\n",
" ranges = RHR_MASTER_CHART[gender_key][age_range]\n",
" \n",
" categories = ['Poor', 'Below Average', 'Average', 'Above Average', 'Good', 'Excellent', 'Athlete']\n",
" \n",
" # Check Poor category first (open-ended at top)\n",
" min_val, max_val = ranges['Poor']\n",
" if max_val is None and rhr >= min_val:\n",
" return 'Poor'\n",
" \n",
" # Check other categories from Below Average down to Athlete\n",
" # For RHR, lower is better, so we check from highest to lowest\n",
" for category in ['Below Average', 'Average', 'Above Average', 'Good', 'Excellent', 'Athlete']:\n",
" min_val, max_val = ranges[category]\n",
" # Check if value falls in this range (inclusive of min, exclusive of max)\n",
" if min_val <= rhr < max_val:\n",
" return category\n",
" \n",
" # If value is below all ranges (below Athlete minimum), return Athlete\n",
" # This handles the case where rhr < min of Athlete\n",
" return 'Athlete'\n",
"\n",
"def format_rhr_range(min_val, max_val):\n",
" \"\"\"Format RHR range as string.\"\"\"\n",
" if max_val is None:\n",
" return f\"{min_val}bpm +\"\n",
" else:\n",
" return f\"{min_val}-{max_val}bpm\"\n",
"\n",
"def generate_resting_heart_rate_table(age, gender, rhr_value, save_path=None):\n",
" \"\"\"\n",
" Generate Resting Heart Rate table with indicator arrow.\n",
" \n",
" Args:\n",
" age: Patient age in years\n",
" gender: 'male' or 'female' (or 'm'/'f')\n",
" rhr_value: Patient's resting heart rate value in bpm\n",
" save_path: Optional path to save the figure\n",
" \n",
" Returns:\n",
" fig, ax: matplotlib figure and axes\n",
" \"\"\"\n",
" # Determine age range and category\n",
" age_range = get_rhr_age_range(age)\n",
" category = determine_rhr_category(rhr_value, age_range, gender)\n",
" \n",
" # Debug: print the determined category\n",
" print(f\"Age: {age}, Gender: {gender}, Resting Heart Rate: {rhr_value}\")\n",
" print(f\"Age Range: {age_range}, Category: {category}\")\n",
" \n",
" # Get the appropriate data\n",
" gender_key = 'male' if gender.lower().startswith('m') else 'female'\n",
" ranges = RHR_MASTER_CHART[gender_key][age_range]\n",
" \n",
" # Prepare table data\n",
" headers = ['Age', 'Poor', 'Below Average', 'Average', 'Above Average', 'Good', 'Excellent', 'Athlete']\n",
" age_label = f\"{age_range} ({gender[0].upper()})\"\n",
" \n",
" row_data = [age_label]\n",
" for cat in ['Poor', 'Below Average', 'Average', 'Above Average', 'Good', 'Excellent', 'Athlete']:\n",
" min_val, max_val = ranges[cat]\n",
" row_data.append(format_rhr_range(min_val, max_val))\n",
" \n",
" # Create figure\n",
" fig, ax = plt.subplots(figsize=(16, 3))\n",
" ax.axis('off')\n",
" \n",
" # Create table\n",
" table_data = [headers, row_data]\n",
" table = ax.table(cellText=table_data,\n",
" cellLoc='center',\n",
" loc='center',\n",
" bbox=[0, 0, 1, 1])\n",
" \n",
" # Style the table\n",
" table.auto_set_font_size(False)\n",
" table.set_fontsize(11)\n",
" table.scale(1, 2.5)\n",
" \n",
" # Header row styling (cyan background)\n",
" for i in range(len(headers)):\n",
" cell = table[(0, i)]\n",
" cell.set_facecolor('#7dd3fc') # cyan-300 equivalent\n",
" cell.set_text_props(weight='bold', color='black')\n",
" cell.set_edgecolor('#9ca3af') # gray-400\n",
" cell.set_linewidth(1)\n",
" \n",
" # Find the column index for the category (before styling)\n",
" category_index = headers.index(category)\n",
" \n",
" # Data row styling\n",
" for i in range(len(row_data)):\n",
" cell = table[(1, i)]\n",
" if i == 0: # Age column\n",
" cell.set_facecolor('#a5f3fc') # cyan-200\n",
" cell.set_text_props(weight='semibold', color='black')\n",
" else:\n",
" # Highlight the category cell with light green background\n",
" if i == category_index:\n",
" cell.set_facecolor('#d1fae5') # green-200 equivalent\n",
" cell.set_text_props(weight='bold', color='black')\n",
" else:\n",
" cell.set_facecolor('#f3f4f6') # gray-100\n",
" cell.set_text_props(color='black')\n",
" cell.set_edgecolor('#9ca3af') # gray-400\n",
" cell.set_linewidth(1)\n",
" \n",
" # Add arrow indicator below the category column\n",
" # Calculate position\n",
" cell_width = 1.0 / len(headers)\n",
" arrow_x = (category_index + 0.5) * cell_width\n",
" \n",
" # Draw arrow pointing up\n",
" arrow = patches.FancyArrowPatch(\n",
" (arrow_x, -0.15), (arrow_x, -0.05),\n",
" arrowstyle='->', mutation_scale=20, \n",
" linewidth=2, color='black',\n",
" transform=ax.transAxes\n",
" )\n",
" ax.add_patch(arrow)\n",
" \n",
" # Add triangle at the top\n",
" triangle = patches.RegularPolygon(\n",
" (arrow_x, -0.05), 3, radius=0.02,\n",
" orientation=np.pi/2, color='black',\n",
" transform=ax.transAxes\n",
" )\n",
" ax.add_patch(triangle)\n",
" \n",
" # Set title\n",
" title = f\"Resting Heart Rate - {rhr_value:.0f}bpm\"\n",
" ax.set_title(title, fontsize=14, fontweight='bold', pad=20)\n",
" \n",
" plt.tight_layout()\n",
" \n",
" if save_path:\n",
" plt.savefig(save_path, dpi=300, bbox_inches='tight')\n",
" \n",
" return fig, ax\n",
"\n",
"# Test the function\n",
"# Example: 30-year-old female with resting heart rate of 53bpm\n",
"fig, ax = generate_resting_heart_rate_table(\n",
" age=50,\n",
" gender='male',\n",
" rhr_value=76,\n",
" save_path=f'{base_dir}/graphs/resting_heart_rate_table.png'\n",
")\n",
"plt.show()\n"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 19,
2025-11-17 17:15:44 +01:00
"id": "84addc63",
2025-11-08 19:49:57 +01:00
"metadata": {},
2025-11-21 10:01:07 +01:00
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"==================================================\n",
"Optimal Fat Burning Zone (highest fat:carb ratio):\n",
"Time: 164.0 seconds\n",
"Fat burn rate: 3.894 kcal/min\n",
"Carb burn rate: 1.575 kcal/min\n",
"Fat:Carb ratio: 2.47\n",
"Heart Rate: 96.7 bpm\n",
"VO2: 1147.9 ml/min\n",
2025-11-24 17:52:56 +01:00
"VT1: {'HeartRate': 100.5, 'Speed': 4.0, 'Time': 251.0}\n",
"VT2: {'HeartRate': 189.71300000000002, 'Speed': 7.5, 'Time': 1524.0}\n",
2025-11-21 10:01:07 +01:00
"Zone 1 (Active Recovery): 81.7 - 96.7 bpm\n",
"Zone 2 (Aerobic Base): 96.7 - 100.5 bpm\n",
"Zone 3 (Aerobic): 100.5 - 179.7 bpm\n",
"Zone 4 (Lactate Threshold): 179.7 - 189.7 bpm\n",
"Zone 5 (VO2 Max): 189.7 - 199.7 bpm\n"
]
}
],
"source": [
"\n",
"# Find the point where fat burning is highest and carb burning is lowest\n",
"# Using the smoothed data for more stable results\n",
"fat_burn_max_idx = df['FAT_smoothed'].idxmax()\n",
"carb_burn_min_idx = df['CHO_smoothed'].idxmin()\n",
"\n",
"\n",
"print(\"\\n\" + \"=\"*50)\n",
"\n",
"# Find the optimal fat burning zone (highest fat:carb ratio)\n",
"df['fat_carb_ratio'] = df['FAT_smoothed'] / (df['CHO_smoothed'] + 0.00000001) # Add small value to avoid division by zero\n",
"optimal_fat_idx = df['fat_carb_ratio'].idxmax()\n",
"optimal_row = df.loc[optimal_fat_idx]\n",
"\n",
"print(\"Optimal Fat Burning Zone (highest fat:carb ratio):\")\n",
"print(f\"Time: {optimal_row['T(sec)']} seconds\")\n",
"print(f\"Fat burn rate: {optimal_row['FAT_smoothed']:.3f} kcal/min\")\n",
"print(f\"Carb burn rate: {optimal_row['CHO_smoothed']:.3f} kcal/min\")\n",
"print(f\"Fat:Carb ratio: {optimal_row['fat_carb_ratio']:.2f}\")\n",
"print(f\"Heart Rate: {optimal_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
"print(f\"VO2: {optimal_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
"\n",
"def detect_vt1(df, fat_col=\"FAT_smoothed\", carb_col=\"CHO_smoothed\"):\n",
" \"\"\"\n",
" Detect VT1 as the first index where carb burn > fat burn and remains higher.\n",
" \"\"\"\n",
" condition = df[carb_col] > df[fat_col]\n",
" crossover_indices = condition[condition].index\n",
"\n",
" if len(crossover_indices) == 0:\n",
" return None # No crossover found\n",
" \n",
" # Find first crossover where carbs remain higher for the rest\n",
" for idx in crossover_indices:\n",
" if all(df.loc[idx:][carb_col] > df.loc[idx:][fat_col]):\n",
" return idx\n",
" return None\n",
"\n",
"\n",
"def detect_vt2(df, vent_col=\"VE(l/min)_smoothed\", bf_col=\"BF(bpm)_smoothed\", smooth_window=5):\n",
" \"\"\"\n",
" Detect VT2 using slope/inflection method.\n",
" Works with either Ventilation (VE) or Breathing Frequency (Bf).\n",
" \"\"\"\n",
" col = vent_col if vent_col in df.columns else bf_col\n",
" \n",
" # Use already smoothed data\n",
" smoothed_col = col\n",
" \n",
" # Compute slope (first derivative)\n",
" df[\"slope\"] = df[smoothed_col].diff()\n",
" \n",
" # Detect inflection: largest change in slope (second derivative peak)\n",
" df[\"second_derivative\"] = df[\"slope\"].diff()\n",
" inflection_idx = df[\"second_derivative\"].idxmax()\n",
" \n",
" return inflection_idx\n",
"\n",
"\n",
"def analyze_thresholds(df_input):\n",
" # Use the existing dataframe\n",
" df_copy = df_input.copy()\n",
" \n",
" # --- Detect VT1 ---\n",
" vt1_idx = detect_vt1(df_copy)\n",
" vt1 = None\n",
" if vt1_idx is not None:\n",
" vt1 = {\n",
" \"HeartRate\": df_copy.loc[vt1_idx, \"HR(bpm)_smoothed\"],\n",
" \"Speed\": df_copy.loc[vt1_idx, \"Speed\"],\n",
" \"Time\": df_copy.loc[vt1_idx, \"T(sec)\"]\n",
" }\n",
" \n",
" # --- Detect VT2 ---\n",
" vt2_idx = detect_vt2(df_copy)\n",
" vt2 = None\n",
" if vt2_idx is not None:\n",
" vt2 = {\n",
" \"HeartRate\": df_copy.loc[vt2_idx, \"HR(bpm)_smoothed\"],\n",
" \"Speed\": df_copy.loc[vt2_idx, \"Speed\"],\n",
" \"Time\": df_copy.loc[vt2_idx, \"T(sec)\"]\n",
" }\n",
" \n",
" return vt1, vt2\n",
"\n",
"\n",
"vt1, vt2 = analyze_thresholds(df)\n",
"print(\"VT1:\", vt1)\n",
"print(\"VT2:\", vt2)\n",
"zone_1_start = optimal_row['HR(bpm)_smoothed'] - 15\n",
"zone_2_start = optimal_row['HR(bpm)_smoothed']\n",
"zone_3_start = vt1\n",
"zone_4_start = vt2['HeartRate'] - 10\n",
"zone_5_start = vt2['HeartRate']\n",
"zone_5_end = vt2['HeartRate'] + 10\n",
"\n",
"zone_1_end = zone_2_start\n",
"zone_2_end = vt1['HeartRate']\n",
"zone_3_end = zone_4_start\n",
"zone_4_end = zone_5_start\n",
"\n",
"print(f\"Zone 1 (Active Recovery): {zone_1_start:.1f} - {zone_1_end:.1f} bpm\")\n",
"print(f\"Zone 2 (Aerobic Base): {zone_2_start:.1f} - {zone_2_end:.1f} bpm\")\n",
"print(f\"Zone 3 (Aerobic): {zone_3_start['HeartRate']:.1f} - {zone_3_end:.1f} bpm\")\n",
"print(f\"Zone 4 (Lactate Threshold): {zone_4_start:.1f} - {zone_4_end:.1f} bpm\")\n",
"print(f\"Zone 5 (VO2 Max): {zone_5_start:.1f} - {zone_5_end:.1f} bpm\")"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 20,
2025-11-21 10:01:07 +01:00
"id": "f324fe94",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 11:04:23 +01:00
"image/png": "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
2025-11-21 10:01:07 +01:00
"text/plain": [
2025-11-21 11:04:23 +01:00
"<Figure size 1200x800 with 1 Axes>"
2025-11-21 10:01:07 +01:00
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
2025-11-21 11:04:23 +01:00
"import math\n",
"\n",
2025-11-21 10:01:07 +01:00
"# Define the fixed rows\n",
"descriptions = [\n",
" \"Improves health and\\nrecovery capacity\",\n",
" \"Improves endurance\\nand fat burning\",\n",
" \"Improves Aerobic\\nfitness\",\n",
" \"Improves maximum\\nperformance capacity\",\n",
" \"Develops maximum\\nperformance and speed\"\n",
"]\n",
"\n",
"hr_percentages = [\n",
" \"55-65% of Max Heart Rate\",\n",
" \"65-75% of Max Heart Rate\",\n",
" \"80-85% of Max Heart Rate\",\n",
" \"85-88% of Max Heart Rate\",\n",
2025-11-21 10:34:09 +01:00
" \"90%+ of Max Heart Rate\"\n",
2025-11-21 10:01:07 +01:00
"]\n",
"\n",
2025-11-21 10:12:00 +01:00
"ideal_breath_ranges = [\n",
" \"Ideal Range: 15-20 breaths\",\n",
" \"Ideal Range: 20-25 breaths\",\n",
" \"Ideal Range: 25-30 breaths\",\n",
" \"Ideal Range: 30-35 breaths\",\n",
" \"Ideal Range: 40+ breaths\"\n",
"]\n",
"\n",
2025-11-21 10:01:07 +01:00
"# Define the zones for iteration\n",
"# Note: zone_3_start is a dictionary (vt1), others are floats\n",
2025-11-21 11:04:23 +01:00
"# Use math.floor to ensure values match the exact integer values requested\n",
2025-11-21 10:01:07 +01:00
"zones_list = [\n",
2025-11-21 11:04:23 +01:00
" (\"Zone 1\", math.floor(zone_1_start), math.floor(zone_1_end)),\n",
" (\"Zone 2\", math.floor(zone_2_start), math.floor(zone_2_end)),\n",
" (\"Zone 3\", math.floor(zone_3_start['HeartRate']), math.floor(zone_3_end)),\n",
" (\"Zone 4\", math.floor(zone_4_start), math.floor(zone_4_end)),\n",
" (\"Zone 5\", math.floor(zone_5_start), math.floor(zone_5_end))\n",
2025-11-21 10:01:07 +01:00
"]\n",
"\n",
"# Calculate metrics for each zone\n",
"zone_metrics = {\n",
" \"HR BPM\": [],\n",
" \"Speed\": [],\n",
" \"Pace\": [],\n",
" \"Calories\": [],\n",
" \"Carb Utilization\": [],\n",
" \"Breathing\": []\n",
"}\n",
"\n",
2025-11-21 10:12:00 +01:00
"for i, (name, start, end) in enumerate(zones_list):\n",
2025-11-21 10:01:07 +01:00
" # Filter dataframe for the current zone\n",
" mask = (df['HR(bpm)_smoothed'] >= start) & (df['HR(bpm)_smoothed'] <= end)\n",
" zone_df = df[mask]\n",
" \n",
" # HR BPM Range\n",
" zone_metrics[\"HR BPM\"].append(f\"{int(start)}-{int(end)} bpm\")\n",
" \n",
" if not zone_df.empty:\n",
2025-11-21 10:12:00 +01:00
" # Speed (Range)\n",
2025-11-21 10:34:09 +01:00
" # df['Speed'] is in mph\n",
" # Filter out 0 speeds for range calculation\n",
" speed_series = zone_df[zone_df['Speed'] > 0.1]['Speed']\n",
2025-11-21 10:12:00 +01:00
" \n",
2025-11-21 10:34:09 +01:00
" if not speed_series.empty:\n",
" min_speed = speed_series.min()\n",
" max_speed = speed_series.max()\n",
" \n",
" if abs(min_speed - max_speed) < 0.1:\n",
" zone_metrics[\"Speed\"].append(f\"{min_speed:.1f} mph\\n2% Incline\")\n",
" else:\n",
" zone_metrics[\"Speed\"].append(f\"{min_speed:.1f}-{max_speed:.1f} mph\\n2% Incline\")\n",
" \n",
" # Pace (Range)\n",
" # Higher speed = Lower pace value (faster)\n",
" # So max_speed -> min_pace_val, min_speed -> max_pace_val\n",
" \n",
" def speed_to_pace(s_mph):\n",
" if s_mph <= 0: return 0, 0\n",
" s_kmh = s_mph * 1.60934\n",
" p_min = 60 / s_kmh\n",
" p_m = int(p_min)\n",
" p_s = int((p_min % 1) * 60)\n",
" return p_m, p_s\n",
"\n",
" min_pace_m, min_pace_s = speed_to_pace(max_speed) # Fastest speed -> Lowest pace number\n",
" max_pace_m, max_pace_s = speed_to_pace(min_speed) # Slowest speed -> Highest pace number\n",
" \n",
" if min_pace_m == max_pace_m and min_pace_s == max_pace_s:\n",
" pace_str = f\"{min_pace_m}:{min_pace_s:02d} min/km Pace\"\n",
" else:\n",
" pace_str = f\"{max_pace_m}:{max_pace_s:02d}-{min_pace_m}:{min_pace_s:02d}\\nmin/km Pace\"\n",
2025-11-21 10:12:00 +01:00
" zone_metrics[\"Pace\"].append(pace_str)\n",
2025-11-21 10:01:07 +01:00
" else:\n",
2025-11-21 10:34:09 +01:00
" zone_metrics[\"Speed\"].append(\"-\\n2% Incline\")\n",
2025-11-21 10:01:07 +01:00
" zone_metrics[\"Pace\"].append(\"-\")\n",
" \n",
2025-11-21 10:34:09 +01:00
" # Calories (EE)\n",
" # Use raw EE(kcal/min) instead of smoothed sum of FAT+CHO\n",
" avg_cals = zone_df['EE(kcal/min)'].mean()\n",
2025-11-21 10:12:00 +01:00
" zone_metrics[\"Calories\"].append(f\"Avg:\\n{avg_cals:.1f} kcals/minute\")\n",
2025-11-21 10:01:07 +01:00
" \n",
" # Carb Utilization (g/min)\n",
2025-11-21 10:34:09 +01:00
" # Use raw CHO\n",
" avg_carbs_g = zone_df['CHO'].mean() / 4\n",
2025-11-21 10:12:00 +01:00
" zone_metrics[\"Carb Utilization\"].append(f\"Avg: {avg_carbs_g:.1f}g/min\\nCarb Utilization\")\n",
2025-11-21 10:01:07 +01:00
" \n",
" # Breathing (BF)\n",
" avg_breaths = zone_df['BF(bpm)_smoothed'].mean()\n",
2025-11-21 10:12:00 +01:00
" ideal_range = ideal_breath_ranges[i]\n",
" zone_metrics[\"Breathing\"].append(f\"Avg: {int(avg_breaths)} breaths\\n{ideal_range}\")\n",
2025-11-21 10:01:07 +01:00
" \n",
" else:\n",
2025-11-21 10:12:00 +01:00
" zone_metrics[\"Speed\"].append(\"-\\n2% Incline\")\n",
2025-11-21 10:01:07 +01:00
" zone_metrics[\"Pace\"].append(\"-\")\n",
" zone_metrics[\"Calories\"].append(\"-\")\n",
" zone_metrics[\"Carb Utilization\"].append(\"-\")\n",
2025-11-21 10:12:00 +01:00
" zone_metrics[\"Breathing\"].append(f\"-\\n{ideal_breath_ranges[i]}\")\n",
2025-11-21 10:01:07 +01:00
"\n",
"# Prepare data for the table\n",
"table_data = []\n",
"table_data.append(descriptions)\n",
"table_data.append(hr_percentages)\n",
"table_data.append(zone_metrics[\"HR BPM\"])\n",
"table_data.append(zone_metrics[\"Speed\"])\n",
"table_data.append(zone_metrics[\"Pace\"])\n",
"table_data.append(zone_metrics[\"Calories\"])\n",
"table_data.append(zone_metrics[\"Carb Utilization\"])\n",
"table_data.append(zone_metrics[\"Breathing\"])\n",
"\n",
"col_labels = [\"Zone 1\", \"Zone 2\", \"Zone 3\", \"Zone 4\", \"Zone 5\"]\n",
"\n",
"# Create the table plot\n",
2025-11-21 11:04:23 +01:00
"fig, ax = plt.subplots(figsize=(12, 8)) # Reduced width from 16 to 12\n",
2025-11-21 10:01:07 +01:00
"ax.axis('off')\n",
"\n",
2025-11-21 10:12:00 +01:00
"# Create table without rowLabels\n",
2025-11-21 10:01:07 +01:00
"table = ax.table(cellText=table_data,\n",
" colLabels=col_labels,\n",
" loc='center',\n",
" cellLoc='center')\n",
"\n",
"table.auto_set_font_size(False)\n",
"table.set_fontsize(10)\n",
2025-11-21 10:12:00 +01:00
"table.scale(1, 3.5) # Increased vertical scale for multi-line text\n",
2025-11-21 10:01:07 +01:00
"\n",
"# Styling\n",
"# Header row\n",
"for j, label in enumerate(col_labels):\n",
" cell = table[(0, j)]\n",
" cell.set_facecolor('#7dd3fc') # cyan-300\n",
" cell.set_text_props(weight='bold')\n",
"\n",
"# Row specific styling\n",
"colors = ['#fecaca', '#fecaca', '#fef08a', '#bbf7d0', '#bbf7d0']\n",
"\n",
"for j in range(len(col_labels)):\n",
2025-11-21 10:12:00 +01:00
" # HR BPM row is at index 2 (0-based in data) -> row 3 in table (0 is header, 1 is desc, 2 is HR%, 3 is HR BPM)\n",
2025-11-21 10:01:07 +01:00
" cell = table[(3, j)] \n",
" cell.set_facecolor(colors[j])\n",
" cell.set_text_props(weight='bold')\n",
" \n",
" # Breathing row is at index 7 -> row 8 in table\n",
" cell = table[(8, j)]\n",
" cell.set_facecolor(colors[j])\n",
" cell.set_text_props(weight='bold')\n",
"\n",
2025-11-21 11:04:23 +01:00
"plt.title(\"Personalized Heart Rate Zones\", fontsize=16, fontweight='bold', pad=5) # Reduced pad from 20 to 5\n",
2025-11-21 10:01:07 +01:00
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-24 17:52:56 +01:00
"execution_count": 26,
2025-11-21 10:01:07 +01:00
"id": "9c00c366",
"metadata": {},
2025-11-24 17:52:56 +01: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>Timestamp (seconds passed)</th>\n",
" <th>Lap/Event</th>\n",
" <th>Unnamed: 2</th>\n",
" <th>SmO2</th>\n",
" <th>HBDiff</th>\n",
" <th>Muscle state</th>\n",
" <th>Muscle trend</th>\n",
" <th>SmO2 unfiltered</th>\n",
" <th>O2HB unfiltered</th>\n",
" <th>HHb unfiltered</th>\n",
" <th>...</th>\n",
" <th>SmO2.1</th>\n",
" <th>HBDiff.1</th>\n",
" <th>Muscle state.1</th>\n",
" <th>Muscle trend.1</th>\n",
" <th>SmO2 unfiltered.1</th>\n",
" <th>O2HB unfiltered.1</th>\n",
" <th>HHb unfiltered.1</th>\n",
" <th>THb unfiltered.1</th>\n",
" <th>HBDiff unfiltered.1</th>\n",
" <th>Heart Rate (BPM).1</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>0.37</td>\n",
" <td>0</td>\n",
" <td>NaN</td>\n",
" <td>72.40</td>\n",
" <td>-0.26</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>72.52</td>\n",
" <td>-3.27</td>\n",
" <td>-2.29</td>\n",
" <td>...</td>\n",
" <td>87.04</td>\n",
" <td>1.57</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>86.46</td>\n",
" <td>-0.64</td>\n",
" <td>-0.74</td>\n",
" <td>-20.30</td>\n",
" <td>1.78</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>0.46</td>\n",
" <td>0</td>\n",
" <td>NaN</td>\n",
" <td>72.41</td>\n",
" <td>-0.26</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>73.06</td>\n",
" <td>-3.27</td>\n",
" <td>-2.31</td>\n",
" <td>...</td>\n",
" <td>87.04</td>\n",
" <td>1.57</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>86.66</td>\n",
" <td>-0.66</td>\n",
" <td>-0.66</td>\n",
" <td>-20.24</td>\n",
" <td>1.68</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>0.57</td>\n",
" <td>0</td>\n",
" <td>NaN</td>\n",
" <td>72.41</td>\n",
" <td>-0.26</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>72.58</td>\n",
" <td>-3.31</td>\n",
" <td>-2.25</td>\n",
" <td>...</td>\n",
" <td>87.03</td>\n",
" <td>1.57</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>86.81</td>\n",
" <td>-0.69</td>\n",
" <td>-0.67</td>\n",
" <td>-20.28</td>\n",
" <td>1.66</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>0.66</td>\n",
" <td>0</td>\n",
" <td>NaN</td>\n",
" <td>72.42</td>\n",
" <td>-0.25</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>73.18</td>\n",
" <td>-3.29</td>\n",
" <td>-2.32</td>\n",
" <td>...</td>\n",
" <td>87.03</td>\n",
" <td>1.57</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>88.42</td>\n",
" <td>-0.70</td>\n",
" <td>-0.66</td>\n",
" <td>-20.28</td>\n",
" <td>1.64</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>0.75</td>\n",
" <td>0</td>\n",
" <td>NaN</td>\n",
" <td>72.43</td>\n",
" <td>-0.25</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>72.61</td>\n",
" <td>-3.23</td>\n",
" <td>-2.36</td>\n",
" <td>...</td>\n",
" <td>87.03</td>\n",
" <td>1.58</td>\n",
" <td>0</td>\n",
" <td>2</td>\n",
" <td>88.59</td>\n",
" <td>-0.51</td>\n",
" <td>-0.71</td>\n",
" <td>-20.14</td>\n",
" <td>1.88</td>\n",
" <td>72</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>5 rows × 24 columns</p>\n",
"</div>"
],
"text/plain": [
" Timestamp (seconds passed) Lap/Event Unnamed: 2 SmO2 HBDiff \\\n",
"0 0.37 0 NaN 72.40 -0.26 \n",
"1 0.46 0 NaN 72.41 -0.26 \n",
"2 0.57 0 NaN 72.41 -0.26 \n",
"3 0.66 0 NaN 72.42 -0.25 \n",
"4 0.75 0 NaN 72.43 -0.25 \n",
"\n",
" Muscle state Muscle trend SmO2 unfiltered O2HB unfiltered \\\n",
"0 0 2 72.52 -3.27 \n",
"1 0 2 73.06 -3.27 \n",
"2 0 2 72.58 -3.31 \n",
"3 0 2 73.18 -3.29 \n",
"4 0 2 72.61 -3.23 \n",
"\n",
" HHb unfiltered ... SmO2.1 HBDiff.1 Muscle state.1 Muscle trend.1 \\\n",
"0 -2.29 ... 87.04 1.57 0 2 \n",
"1 -2.31 ... 87.04 1.57 0 2 \n",
"2 -2.25 ... 87.03 1.57 0 2 \n",
"3 -2.32 ... 87.03 1.57 0 2 \n",
"4 -2.36 ... 87.03 1.58 0 2 \n",
"\n",
" SmO2 unfiltered.1 O2HB unfiltered.1 HHb unfiltered.1 THb unfiltered.1 \\\n",
"0 86.46 -0.64 -0.74 -20.30 \n",
"1 86.66 -0.66 -0.66 -20.24 \n",
"2 86.81 -0.69 -0.67 -20.28 \n",
"3 88.42 -0.70 -0.66 -20.28 \n",
"4 88.59 -0.51 -0.71 -20.14 \n",
"\n",
" HBDiff unfiltered.1 Heart Rate (BPM).1 \n",
"0 1.78 72 \n",
"1 1.68 72 \n",
"2 1.66 72 \n",
"3 1.64 72 \n",
"4 1.88 72 \n",
"\n",
"[5 rows x 24 columns]"
]
},
"execution_count": 26,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"oxygenation_2.head()"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "0a624fa0",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Index(['Timestamp (seconds passed)', 'Lap/Event', 'Unnamed: 2', 'SmO2',\n",
" 'HBDiff', 'Muscle state', 'Muscle trend', 'SmO2 unfiltered',\n",
" 'O2HB unfiltered', 'HHb unfiltered', 'THb unfiltered',\n",
" 'HBDiff unfiltered', 'Heart Rate (BPM)', 'Unnamed: 13', 'SmO2.1',\n",
" 'HBDiff.1', 'Muscle state.1', 'Muscle trend.1', 'SmO2 unfiltered.1',\n",
" 'O2HB unfiltered.1', 'HHb unfiltered.1', 'THb unfiltered.1',\n",
" 'HBDiff unfiltered.1', 'Heart Rate (BPM).1'],\n",
" dtype='object')"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"oxygenation_2.columns"
]
},
{
"cell_type": "markdown",
"id": "803c7174",
"metadata": {},
"source": [
"# Train.Red SmO₂ Analysis\n",
"\n",
"Following the instructions from the PDF, we'll create a comprehensive muscle oxygenation plot with:\n",
"- Smoothed SmO₂ data for left and right legs\n",
"- Heart rate on secondary axis\n",
"- Stage annotations (warm-up, active laps, recovery)\n",
"- Recovery percentage calculations\n",
"- Optional breakpoint detection"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "d18e5c4a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Data shape: (17020, 28)\n",
"Time range: 0.37 to 1702.26 seconds\n",
"Unique laps: [0, 1, 2, 3, 4, 5, 6, 7]\n",
"\n",
"First few rows:\n"
]
},
{
"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>Timestamp (seconds passed)</th>\n",
" <th>Lap</th>\n",
" <th>Left_SmO2</th>\n",
" <th>Right_SmO2</th>\n",
" <th>Heart_Rate</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>0.37</td>\n",
" <td>0</td>\n",
" <td>72.40</td>\n",
" <td>87.04</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>0.46</td>\n",
" <td>0</td>\n",
" <td>72.41</td>\n",
" <td>87.04</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>0.57</td>\n",
" <td>0</td>\n",
" <td>72.41</td>\n",
" <td>87.03</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>0.66</td>\n",
" <td>0</td>\n",
" <td>72.42</td>\n",
" <td>87.03</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>0.75</td>\n",
" <td>0</td>\n",
" <td>72.43</td>\n",
" <td>87.03</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>0.86</td>\n",
" <td>0</td>\n",
" <td>72.44</td>\n",
" <td>87.04</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>0.95</td>\n",
" <td>0</td>\n",
" <td>72.45</td>\n",
" <td>87.05</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>1.09</td>\n",
" <td>0</td>\n",
" <td>72.47</td>\n",
" <td>87.05</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>1.15</td>\n",
" <td>0</td>\n",
" <td>72.48</td>\n",
" <td>87.06</td>\n",
" <td>72</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>1.27</td>\n",
" <td>0</td>\n",
" <td>72.49</td>\n",
" <td>87.07</td>\n",
" <td>72</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Timestamp (seconds passed) Lap Left_SmO2 Right_SmO2 Heart_Rate\n",
"0 0.37 0 72.40 87.04 72\n",
"1 0.46 0 72.41 87.04 72\n",
"2 0.57 0 72.41 87.03 72\n",
"3 0.66 0 72.42 87.03 72\n",
"4 0.75 0 72.43 87.03 72\n",
"5 0.86 0 72.44 87.04 72\n",
"6 0.95 0 72.45 87.05 72\n",
"7 1.09 0 72.47 87.05 72\n",
"8 1.15 0 72.48 87.06 72\n",
"9 1.27 0 72.49 87.07 72"
]
},
"execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Step 1: Data Preparation\n",
"# Clean and prepare the oxygenation_2 dataframe\n",
"\n",
"# Rename columns for clarity\n",
"df_oxy = oxygenation_2.copy()\n",
"\n",
"# Convert timestamp to numeric\n",
"df_oxy['Timestamp (seconds passed)'] = pd.to_numeric(df_oxy['Timestamp (seconds passed)'], errors='coerce')\n",
"\n",
"# Convert SmO2 columns to numeric\n",
"df_oxy['Left_SmO2'] = pd.to_numeric(df_oxy['SmO2'], errors='coerce')\n",
"df_oxy['Right_SmO2'] = pd.to_numeric(df_oxy['SmO2.1'], errors='coerce')\n",
"df_oxy['Heart_Rate'] = pd.to_numeric(df_oxy['Heart Rate (BPM)'], errors='coerce')\n",
"df_oxy['Lap'] = pd.to_numeric(df_oxy['Lap/Event'], errors='coerce')\n",
"\n",
"# Drop rows with missing timestamps\n",
"df_oxy = df_oxy.dropna(subset=['Timestamp (seconds passed)'])\n",
"\n",
"# Sort by timestamp\n",
"df_oxy = df_oxy.sort_values('Timestamp (seconds passed)').reset_index(drop=True)\n",
"\n",
"print(f\"Data shape: {df_oxy.shape}\")\n",
"print(f\"Time range: {df_oxy['Timestamp (seconds passed)'].min():.2f} to {df_oxy['Timestamp (seconds passed)'].max():.2f} seconds\")\n",
"print(f\"Unique laps: {sorted(df_oxy['Lap'].dropna().unique())}\")\n",
"print(f\"\\nFirst few rows:\")\n",
"df_oxy[['Timestamp (seconds passed)', 'Lap', 'Left_SmO2', 'Right_SmO2', 'Heart_Rate']].head(10)"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "a6f97a9b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Average sampling interval: 0.090 seconds\n",
"Estimated sampling frequency: 11.11 Hz\n",
"Smoothing window: 111 samples (~10 seconds)\n",
"\n",
"Smoothing complete!\n"
]
},
{
"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>Timestamp (seconds passed)</th>\n",
" <th>Left_SmO2</th>\n",
" <th>Left_SmO2_smooth</th>\n",
" <th>Right_SmO2</th>\n",
" <th>Right_SmO2_smooth</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>0.37</td>\n",
" <td>72.40</td>\n",
" <td>72.594643</td>\n",
" <td>87.04</td>\n",
" <td>87.107500</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>0.46</td>\n",
" <td>72.41</td>\n",
" <td>72.594386</td>\n",
" <td>87.04</td>\n",
" <td>87.105614</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>0.57</td>\n",
" <td>72.41</td>\n",
" <td>72.594138</td>\n",
" <td>87.03</td>\n",
" <td>87.103793</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>0.66</td>\n",
" <td>72.42</td>\n",
" <td>72.593898</td>\n",
" <td>87.03</td>\n",
" <td>87.101864</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>0.75</td>\n",
" <td>72.43</td>\n",
" <td>72.593667</td>\n",
" <td>87.03</td>\n",
" <td>87.099833</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" Timestamp (seconds passed) Left_SmO2 Left_SmO2_smooth Right_SmO2 \\\n",
"0 0.37 72.40 72.594643 87.04 \n",
"1 0.46 72.41 72.594386 87.04 \n",
"2 0.57 72.41 72.594138 87.03 \n",
"3 0.66 72.42 72.593898 87.03 \n",
"4 0.75 72.43 72.593667 87.03 \n",
"\n",
" Right_SmO2_smooth \n",
"0 87.107500 \n",
"1 87.105614 \n",
"2 87.103793 \n",
"3 87.101864 \n",
"4 87.099833 "
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Step 2: Apply 10-second rolling mean smoothing\n",
"# Following PDF instructions: centered 10 second rolling mean\n",
"\n",
"# Estimate sampling frequency\n",
"time_diffs = df_oxy['Timestamp (seconds passed)'].diff().dropna()\n",
"avg_sampling_interval = time_diffs.median()\n",
"sampling_freq = 1 / avg_sampling_interval if avg_sampling_interval > 0 else 10\n",
"window_samples = int(10 * sampling_freq) # 10 seconds worth of samples\n",
"\n",
"print(f\"Average sampling interval: {avg_sampling_interval:.3f} seconds\")\n",
"print(f\"Estimated sampling frequency: {sampling_freq:.2f} Hz\")\n",
"print(f\"Smoothing window: {window_samples} samples (~10 seconds)\")\n",
"\n",
"# Apply centered rolling mean\n",
"df_oxy['Left_SmO2_smooth'] = df_oxy['Left_SmO2'].rolling(window=window_samples, center=True, min_periods=1).mean()\n",
"df_oxy['Right_SmO2_smooth'] = df_oxy['Right_SmO2'].rolling(window=window_samples, center=True, min_periods=1).mean()\n",
"df_oxy['Heart_Rate_smooth'] = df_oxy['Heart_Rate'].rolling(window=window_samples, center=True, min_periods=1).mean()\n",
"\n",
"print(\"\\nSmoothing complete!\")\n",
"df_oxy[['Timestamp (seconds passed)', 'Left_SmO2', 'Left_SmO2_smooth', 'Right_SmO2', 'Right_SmO2_smooth']].head()"
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "e6943523",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Lap start times:\n",
" Lap 0: 0.37 seconds (0.01 minutes)\n",
" Lap 1: 256.54 seconds (4.28 minutes)\n",
" Lap 2: 495.34 seconds (8.26 minutes)\n",
" Lap 3: 735.03 seconds (12.25 minutes)\n",
" Lap 4: 976.45 seconds (16.27 minutes)\n",
" Lap 5: 1215.00 seconds (20.25 minutes)\n",
" Lap 6: 1454.76 seconds (24.25 minutes)\n",
" Lap 7: 1543.36 seconds (25.72 minutes)\n",
"\n",
"Stage breakdown:\n",
" Warm-up: 0 - 256.54 seconds\n",
" Active test: 256.54 - 1543.36 seconds\n",
" Recovery: 1543.36 - 1702.26 seconds\n"
]
}
],
"source": [
"# Step 3: Identify test stages based on laps\n",
"# Find when each lap starts\n",
"\n",
"lap_changes = df_oxy[df_oxy['Lap'].diff() != 0].copy()\n",
"lap_starts = {}\n",
"\n",
"for idx, row in lap_changes.iterrows():\n",
" lap_num = int(row['Lap'])\n",
" lap_starts[lap_num] = row['Timestamp (seconds passed)']\n",
"\n",
"print(\"Lap start times:\")\n",
"for lap, time in sorted(lap_starts.items()):\n",
" print(f\" Lap {lap}: {time:.2f} seconds ({time/60:.2f} minutes)\")\n",
"\n",
"# Identify stages\n",
"# Assuming: Lap 0 = warm-up, Laps 1-6 = active test, Lap 7 = recovery\n",
"warm_up_end = lap_starts.get(1, df_oxy['Timestamp (seconds passed)'].max())\n",
"recovery_start = lap_starts.get(7, df_oxy['Timestamp (seconds passed)'].max())\n",
"\n",
"print(f\"\\nStage breakdown:\")\n",
"print(f\" Warm-up: 0 - {warm_up_end:.2f} seconds\")\n",
"print(f\" Active test: {warm_up_end:.2f} - {recovery_start:.2f} seconds\")\n",
"print(f\" Recovery: {recovery_start:.2f} - {df_oxy['Timestamp (seconds passed)'].max():.2f} seconds\")"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "f4d26f1b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Recovery Percentages:\n",
" Left Leg:\n",
" Warm-up avg: 75.37%\n",
" Recovery avg: 82.47%\n",
" Recovery: 109% of warm-up\n",
"\n",
" Right Leg:\n",
" Warm-up avg: 82.91%\n",
" Recovery avg: 80.03%\n",
" Recovery: 97% of warm-up\n"
]
}
],
"source": [
"# Step 4: Calculate recovery percentages\n",
"# Average SmO2 over last 30 seconds of warm-up and recovery\n",
"\n",
"# Last 30 seconds of warm-up\n",
"warm_up_last_30_start = warm_up_end - 30\n",
"warm_up_mask = (df_oxy['Timestamp (seconds passed)'] >= warm_up_last_30_start) & \\\n",
" (df_oxy['Timestamp (seconds passed)'] <= warm_up_end)\n",
"\n",
"# Last 30 seconds of recovery\n",
"recovery_end = df_oxy['Timestamp (seconds passed)'].max()\n",
"recovery_last_30_start = recovery_end - 30\n",
"recovery_mask = (df_oxy['Timestamp (seconds passed)'] >= recovery_last_30_start) & \\\n",
" (df_oxy['Timestamp (seconds passed)'] <= recovery_end)\n",
"\n",
"# Calculate averages\n",
"left_warmup_avg = df_oxy.loc[warm_up_mask, 'Left_SmO2_smooth'].mean()\n",
"left_recovery_avg = df_oxy.loc[recovery_mask, 'Left_SmO2_smooth'].mean()\n",
"left_recovery_pct = round((left_recovery_avg / left_warmup_avg) * 100)\n",
"\n",
"right_warmup_avg = df_oxy.loc[warm_up_mask, 'Right_SmO2_smooth'].mean()\n",
"right_recovery_avg = df_oxy.loc[recovery_mask, 'Right_SmO2_smooth'].mean()\n",
"right_recovery_pct = round((right_recovery_avg / right_warmup_avg) * 100)\n",
"\n",
"print(\"Recovery Percentages:\")\n",
"print(f\" Left Leg:\")\n",
"print(f\" Warm-up avg: {left_warmup_avg:.2f}%\")\n",
"print(f\" Recovery avg: {left_recovery_avg:.2f}%\")\n",
"print(f\" Recovery: {left_recovery_pct}% of warm-up\")\n",
"print(f\"\\n Right Leg:\")\n",
"print(f\" Warm-up avg: {right_warmup_avg:.2f}%\")\n",
"print(f\" Recovery avg: {right_recovery_avg:.2f}%\")\n",
"print(f\" Recovery: {right_recovery_pct}% of warm-up\")"
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "bf54eda5",
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1800x800 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"✓ Plot created successfully!\n"
]
}
],
"source": [
"# Step 5: Create comprehensive SmO₂ plot following PDF instructions\n",
"\n",
"fig, ax1 = plt.subplots(figsize=(18, 8))\n",
"\n",
"# Plot SmO₂ data on primary axis\n",
"time = df_oxy['Timestamp (seconds passed)']\n",
"ax1.plot(time, df_oxy['Left_SmO2_smooth'], \n",
" label=f'Left SmO₂ (Rec {left_recovery_pct}% of warm-up)', \n",
" color='#2E86AB', linewidth=2)\n",
"ax1.plot(time, df_oxy['Right_SmO2_smooth'], \n",
" label=f'Right SmO₂ (Rec {right_recovery_pct}% of warm-up)', \n",
" color='#A23B72', linewidth=2)\n",
"\n",
"ax1.set_xlabel('Time (seconds)', fontsize=12, fontweight='bold')\n",
"ax1.set_ylabel('SmO₂ (%)', fontsize=12, fontweight='bold')\n",
"ax1.tick_params(axis='y', labelcolor='black')\n",
"ax1.grid(True, alpha=0.3, linestyle='--')\n",
"\n",
"# Add secondary axis for heart rate\n",
"ax2 = ax1.twinx()\n",
"ax2.plot(time, df_oxy['Heart_Rate_smooth'], \n",
" label='Heart Rate', \n",
" color='red', linewidth=1.5, linestyle='--', alpha=0.7)\n",
"ax2.set_ylabel('Heart Rate (BPM)', fontsize=12, fontweight='bold', color='red')\n",
"ax2.tick_params(axis='y', labelcolor='red')\n",
"\n",
"# Add shaded regions for stages\n",
"# Warm-up (light shade)\n",
"ax1.axvspan(0, warm_up_end, alpha=0.15, color='blue', label='Warm-up')\n",
"\n",
"# Active test laps (alternating shades)\n",
"active_laps = [1, 2, 3, 4, 5, 6]\n",
"colors_active = ['yellow', 'orange'] * 3\n",
"for i, lap in enumerate(active_laps):\n",
" start = lap_starts.get(lap, 0)\n",
" end = lap_starts.get(lap + 1, recovery_start) if lap < 6 else recovery_start\n",
" ax1.axvspan(start, end, alpha=0.1, color=colors_active[i])\n",
"\n",
"# Recovery (gray shade)\n",
"ax1.axvspan(recovery_start, df_oxy['Timestamp (seconds passed)'].max(), \n",
" alpha=0.2, color='gray', label='Recovery')\n",
"\n",
"# Add vertical line at recovery start\n",
"ax1.axvline(x=recovery_start, color='black', linestyle='-', linewidth=2, alpha=0.7)\n",
"\n",
"# Add lap labels\n",
"for lap in range(1, 7):\n",
" start = lap_starts.get(lap, 0)\n",
" end = lap_starts.get(lap + 1, recovery_start) if lap < 6 else recovery_start\n",
" mid = (start + end) / 2\n",
" ax1.text(mid, ax1.get_ylim()[1] * 0.97, f'Lap {lap}', \n",
" ha='center', va='top', fontsize=10, fontweight='bold', \n",
" bbox=dict(boxstyle='round,pad=0.3', facecolor='white', alpha=0.7))\n",
"\n",
"# Title and legends\n",
"plt.title('Train.Red SmO₂ Ramp - Muscle Oxygenation Analysis', \n",
" fontsize=16, fontweight='bold', pad=20)\n",
"\n",
"# Combine legends from both axes\n",
"lines1, labels1 = ax1.get_legend_handles_labels()\n",
"lines2, labels2 = ax2.get_legend_handles_labels()\n",
"ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left', fontsize=10, framealpha=0.9)\n",
"\n",
"plt.tight_layout()\n",
"plt.savefig(f'{base_dir}/graphs/muscle_oxygenation_plot.png', dpi=300, bbox_inches='tight')\n",
"plt.show()\n",
"\n",
"print(\"✓ Plot created successfully!\")"
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "128c9f3e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"MUSCLE OXYGENATION ANALYSIS - KEY VALUES\n",
"======================================================================\n",
"\n",
"1. BASELINE (Warm-up averages):\n",
" Left SmO₂: 75.37%\n",
" Right SmO₂: 82.91%\n",
"\n",
"2. RECOVERY (Last 30 seconds):\n",
" Left SmO₂: 82.47% (109% of baseline)\n",
" Right SmO₂: 80.03% (97% of baseline)\n",
"\n",
"3. MINIMUM VALUES (During active test):\n",
" Left SmO₂: 69.34% at 1536.6s (Lap 6)\n",
" Right SmO₂: 73.65% at 1535.5s (Lap 6)\n",
"\n",
"4. MAXIMUM VALUES (During active test):\n",
" Left SmO₂: 78.24% at 915.2s (Lap 3)\n",
" Right SmO₂: 82.59% at 256.5s (Lap 1)\n",
"\n",
"5. OXYGENATION DROP (Baseline to Minimum):\n",
" Left SmO₂: 6.03% drop (8.0% decrease)\n",
" Right SmO₂: 9.26% drop (11.2% decrease)\n",
"\n",
"6. AVERAGE SmO₂ PER LAP:\n",
" Lap 1: Left=75.42%, Right=81.27%, HR=107.3 bpm\n",
" Lap 2: Left=76.51%, Right=81.76%, HR=118.8 bpm\n",
" Lap 3: Left=77.35%, Right=81.28%, HR=131.2 bpm\n",
" Lap 4: Left=76.98%, Right=79.66%, HR=145.2 bpm\n",
" Lap 5: Left=74.28%, Right=77.96%, HR=157.7 bpm\n",
" Lap 6: Left=71.27%, Right=75.98%, HR=165.0 bpm\n",
"\n",
"7. HEART RATE:\n",
" Warm-up avg: 93.2 bpm\n",
" Max during test: 168.2 bpm\n",
" Recovery avg: 107.7 bpm\n",
"\n",
"======================================================================\n"
]
}
],
"source": [
"# Step 6: Extract key values and metrics from the test\n",
"\n",
"print(\"=\"*70)\n",
"print(\"MUSCLE OXYGENATION ANALYSIS - KEY VALUES\")\n",
"print(\"=\"*70)\n",
"\n",
"# 1. Baseline values (warm-up averages)\n",
"print(\"\\n1. BASELINE (Warm-up averages):\")\n",
"print(f\" Left SmO₂: {left_warmup_avg:.2f}%\")\n",
"print(f\" Right SmO₂: {right_warmup_avg:.2f}%\")\n",
"\n",
"# 2. Recovery values\n",
"print(\"\\n2. RECOVERY (Last 30 seconds):\")\n",
"print(f\" Left SmO₂: {left_recovery_avg:.2f}% ({left_recovery_pct}% of baseline)\")\n",
"print(f\" Right SmO₂: {right_recovery_avg:.2f}% ({right_recovery_pct}% of baseline)\")\n",
"\n",
"# 3. Minimum values during active test\n",
"active_mask = (df_oxy['Timestamp (seconds passed)'] >= warm_up_end) & \\\n",
" (df_oxy['Timestamp (seconds passed)'] <= recovery_start)\n",
"active_data = df_oxy[active_mask]\n",
"\n",
"left_min = active_data['Left_SmO2_smooth'].min()\n",
"left_min_time = active_data.loc[active_data['Left_SmO2_smooth'].idxmin(), 'Timestamp (seconds passed)']\n",
"left_min_lap = active_data.loc[active_data['Left_SmO2_smooth'].idxmin(), 'Lap']\n",
"\n",
"right_min = active_data['Right_SmO2_smooth'].min()\n",
"right_min_time = active_data.loc[active_data['Right_SmO2_smooth'].idxmin(), 'Timestamp (seconds passed)']\n",
"right_min_lap = active_data.loc[active_data['Right_SmO2_smooth'].idxmin(), 'Lap']\n",
"\n",
"print(\"\\n3. MINIMUM VALUES (During active test):\")\n",
"print(f\" Left SmO₂: {left_min:.2f}% at {left_min_time:.1f}s (Lap {int(left_min_lap)})\")\n",
"print(f\" Right SmO₂: {right_min:.2f}% at {right_min_time:.1f}s (Lap {int(right_min_lap)})\")\n",
"\n",
"# 4. Maximum values during active test\n",
"left_max = active_data['Left_SmO2_smooth'].max()\n",
"left_max_time = active_data.loc[active_data['Left_SmO2_smooth'].idxmax(), 'Timestamp (seconds passed)']\n",
"left_max_lap = active_data.loc[active_data['Left_SmO2_smooth'].idxmax(), 'Lap']\n",
"\n",
"right_max = active_data['Right_SmO2_smooth'].max()\n",
"right_max_time = active_data.loc[active_data['Right_SmO2_smooth'].idxmax(), 'Timestamp (seconds passed)']\n",
"right_max_lap = active_data.loc[active_data['Right_SmO2_smooth'].idxmax(), 'Lap']\n",
"\n",
"print(\"\\n4. MAXIMUM VALUES (During active test):\")\n",
"print(f\" Left SmO₂: {left_max:.2f}% at {left_max_time:.1f}s (Lap {int(left_max_lap)})\")\n",
"print(f\" Right SmO₂: {right_max:.2f}% at {right_max_time:.1f}s (Lap {int(right_max_lap)})\")\n",
"\n",
"# 5. Range/Drop during test\n",
"left_drop = left_warmup_avg - left_min\n",
"right_drop = right_warmup_avg - right_min\n",
"\n",
"print(\"\\n5. OXYGENATION DROP (Baseline to Minimum):\")\n",
"print(f\" Left SmO₂: {left_drop:.2f}% drop ({left_drop/left_warmup_avg*100:.1f}% decrease)\")\n",
"print(f\" Right SmO₂: {right_drop:.2f}% drop ({right_drop/right_warmup_avg*100:.1f}% decrease)\")\n",
"\n",
"# 6. Average values per lap\n",
"print(\"\\n6. AVERAGE SmO₂ PER LAP:\")\n",
"for lap in range(1, 7):\n",
" lap_mask = df_oxy['Lap'] == lap\n",
" lap_data = df_oxy[lap_mask]\n",
" if len(lap_data) > 0:\n",
" left_avg = lap_data['Left_SmO2_smooth'].mean()\n",
" right_avg = lap_data['Right_SmO2_smooth'].mean()\n",
" hr_avg = lap_data['Heart_Rate_smooth'].mean()\n",
" print(f\" Lap {lap}: Left={left_avg:.2f}%, Right={right_avg:.2f}%, HR={hr_avg:.1f} bpm\")\n",
"\n",
"# 7. Heart rate data\n",
"print(\"\\n7. HEART RATE:\")\n",
"hr_warmup = df_oxy[df_oxy['Timestamp (seconds passed)'] <= warm_up_end]['Heart_Rate_smooth'].mean()\n",
"hr_max = active_data['Heart_Rate_smooth'].max()\n",
"hr_recovery = df_oxy[recovery_mask]['Heart_Rate_smooth'].mean()\n",
"print(f\" Warm-up avg: {hr_warmup:.1f} bpm\")\n",
"print(f\" Max during test: {hr_max:.1f} bpm\")\n",
"print(f\" Recovery avg: {hr_recovery:.1f} bpm\")\n",
"\n",
"print(\"\\n\" + \"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "e80da1c3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"======================================================================\n",
"ANALYSIS SUMMARY & INTERPRETATION\n",
"======================================================================\n",
"\n",
"📊 WHAT THE DATA SHOWS:\n",
"\n",
"1. MUSCLE OXYGEN PATTERNS:\n",
" • Both legs started with good oxygen levels (Left: 75.4%, Right: 82.9%)\n",
" • The right leg showed higher baseline oxygenation\n",
" • During the test, oxygen levels dropped as intensity increased\n",
" • Left leg dropped 6.0% and right leg dropped 9.3%\n",
"\n",
"2. LEG COMPARISON:\n",
" • Left leg maintained oxygen better (only 6.0% drop vs 9.3%)\n",
" • Right leg experienced more oxygen desaturation\n",
"\n",
"3. RECOVERY PERFORMANCE:\n",
" ✓ Excellent left leg recovery (109% - exceeded baseline!)\n",
" • Right leg recovered to 97% of baseline\n",
" ✓ Overall excellent recovery capacity\n",
"\n",
"4. INTENSITY PROGRESSION:\n",
" Heart rate increased steadily through the test:\n",
" Lap 1: 107 bpm\n",
" Lap 2: 119 bpm\n",
" Lap 3: 131 bpm\n",
" Lap 4: 145 bpm\n",
" Lap 5: 158 bpm\n",
" Lap 6: 165 bpm\n",
"\n",
"💡 TRAINING INSIGHTS:\n",
"\n",
"1. Oxygen Utilization:\n",
" ✓ Good oxygen extraction efficiency\n",
" → Muscles are effectively using available oxygen\n",
"\n",
"2. Endurance Capacity:\n",
" ✓ Maintained good oxygen levels through high intensity\n",
" → Strong aerobic capacity\n",
"\n",
"3. Recovery Ability:\n",
" Recovery period: 159 seconds (2.6 minutes)\n",
" ✓ Fast recovery - good cardiovascular fitness\n",
" → Can handle high-intensity training\n",
"\n",
"🎯 RECOMMENDATIONS:\n",
"\n",
"• Monitor the leg with larger oxygen drop for potential weakness\n",
"• Use these SmO₂ patterns to optimize training intensity zones\n",
"• Recovery percentages indicate readiness for next training session\n",
"• Consider interval training at intensities where oxygen starts to drop\n",
"\n",
"======================================================================\n"
]
}
],
"source": [
"# Step 7: Analysis Summary and Interpretation\n",
"\n",
"print(\"=\"*70)\n",
"print(\"ANALYSIS SUMMARY & INTERPRETATION\")\n",
"print(\"=\"*70)\n",
"\n",
"print(\"\\n📊 WHAT THE DATA SHOWS:\")\n",
"print(\"\\n1. MUSCLE OXYGEN PATTERNS:\")\n",
"print(f\" • Both legs started with good oxygen levels (Left: {left_warmup_avg:.1f}%, Right: {right_warmup_avg:.1f}%)\")\n",
"print(f\" • The right leg showed higher baseline oxygenation\")\n",
"print(f\" • During the test, oxygen levels dropped as intensity increased\")\n",
"print(f\" • Left leg dropped {left_drop:.1f}% and right leg dropped {right_drop:.1f}%\")\n",
"\n",
"print(\"\\n2. LEG COMPARISON:\")\n",
"if abs(left_drop - right_drop) > 2:\n",
" if left_drop < right_drop:\n",
" print(f\" • Left leg maintained oxygen better (only {left_drop:.1f}% drop vs {right_drop:.1f}%)\")\n",
" print(f\" • Right leg experienced more oxygen desaturation\")\n",
" else:\n",
" print(f\" • Right leg maintained oxygen better (only {right_drop:.1f}% drop vs {left_drop:.1f}%)\")\n",
" print(f\" • Left leg experienced more oxygen desaturation\")\n",
"else:\n",
" print(f\" • Both legs showed similar oxygen patterns (balanced)\")\n",
"\n",
"print(\"\\n3. RECOVERY PERFORMANCE:\")\n",
"if left_recovery_pct > 100:\n",
" print(f\" ✓ Excellent left leg recovery ({left_recovery_pct}% - exceeded baseline!)\")\n",
"else:\n",
" print(f\" • Left leg recovered to {left_recovery_pct}% of baseline\")\n",
"\n",
"if right_recovery_pct > 100:\n",
" print(f\" ✓ Excellent right leg recovery ({right_recovery_pct}% - exceeded baseline!)\")\n",
"else:\n",
" print(f\" • Right leg recovered to {right_recovery_pct}% of baseline\")\n",
"\n",
"avg_recovery = (left_recovery_pct + right_recovery_pct) / 2\n",
"if avg_recovery >= 100:\n",
" print(f\" ✓ Overall excellent recovery capacity\")\n",
"elif avg_recovery >= 95:\n",
" print(f\" ✓ Good recovery capacity\")\n",
"else:\n",
" print(f\" ⚠ Recovery may need attention (avg {avg_recovery:.0f}%)\")\n",
"\n",
"print(\"\\n4. INTENSITY PROGRESSION:\")\n",
"print(\" Heart rate increased steadily through the test:\")\n",
"for lap in range(1, 7):\n",
" lap_mask = df_oxy['Lap'] == lap\n",
" lap_hr = df_oxy[lap_mask]['Heart_Rate_smooth'].mean()\n",
" print(f\" Lap {lap}: {lap_hr:.0f} bpm\")\n",
"\n",
"print(\"\\n💡 TRAINING INSIGHTS:\")\n",
"print(\"\\n1. Oxygen Utilization:\")\n",
"if left_drop < 10 and right_drop < 10:\n",
" print(\" ✓ Good oxygen extraction efficiency\")\n",
" print(\" → Muscles are effectively using available oxygen\")\n",
"else:\n",
" print(\" ⚠ Significant oxygen desaturation observed\")\n",
" print(\" → May indicate approaching oxygen delivery limits\")\n",
"\n",
"print(\"\\n2. Endurance Capacity:\")\n",
"# Check if oxygen drops significantly in later laps\n",
"lap5_left = df_oxy[df_oxy['Lap'] == 5]['Left_SmO2_smooth'].mean()\n",
"lap5_right = df_oxy[df_oxy['Lap'] == 5]['Right_SmO2_smooth'].mean()\n",
"if (lap5_left > left_warmup_avg - 5) or (lap5_right > right_warmup_avg - 5):\n",
" print(\" ✓ Maintained good oxygen levels through high intensity\")\n",
" print(\" → Strong aerobic capacity\")\n",
"else:\n",
" print(\" • Oxygen levels decreased progressively with intensity\")\n",
" print(\" → Normal response to increasing workload\")\n",
"\n",
"print(\"\\n3. Recovery Ability:\")\n",
"recovery_time_sec = df_oxy['Timestamp (seconds passed)'].max() - recovery_start\n",
"print(f\" Recovery period: {recovery_time_sec:.0f} seconds ({recovery_time_sec/60:.1f} minutes)\")\n",
"if avg_recovery > 95:\n",
" print(\" ✓ Fast recovery - good cardiovascular fitness\")\n",
" print(\" → Can handle high-intensity training\")\n",
"else:\n",
" print(\" • Moderate recovery observed\")\n",
" print(\" → Consider extending rest periods between hard efforts\")\n",
"\n",
"print(\"\\n🎯 RECOMMENDATIONS:\")\n",
"print(\"\\n• Monitor the leg with larger oxygen drop for potential weakness\")\n",
"print(\"• Use these SmO₂ patterns to optimize training intensity zones\")\n",
"print(\"• Recovery percentages indicate readiness for next training session\")\n",
"print(\"• Consider interval training at intensities where oxygen starts to drop\")\n",
"\n",
"print(\"\\n\" + \"=\"*70)"
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "fbeb6168",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✓ Summary data exported to: /home/oluwasanmi/Documents/Work/MKD/report_generation/notebooks/oxygenation_2_output.csv\n",
"\n",
"Summary Table:\n",
" Metric Value\n",
" Left Baseline SmO2 (%) 75.37\n",
" Right Baseline SmO2 (%) 82.91\n",
" Left Minimum SmO2 (%) 69.34\n",
" Right Minimum SmO2 (%) 73.65\n",
" Left Maximum SmO2 (%) 78.24\n",
" Right Maximum SmO2 (%) 82.59\n",
" Left Recovery SmO2 (%) 82.47\n",
" Right Recovery SmO2 (%) 80.03\n",
" Left Recovery Percentage (%) 109\n",
"Right Recovery Percentage (%) 97\n",
" Left Oxygen Drop (%) 6.03\n",
" Right Oxygen Drop (%) 9.26\n",
" Warmup HR (bpm) 93.2\n",
" Maximum HR (bpm) 168.2\n",
" Recovery HR (bpm) 107.7\n",
" Test Duration (seconds) 1287\n",
" Recovery Duration (seconds) 159\n"
]
}
],
"source": [
"# Step 8: Export summary data to CSV for reporting\n",
"\n",
"# Create summary dataframe\n",
"summary_data = {\n",
" 'Metric': [\n",
" 'Left Baseline SmO2 (%)',\n",
" 'Right Baseline SmO2 (%)',\n",
" 'Left Minimum SmO2 (%)',\n",
" 'Right Minimum SmO2 (%)',\n",
" 'Left Maximum SmO2 (%)',\n",
" 'Right Maximum SmO2 (%)',\n",
" 'Left Recovery SmO2 (%)',\n",
" 'Right Recovery SmO2 (%)',\n",
" 'Left Recovery Percentage (%)',\n",
" 'Right Recovery Percentage (%)',\n",
" 'Left Oxygen Drop (%)',\n",
" 'Right Oxygen Drop (%)',\n",
" 'Warmup HR (bpm)',\n",
" 'Maximum HR (bpm)',\n",
" 'Recovery HR (bpm)',\n",
" 'Test Duration (seconds)',\n",
" 'Recovery Duration (seconds)',\n",
" ],\n",
" 'Value': [\n",
" f\"{left_warmup_avg:.2f}\",\n",
" f\"{right_warmup_avg:.2f}\",\n",
" f\"{left_min:.2f}\",\n",
" f\"{right_min:.2f}\",\n",
" f\"{left_max:.2f}\",\n",
" f\"{right_max:.2f}\",\n",
" f\"{left_recovery_avg:.2f}\",\n",
" f\"{right_recovery_avg:.2f}\",\n",
" f\"{left_recovery_pct}\",\n",
" f\"{right_recovery_pct}\",\n",
" f\"{left_drop:.2f}\",\n",
" f\"{right_drop:.2f}\",\n",
" f\"{hr_warmup:.1f}\",\n",
" f\"{hr_max:.1f}\",\n",
" f\"{hr_recovery:.1f}\",\n",
" f\"{recovery_start - warm_up_end:.0f}\",\n",
" f\"{df_oxy['Timestamp (seconds passed)'].max() - recovery_start:.0f}\",\n",
" ]\n",
"}\n",
"\n",
"summary_df = pd.DataFrame(summary_data)\n",
"\n",
"# Save to CSV\n",
"output_path = f'{base_dir}/notebooks/oxygenation_2_output.csv'\n",
"summary_df.to_csv(output_path, index=False)\n",
"\n",
"print(\"✓ Summary data exported to:\", output_path)\n",
"print(\"\\nSummary Table:\")\n",
"print(summary_df.to_string(index=False))"
]
},
{
"cell_type": "markdown",
"id": "f383e0a8",
"metadata": {},
"source": [
"## ✅ Analysis Complete!\n",
"\n",
"**Outputs Generated:**\n",
"1. **Graph**: `graphs/muscle_oxygenation_plot.png` - Comprehensive SmO₂ visualization with:\n",
" - Left and right leg muscle oxygenation (smoothed)\n",
" - Heart rate on secondary axis\n",
" - Stage shading (warm-up, active laps, recovery)\n",
" - Recovery percentages in legend\n",
" \n",
"2. **Data**: `notebooks/oxygenation_2_output.csv` - All key metrics exported\n",
"\n",
"**Key Findings:**\n",
"- Left leg showed better oxygen maintenance (6% drop vs 9.3% for right)\n",
"- Excellent recovery capacity (109% left, 97% right)\n",
"- Strong aerobic capacity maintained through high intensity\n",
"- Heart rate progressed steadily from 107 to 165 bpm\n",
"\n",
"The analysis follows the PDF instructions for Train.Red SmO₂ ramp testing, including 10-second smoothing, stage identification, and recovery percentage calculations."
]
2025-11-08 19:49:57 +01:00
}
],
"metadata": {
"kernelspec": {
2025-11-21 14:15:29 +01:00
"display_name": "report-generation",
2025-11-08 19:49:57 +01:00
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
2025-11-21 09:23:13 +01:00
"version": "3.12.6"
2025-11-08 19:49:57 +01:00
}
},
"nbformat": 4,
"nbformat_minor": 5
}