Files
bio-performx/notebooks/graphs.ipynb
T

1789 lines
1.1 MiB
Plaintext
Raw Normal View History

{
"cells": [
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 1,
"id": "63f43af5",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import seaborn as sns\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"import os"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 2,
"id": "97da3d1c",
"metadata": {},
"outputs": [],
"source": [
"base_dir = os.path.dirname(os.path.abspath('.'))"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 3,
"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-21 10:01:07 +01:00
"/tmp/ipykernel_67838/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",
" df = df.apply(pd.to_numeric, errors='ignore')\n"
]
}
],
"source": [
"df = pd.read_csv(f'{base_dir}/data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
"# 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-21 10:01:07 +01:00
"execution_count": 4,
"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",
"oxygenation = pd.read_csv(f'{base_dir}/data/Keirstyn Train Red NIRS Muscle Oxygen.csv')"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 5,
"id": "fbd292c3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"22.369999999999997\n"
]
}
],
"source": [
"print(df['VO2 Pulse'].max())"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 6,
"id": "4c439b2c",
"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
"text/plain": [
"<Figure size 800x800 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# 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",
"\n",
"# Create donut chart\n",
"fat_mass_lbs = 27.6\n",
"lean_mass_lbs = 95.4\n",
"\n",
"# 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",
"\n",
"# Data for the chart\n",
"sizes = [fat_percentage, lean_percentage]\n",
"colors = ['#fde3ac', '#ff9966'] # Light yellow/tan and orange from the image\n",
"\n",
"plt.figure(figsize=(8, 8))\n",
"\n",
"# 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",
"\n",
"# 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",
"\n",
"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",
"\n",
"# 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",
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 7,
"id": "a565f1b3",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA9wAAAC2CAYAAAAr14W8AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAIFBJREFUeJzt3XlU1XXi//EXGLivM2hOouQCIqJoKaalGYkKFkXj6MyQpuY2apllOX1Hf5WmVNboccaj5Z6Ok9pii5mlpqdcJjW0XEohFc0QNZNF1vv+/eHh5hWQRd6Xqz0f53iCz+dzL+/76u3H++KzXC9jjBEAAAAAAKhQ3pU9AAAAAAAAbkQUbgAAAAAALKBwAwAAAABgAYUbAAAAAAALKNwAAAAAAFhA4QYAAAAAwAIKNwAAAAAAFlC4AQAAAACw4KbKHgAAAL81ycnJSk1NLfPjGjZsqCZNmlgYEQAAsMHLGGMqexAAAPxWZGdnq1mzZkpJSSnzY2+++WYdPXpUVatWtTAyAABQ0TilHAAAN/L19VXTpk3l7V22f4K9vb3l7+8vX19fSyMDAAAVjcINAIAbeXl5aerUqXI4HGV6nMPh0NSpU+Xl5WVpZAAAoKJxSjkAAG5mjFF4eLj27Nmj/Pz8ErevUqWKOnbsqJ07d1K4AQC4jnCEGwAANys4yl2asi1J+fn5HN0GAOA6xBFuAAAqQWmPcnN0GwCA6xdHuAEAqASlPcrN0W0AAK5fHOEGAKCSlHSUm6PbAABc3zjCDQBAJSnpKDdHtwEAuL5xhBsAgEpU3FFujm4DAHD94wg3AACVqLij3BzdBgDg+scRbgAAKtmVR7k5ug0AwI2BI9wAAFSyK49yc3QbAIAbA4XbA+3evVvZ2dmVPYwbSnZ2NrlaQK52kKsdnp5rZGSkOnXqJEnq1KmTIiMjK3lEpePpuV6vyNUOcrWDXO0gVzvcnSeF20OV9LmsKJvLjxqh4pCrHeRqh6fn6uXlpenTpys4OFjTp0+/bo5ue3qu1ytytYNc7SBXO8jVDnfneZNbfxoAACjWvffeqwMHDlT2MAAAQAXhCDcAAAAAABZQuAEAAAAAsICPBfNA9/furfysLHl78/uQiuJwOJSTkyNfX19yrUDkaofD4dCx06cUcGszeV8n1/FeDxzG6Mixo/Jv1lTe3uRaURwOo5ycbPn6ViXXCkSudjgcRqeOnVBAs6bsXyuQwxgd/T5JzW5pwvuBCsT7LDscDoc+2rLFbT+Pa7g90MULF/Th+PGVPQwAlajLtP+nD1fEV/Ywbjht7v6LZq6cUtnDAFCJHuk+kv2rBR06/Unv/O1vlT0MwOPwqxIAAAAAACygcAMAAAAAYAGFGwAAAAAACyjcAAAAAABYQOEGAAAAAMACCjcAAAAAABZQuAEAAAAAsIDCDQAAAACABRRuAAAAAAAsoHADAAAAAGABhRsAAAAAAAso3AAAAAAAWEDhBgAAAADAAgo3AAAAAAAWULgBAAAAALCAwg0AAAAAgAUUbgAAAAAALKBwAwAAAABgAYUbAAAAAAALKNwAAAAAAFhA4QYAAAAAwAIKNwAAAAAAFlC4AQAAAACwgMINAAAAAIAFFG4AAAAAACygcAMAAAAAYAGFGwAAAAAACyjcAAAAAABYQOEGAAAAAMACCjcAAAAAABZQuAEAAAAAsIDCDQAAAACABRRuAAAAAAAsoHADAAAAAGABhRsAAAAAAAtuKsvG+/bt03vvvaedO3fq5MmTqlevntq3b6/x48fr1ltvddk2MTFR06dP1549e+Tj46MePXro73//uxo0aFDizzl8+LDmzJmj/fv368yZM6pWrZpatmypYcOG6Z577im0/fLly7VixQolJyerfv36ioqK0uOPP64aNWqU5eUBAAAAAFBhylS4FyxYoD179qhPnz4KCgpSamqqVqxYodjYWL311lsKDAyUJP3000/661//qtq1a+uJJ55QZmamFi1apO+//16rV6+Wr6/vVX/Ojz/+qIyMDD344INq2LChLl68qA0bNmj06NF64YUXNGDAAOe2r7zyihYsWKDevXtr0KBBSkxM1PLly3XkyBEtXLiwHJEAAAAAAHDtylS4H3nkEc2cOdOlMEdFRem+++7T66+/rpkzZ0qS5s2bp4sXL+qdd97RH/7wB0lSu3btNGTIEL377rsuhbkoPXr0UI8ePVyWxcXFKTY2VosXL3Y+/vTp01qyZIliYmL08ssvO7cNCAjQ1KlTtWnTpiKPiAMAAAAAYFuZruHu2LFjoaPTAQEBatWqlZKSkpzLNmzYoLvvvttZtiWpa9euCggI0Mcff1yugVapUkWNGzdWWlqac1lCQoLy8vIUHR3tsm1UVJQk6aOPPirXzwIAAAAA4Fpd803TjDE6c+aM6tevL0lKSUnR2bNn1bZt20LbtmvXTgcPHiz1c2dmZurcuXM6fvy4lixZoq1bt6pLly7O9Tk5OZKkqlWrujyuevXqkqT9+/eX+fUAAAAAAFARynRKeVHef/99paSk6LHHHpN06TRvSfLz8yu0rZ+fn86fP6+cnJwSr+OWpPj4eL311luSJG9vb/Xq1UtTpkxxri+4UduePXtciviuXbskXSr/AAAAAABUhmsq3ImJiXrhhRfUoUMHPfjgg5Kk7OxsSSqyUBccic7KyipV4R48eLD69Omj06dP6+OPP5bD4VBubq5zfUhIiNq3b6833nhDjRo1Unh4uBITE/X888/Lx8fHORYAAAAAANyt3IU7NTVVI0eOVO3atTV79mxVqVJF0q+luuB078sVFOBq1aopPz9f586dc1lft25dlyLeokULtWjRQpL0wAMPaOjQoRo1apRWr14tLy8vSdKcOXM0fvx4Pfvss5IuXev9yCOP6KuvvtIPP/xQ3pcHAAAAAMA1KVfhTktL0/Dhw5WWlqYVK1aoUaNGznUNGzaUdKmQXyk1NVX16tWTr6+vTpw4oYiICJf1y5YtU3h4eLE/t3fv3poyZYp++OEHNW/eXJLUqFEjrVy5UkePHtWZM2fUrFkz+fn56c4771RAQEB5Xh4AAAAAANeszIU7Oztbo0aN0tGjR7V48WK1bNnSZX2jRo3UoEEDffvtt4Ueu2/fPrVu3VrSpeu5Fy9e7LK+YF1xsrKyJEnp6emF1gUEBDgL9pEjR5SamqrY2NhSvy4AAAAAACpSmQp3fn6+xo8fr4SEBM2dO1cdOnQocrvIyEi99957OnXqlBo3bixJ2r59u44ePapHHnlE0qVTz7t27Vrk48+ePavf/e53Lstyc3O1du1aVatWzXmaeVEcDodeeeUVVa9eXQMHDizLywMAAAAAoMKUqXDHx8dr06ZN6tmzp86fP6+1a9e6rI+JiZEkjRo1SuvXr9egQYM0aNAgZWZmauHChQoMDNRDDz1U4s+ZMmWK0tPT1alTJzVq1Eipqan64IMPlJSUpEmTJqlmzZrObadNm6acnBy1bt1aeXl5+vDDD7Vv3z7Fx8e7fA44AAAAAADuVKbCfejQIUnS5s2btXnz5kLrCwp348aNtXz5csXHx+vVV1+Vj4+PevTooUmTJpXq7uRRUVFas2aNVq5cqfPnz6tmzZoKCQnRU089Vei67zZt2mjp0qX64IMP5OXlpXbt2mnJkiUuHxMGAAAAAIC7lalwv/nmm6XetlWrVlq4cGGZByRJ0dHRio6OLtW2sbGxXKsNAAAAAPA43pU9AAAAAAAAbkQUbgAAAAAALKBwAwAAAABgAYUbAAAAAAALKNwAAAAAAFhA4QYAAAAAwAKPLtxvvPGG+vTpI4fDUerHzJw5U/3797c4KgAAAAAASlamz+F2p/T0dC1YsEBPP/20vL0v/V4gKCioyG1///vf68svv5QkDR48WEuXLtXGjRsVERHhtvECAAAAAHA5jy3ca9asUV5envr16+eyvFu3boqJiXFZVq1aNefXfn5+ioiI0KJFiyjcAAAAAIBK47GF+5133tE999yjqlWruiwPCAgoVLiv1LdvXz3++ONKTk6Wv7+/zWECAAAAAFAkj7yGOzk5Wd999526du1arscXPG7jxo0
"text/plain": [
"<Figure size 1000x200 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"\n",
"# Set a common style\n",
"sns.set_theme(style=\"whitegrid\")\n",
"\n",
"# 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",
"\n",
"target_value = 22.4\n",
"demographic = \"20-39\\n(F)\"\n",
"\n",
"fig, ax = plt.subplots(figsize=(10, 2))\n",
"\n",
"# 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",
"\n",
"# 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",
"\n",
"# 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",
"\n",
"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",
"\n",
"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",
"\n",
"plt.tight_layout()\n",
"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."
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 8,
"id": "470e871e",
"metadata": {},
"outputs": [
{
"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"
]
},
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAAA8sAAADqCAYAAACV6dehAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAARTBJREFUeJzt3XlYVmXixvEbcENkEzVEXHBJFBcQRVByw0RNTa3UFk1rLKexzWnaZ9RspsxsM2eysrSUcsw0l1DcTcUFxQ3c910EQVxAlvP7wx9vnPOCgqJo8/1cl1ee52zPgePbud9nOQ6GYRgCAAAAAAA2jqVdAQAAAAAA7jSEZQAAAAAALAjLAAAAAABYEJYBAAAAALAgLAMAAAAAYEFYBgAAAADAgrAMAAAAAIAFYRkAAAAAAAvCMgAAAAAAFndsWO7UqZMaNmxo+zNhwoTSrhKAQrz++uumf68DBw4stbpMmDDBVJdOnTrZbcPnCwAAAK6nzK06cGJion766Sdt3rxZx48f16VLl+Ti4iJ3d3d5enqqXr168vf3V0BAgFq2bHmrqoGbsH79eg0aNKhY+7zxxhsaPHjwralQMf388886fvy4bblRo0bq3LnzDR+vsJ+Hm5ubVq1aJWdn5wL327Rpkx577LEC1+3evfuG61OYY8eOafbs2aayJ598Um5ubiV+LgAAAOCP6paE5bFjx+rbb7+VYRim8rS0NKWlpenIkSPaunWrJMnDw0Pr16+/FdXA/7jZs2drw4YNtuU+ffrcVFguzPnz5zV37lz179+/wPXff/99iZ/zWo4fP67PP//cVNanTx/CMgAAAFAMJR6Wp0yZom+++aakDwvc0aZNm1ZgWD59+rQWL15cCjXCtURFRSk7O9u2zBcJAAAAsCrRsJybm6tJkyaZyvz9/TV06FDVq1dPzs7OOn/+vA4cOKBNmzZp1apVysjIKMkq4BYbNGiQnnzyyULXe3h43L7K3EH27Nmj9evXq3Xr1qZyayjDncHb27u0qwAAAIA7XImG5QMHDiglJcVU9u9//1s1atQwlTVr1ky9e/dWbm6uNm/efNPn3bBhg+bMmaP4+HidOXNGmZmZcnV1Vb169dSmTRsNGDBAlStXNu1z/Phxu4l/li9fLh8fH9vyJ598ov/85z+25bFjx6p379625djYWNP4XGdnZ23cuFFly5a9bp3/8pe/aMmSJbblLl26FDjJkGEYioiIMI29ff755zV8+HBJUnZ2tubOnauYmBjt3r1bKSkpysnJkYeHhzw9PdWgQQM1a9ZMrVu3VqNGja5br+txc3OTr69vkbbdunWrYmNjlZiYqEOHDik1NVWpqakyDEOurq7y8/NTSEiIHnnkEdPPPb8bub5OnTqZfl55Zs+ebTeWd+nSpUW+noI4ODjYhhtMnz7dFJavXLmi//73vwVuez2ZmZmaO3euli5dqp07d+rcuXNycnJStWrVFBwcrAEDBqhZs2amfX7++We98cYbBR4vIiLCtNynTx+9//77kq6OcV62bJl27typPXv26Ny5c0pNTVVmZqZcXFxUo0YNNW/eXH379rU75/XExsZqypQp2rZtmy5evKgaNWqoa9euGjp0qCpWrFjofmlpaZo5c6ZWr16tvXv3Ki0tTWXLllXVqlXVvHlz9erVS/fdd1+x6pKf9R4ZPny4nn/+edM2p0+fVlRUlGJjY3X48GFdvHhRZcuWlaenp6pVq6aAgAA1bdpU7du3l6enp22/CRMmmLrB16hRQ8uWLdOyZcs0depUJSQkyDAM+fv7a8iQIbahATk5OYqKitKsWbN06NAhlS1bVk2aNNHQoUPVpk2bG75WAAAA3JgSDcvnz5+3K7t48WKh2zs6Ot7U5F5paWl6/fXXtWzZMrt1KSkpSklJ0caNG/XVV19p5MiRpqBbo0YN1axZU0ePHrWVbdq0yRTaNm3aZDrmxo0bTcfIPx5WkoKDg4sUlCXpoYceMoXllStX6sKFC6pUqZJpu7wJ0vI4Ojqqb9++kq4GqiFDhtjVU5KSkpKUlJSkPXv2aMGCBQoPD9fkyZOLVLeSMmnSJC1durTAdcnJyUpOTlZcXJymTJmif/7zn+revbtpmzv9+iSpbdu2Wr16tSRpyZIlOnnypKpXry5JWrBggenLo/DwcP3222/XPeaWLVs0YsSIAgP/oUOHdOjQIc2aNUsDBgzQ22+/XeR7rjBLlizRe++9V+C6vHkGEhMT9eOPP2rw4MF6/fXXi3TciRMnasKECaYvCA4cOKB///vfio6O1tSpU3XPPffY7RcTE6O33nrL7vMkKytLhw8f1uHDhzV37lyFhYVp/Pjx8vLyKsbVFs3mzZs1dOhQXbhwwa4Oly5d0vHjxxUfHy9J+uijj/TAAw9c83jjxo3T119/bSqLi4tTXFycRowYoUGDBmno0KHauHGjbf3ly5e1du1axcbG6v333zd99gAAAODWK9FXR1WtWtWubNCgQfrggw+0cuVKJScnl9i5rly5omHDhhUYlK0uXbqk1157TXPnzjWVh4aGmpbj4uJMx8+bhKyg9QUtW7vgXku7du1MP6/MzEwtWrTIbrv58+ebltu0aWML9NOnTy8wSN5tLl26pFdffVX79+83ld8N19evXz+VL19e0tWWwR9++MG2Lv/EXk2bNlXz5s2ve7zExEQNGTKkwKBs9eOPP2rkyJE3UOsbYxiGvv32W82cOfO6227fvl2fffZZoS3pBw8e1Isvvqjc3FxT+dKlS/Xiiy8W+MWbVWxsrP70pz/p0qVLRbuAYhg5cqRdUL5RJ06csAvK+X366ad2QTk/wzD0zjvvlFh9AAAAUDQl2rJcs2ZN3XvvvdqzZ4+t7Ny5c5o8ebKt1c/b21tBQUHq0KGDunTpcs2umNfy/fff23XhbtmypYYNG6aqVasqLi5O48ePNz1IjxkzRh06dLBN5hMaGmp68M8fzLZv367MzEzT8Q8dOqSzZ8+qSpUqBYZpa/i+ljJlyqh379766quvbGXz58/XQw89ZFvOzs7WwoULTfvlX29t2e7Ro4eeeOIJVa5cWZcvX9axY8e0Y8cOrV27Vo6OJfO9yOeff24303IeV1dX0xcIbm5u6tq1q8LDw+Xj4yMvLy9VqFBBaWlpio+P18SJE22hKCsrS1OnTtU777xz09eXN054xIgRpt9RZGSkXn31VdMxb3bsqoeHh3r06KFZs2ZJkmbOnKnhw4drx44dSkhIsG03cOBAHTly5JrHMgxDb731lume9fPz01/+8hf5+/srMzNTMTEx+vLLL20hdNasWerZs6fCwsIUGRmpkJAQbd26VSNGjDAde/r06aZrzf/vrmzZsgoNDVXHjh3l5+cnT09Pubu76+LFi9qzZ4++/vpr7d2717b95MmT9cgjj1zzWi5fviwXFxe9+uqrCgwM1PHjx/Xhhx/qwIEDtm3i4+O1cOFCW4+CS5cu6R//+IcpQJcrV04vvfSSwsLClJqaqsmTJ9ta8qWrXy5MnjzZrgv1zUhNTTV9hpUrV05vvPGGWrZsqXLlyik1NVUHDhzQ5s2btXLlyusezzAMVaxYUW+99ZaCgoIUGxurMWPG2Nbn5ORo48aNqlq1qv7+97+rbt26+vnnn00TJV68eFFLly7Vgw8+WGLXCQAAgGsr8dmwx4wZoyFDhhTa2nPq1ClFR0crOjpa77//vt54440begDM34InSb6+vpoyZYqtS6q/v788PT1NoeH8+fOaP3++7Z231nC7b98+paamysPDwxScq1atqqSkJElXW5O7du2qbdu2mcK0q6urAgICinUNDz30kCksr1+/XklJSbYW57Vr15q68Xp4eJhefZSTk2M63qBBg0ytl/7+/urcubNeeumlUmmVyhsTW5DmzZsrNzdXY8eOtZVZW9Zu9PryQmFei2+eihUr3tT45MIMGjTIFpZTUlK0YMECU3frKlWqqFu3bnaT31lt2rRJiYmJtuWyZcvadVVu0qSJTp48aeol8cMPPygsLEwuLi5ycXEpsFXa29u70Gt//PHH9fjjjxe4rnHjxqpfv77pS5qDBw/avjS
"text/plain": [
"<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"
]
},
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
"text/plain": [
"<Figure size 1000x250 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"import matplotlib.patches as patches\n",
"\n",
"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",
"\n",
" # --- Chart data and positions ---\n",
" categories = ['Very Slow', 'Slow', 'Average', 'Fast', 'Very Fast']\n",
" positions = [1500, 3000, 4500, 6000, 7500]\n",
" # 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",
" # 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",
"\n",
" # --- 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",
"\n",
" # --- 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",
"\n",
" # --- 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",
"\n",
" # --- Indicator Triangle ---\n",
" ax.plot(indicator_pos, 0.65, 'v', markersize=15, color='#606060', clip_on=False)\n",
"\n",
" # --- 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",
"\n",
"\n",
" # --- 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",
" \n",
" # Hide axes and spines for a cleaner look\n",
" ax.axis('off')\n",
" \n",
" plt.tight_layout()\n",
" plt.savefig(f'{base_dir}/graphs/metabolism_chart.png', bbox_inches='tight', dpi=300)\n",
" plt.show()\n",
"\n",
"\n",
"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",
"\n",
" # --- Chart data and positions ---\n",
" 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",
" carb_percentage = 100 - fat_percentage\n",
" optimal_point = 75\n",
"\n",
" # --- 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",
"\n",
" # 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",
"\n",
" # --- 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",
"\n",
" # --- 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",
"\n",
" # --- 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",
"\n",
" # Hide axes and spines\n",
" ax.axis('off')\n",
"\n",
" plt.tight_layout()\n",
" plt.savefig(f'{base_dir}/graphs/fuel_source_chart.png', bbox_inches='tight', dpi=300)\n",
" plt.show()\n",
"\n",
"\n",
"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"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 9,
"id": "0ab6f0b0",
"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+
"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",
" # 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-21 10:01:07 +01:00
"execution_count": 10,
"id": "ef8bc7ac",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAABfIAAAHOCAYAAADT8PiEAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAA4+BJREFUeJzs3Xd8HOW18PHfbFPvvVvVktwL7t2AMaaYbjoYCCEQQsp9Q9pNclNu2iUJhJLQiwHTDcbG3ca9y7Zk2eq997K72jbvHyvLCHdb0q7k8+Xjj83u7MxZ7Wh25szznKOoqqoihBBCCCGEEEIIIYQQQgi3pHF1AEIIIYQQQgghhBBCCCGEODNJ5AshhBBCCCGEEEIIIYQQbkwS+UIIIYQQQgghhBBCCCGEG5NEvhBCCCGEEEIIIYQQQgjhxiSRL4QQQgghhBBCCCGEEEK4MUnkCyGEEEIIIYQQQgghhBBuTBL5QgghhBBCCCGEEEIIIYQbk0S+EEIIIYQQQgghhBBCCOHGJJEvhBBCCCGEEEIIIYQQQrgxSeQLIYQQQgghhBBCCCGEuGxt2rSJm266iZEjRzJ79myeffZZ7Ha7q8PqRRL5QgghhBBCCCGEEEIIIS5LWVlZfO973yM5OZkXX3yRBx54gFdffZW//e1vrg6tF0VVVdXVQQghhBBCCCGEEEIIIYQQA+2hhx6iubmZTz75pOex1157jWeeeYbNmzcTGhrqwuhOkhH5QgghhBBCCCGEEEIIIS5Lubm5TJ8+vddjM2bMwGq1sm3bNhdFdSqdqwNwV/v373d1CEIIIYQQQgghhBBCCCHOg8lk4te//vUZn9+wYcNpH+/q6sJgMPR67MT/FxYW9l2Al0hG5AshhBBCCCGEEEIIIYQY1HS6ixuznpCQwOHDh3s9lpWVBUBra+ulhtVnZET+OYQnJKFR3O9+h9VspS6rmgBvAzoP+RgHDRVsdhs6rQ6U/tmExWqhxdBCaEooeoO+fzYi3JLdZker07o6DDHEyH4l+oPsV6I/XO77lYrKh1+UU1DS0fNYeoo/Ny2MQTnHiaeKes5lLmeX+74l+ofsV6I/yH7lPoxmB0cO6gj1DMBrkOdmVFXFbreh1epQFPc9XzAYctHr9WccdX82d911F7/4xS948803ufHGGykoKOAf//gHWq17/T5JBvgcPL280WndL5FvVi1g1WDQeeLhMbgPCJcTVVWx2WzodP138FNQUFUVg6cBTy/PftmGcEMqPfuWXIeLPiP7legPsl+J/iD7FTsP1LP1YDM6rcI9NyXy9qfFVO9tJCzCl6tnRvVa1uFQKans5PDRZg7lNlNdb+LOG4Yxe3KEi6J3Y7Jvif4g+5XoD7JfuRUbdsw2DTqd96DP2w1ELqsvqOrFv/bmm28mLy+Pv/zlL/zxj39Er9fzxBNP8OabbxIeHt53QV4iSeQLIYQQQgghxCDW2m7hvc9LALj+ylhmXBGOxerg3RUlfLSqlOR4X5IT/AAoqejghbfzaGqx9FrH258U02VxnJL0F0IIIYQY6jQaDT//+c/5/ve/T2VlJdHR0dhsNv7+978zZswYV4fXw/2GmgshhBBCCCGEOC+qqvL2J8UYTXbiY3xYMNuZiJ87NYJJY0JwOODtT4ux21Va2y08/6Yzie/poWXCqGAeuiOZBbOcr/lgZSkrN1a68u0IIYQQQriMn58f6enp+Pv78/bbbxMbG8u0adNcHVYPGZEvhBBCiEHNZnew+2ADaUn+hAVffEmxTTtrAJgzJcKtp4wKIcQ37TvcRNbRZrRahQdvS+opC6ooCnfeOIyc/FYqqo2s21ZN1tFmmtssRIZ58vPHR+Lt5bwcVFUVTw8tK9ZV8NmaciwWOzctiJNjoRBCCCEuC4cPH2bPnj1kZGRgNpvZuHEjK1as4OWXX3arOvmSyBdCCCHEoPbJ6nLWbq0mPMST3/xwNAb9hU84LCrvYNlnJQDU1JtZcn2CJLCEEG7PZLbx/hclAFw7N5q4KJ9ez/v56Ln5mjje/qSYj1aVAeDlqeWJ+4f3JPHBmfS//spYDAYNH35ZxqpNVXRZHHIsFEIIIcRlQa/Xs3btWp5//nkAxowZw9tvv824ceNcHFlvksjvAw67HZvNBlxCV4ULZLVaQefArtiwSReTHpru/4QQQlwecvJaWLu1GoC6RjOfr6/g1oXxF7yeDduqT/57ew02m4O7Fyei0ch3rBDCfX22toLWdisRoZ5cOzfmtMvMvCKcrXvqKKnoRFHgkTtTiAzzOu2yC2ZFY9BrWPZZCRu212C1OrjnJjkWCiGEEGJoy8jI4IMPPnB1GOckifxLoKoqLQ11GNvbBjyVrjpUDFEqJo0Rs5xYA87PAxUMDgO++KLIDQ4hhBjS2jutvPZBIQDJCb4Ulnaw9usqJo4KZlis73mvp7nVwr7DTQAsmBXF2q3VbNldh0ajcPfixH6JXQghLlVpZScbdzhLgt29OBG97vSDWTQahQduTeaV5QXMnhzO6PSgs6537tRIDAYtb3xYyNd76rBYHSy9PVmS+UIIIYQQLiaJ/EvQ0lCHqb2N8LAwvLy9B3TaqcOuYjNa0ek0SL66mwqmLhP1DfV0WDvww8/VEQkhhOgnqqry5kdFtLZbiQr34kcPZ/DGh0XsPdzImx8V8Yvvj+ypE30um3bWYHeopCb6cduiBOKjfXj5/QI27azl5mvi8PKU0yUhhHtxOFTe+bQIVYVJY0LITA046/KxUd785qnR573+6RPCMOg0vPJ+Abu6e5DMmhR+qWELIYQQQohLIFemF8lht2PsTuIHh4S4YPsqVpsGnU6DIqNjenh6Opsc1tXW4XA4pMyOEEIMQQ6HygcrS8k62oxOq/CdO1PwMGi588ZhHC1opbzayJot1Syad/oyE9/UZbHz9e46AK6cEQXA5HGhvPd5CR1GG43NFmKj5HRJCOFevt5TR3F5J14eWm6/LqFftnHFmBCaWy188GUpn35VxsRRwb3q6gshhBBCiIElWc6LZLM5K9N7eXu7OhTxLV4eXqCAA4erQxFCCNHHbDYHrywvYP12ZzmJu24cRly0s7mjv6+eJdc7E1pfrK+gus50zvXtOthAh9FGaJAH4zJPlpsICfIAoKG5q6/fghBCXJLWdguffOVsXLt4QRyB/oZ+29a8aRFEhnnS3mnjiw2V/bYdIYQQQghxbpLIv2jOxrYDWU5HnCdFPhchhBiKzF12/vn6cfZkNaLVKDyyJIVZkyN6LTNlXCgjhwdis6u8+XERdvuZG9Grqsr6bc4bAvOmR/aq/xzak8g398M7EUKIi/fRqjKMJjvx0d7MnRpx7hdcAp1Ow5LrhwGwcXsNNfXnvkEqhBBCCCH6hyTyhRBCCOH27HaVZ17OJbegFQ+DhicfHM7kcaGnLKcoCvfelIiHQUNBSTsvLcvDaj39DK2j+a1U15nwMGiYMTGs13MnRuQ3NsmIfCGE+zhW2MrOAw0oCtxzc9KANKAdOTyQ0emB2B0q739R2u/bE0IIIYQQpyeJfCGEEEK4vbziNorKO/Dy0PKT72QyIi3wjMuGBHnw8JIUdFqFgznN/PP1Y5jMtl7LNDSZ+XCVszTFjCvCT6n7HBospXWEEO7FYnXw9ifFAMyaHE5SnO+Abfv26xLQahWyj7dw+FjzgG1XCCGEEEKcJN2KLmNP/OAJikuK+XLFl6d9ftn7y/jTX/7Edddex8pVK8+6rokTJvL6y6+f8fm8/DzuffBeVn+xmuCgYABGjR/Fj5/6MQ/c98BpX/Ob3/3G+fevfnPO9yKEEGJoO3KsBYBxI4NJPI/k1bgRwTz1UDr/ejOPY4Vt/O7ZbIbF+uDvq8dg0LBpRy2mLjveXlqumhF5yut7RuRLIl8I4SY+X19BbYOZAD89t1wTP6Dbjgzz4srpkaz5upr3Py9leJI/HgbtgMYghBBCCHG5k0T+Zezahdfy05//lOycbEaOGHnK86u/Ws3oUaP53qPfY8ntS3oe//cr/6a4pJg//f5PPY/5+p49qfLcC89x4/U39iTxz8fS+5ey+LbFPHj/gyTEJ5z364QQQgw9R46
"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-21 10:01:07 +01:00
"execution_count": 11,
"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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
"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",
"\n",
"# Create secondary y-axis for heart rate\n",
"ax2 = ax1.twinx()\n",
"\n",
"# 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",
"\n",
"# 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",
"\n",
"# 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",
"\n",
"\n",
"# 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",
"\n",
"# Set x-axis formatting\n",
"ax1.set_xticks(x_positions)\n",
"ax1.set_xticklabels(stage_labels, fontsize=11)\n",
"\n",
"# Add title\n",
"plt.suptitle('Fuel Utilization Report - Institute of Science, Health and Performance',\n",
" fontsize=14, fontweight='bold', y=0.95)\n",
"\n",
"# 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",
"\n",
"# 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",
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 12,
"id": "8a1878a0",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
"text/plain": [
"<Figure size 1800x500 with 3 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 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",
"\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/vo2_pulse_chart.png', bbox_inches='tight', dpi=300)\n",
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 13,
"id": "7361fb05",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "iVBORw0KGgoAAAANSUhEUgAABdwAAAHFCAYAAADoozskAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd4VNXWwOHfmZpeSSc9QOgdpAkiqAj2LtiwfYqKYi/Yy7XjvRasiL0LWLCAgqD03kkjISGN9Db9fH9MEoyAJJDJTJL1+uSRzJw5Z83MzpR11l5bUVVVRQghhBBCCCGEEEIIIYQQJ0Tj7gCEEEIIIYQQQgghhBBCiI5AEu5CCCGEEEIIIYQQQgghRCuQhLsQQgghhBBCCCGEEEII0Qok4S6EEEIIIYQQQgghhBBCtAJJuAshhBBCCCGEEEIIIYQQrUAS7kIIIYQQQgghhBBCCCFEK5CEuxBCCCGEEEIIIYQQQgjRCiThLoQQQgghhBBCCCGEEEK0Akm4CyGEEEIIIYQQQgghhBCtQBLuQgghhBBCCCGEEEIIIUQrkIS7EEIIIYQQQgghhBBCiHanqqqK22+/nfj4eLy9vRk5ciTr1q1za0yScBdCCCGEEEIIIYQQQgjR7lx33XX8+uuvfPjhh2zbto3TTjuNCRMmkJeX57aYFFVVVbcdXQghhBBCCCGEEEIIIYRoobq6Ovz9/Vm4cCGTJ09uvHzw4MFMmjSJJ5980i1x6dxy1DZks9nYtGkTERERaDRS0C+EEEIIIYQQQgghhBCeyOFwkJOTQ69evdDpDqWujUYjRqOxybY2mw273Y6Xl1eTy729vVm5cmWbxHskHT7hvmnTJoYNG+buMIQQQgghhBBCCCGEEEIch0ceeYRHH320yWX+/v6MGDGCJ554gp49exIREcGnn37KqlWrSElJcU+gdIKEe0REBACLl60gOjrGzdEczlJrIX9dHkG+BgxeeneHI5pJVVVsqg2dokNRFJccw2w1U2ooJTw1HIOXwSXHEJ5HVVWsZit6o95lY0t0PjKuhCvIuBKuImNLuIKMK+EKMq6EK8i48iw1tQ42rtUT7hWMj7F9p1FVVUVVzSiK0aPHVn5+PjNmDGP79u3ExsY2Xv7P6vYGH374IdOnTycmJgatVsugQYO47LLL2LBhQ1uFfJj2PVKaoaGNTFRUNLGxXd0czeFM1RYcWTbCQv0w+kjCvb1QVRWrw4pe47o3QJPZhA4dMTExeHl7HfsGokOQD1fCFWRcCVeQcSVcRcaWcAUZV8IVZFwJV5Bx5Vmqauzs66Kha1Aoft7tO2+nqioOhwmNxqtdjK3AwEACAgKOuV1ycjLLly+npqaGyspKoqKiuOSSS0hKSmqDKI9MmpoLIYQQwiNU1Nq4dd4eFm046O5QhBBCCCGEEEK0I76+vkRFRVFWVsbPP//MOeec47ZYOnyFuxBCCCHah1+2lrI2o4pN+6oZEO9HXBeZ3SOEEEIIIYQQ4uh+/vlnVFWlR48epKenc/fdd5Oamso111zjtpikwl0IIYQQHmFXXg0AVrvKC9/noKqqmyMSQgghhBBCCOHJKioqmDFjBqmpqVx55ZWMHj2an3/+Gb3efS2ApMKd+h5Gdjt2ux1o2y/3VqsVdA7sig0bnt8/STipqNixo9T/5wp2xbl/m8WGVbG65BgdguJcq0Gj07SLHmRCiKPbWZ9wB1iTXsnvO8sZ3zvYjREJIYQQQgghhPBkF198MRdffLG7w2ii0yfcbVYrZcWFWEx1bkl3qw4VQ5RKnaYWk0aShe3F36suXZXkVbUq3nhTW1BLnVLnkmN0JDpvHQGRAWj1WneHIoQ4DjVmO/uKTQCcM6QLC9cf5OUfcjgpJQAfo/xdCyGEEEIIIYRoHzp1wl11OCjKzUGv0xIdHY1Br4c2rpB12FVstVZ0Og1S4C6aUMGKFb2XHkVOxhyd6pwpcrDoIKXZpXRJ6iKPlxDt0J4DtagqRAQamHVmLGvTK8kvtzBvWT4zTu/q7vCEEEIIIYQQQohm6dQJd5vVCqqDqKiuePv4uCUGh13FatOg02kkSdjOqKguaycDzip6DRr0Xno0Gllu4d94eXuh0+nIyc7BbrWjM3bqlzYh2qUduc52Mj1jfPAyaLlzShx3fZTOx38WcubAUBLDvZu1H5PVwYJ1xfh7aZk8qIsrQxZCCCGEEEIIIQ7TqbNSan2/dkWSmUK0ew0nrGSRRSHap4YFU3vF+AIwJjWI0T0CWbmnghe+z+HVa7ofs4XXn3vKefH7/eSVmQFIjvQmNdrXtYELIYQQQgghhBB/I5lmIYQQQrjdzvoK995dDyXI75wch1GnsD6zih83lRz1tkWVFu79JJ1ZH6aTV2Zu7A734YoCl8YshBBCCCGEEEL8kyTchRBCCOFWZTVW8sstAKTGHGrxFh1iZPop0QC8/ON+iisth922ss7G/729h2U7y9FqYOroCN66PhWA37aXsb/E1Ab3QAghhBBCCCGEcJKEuxDHafkfyzH6GSkvL2/xbefNn8eZZ5/Z+kEdw4RTJnDn7Xe2+XH/zbFislgsdEvsxob1G9owKiFEW2qobo/v4oWfV9Nud9NGR9Izxocqk53/LMxu0jbK4VB55Mss8srMRAUZ+ODmXtx2Riz94vwY2T0Qhwofryxs0/sihBBCCCGEEKJzk4R7O3PeRecx5dwpR7xu5Z8rMfoZ2bZ9GwB1dXU8/uTj9B7QG/8Qf6Ljorls2mXs3Lmzye3enfcu4yeOJ6JrBBFdIzhjyhmsW7/O5felJbr36o7Rz4jRz4h3gDcJKQncePONlJWVtcnxJ54xkTvvaZ1Etclk4rEnHuOh+x864X0ZNAYWLlj4r9v8sfwPkuKSTvhYJ2r5suUYNIYWn6AwGAzccecdPHDfA64JTAjhdrvyagHo1fXwfus6rcLs8xPRaxVW7qlg8eYSSqutbMyq4vnvc/hrbwVGncKzl6eQEnmoOv7KMZEA/LDpICVV1ra5I0IIIYQQQgghOj1JuLczV195NUt/W0puXu5h133w0QcMHjSYvn36YjabmXTWJOZ/OJ/HZj/G9s3bWfjNQmw2G6NPGc2atWsab/fHij+4+KKL+eXHX1i+dDmxXWOZfM5k8g7kteVdA8BqPXpS5JGHHiE7I5v03em8/+77rPhzBbPunnXU7e12Ow6HwxVhnpBvFnyDv78/I0eMbJPjfbfwOyZPmXzct7dYDm/h0NYum3oZf678kx07drg7FCGEC+xsXDDV54jXJ0d4c119a5nHvt7HpP9s4aZ39/DN2mIA7j07nh7RTW87IMGPvrG+WGwqn62SKnchhBBCCCGEEG1DEu5/o6oqtRabG37sTabI/5vJkyYT1iWMDz/6sMnl1dXVfP3t11x95dUA/O+1/7F6zWq+/epbLrzgQuLj4hk6ZCiff/I5qT1SufHmGxuPOf+9+fzfDf9H/379Se2RytzX5uJwOPh92e9HjeOJp55g6IihvP3u2yT3SCYoLIjLr7icioqKJtu99/579BvUj4DQAPoO7Mvct+Y2Xrcvex9GPyNffvUlE06fQEBoAJ9+/ulRj+nn70dkRCQx0TGMGzuOKy6/gk2bNzVe/8FHHxAeE853P3xH/8H98Q/xJ2d/DmazmXsfuJfEbokEhwczetxolv+xvPF2JSUlXHH1FSR2SyQoLIhBwwbx+RefN15/3Y3X8cfKP3j19Vcbq+z3Ze9rvH7jpo2MGDOCoLAgxp46lj179xz1PgB88dUXTD6zaQL8uhuv48JLL+TZ558lNjGW8JhwnvrPU9hsNu6/534iQiNIjE1k/rz5/7rvI/n+u++ZcvahWRE2m42Zt8ykS1AXosKieGT2I03GX7fEbjz1xFNcc9U1hAaGctONNwHw58o/OeXkUwjwCSApLok7bruDmpqaxtt99OFHnDT0JEICQoiNiuWKqVdQVFQEwL59+5g4fiIA4SHhGDQGrr3m2sbbOhwO7rvnPiJCI4iNiuXxRx9vch+Cg4MZOWokX3z2RYvvvxDHoqoqmYV1zX4dFq1LVdXGljI9j1Dh3mDamEj6xDqvVxS
"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='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",
"\n",
"plt.savefig(f'{base_dir}/graphs/vo2_breath_chart.png', bbox_inches='tight', dpi=300)\n",
"plt.show()"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 14,
"id": "c89478ff",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
"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",
"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()"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 15,
"id": "1db16040",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 09:23:13 +01:00
"image/png": "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
"text/plain": [
"<Figure size 1800x500 with 3 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 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()"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 16,
"id": "c8ad6076",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 10:01:07 +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
"text/plain": [
"<Figure size 1200x550 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# 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",
"plt.tight_layout()\n",
"plt.grid(alpha=0.25)\n",
"# plt.savefig('graphs/tsi_comparison_with_trends.png', bbox_inches='tight', dpi=160)\n",
"plt.show()"
]
},
2025-11-21 09:23:13 +01:00
{
"cell_type": "code",
2025-11-21 10:01:07 +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"
]
},
{
"cell_type": "code",
2025-11-21 10:01:07 +01:00
"execution_count": 18,
"id": "84addc63",
"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",
"VT1: {'HeartRate': 100.5, 'Speed': 4.0, 'Time': 251.0}\n",
"VT2: {'HeartRate': 189.71300000000002, 'Speed': 7.5, 'Time': 1524.0}\n",
"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",
"# # Get the data at maximum fat burning point\n",
"# max_fat_row = df.loc[fat_burn_max_idx]\n",
"# print(f\"Maximum Fat Burning Point:\")\n",
"# print(f\"Time: {max_fat_row['T(sec)']} seconds\")\n",
"# print(f\"Fat burn rate: {max_fat_row['FAT_smoothed']:.3f} kcal/min\")\n",
"# print(f\"Carb burn rate: {max_fat_row['CHO_smoothed']:.3f} kcal/min\")\n",
"# print(f\"Heart Rate: {max_fat_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
"# print(f\"VO2: {max_fat_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
"\n",
"# print(\"\\n\" + \"=\"*50)\n",
"\n",
"# # Get the data at minimum carb burning point\n",
"# min_carb_row = df.loc[carb_burn_min_idx]\n",
"# print(f\"Minimum Carbohydrate Burning Point:\")\n",
"# print(f\"Time: {min_carb_row['T(sec)']} seconds\")\n",
"# print(f\"Fat burn rate: {min_carb_row['FAT_smoothed']:.3f} kcal/min\")\n",
"# print(f\"Carb burn rate: {min_carb_row['CHO_smoothed']:.3f} kcal/min\")\n",
"# print(f\"Heart Rate: {min_carb_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
"# print(f\"VO2: {min_carb_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\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-21 10:34:09 +01:00
"execution_count": 32,
2025-11-21 10:01:07 +01:00
"id": "f324fe94",
"metadata": {},
"outputs": [
{
"data": {
2025-11-21 10:34:09 +01:00
"image/png": "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
2025-11-21 10:01:07 +01:00
"text/plain": [
2025-11-21 10:12:00 +01:00
"<Figure size 1600x1200 with 1 Axes>"
2025-11-21 10:01:07 +01:00
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# 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",
"zones_list = [\n",
" (\"Zone 1\", zone_1_start, zone_1_end),\n",
" (\"Zone 2\", zone_2_start, zone_2_end),\n",
" (\"Zone 3\", zone_3_start['HeartRate'], zone_3_end),\n",
" (\"Zone 4\", zone_4_start, zone_4_end),\n",
" (\"Zone 5\", zone_5_start, zone_5_end)\n",
"]\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 10:12:00 +01:00
"fig, ax = plt.subplots(figsize=(16, 12)) # Increased height slightly\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",
"plt.title(\"Personalized Heart Rate Zones\", fontsize=16, fontweight='bold', pad=20)\n",
"plt.tight_layout()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9c00c366",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
2025-11-21 10:01:07 +01:00
"display_name": "report-generation",
"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"
}
},
"nbformat": 4,
"nbformat_minor": 5
}