Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,807 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "b18c1027",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"{'id': 'gen-1759135172-DIhs7TMuaaVY0h3T2ibV', 'provider': 'Google', 'model': 'google/gemini-2.5-flash-lite', 'object': 'chat.completion', 'created': 1759135172, 'choices': [{'logprobs': None, 'finish_reason': 'stop', 'native_finish_reason': 'STOP', 'index': 0, 'message': {'role': 'assistant', 'content': 'Parameters,Best,LLN,Pred.,%Pred.,ZScore,PRE#1,PRE#2,PRE#3\\nFVC,L,4.24,3.03,3.79,112.0,0.95,4.24,4.17,4.15\\nFEV1,L,3.26,2.53,3.16,103.3,0.28,3.26,3.21,3.14\\nFEV1/FVC%,76.89,72.47,83.78,91.8,-1.05,76.9,77.0,75.7\\nPEF,L/m,684,222,384,178.7,-,444,438,684\\nFEF2575,L/s,2.74,2.15,3.42,80.2,-0.84,2.74,2.68,2.48\\nFEF25,L/s,6.08,-,-,-,6.08,6.0,5.53\\nFEF50,L/s,3.06,-,-,-,3.06,3.1,2.77\\nFEF75,L/s,1.06,0.71,1.41,75.1,-0.72,1.06,1.12,0.94\\nPEFTime,ms,-,-,79,-,79,49,39\\nEvol,mL,-,-,78.0,-,78.0,77.0,197.0\\nFEV6,L,4.22,3.03,3.79,111.4,-,4.22,4.17,4.13', 'refusal': None, 'reasoning': None}}], 'usage': {'prompt_tokens': 1350, 'completion_tokens': 454, 'total_tokens': 1804, 'prompt_tokens_details': {'cached_tokens': 0}, 'completion_tokens_details': {'reasoning_tokens': 0, 'image_tokens': 0}}}\n",
|
||||
"Content saved to extracted_table.csv\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"\n",
|
||||
"import requests\n",
|
||||
"import json\n",
|
||||
"import base64\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"API_KEY_REF = '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'\n",
|
||||
"def encode_pdf_to_base64(pdf_path):\n",
|
||||
" with open(pdf_path, \"rb\") as pdf_file:\n",
|
||||
" return base64.b64encode(pdf_file.read()).decode('utf-8')\n",
|
||||
"\n",
|
||||
"url = \"https://openrouter.ai/api/v1/chat/completions\"\n",
|
||||
"headers = {\n",
|
||||
" \"Authorization\": f\"Bearer {API_KEY_REF}\",\n",
|
||||
" \"Content-Type\": \"application/json\"\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"# Read and encode the PDF\n",
|
||||
"pdf_path = \"data/~Moran~K~19910201~Spirometry Exam~20250729~20250729032843.pdf\"\n",
|
||||
"base64_pdf = encode_pdf_to_base64(pdf_path)\n",
|
||||
"data_url = f\"data:application/pdf;base64,{base64_pdf}\"\n",
|
||||
"\n",
|
||||
"messages = [\n",
|
||||
" {\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": [\n",
|
||||
" {\n",
|
||||
" \"type\": \"text\",\n",
|
||||
" \"text\": \"Please extract the Spirometry table from the pdf and return the values in csv format, \"\n",
|
||||
" \"note that it is the unit of parameter that is beside it and it should not be a column. \"\n",
|
||||
" \"The '-' Should be treated as empty values.\"\n",
|
||||
" \"do not add 'csv' at the start or end of the response\"\n",
|
||||
" },\n",
|
||||
" {\n",
|
||||
" \"type\": \"file\",\n",
|
||||
" \"file\": {\n",
|
||||
" \"filename\": \"document.pdf\",\n",
|
||||
" \"file_data\": data_url\n",
|
||||
" }\n",
|
||||
" },\n",
|
||||
" ]\n",
|
||||
" }\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"# Optional: Configure PDF processing engine\n",
|
||||
"# PDF parsing will still work even if the plugin is not explicitly set\n",
|
||||
"plugins = [\n",
|
||||
" {\n",
|
||||
" \"id\": \"file-parser\",\n",
|
||||
" \"pdf\": {\n",
|
||||
" \"engine\": \"pdf-text\" # defaults to \"mistral-ocr\". See Pricing above\n",
|
||||
" }\n",
|
||||
" }\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"payload = {\n",
|
||||
" \"model\": \"google/gemini-2.5-flash-lite\",\n",
|
||||
" \"messages\": messages,\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"response = requests.post(url, headers=headers, json=payload)\n",
|
||||
"# Get the response content\n",
|
||||
"response_data = response.json()\n",
|
||||
"print(response_data)\n",
|
||||
"\n",
|
||||
"# Extract the content from the response\n",
|
||||
"if 'choices' in response_data and len(response_data['choices']) > 0:\n",
|
||||
" content = response_data['choices'][0]['message']['content']\n",
|
||||
" \n",
|
||||
" # Save to a CSV file\n",
|
||||
" output_file = \"extracted_table.csv\"\n",
|
||||
" with open(output_file, 'w', encoding='utf-8') as f:\n",
|
||||
" f.write(content)\n",
|
||||
" \n",
|
||||
" print(f\"Content saved to {output_file}\")\n",
|
||||
"else:\n",
|
||||
" print(\"No content found in response\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "56a9d655",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"FVC Best: 4.24, FVC Pred: 112.0\n",
|
||||
"FEV1 Best: 3.26, FEV1 Pred: 103.3\n",
|
||||
"FEV1/FVC% Best: 76.89, FEV1/FVC% Pred: 91.8\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"spirometry_df = pd.read_csv(\"extracted_table.csv\")\n",
|
||||
"\n",
|
||||
"fvc_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', 'Best'].values[0]\n",
|
||||
"fvc_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FVC', '%Pred.'].values[0]\n",
|
||||
"\n",
|
||||
"fev1_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FEV1', 'Best'].values[0]\n",
|
||||
"fev1_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FEV1', '%Pred.'].values[0]\n",
|
||||
"\n",
|
||||
"fev1_fevc_best = spirometry_df.loc[spirometry_df['Parameters'] == 'FEV1/FVC%', 'Best'].values[0]\n",
|
||||
"fev1_fevc_pred = spirometry_df.loc[spirometry_df['Parameters'] == 'FEV1/FVC%', '%Pred.'].values[0]\n",
|
||||
"\n",
|
||||
"print(f\"FVC Best: {fvc_best}, FVC Pred: {fvc_pred}\")\n",
|
||||
"print(f\"FEV1 Best: {fev1_best}, FEV1 Pred: {fev1_pred}\")\n",
|
||||
"print(f\"FEV1/FVC% Best: {fev1_fevc_best}, FEV1/FVC% Pred: {fev1_fevc_pred}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"id": "990f4b4f",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Peak VT: 2.75\n",
|
||||
"HR at Peak VT: 155.0\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"df = pd.read_csv('data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n",
|
||||
"peak_vt = df['VT(l)'].max()\n",
|
||||
"max_vt_row = df.loc[df['VT(l)'].idxmax()]\n",
|
||||
"print(f\"Peak VT: {peak_vt}\")\n",
|
||||
"hr = max_vt_row['HR(bpm)']\n",
|
||||
"print(f\"HR at Peak VT: {hr}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 18,
|
||||
"id": "041cbc3d",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Peak VT: 2.3770000000000002\n",
|
||||
"HR at Peak VT: 171.525\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/tmp/ipykernel_301535/4157056299.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('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).mean()\n",
|
||||
" \n",
|
||||
"peak_vt = df['VT(l)_smoothed'].max()\n",
|
||||
"max_vt_row = df.loc[df['VT(l)_smoothed'].idxmax()]\n",
|
||||
"print(f\"Peak VT: {peak_vt}\")\n",
|
||||
"hr = max_vt_row['HR(bpm)_smoothed']\n",
|
||||
"print(f\"HR at Peak VT: {hr}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 20,
|
||||
"id": "de7cadd1",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Percent FEV: 72.91411042944786\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"percent_fev = (peak_vt / fev1_best) * 100\n",
|
||||
"print(f\"Percent FEV: {percent_fev}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 24,
|
||||
"id": "cb972ed3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>MeasurementDate</th>\n",
|
||||
" <th>Comment</th>\n",
|
||||
" <th>ExternalDeviceId</th>\n",
|
||||
" <th>ExternalPatientId</th>\n",
|
||||
" <th>FirstName</th>\n",
|
||||
" <th>LastName</th>\n",
|
||||
" <th>BirthDate</th>\n",
|
||||
" <th>Age</th>\n",
|
||||
" <th>Ethnicity</th>\n",
|
||||
" <th>Gender</th>\n",
|
||||
" <th>...</th>\n",
|
||||
" <th>Child_XC</th>\n",
|
||||
" <th>Child_XC_Unit</th>\n",
|
||||
" <th>Child_BIVA_ZRh</th>\n",
|
||||
" <th>Child_BIVA_ZXcH</th>\n",
|
||||
" <th>Child_PhA</th>\n",
|
||||
" <th>Child_PhA_Unit</th>\n",
|
||||
" <th>Child_REE_Kcal</th>\n",
|
||||
" <th>Child_REE_MJ</th>\n",
|
||||
" <th>Child_TEE_Kcal</th>\n",
|
||||
" <th>Child_TEE_MJ</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>13</th>\n",
|
||||
" <td>2025-07-29T18:58:54.0000000Z</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>10000001583275_0055003f5631501320313557</td>\n",
|
||||
" <td>KM6479696509</td>\n",
|
||||
" <td>Keirstyn</td>\n",
|
||||
" <td>Moran</td>\n",
|
||||
" <td>1991-02-01T00:00:00.0000000Z</td>\n",
|
||||
" <td>34</td>\n",
|
||||
" <td>Caucasian</td>\n",
|
||||
" <td>Female</td>\n",
|
||||
" <td>...</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" <td>NaN</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"<p>1 rows × 147 columns</p>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" MeasurementDate Comment \\\n",
|
||||
"13 2025-07-29T18:58:54.0000000Z NaN \n",
|
||||
"\n",
|
||||
" ExternalDeviceId ExternalPatientId FirstName \\\n",
|
||||
"13 10000001583275_0055003f5631501320313557 KM6479696509 Keirstyn \n",
|
||||
"\n",
|
||||
" LastName BirthDate Age Ethnicity Gender ... \\\n",
|
||||
"13 Moran 1991-02-01T00:00:00.0000000Z 34 Caucasian Female ... \n",
|
||||
"\n",
|
||||
" Child_XC Child_XC_Unit Child_BIVA_ZRh Child_BIVA_ZXcH Child_PhA \\\n",
|
||||
"13 NaN NaN NaN NaN NaN \n",
|
||||
"\n",
|
||||
" Child_PhA_Unit Child_REE_Kcal Child_REE_MJ Child_TEE_Kcal Child_TEE_MJ \n",
|
||||
"13 NaN NaN NaN NaN NaN \n",
|
||||
"\n",
|
||||
"[1 rows x 147 columns]"
|
||||
]
|
||||
},
|
||||
"execution_count": 24,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"personal_df = pd.read_excel('data/SECA body comp for all patients.xlsx')\n",
|
||||
"\n",
|
||||
"keirstyn_data = personal_df[personal_df['LastName'].str.contains('Moran', case=False, na=False)]\n",
|
||||
"keirstyn_data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 26,
|
||||
"id": "98d9295a",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"VO2 Max: 47.906290322580645\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"v02_max = df['VO2(ml/min)_smoothed'].max()\n",
|
||||
"weight = keirstyn_data['Weight'].iloc[0]\n",
|
||||
"print(f\"VO2 Max: {v02_max/weight}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 32,
|
||||
"id": "cdfeb309",
|
||||
"metadata": {},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# 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(f\"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\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 33,
|
||||
"id": "4420cfea",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Found 2 intersections at indices: [18, 47]\n",
|
||||
"\n",
|
||||
"Last intersection at index 47:\n",
|
||||
"Time: 251.0 seconds\n",
|
||||
"Fat burn rate: 3.040 kcal/min\n",
|
||||
"Carb burn rate: 3.166 kcal/min\n",
|
||||
"Heart Rate: 100.5 bpm\n",
|
||||
"VO2: 1283.0 ml/min\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Find intersections where FAT_smoothed and CHO_smoothed cross each other\n",
|
||||
"intersections = []\n",
|
||||
"\n",
|
||||
"for i in range(1, len(df)):\n",
|
||||
" # Check if there's a crossover between consecutive points\n",
|
||||
" prev_fat = df.iloc[i-1]['FAT_smoothed']\n",
|
||||
" prev_cho = df.iloc[i-1]['CHO_smoothed']\n",
|
||||
" curr_fat = df.iloc[i]['FAT_smoothed']\n",
|
||||
" curr_cho = df.iloc[i]['CHO_smoothed']\n",
|
||||
" \n",
|
||||
" # Skip if any values are NaN\n",
|
||||
" if pd.isna(prev_fat) or pd.isna(prev_cho) or pd.isna(curr_fat) or pd.isna(curr_cho):\n",
|
||||
" continue\n",
|
||||
" \n",
|
||||
" # Check if lines cross (fat was above/below cho and now it's below/above)\n",
|
||||
" if ((prev_fat > prev_cho and curr_fat < curr_cho) or \n",
|
||||
" (prev_fat < prev_cho and curr_fat > curr_cho)):\n",
|
||||
" intersections.append(i)\n",
|
||||
"\n",
|
||||
"print(f\"Found {len(intersections)} intersections at indices: {intersections}\")\n",
|
||||
"\n",
|
||||
"if intersections:\n",
|
||||
" # Get the last intersection\n",
|
||||
" last_intersection_idx = intersections[-1]\n",
|
||||
" last_intersection_row = df.iloc[last_intersection_idx]\n",
|
||||
" \n",
|
||||
" print(f\"\\nLast intersection at index {last_intersection_idx}:\")\n",
|
||||
" print(f\"Time: {last_intersection_row['T(sec)']} seconds\")\n",
|
||||
" print(f\"Fat burn rate: {last_intersection_row['FAT_smoothed']:.3f} kcal/min\")\n",
|
||||
" print(f\"Carb burn rate: {last_intersection_row['CHO_smoothed']:.3f} kcal/min\")\n",
|
||||
" print(f\"Heart Rate: {last_intersection_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
|
||||
" print(f\"VO2: {last_intersection_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
|
||||
"else:\n",
|
||||
" print(\"No intersections found between FAT and CHO curves\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 37,
|
||||
"id": "62803668",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"VT1: {'HeartRate': 100.5, 'Speed': 4.0, 'Time': 251.0}\n",
|
||||
"VT2: {'HeartRate': 189.71300000000002, 'Speed': 7.5, 'Time': 1524.0}\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 40,
|
||||
"id": "07593b56",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"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 - 199.7 bpm\n",
|
||||
"Zone 5 (VO2 Max): 199.7+ bpm\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"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'] + 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}+ bpm\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 60,
|
||||
"id": "c90415b2",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"VO2 Max detected at index 202:\n",
|
||||
"Time: 985.0 seconds\n",
|
||||
"VO2 Breath: 58.2 ml/breath\n",
|
||||
"VO2: 2167.8 ml/min\n",
|
||||
"VO2 per kg: 38.8 ml/kg/min\n",
|
||||
"Heart Rate: 170.5 bpm\n",
|
||||
"Speed: 6.0 km/h\n",
|
||||
"VO2 Breath Slope: -0.02\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Calculate the slope of VO2 Breath (first derivative)\n",
|
||||
"df['vo2_breath_slope'] = df['VO2 Breath_smoothed'].diff()\n",
|
||||
"\n",
|
||||
"# Find points where slope is consistently zero or negative\n",
|
||||
"# We'll use a rolling window to check for consistent negative/zero slope\n",
|
||||
"window = len(df) // 3 # Number of consecutive points to check\n",
|
||||
"\n",
|
||||
"# Calculate rolling mean of slope to smooth out noise\n",
|
||||
"df['vo2_breath_slope_smoothed'] = df['vo2_breath_slope'].rolling(window=window).mean()\n",
|
||||
"\n",
|
||||
"# Find where slope becomes consistently zero or negative\n",
|
||||
"mask = df['vo2_breath_slope_smoothed'] <= 0\n",
|
||||
"consistent_negative_indices = mask[mask].index\n",
|
||||
"\n",
|
||||
"if len(consistent_negative_indices) > 0:\n",
|
||||
" # Find the first point where slope becomes consistently negative/zero\n",
|
||||
" vo2_max_idx = consistent_negative_indices[0]\n",
|
||||
" vo2_max_row = df.loc[vo2_max_idx]\n",
|
||||
" \n",
|
||||
" print(f\"VO2 Max detected at index {vo2_max_idx}:\")\n",
|
||||
" print(f\"Time: {vo2_max_row['T(sec)']} seconds\")\n",
|
||||
" print(f\"VO2 Breath: {vo2_max_row['VO2 Breath_smoothed']:.1f} ml/breath\")\n",
|
||||
" print(f\"VO2: {vo2_max_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
|
||||
" print(f\"VO2 per kg: {vo2_max_row['VO2(ml/min)_smoothed']/weight:.1f} ml/kg/min\")\n",
|
||||
" print(f\"Heart Rate: {vo2_max_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
|
||||
" print(f\"Speed: {vo2_max_row['Speed']} km/h\")\n",
|
||||
" print(f\"VO2 Breath Slope: {vo2_max_row['vo2_breath_slope_smoothed']:.2f}\")\n",
|
||||
"else:\n",
|
||||
" # If no consistent negative slope found, use the maximum VO2 Breath value\n",
|
||||
" vo2_max_idx = df['VO2 Breath_smoothed'].idxmax()\n",
|
||||
" vo2_max_row = df.loc[vo2_max_idx]\n",
|
||||
" \n",
|
||||
" print(f\"No consistent negative slope found. Using peak VO2 Breath at index {vo2_max_idx}:\")\n",
|
||||
" print(f\"Time: {vo2_max_row['T(sec)']} seconds\")\n",
|
||||
" print(f\"VO2 Breath: {vo2_max_row['VO2 Breath_smoothed']:.1f} ml/breath\")\n",
|
||||
" print(f\"VO2: {vo2_max_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
|
||||
" print(f\"VO2 per kg: {vo2_max_row['VO2(ml/min)_smoothed']/weight:.1f} ml/kg/min\")\n",
|
||||
" print(f\"Heart Rate: {vo2_max_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
|
||||
" print(f\"Speed: {vo2_max_row['Speed']} km/h\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 66,
|
||||
"id": "c3b2cc59",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"VO2 Pulse and HR slopes diverge consistently starting at index 89:\n",
|
||||
"Time: 485.0 seconds\n",
|
||||
"VO2 Pulse (smoothed): 13.91\n",
|
||||
"Heart Rate (smoothed): 136.2 bpm\n",
|
||||
"VO2 Pulse Slope: 0.672\n",
|
||||
"HR Slope: 1.000\n",
|
||||
"Slope Difference: 1.006\n",
|
||||
"VO2: 1897.8 ml/min\n",
|
||||
"Speed: 4.5 km/h\n",
|
||||
"Threshold used: 0.615\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Calculate slopes for both VO2 Pulse and HR\n",
|
||||
"df['vo2_pulse_slope'] = df['VO2 Pulse_smoothed'].diff()\n",
|
||||
"df['hr_slope'] = df['HR(bpm)_smoothed'].diff()\n",
|
||||
"\n",
|
||||
"# Calculate the difference between the slopes\n",
|
||||
"df['slope_difference'] = abs(df['vo2_pulse_slope'] - df['hr_slope'])\n",
|
||||
"\n",
|
||||
"# Find where the slope difference becomes consistently large (slopes diverge)\n",
|
||||
"# Use a rolling window to smooth out noise\n",
|
||||
"window_size = len(df) // 5 # Adjust window size as needed\n",
|
||||
"df['slope_difference_smoothed'] = df['slope_difference'].rolling(window=window_size).mean()\n",
|
||||
"\n",
|
||||
"# Find the threshold - we'll use the 75th percentile of slope differences as threshold\n",
|
||||
"threshold = df['slope_difference_smoothed'].quantile(0.75)\n",
|
||||
"\n",
|
||||
"# Find points where slope difference exceeds threshold\n",
|
||||
"divergence_mask = df['slope_difference_smoothed'] > threshold\n",
|
||||
"divergence_indices = divergence_mask[divergence_mask].index\n",
|
||||
"\n",
|
||||
"if len(divergence_indices) > 0:\n",
|
||||
" # Find the first sustained divergence point\n",
|
||||
" min_consecutive_points = 5\n",
|
||||
" consistent_divergence_idx = None\n",
|
||||
" \n",
|
||||
" for start_idx in divergence_indices:\n",
|
||||
" # Check if divergence is sustained for consecutive points\n",
|
||||
" consecutive_count = 0\n",
|
||||
" for j in range(start_idx, min(start_idx + min_consecutive_points, len(df))):\n",
|
||||
" if j in divergence_indices:\n",
|
||||
" consecutive_count += 1\n",
|
||||
" else:\n",
|
||||
" break\n",
|
||||
" \n",
|
||||
" if consecutive_count >= min_consecutive_points:\n",
|
||||
" consistent_divergence_idx = start_idx\n",
|
||||
" break\n",
|
||||
" \n",
|
||||
" if consistent_divergence_idx is not None:\n",
|
||||
" divergence_row = df.iloc[consistent_divergence_idx]\n",
|
||||
" \n",
|
||||
" print(f\"VO2 Pulse and HR slopes diverge consistently starting at index {consistent_divergence_idx}:\")\n",
|
||||
" print(f\"Time: {divergence_row['T(sec)']} seconds\")\n",
|
||||
" print(f\"VO2 Pulse (smoothed): {divergence_row['VO2 Pulse_smoothed']:.2f}\")\n",
|
||||
" print(f\"Heart Rate (smoothed): {divergence_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
|
||||
" print(f\"VO2 Pulse Slope: {divergence_row['vo2_pulse_slope']:.3f}\")\n",
|
||||
" print(f\"HR Slope: {divergence_row['hr_slope']:.3f}\")\n",
|
||||
" print(f\"Slope Difference: {divergence_row['slope_difference_smoothed']:.3f}\")\n",
|
||||
" print(f\"VO2: {divergence_row['VO2(ml/min)_smoothed']:.1f} ml/min\")\n",
|
||||
" print(f\"Speed: {divergence_row['Speed']} km/h\")\n",
|
||||
" print(f\"Threshold used: {threshold:.3f}\")\n",
|
||||
" else:\n",
|
||||
" print(f\"No sustained divergence found. Threshold: {threshold:.3f}\")\n",
|
||||
" # Show the point with maximum slope difference instead\n",
|
||||
" max_diff_idx = df['slope_difference_smoothed'].idxmax()\n",
|
||||
" max_diff_row = df.iloc[max_diff_idx]\n",
|
||||
" \n",
|
||||
" print(f\"\\nPoint with maximum slope difference at index {max_diff_idx}:\")\n",
|
||||
" print(f\"Time: {max_diff_row['T(sec)']} seconds\")\n",
|
||||
" print(f\"VO2 Pulse (smoothed): {max_diff_row['VO2 Pulse_smoothed']:.2f}\")\n",
|
||||
" print(f\"Heart Rate (smoothed): {max_diff_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
|
||||
" print(f\"Slope Difference: {max_diff_row['slope_difference_smoothed']:.3f}\")\n",
|
||||
"else:\n",
|
||||
" print(\"No significant slope divergence found between VO2 Pulse and HR\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "672d68f3",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Maximum FAT_smoothed occurs at index 30:\n",
|
||||
"Heart Rate (smoothed): 96.7 bpm\n",
|
||||
"FAT (smoothed): 3.894 kcal/min\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"max_fat_smoothed_idx = df['FAT_smoothed'].idxmax()\n",
|
||||
"max_fat_smoothed_row = df.loc[max_fat_smoothed_idx]\n",
|
||||
"max_heart_rate = 220 - keirstyn_data['Age'].iloc[0]\n",
|
||||
"\n",
|
||||
"print(f\"Maximum FAT_smoothed occurs at index {max_fat_smoothed_idx}:\")\n",
|
||||
"print(f\"Heart Rate (smoothed): {max_fat_smoothed_row['HR(bpm)_smoothed']:.1f} bpm\")\n",
|
||||
"print(f\"FAT (smoothed): {max_fat_smoothed_row['FAT_smoothed']:.3f} kcal/min\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "fe3b7605",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"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",
|
||||
"version": "3.12.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
import base64
|
||||
|
||||
|
||||
def image_to_base64(image_path):
|
||||
try:
|
||||
with open(image_path, "rb") as image_file:
|
||||
return base64.b64encode(image_file.read()).decode("utf-8")
|
||||
except FileNotFoundError:
|
||||
print(f"Warning: Image not found at {image_path}")
|
||||
return ""
|
||||
|
||||
|
||||
### Defining Page Contexts ###
|
||||
page_1_context = {
|
||||
"name": "John Doe",
|
||||
"surname": "Moran",
|
||||
"date": "July 29, 2025",
|
||||
}
|
||||
|
||||
page_2_context = {
|
||||
"content": "This is page 2 content",
|
||||
}
|
||||
|
||||
page_3_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
}
|
||||
|
||||
page_4_context = {
|
||||
"body_composition_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/body_composition_chart.png"
|
||||
),
|
||||
"body_fat_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/body_fat_percent_chart.png"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
page_5_context = {
|
||||
"metabolism_chart": "",
|
||||
"fuel_source_chart": "",
|
||||
"resting_calories": 1540,
|
||||
"neat_calories": 310,
|
||||
"weight_loss_calories": 1725,
|
||||
"weight_loss_rate": "1lb/week",
|
||||
"total_calories": 3575,
|
||||
}
|
||||
|
||||
page_6_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
"age": "34",
|
||||
"height": "5'4\"",
|
||||
"weight": "123lbs",
|
||||
"focus": "Endurance",
|
||||
"deficit_calories": "1725KCals",
|
||||
"deficit_protein": "120g Protein",
|
||||
"deficit_carbs": "155g Carbs",
|
||||
"deficit_fat": "69g Fat",
|
||||
"deficit_fiber": "25g Fibre",
|
||||
"refeed_weekday_calories": "1615KCals",
|
||||
"refeed_weekday_protein": "120g Protein",
|
||||
"refeed_weekday_carbs": "142g Carbs",
|
||||
"refeed_weekday_fat": "63g Fat",
|
||||
"refeed_weekday_fiber": "24g Fibre",
|
||||
"refeed_weekend_calories": "2000KCals",
|
||||
"refeed_weekend_protein": "120g Protein",
|
||||
"refeed_weekend_carbs": "190g Carbs",
|
||||
"refeed_weekend_fat": "84g Fat",
|
||||
"refeed_weekend_fiber": "30g Fibre",
|
||||
"protein_percentage": "28%",
|
||||
"carbs_percentage": "36%",
|
||||
"fats_percentage": "36%",
|
||||
"page_number": "6",
|
||||
}
|
||||
|
||||
page_7_context = {
|
||||
"indication": "No Respiratory Capacity Limitation",
|
||||
"peak_vt": 3.2,
|
||||
"peak_vt_bpm": 198,
|
||||
"peak_vt_zone": 3,
|
||||
"fev1_percentage": 85,
|
||||
"lung_analysis_chart": image_to_base64("/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/spirometry_chart.png"),
|
||||
"respiratory_analysis_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/respiratory.png"
|
||||
),
|
||||
}
|
||||
|
||||
page_8_context = {
|
||||
"vo2_max_value": "49.5",
|
||||
"vo2_max_percentile": "100th percentile",
|
||||
"age_range": "30-39",
|
||||
"very_poor_range": "19.0-24.1",
|
||||
"poor_range": "24.1-28.2",
|
||||
"fair_range": "28.2-32.2",
|
||||
"good_range": "32.2-35.7",
|
||||
"excellent_range": "35.7-45.8",
|
||||
"superior_range": "45.8+",
|
||||
"zone1_percentage": "55-65% of Max Heart Rate",
|
||||
"zone2_percentage": "65-75% of Max Heart Rate",
|
||||
"zone3_percentage": "80-85% of Max Heart Rate",
|
||||
"zone4_percentage": "85-88% of Max Heart Rate",
|
||||
"zone5_percentage": "90% of Max Heart Rate",
|
||||
"zone1_bpm": "81-96bpm",
|
||||
"zone2_bpm": "96-100bpm",
|
||||
"zone3_bpm": "100-178bpm",
|
||||
"zone4_bpm": "178-188bpm",
|
||||
"zone5_bpm": "188-198bpm",
|
||||
"zone1_speed": "3.5mph",
|
||||
"zone2_speed": "3.5-4.0mph",
|
||||
"zone3_speed": "4.0-6.5mph",
|
||||
"zone4_speed": "6.5-7.0mph",
|
||||
"zone5_speed": "7.0-8.0mph",
|
||||
"zone1_incline": "2% Incline",
|
||||
"zone2_incline": "2% Incline",
|
||||
"zone3_incline": "2% Incline",
|
||||
"zone4_incline": "2% Incline",
|
||||
"zone5_incline": "2% Incline",
|
||||
"zone1_pace": "10:39min/km Pace",
|
||||
"zone2_pace": "10:39-9:19min/km Pace",
|
||||
"zone3_pace": "9:19-5:44min/km Pace",
|
||||
"zone4_pace": "5:44-5:20min/km Pace",
|
||||
"zone5_pace": "5:20-4:40min/km Pace",
|
||||
"zone1_calories": "4.4kcals/minute",
|
||||
"zone2_calories": "5.9kcals/minute",
|
||||
"zone3_calories": "9.4kcals/minute",
|
||||
"zone4_calories": "12.5kcals/minute",
|
||||
"zone5_calories": "12.8kcals/minute",
|
||||
"zone1_carb": "Avg: 0.4g/min Carb Utilization",
|
||||
"zone2_carb": "Avg: 0.6g/min Carb Utilization",
|
||||
"zone3_carb": "Avg: 1.9g/min Carb Utilization",
|
||||
"zone4_carb": "Avg: 2.9g/min Carb Utilization",
|
||||
"zone5_carb": "Avg: 3.1g/min Carb Utilization",
|
||||
"zone1_breaths": "Avg: 27 breaths",
|
||||
"zone2_breaths": "Avg: 28 breaths",
|
||||
"zone3_breaths": "Avg: 31 breaths",
|
||||
"zone4_breaths": "Avg: 42 breaths",
|
||||
"zone5_breaths": "Avg: 51 breaths",
|
||||
"zone1_breath_range": "Ideal Range: 15-20 breaths",
|
||||
"zone2_breath_range": "Ideal Range: 20-25 breaths",
|
||||
"zone3_breath_range": "Ideal Range: 25-30 breaths",
|
||||
"zone4_breath_range": "Ideal Range: 30-35 breaths",
|
||||
"zone5_breath_range": "Ideal Range: 40+ breaths",
|
||||
}
|
||||
|
||||
page_9_context = {
|
||||
"fuel_utilization_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/fuel_utilization_chart.png"
|
||||
),
|
||||
}
|
||||
|
||||
page_10_context = {
|
||||
"vo2_pulse_drop_bpm": "180 bpm",
|
||||
"vo2_pulse_drop_zone": "Zone 4",
|
||||
"vo2_pulse_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/vo2_pulse_chart.png"
|
||||
),
|
||||
"vo2_breath_drop_bpm": "173 bpm",
|
||||
"vo2_breath_drop_zone": "Zone 3",
|
||||
"vo2_breath_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/vo2_breath_chart.png"
|
||||
),
|
||||
}
|
||||
|
||||
page_11_context = {
|
||||
"fat_max_optimal": "*Optimal 10-12Kcals/minute",
|
||||
"fat_max_value": "3.8Kcals/min",
|
||||
"fat_max_heart_rate": "49% of Max Heart Rate",
|
||||
"fat_max_bpm": "97 bpm",
|
||||
"crossover_bpm": "100bpm",
|
||||
"crossover_heart_rate": "51% of Max Heart Rate",
|
||||
"fat_metabolism_note": "100bpm at a speed of 4.0mph and incline of 2%",
|
||||
"fat_metabolism_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/fat_metabolism_chart.png"
|
||||
),
|
||||
"cardiac_recovery_time": "(1 minute)",
|
||||
"cardiac_recovery_percentage": "33%",
|
||||
"metabolic_recovery_time": "(2 minute)",
|
||||
"metabolic_recovery_percentage": "65%",
|
||||
"breath_recovery_time": "(2.5 minute)",
|
||||
"breath_recovery_percentage": "76%",
|
||||
"recovery_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/recovery_chart.png"
|
||||
),
|
||||
"resting_heart_rate": "53bpm",
|
||||
"hr_age_range": "26-35",
|
||||
"hr_poor": "82bpm +",
|
||||
"hr_below_avg": "75-81bpm",
|
||||
"hr_average": "71-74bpm",
|
||||
"hr_above_avg": "66-70bpm",
|
||||
"hr_good": "62-65bpm",
|
||||
"hr_excellent": "55-61bpm",
|
||||
"hr_athlete": "44-54bpm",
|
||||
}
|
||||
|
||||
page_12_context = {
|
||||
|
||||
}
|
||||
|
||||
page_13_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
"age": "34",
|
||||
"height": "5'4\"",
|
||||
"weight": "123lbs",
|
||||
"focus": "Endurance",
|
||||
"zone2_frequency": "3-4x/week",
|
||||
"zone2_duration": "40+ minutes",
|
||||
"zone2_hr_range": "96-110bpm",
|
||||
"zone2_speed": "3.5-4.0mph",
|
||||
"zone2_incline": "2% Incline",
|
||||
"zone3_frequency": "1-2x/week",
|
||||
"zone3_duration": "10-20 minutes",
|
||||
"zone3_hr_range": "100-178bpm",
|
||||
"zone3_speed": "4.0-6.5mph",
|
||||
"zone3_incline": "2% Incline",
|
||||
"zone3_target_hr": "140bpm",
|
||||
"zone3_recovery_speed": "3.5mph",
|
||||
"zone3_recovery_incline": "2% Incline",
|
||||
"zone1_hr_range": "81-96bpm",
|
||||
"zone1_duration": "4-8 minutes",
|
||||
"zone3_repeats": "2-3 times",
|
||||
"short_sets": "8-10",
|
||||
"short_duration": "10-30 seconds",
|
||||
"short_zone": "5",
|
||||
"short_rpe": "10",
|
||||
"short_recovery": "20-60 seconds",
|
||||
"medium_sets": "6-8",
|
||||
"medium_duration": "30-90 seconds",
|
||||
"medium_zone": "4",
|
||||
"medium_rpe": "8-9",
|
||||
"medium_recovery": "30-90 seconds",
|
||||
"long_sets": "4-6",
|
||||
"long_duration": "5-10 minutes",
|
||||
"long_zone": "3/4",
|
||||
"long_rpe": "7-8",
|
||||
"long_recovery": "2.5-5 minutes",
|
||||
"tempo_sets": "2-3",
|
||||
"tempo_duration": "10-20 minutes",
|
||||
"tempo_zone": "3",
|
||||
"tempo_rpe": "6-7",
|
||||
"tempo_recovery": "4-8 minutes",
|
||||
"cardio_sets": "1",
|
||||
"cardio_duration": ">40 minutes",
|
||||
"cardio_zone": "2",
|
||||
"cardio_rpe": "4-5",
|
||||
"cardio_recovery": "N/A",
|
||||
"week1_mon_zone": "Zone 2",
|
||||
"week1_mon_duration": "45 mins",
|
||||
"week1_tue_zone": "Zone 2",
|
||||
"week1_tue_duration": "45 mins",
|
||||
"week1_wed_zone": "Zone 3",
|
||||
"week1_wed_duration1": "10mins On",
|
||||
"week1_wed_duration2": "8mins Rest",
|
||||
"week1_wed_sets": "x2",
|
||||
"week1_thu_content": "",
|
||||
"week1_fri_zone": "Zone 2",
|
||||
"week1_fri_duration": "45 mins",
|
||||
"week1_sat_content": "",
|
||||
"week1_sun_content": "",
|
||||
"week2_mon_zone": "Zone 2",
|
||||
"week2_mon_duration": "50 mins",
|
||||
"week2_tue_zone": "Zone 2",
|
||||
"week2_tue_duration": "50 mins",
|
||||
"week2_wed_zone": "Zone 3",
|
||||
"week2_wed_duration1": "10mins On",
|
||||
"week2_wed_duration2": "6mins Rest",
|
||||
"week2_wed_sets": "x2",
|
||||
"week2_thu_content": "",
|
||||
"week2_fri_zone": "Zone 2",
|
||||
"week2_fri_duration": "50 mins",
|
||||
"week2_sat_content": "",
|
||||
"week2_sun_content": "",
|
||||
"contact_email": "info@ishplabs.com",
|
||||
"website": "www.ishplabs.com",
|
||||
"social": "@ishplabs",
|
||||
"page_number": "13",
|
||||
}
|
||||
|
||||
page_14_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
"contact_email": "info@ishplabs.com",
|
||||
"website": "www.ishplabs.com",
|
||||
"social": "@ishplabs",
|
||||
"page_number": "14",
|
||||
}
|
||||
|
||||
page_15_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
"contact_email": "info@ishplabs.com",
|
||||
"website": "www.ishplabs.com",
|
||||
"social": "@ishplabs",
|
||||
"page_number": "15",
|
||||
}
|
||||
|
||||
page_16_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
"contact_email": "info@ishplabs.com",
|
||||
"website": "www.ishplabs.com",
|
||||
"social": "@ishplabs",
|
||||
"page_number": "16",
|
||||
}
|
||||
|
||||
page_17_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
"contact_email": "info@ishplabs.com",
|
||||
"website": "www.ishplabs.com",
|
||||
"social": "@ishplabs",
|
||||
"page_number": "17",
|
||||
}
|
||||
|
||||
page_18_context = {
|
||||
"body_fat_percentage_chart": image_to_base64(
|
||||
"/home/oluwasanmi/Documents/Work/MKD/report_generation/graphs/fat_percent_master_chart.png"
|
||||
),
|
||||
}
|
||||
|
||||
page_19_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
"contact_email": "info@ishplabs.com",
|
||||
"website": "www.ishplabs.com",
|
||||
"social": "@ishplabs",
|
||||
"page_number": "19",
|
||||
}
|
||||
|
||||
context_list = [
|
||||
page_1_context,
|
||||
page_2_context,
|
||||
page_3_context,
|
||||
page_4_context,
|
||||
page_5_context,
|
||||
page_6_context,
|
||||
page_7_context,
|
||||
page_8_context,
|
||||
page_9_context,
|
||||
page_10_context,
|
||||
page_11_context,
|
||||
page_12_context,
|
||||
page_13_context,
|
||||
page_14_context,
|
||||
page_15_context,
|
||||
page_16_context,
|
||||
page_17_context,
|
||||
page_18_context,
|
||||
page_19_context,
|
||||
]
|
||||
@@ -0,0 +1,319 @@
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class ReportGenerator:
|
||||
def __init__(self):
|
||||
self.pnoe_df = None
|
||||
self.patient_df = None
|
||||
self.spirometry_df = None
|
||||
self.seca_df = None
|
||||
self.patient_info = {}
|
||||
self.charts_dir = Path("graphs")
|
||||
self.charts_dir.mkdir(exist_ok=True)
|
||||
|
||||
def load_data(
|
||||
self,
|
||||
pnoe_path: str,
|
||||
patient_path: str,
|
||||
spirometry_path: str,
|
||||
seca_path: str = None,
|
||||
):
|
||||
"""Load all required datasets"""
|
||||
self.pnoe_df = pd.read_csv(pnoe_path, delimiter=";")
|
||||
self.patient_df = pd.read_csv(patient_path)
|
||||
self.spirometry_df = pd.read_csv(spirometry_path)
|
||||
if seca_path:
|
||||
self.seca_df = pd.read_excel(seca_path)
|
||||
|
||||
# Apply preprocessing
|
||||
self._preprocess_data()
|
||||
|
||||
def _preprocess_data(self):
|
||||
"""Apply preprocessing steps from your notebook"""
|
||||
# Convert to numeric
|
||||
self.pnoe_df = self.pnoe_df.apply(pd.to_numeric, errors="ignore")
|
||||
|
||||
# Calculate derived columns
|
||||
self.pnoe_df["VO2 Pulse"] = (
|
||||
self.pnoe_df["VO2(ml/min)"] / self.pnoe_df["HR(bpm)"]
|
||||
)
|
||||
self.pnoe_df["VO2 Breath"] = (
|
||||
self.pnoe_df["VO2(ml/min)"] / self.pnoe_df["BF(bpm)"]
|
||||
)
|
||||
self.pnoe_df["CHO"] = (
|
||||
self.pnoe_df["EE(kcal/min)"] * self.pnoe_df["CARBS(%)"] / 100
|
||||
)
|
||||
self.pnoe_df["FAT"] = (
|
||||
self.pnoe_df["EE(kcal/min)"] * self.pnoe_df["FAT(%)"] / 100
|
||||
)
|
||||
|
||||
# Apply smoothing
|
||||
window_size = 10
|
||||
columns_to_smooth = [
|
||||
"VO2(ml/min)",
|
||||
"VCO2(ml/min)",
|
||||
"HR(bpm)",
|
||||
"VT(l)",
|
||||
"BF(bpm)",
|
||||
"VE(l/min)",
|
||||
"VO2 Pulse",
|
||||
"VO2 Breath",
|
||||
"CHO",
|
||||
"FAT",
|
||||
]
|
||||
|
||||
for col in columns_to_smooth:
|
||||
if col in self.pnoe_df.columns:
|
||||
self.pnoe_df[f"{col}_smoothed"] = (
|
||||
self.pnoe_df[col].rolling(window=window_size, min_periods=1).mean()
|
||||
)
|
||||
|
||||
def extract_patient_info(self, last_name: str) -> Dict:
|
||||
"""Extract patient information from datasets"""
|
||||
if self.seca_df is not None:
|
||||
patient_data = self.seca_df[
|
||||
self.seca_df["LastName"].str.contains(last_name, case=False, na=False)
|
||||
]
|
||||
if not patient_data.empty:
|
||||
row = patient_data.iloc[0]
|
||||
self.patient_info = {
|
||||
"name": f"{row.get('FirstName', '')} {last_name}",
|
||||
"age": int(row.get("Age", 0)),
|
||||
"height": f"{row.get('Height', '')}",
|
||||
"weight": float(row.get("Weight", 0)),
|
||||
"gender": row.get("Gender", "").lower(),
|
||||
"fat_percentage": float(row.get("Adult_FMP", 0)),
|
||||
}
|
||||
return self.patient_info
|
||||
|
||||
def calculate_spirometry_metrics(self) -> Dict:
|
||||
"""Calculate spirometry-related metrics"""
|
||||
metrics = {}
|
||||
|
||||
# Extract key spirometry values
|
||||
for param in ["FVC", "FEV1", "FEV1/FVC%"]:
|
||||
row = self.spirometry_df.loc[self.spirometry_df["Parameters"] == param]
|
||||
if not row.empty:
|
||||
metrics[
|
||||
f"{param.lower().replace('/', '_').replace('%', '_pct')}_best"
|
||||
] = row["Best"].values[0]
|
||||
metrics[
|
||||
f"{param.lower().replace('/', '_').replace('%', '_pct')}_pred"
|
||||
] = row["%Pred."].values[0]
|
||||
|
||||
return metrics
|
||||
|
||||
def calculate_pnoe_metrics(self) -> Dict:
|
||||
"""Calculate all Pnoe-derived metrics"""
|
||||
metrics = {}
|
||||
|
||||
# Basic metrics
|
||||
metrics["vo2_max"] = self.pnoe_df["VO2(ml/min)_smoothed"].max()
|
||||
metrics["vo2_max_per_kg"] = metrics["vo2_max"] / self.patient_info["weight"]
|
||||
|
||||
# Peak VT
|
||||
peak_vt_idx = self.pnoe_df["VT(l)_smoothed"].idxmax()
|
||||
peak_vt_row = self.pnoe_df.loc[peak_vt_idx]
|
||||
metrics["peak_vt"] = peak_vt_row["VT(l)_smoothed"]
|
||||
metrics["peak_vt_hr"] = peak_vt_row["HR(bpm)_smoothed"]
|
||||
|
||||
# Fat burning metrics
|
||||
fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax()
|
||||
fat_max_row = self.pnoe_df.loc[fat_max_idx]
|
||||
metrics["fat_max_value"] = fat_max_row["FAT_smoothed"]
|
||||
metrics["fat_max_hr"] = fat_max_row["HR(bpm)_smoothed"]
|
||||
|
||||
# Calculate zones (simplified from your logic)
|
||||
metrics.update(self._calculate_hr_zones())
|
||||
|
||||
# VT1/VT2 detection
|
||||
vt1, vt2 = self._detect_thresholds()
|
||||
metrics["vt1"] = vt1
|
||||
metrics["vt2"] = vt2
|
||||
|
||||
return metrics
|
||||
|
||||
def _detect_thresholds(self) -> Tuple[Optional[Dict], Optional[Dict]]:
|
||||
"""Detect VT1 and VT2 thresholds"""
|
||||
# VT1: First crossover where carbs > fat
|
||||
condition = self.pnoe_df["CHO_smoothed"] > self.pnoe_df["FAT_smoothed"]
|
||||
crossover_indices = condition[condition].index
|
||||
|
||||
vt1 = None
|
||||
if len(crossover_indices) > 0:
|
||||
vt1_idx = crossover_indices[0]
|
||||
vt1_row = self.pnoe_df.loc[vt1_idx]
|
||||
vt1 = {
|
||||
"HeartRate": vt1_row["HR(bpm)_smoothed"],
|
||||
"Speed": vt1_row["Speed"],
|
||||
"Time": vt1_row["T(sec)"],
|
||||
}
|
||||
|
||||
# VT2: Ventilation inflection (simplified)
|
||||
ve_slope = self.pnoe_df["VE(l/min)_smoothed"].diff()
|
||||
second_derivative = ve_slope.diff()
|
||||
vt2_idx = second_derivative.idxmax()
|
||||
|
||||
vt2 = None
|
||||
if pd.notna(vt2_idx):
|
||||
vt2_row = self.pnoe_df.loc[vt2_idx]
|
||||
vt2 = {
|
||||
"HeartRate": vt2_row["HR(bpm)_smoothed"],
|
||||
"Speed": vt2_row["Speed"],
|
||||
"Time": vt2_row["T(sec)"],
|
||||
}
|
||||
|
||||
return vt1, vt2
|
||||
|
||||
def _calculate_hr_zones(self) -> Dict:
|
||||
"""Calculate heart rate zones"""
|
||||
max_hr = 220 - self.patient_info["age"]
|
||||
|
||||
# Simplified zone calculation - you can make this more sophisticated
|
||||
zones = {
|
||||
"zone1_bpm": f"{int(max_hr * 0.55)}-{int(max_hr * 0.65)}bpm",
|
||||
"zone2_bpm": f"{int(max_hr * 0.65)}-{int(max_hr * 0.75)}bpm",
|
||||
"zone3_bpm": f"{int(max_hr * 0.75)}-{int(max_hr * 0.85)}bpm",
|
||||
"zone4_bpm": f"{int(max_hr * 0.85)}-{int(max_hr * 0.95)}bpm",
|
||||
"zone5_bpm": f"{int(max_hr * 0.95)}+bpm",
|
||||
}
|
||||
return zones
|
||||
|
||||
def generate_charts(self) -> Dict[str, str]:
|
||||
"""Generate all charts and return base64 encoded versions"""
|
||||
charts = {}
|
||||
|
||||
# Generate fuel utilization chart
|
||||
charts["fuel_utilization_chart"] = self._create_fuel_chart()
|
||||
|
||||
# Generate VO2 pulse chart
|
||||
charts["vo2_pulse_chart"] = self._create_vo2_pulse_chart()
|
||||
|
||||
# Generate body composition chart
|
||||
charts["body_composition_chart"] = self._create_body_comp_chart()
|
||||
|
||||
# Add more chart generation methods...
|
||||
|
||||
return charts
|
||||
|
||||
def _create_fuel_chart(self) -> str:
|
||||
"""Create and save fuel utilization chart"""
|
||||
# Use your existing chart code but make it dynamic
|
||||
speed_groups = self.pnoe_df.groupby("Speed").mean(numeric_only=True).round(1)
|
||||
speed_groups = speed_groups.iloc[1:-1]
|
||||
filtered_data = speed_groups[
|
||||
(speed_groups.index >= 3.5) & (speed_groups.index <= 7.5)
|
||||
]
|
||||
|
||||
plt.figure(figsize=(15, 8))
|
||||
# ... your chart code here ...
|
||||
|
||||
chart_path = self.charts_dir / "fuel_utilization_chart.png"
|
||||
plt.savefig(chart_path, dpi=300)
|
||||
plt.close()
|
||||
|
||||
return self._image_to_base64(chart_path)
|
||||
|
||||
def _create_vo2_pulse_chart(self) -> str:
|
||||
"""Create VO2 pulse chart"""
|
||||
# Your VO2 pulse chart code here
|
||||
chart_path = self.charts_dir / "vo2_pulse_chart.png"
|
||||
# ... chart generation code ...
|
||||
return self._image_to_base64(chart_path)
|
||||
|
||||
def _create_body_comp_chart(self) -> str:
|
||||
"""Create body composition chart"""
|
||||
# Your body composition chart code here
|
||||
chart_path = self.charts_dir / "body_composition_chart.png"
|
||||
# ... chart generation code ...
|
||||
return self._image_to_base64(chart_path)
|
||||
|
||||
def _image_to_base64(self, image_path: Path) -> str:
|
||||
"""Convert image to base64"""
|
||||
try:
|
||||
with open(image_path, "rb") as image_file:
|
||||
return base64.b64encode(image_file.read()).decode("utf-8")
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
def generate_all_contexts(self, last_name: str = "Moran") -> List[Dict]:
|
||||
"""Main method to generate all page contexts"""
|
||||
# Extract patient info
|
||||
self.extract_patient_info(last_name)
|
||||
|
||||
# Calculate metrics
|
||||
spirometry_metrics = self.calculate_spirometry_metrics()
|
||||
pnoe_metrics = self.calculate_pnoe_metrics()
|
||||
|
||||
# Generate charts
|
||||
charts = self.generate_charts()
|
||||
|
||||
# Build contexts for each page
|
||||
contexts = []
|
||||
|
||||
# Page 1
|
||||
contexts.append(
|
||||
{
|
||||
"name": self.patient_info["name"],
|
||||
"surname": last_name,
|
||||
"date": "July 29, 2025",
|
||||
}
|
||||
)
|
||||
|
||||
# Page 2-6 (add as needed)
|
||||
for i in range(5):
|
||||
contexts.append({})
|
||||
|
||||
# Page 7 - Spirometry
|
||||
contexts.append(
|
||||
{
|
||||
"peak_vt": pnoe_metrics["peak_vt"],
|
||||
"peak_vt_bpm": pnoe_metrics["peak_vt_hr"],
|
||||
"fev1_percentage": (
|
||||
pnoe_metrics["peak_vt"] / spirometry_metrics["fvc_best"]
|
||||
)
|
||||
* 100,
|
||||
"lung_analysis_chart": charts.get("spirometry_chart", ""),
|
||||
"respiratory_analysis_chart": charts.get("respiratory_chart", ""),
|
||||
}
|
||||
)
|
||||
|
||||
# Page 8 - VO2 Max and Zones
|
||||
contexts.append(
|
||||
{
|
||||
"vo2_max_value": f"{pnoe_metrics['vo2_max_per_kg']:.1f}",
|
||||
"age_range": f"{self.patient_info['age'] // 10 * 10}-{self.patient_info['age'] // 10 * 10 + 9}",
|
||||
**pnoe_metrics, # Include all zone calculations
|
||||
}
|
||||
)
|
||||
|
||||
# Continue for all pages...
|
||||
# Add remaining pages as needed
|
||||
|
||||
return contexts
|
||||
|
||||
|
||||
# Usage for backend service
|
||||
def generate_report(
|
||||
pnoe_file, patient_file, spirometry_file, seca_file=None, patient_name="Moran"
|
||||
):
|
||||
"""Main function for backend service"""
|
||||
generator = ReportGenerator()
|
||||
generator.load_data(pnoe_file, patient_file, spirometry_file, seca_file)
|
||||
return generator.generate_all_contexts(patient_name)
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
contexts = generate_report(
|
||||
"data/Pnoe_20250729_1550-Moran_Keirstyn.csv",
|
||||
"data/patient_data.csv",
|
||||
"data/spirometry_data.csv",
|
||||
"data/SECA body comp for all patients.xlsx",
|
||||
)
|
||||
print(f"Generated {len(contexts)} page contexts")
|
||||
@@ -0,0 +1,124 @@
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from context import context_list
|
||||
|
||||
env = Environment(loader=FileSystemLoader("report_gen"))
|
||||
|
||||
html_pages = []
|
||||
|
||||
header_context = {
|
||||
"patient_name": "Keirstyn Moran",
|
||||
"age": 34,
|
||||
"height": "5'4\"",
|
||||
"weight": "123lbs",
|
||||
"focus": "Endurance",
|
||||
}
|
||||
|
||||
footer_context = [
|
||||
{
|
||||
"contact_email": "info@ishplabs.com ",
|
||||
"website": "www.ishplabs.com",
|
||||
"social": "@ishplabs",
|
||||
"page_number": i + 1,
|
||||
}
|
||||
for i in range(len(context_list))
|
||||
]
|
||||
|
||||
|
||||
header_html = env.get_template("header.html").render(header_context)
|
||||
footer_html_list = [
|
||||
env.get_template("footer.html").render(context) for context in footer_context
|
||||
]
|
||||
|
||||
for i, context in enumerate(context_list):
|
||||
template = env.get_template(f"page_{i + 1}.html").render(context)
|
||||
|
||||
if (i + 1) > 2:
|
||||
full_html = f"""
|
||||
<div class="page flex flex-col justify-between">
|
||||
<div>
|
||||
{header_html}
|
||||
</div>
|
||||
<main class="flex-grow p-4">
|
||||
{template}
|
||||
</main>
|
||||
<div class="border-t text-center text-sm text-gray-600">
|
||||
{footer_html_list[i]}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
html_pages.append(full_html)
|
||||
else:
|
||||
html_pages.append(template)
|
||||
|
||||
# Combine with page breaks
|
||||
final_html = "<div class='page-break'></div>".join(html_pages)
|
||||
# Wrap in full HTML document
|
||||
html_doc = f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
html, body {{
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}}
|
||||
.page-break {{ page-break-after: always; }}
|
||||
.page {{
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}}
|
||||
.page main {{
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}}
|
||||
/* Reset margins and padding everywhere */
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
/* Prevent images from being too large */
|
||||
img {{
|
||||
max-height: 300px;
|
||||
}}
|
||||
/* Larger images for specific charts */
|
||||
.chart-large {{
|
||||
max-height: 500px !important;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="m-0 p-0">
|
||||
{final_html}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
# Generate PDF
|
||||
|
||||
|
||||
def html_string_to_pdf(html_content, pdf_path):
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page()
|
||||
|
||||
# Set the HTML directly
|
||||
page.set_content(html_content)
|
||||
|
||||
# Export to PDF
|
||||
page.pdf(path=pdf_path, format="A4", print_background=True)
|
||||
|
||||
browser.close()
|
||||
|
||||
|
||||
html_string_to_pdf(html_doc, "multi_page_report.pdf")
|
||||
# pdfkit.from_string(html_doc, "truth_report.pdf", options=options)
|
||||
|
||||
print("✅ PDF generated: multi_page_report.pdf")
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user