From 35ea522283b9836f73f5fa208f4b79f03bc39af9 Mon Sep 17 00:00:00 2001 From: bolade Date: Fri, 28 Nov 2025 16:19:32 +0100 Subject: [PATCH] Checkpoint 3 --- app/__pycache__/database.cpython-312.pyc | Bin 0 -> 3647 bytes .../session_manager.cpython-312.pyc | Bin 0 -> 7034 bytes app/main.py | 6 +- app/report_gen/page_2.html | 21 ++-- app/report_gen/page_2_minimal.html | 17 ++-- app/report_gen/page_7.html | 2 +- .../context_generator.cpython-312.pyc | Bin 51847 -> 52707 bytes .../graph_generator.cpython-312.pyc | Bin 70163 -> 70163 bytes app/services/context_generator.py | 96 ++++++++++++------ notebooks/analysis.ipynb | 43 ++++---- 10 files changed, 113 insertions(+), 72 deletions(-) create mode 100644 app/__pycache__/database.cpython-312.pyc create mode 100644 app/__pycache__/session_manager.cpython-312.pyc diff --git a/app/__pycache__/database.cpython-312.pyc b/app/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..236c3213731b8334b9aa63a2885b7fad2b8fa2cb GIT binary patch literal 3647 zcmb7GTWl2989sB_+1c3(yXz~)GFvXe8x=1p&=eGkz)Qe@O~6Q1R;othonw3Kxh!X9 z!M32bkR=r)|374gEx90DkwU~$3W^miM6FmM=FdrH+)5M@J{>ltbQ0;Q>qt-I_J&M{-f|rkI`vlQw}O0UU7kbJ zZQ1sjGfH0bv=Pn4nWAHl8e?Umd4^+WG+WQO*mW6gX|^_oEo^(4QHNxP&!06symv$^ zPU41;+o$}4nUdwGtZN?j3=0D-zN2~LS)K~V>}e|IjiQ$gQStQfxgjd`J7(FksSFn2 ziy%&=VUHO07>ySR2A+!TF~er7Bz0UgHD=jmtTJPP7pqxkX_J^x5%$zBvd!_7;d-hw zITNi3xPAJ_jN)E0!RW)eT#lxScUjbqsVN~EC<#?Sd2s&R7!Sx|5>4GjS zijCleeXgvBaAZ+xa21~$#j%O5jT;2C9$E}HBop5z`5*P=yJE0bVN0M7Hbw68HC|Fz za0;h!%P7AUSfjiiy)G77r(@YTjSUA0G3e;noY~XJ9E<(?so{DKi(n&jSX8K755z@q zbF@SZOC!_jB%Y=r@FJdK-k0sNX=)=TrePhAYGu>Q@}wIKAlsnJ3O-F`$1ay(O&oAJ zf1nb9%+7wtzKn_2h+Pm>&&lXe zwEzPUv>_;7r4~)q&`AR9ph{4!x~h3p3BnW5c&cO&>;gY4kqn6X^5i>ADPp(|V2P!9 zWCxqs2_1tA7n`Fl3uNZSiQKqj;T(X(70uOb%gE)OVws_koBNSNCUb*l@;QP_4)N46 zY~uj9a$2dBV`;}=qOUYf!=9tE6U?-3%-+5lcQ0(^y3ke7nuNA(|3&VjT(z}xe&EA_ z>Y*dQ-~PMp3%eL^wQK)U*Z!5RgAerogTLH$@M%;^Dr-oJC~FEj#|?8$#&g&f4Q1P? ztg1ExBb6$u3T`i(jE<@5rLtz$X2PnfJ4Il}RTU0r;($+B5)l^rz{4yX0Sw{1fwSRO zM1&OrmC9bxcCPsDZ~C0DLZO1|5yCD^vIn|P5$R?0Ypm<2cNrkuGIZC`WATlzT6-#k ze@S&LNT1|C6IW74DyJVuQ**oSwA~X|q6aJa$1o><()+92r@7BhtfWrEe5#`=Cl`8| zJMW$P?YUo{dw|ykJ}p&FKb26r6T}a!r1n)#uZeOQ92Y5R6_Ox6vZkyqKqXD2T2B+wcs{G-hz7o=_Rz^=_6U zZJ<1q6{&a}@{GzB_B0>tYSo}>rb(sX0wZsLY}2JQY{0^*K0@9E{vr5WcCHHgn<&3O z_}<{E*u5ln|MInmV)r94>+8LAW_Aky%zyBq&12Q#w>s=xLlNXL3BPRyW(`ROPn!Uk z*MO$FPbQ-E^_pW-Y9iL;)~T!9B!Fu}e@mU;92-{fSNS~paSKA%1h6O7l&Dizh59-i zy4fZokOJvfTYWu_&9iIcmDZe}=652mwLZNIf^$s-9TtXHT2sek*P-74)J67CYufZK z()nxRPsE!?g7FG>1x*R>qAT3a|7uHJ5;AW01#CH&aV7xH=F=)ON*t?^gEPBM5CYGz zqKP$|UAE=YH#81shO&vqeSOv`YNn3@BnKy;p-W{~ z8mm8hPD~YXiQNEHCXQ*ccOMPGHEw$}R5TrUjfE($k5B>Ng@$X!({Sycpd5Bt%4S&e zF@<*erk6`k=+HX0t&T?Y=4WXKDo$vjy z_x4-M>E6|JZYiDnO>A}F$)$ZKAM9M(H@KW0T1{VAN?%w`U%V0iQ%BE@lhs(_*1+t* z+}NX7&zgWbw|^xow<5EVn^Az}Nc5IGEB{RS*LnoS+MXdkbhrxB(b?$D_?L40Ve-Je z^Y{86CVsFgA6k+ReZ3X}iLck>hT;qNb+Gs5v7^Vk(HGs3{GxG#kIljy!2 z0?z%o!02SUe}^!X?IPa=jR2X$C3qv7#t3204k~%mC5UPO$38l{{3^+R$Vex1n1TIF5VD^PKok8F4*NQ0Jdf_uo?&9!&@ujy41!ga|hND49~vD VbFV*(#krQJI}j)TG~%P`zX7}}Mdkni literal 0 HcmV?d00001 diff --git a/app/__pycache__/session_manager.cpython-312.pyc b/app/__pycache__/session_manager.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a96370f860052cc95dffb54f029a789489f5b0a4 GIT binary patch literal 7034 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
3
@@ -35,7 +36,8 @@
4
@@ -52,7 +54,8 @@
5
@@ -66,7 +69,8 @@
9
@@ -80,7 +84,8 @@
10
@@ -94,7 +99,8 @@
12
@@ -111,7 +117,8 @@
13
diff --git a/app/report_gen/page_2_minimal.html b/app/report_gen/page_2_minimal.html index eb21fe1..fb22cc2 100644 --- a/app/report_gen/page_2_minimal.html +++ b/app/report_gen/page_2_minimal.html @@ -15,7 +15,8 @@
3
@@ -35,7 +36,8 @@
4
@@ -49,7 +51,8 @@
5
@@ -66,7 +69,8 @@
6
@@ -82,8 +86,3 @@
- - - - - diff --git a/app/report_gen/page_7.html b/app/report_gen/page_7.html index 556cf35..7703a2f 100644 --- a/app/report_gen/page_7.html +++ b/app/report_gen/page_7.html @@ -26,7 +26,7 @@

Indications

-

{{ indication | default('No Respiratory Capacity Limitations')}}

+

{{ indication | default('No Respiratory Capacity Limitation')}}

diff --git a/app/services/__pycache__/context_generator.cpython-312.pyc b/app/services/__pycache__/context_generator.cpython-312.pyc index 80bbd607544cad88dd39abab2ce686ff0f399ba9..ca6d4e2052869bd115903bd6f295a4f482c33f4d 100644 GIT binary patch delta 6305 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 diff --git a/app/services/__pycache__/graph_generator.cpython-312.pyc b/app/services/__pycache__/graph_generator.cpython-312.pyc index 2508cf6087efa9036081f55499bac3ccc1cac601..61d96ed48fdaf35c78487eac3a68a6956ede235f 100644 GIT binary patch delta 24 ecmbQdgk|y)7OvC0yj%=G@UN|rYbzHcrw{;JCI(sn delta 24 ecmbQdgk|y)7OvC0yj%=Gu&k+(YbzHcrw{;F?gjn; diff --git a/app/services/context_generator.py b/app/services/context_generator.py index d261260..1b5bf5a 100644 --- a/app/services/context_generator.py +++ b/app/services/context_generator.py @@ -232,10 +232,15 @@ class ContextGenerator: if zone_key in metric_overrides: metrics[zone_key] = metric_overrides[zone_key] else: - fat_max_idx = self.pnoe_df["FAT_smoothed"].idxmax() - fat_max_row = self.pnoe_df.loc[fat_max_idx] + # Use optimal fat burning zone (highest fat:carb ratio) - same as _calculate_zone_metrics + # This ensures consistency between zone calculations and zone metrics + self.pnoe_df["fat_carb_ratio"] = self.pnoe_df["FAT_smoothed"] / ( + self.pnoe_df["CHO_smoothed"] + 0.00000001 + ) + optimal_fat_idx = self.pnoe_df["fat_carb_ratio"].idxmax() + optimal_row = self.pnoe_df.loc[optimal_fat_idx] zones = self._calculate_hr_zones( - metrics["vt1"], metrics["vt2"], fat_max_row + metrics["vt1"], metrics["vt2"], optimal_row ) metrics.update(zones) @@ -280,29 +285,46 @@ class ContextGenerator: return vt1, vt2 def _calculate_hr_zones( - self, vt1: Optional[Dict], vt2: Optional[Dict], fat_max_row: pd.Series + self, vt1: Optional[Dict], vt2: Optional[Dict], optimal_row: pd.Series ) -> Dict: - """Calculate heart rate zones based on thresholds""" + """Calculate heart rate zones based on thresholds + + Uses optimal fat burning zone (highest fat:carb ratio) to match _calculate_zone_metrics. + This ensures consistency between zone string calculations and zone metrics table. + """ + import math + zones = {} if vt1 and vt2: - zone_1_start = fat_max_row["HR(bpm)_smoothed"] - 15 - zone_2_start = fat_max_row["HR(bpm)_smoothed"] - zone_3_start = vt1["HeartRate"] - zone_4_start = vt2["HeartRate"] - 10 - zone_5_start = vt2["HeartRate"] + 10 + # Use same zone boundary calculation as _calculate_zone_metrics + zone_1_start = math.floor(optimal_row["HR(bpm)_smoothed"] - 15) + zone_2_start = math.floor(optimal_row["HR(bpm)_smoothed"]) + zone_3_start = math.floor(vt1["HeartRate"]) + zone_4_start = math.floor(vt2["HeartRate"] - 10) + zone_5_start = math.floor(vt2["HeartRate"]) + # zone_5_end is calculated for consistency with _calculate_zone_metrics + # (not used in string format since zone 5 is open-ended: "+bpm") + zone_5_end = math.floor(vt2["HeartRate"] + 10) # noqa: F841 - zones["zone1_bpm"] = f"{int(zone_1_start)}-{int(zone_2_start)}bpm" - zones["zone2_bpm"] = f"{int(zone_2_start)}-{int(vt1['HeartRate'])}bpm" - zones["zone3_bpm"] = f"{int(zone_3_start)}-{int(zone_4_start)}bpm" - zones["zone4_bpm"] = f"{int(zone_4_start)}-{int(zone_5_start)}bpm" - zones["zone5_bpm"] = f"{int(zone_5_start)}+bpm" + # Calculate zone ends to match _calculate_zone_metrics exactly + zone_1_end = zone_2_start + zone_2_end = math.floor(vt1["HeartRate"]) + zone_3_end = zone_4_start + zone_4_end = zone_5_start + + # Format zones to match _calculate_zone_metrics output + zones["zone1_bpm"] = f"{int(zone_1_start)}-{int(zone_1_end)}bpm" + zones["zone2_bpm"] = f"{int(zone_2_start)}-{int(zone_2_end)}bpm" + zones["zone3_bpm"] = f"{int(zone_3_start)}-{int(zone_3_end)}bpm" + zones["zone4_bpm"] = f"{int(zone_4_start)}-{int(zone_4_end)}bpm" + zones["zone5_bpm"] = f"{int(zone_5_start)}-{int(zone_5_end)}bpm" else: max_hr = 220 - self.patient_info["age"] zones["zone1_bpm"] = f"{int(max_hr * 0.55)}-{int(max_hr * 0.65)}bpm" zones["zone2_bpm"] = f"{int(max_hr * 0.65)}-{int(max_hr * 0.75)}bpm" zones["zone3_bpm"] = f"{int(max_hr * 0.75)}-{int(max_hr * 0.85)}bpm" zones["zone4_bpm"] = f"{int(max_hr * 0.85)}-{int(max_hr * 0.95)}bpm" - zones["zone5_bpm"] = f"{int(max_hr * 0.95)}+bpm" + zones["zone5_bpm"] = f"{int(max_hr * 0.95)}-{int(max_hr * 1.05)}bpm" return zones def _calculate_vo2_drop_points(self, pnoe_metrics: Dict) -> Dict: @@ -1180,7 +1202,9 @@ class ContextGenerator: "page_number": 4, "fat_percentage": f"{self.patient_info['fat_percentage']:.1f}", "body_composition_chart": graphs.get("body_composition", ""), - "body_fat_chart": graphs.get("body_fat_percent", ""), # Alias for template + "body_fat_chart": graphs.get( + "body_fat_percent", "" + ), # Alias for template "body_fat_percent_chart": graphs.get( "body_fat_percent", "" ), # Keep for consistency @@ -1199,29 +1223,29 @@ class ContextGenerator: "weight_loss_rate": rmr_metrics.get("weight_loss_rate", 1.0), "total_calories": rmr_metrics.get("total_calories", 1375), } - + # For minimal reports, also generate resting heart rate table for page_5 if report_type == "minimal" and graph_generator: resting_hr_metrics = self._calculate_resting_heart_rate_metrics() rhr_table_info = self._calculate_rhr_table_data( self.patient_info["age"], self.patient_info["gender"] ) - + # Get resting heart rate value and determine category rhr_value_str = resting_hr_metrics.get("resting_heart_rate", "0bpm") rhr_value = float(rhr_value_str.replace("bpm", "").strip()) - + category = self._determine_rhr_category( rhr_value, self.patient_info["age"], self.patient_info["gender"], ) - + gender_label = ( "F" if self.patient_info["gender"].lower().startswith("f") else "M" ) age_range_label = f"{rhr_table_info['age_range']} ({gender_label})" - + rhr_columns = [ "Age", "Poor", @@ -1244,7 +1268,7 @@ class ContextGenerator: rhr_table_info["ranges"]["Athlete"], ] ] - + contexts["page_5"]["rhr_table"] = ( graph_generator.generate_resting_heart_rate_table( data=rhr_data, @@ -1265,12 +1289,16 @@ class ContextGenerator: "deficit_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 0.39 / 4)}g Carbs", "deficit_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 0.39 / 9)}g Fat", "deficit_fiber": "24g Fibre", - "refeed_weekday_calories": int(rmr_metrics.get("total_calories", 1600) * 0.85), + "refeed_weekday_calories": int( + rmr_metrics.get("total_calories", 1600) * 0.85 + ), "refeed_weekday_protein": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.22 / 4)}g Protein", "refeed_weekday_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.39 / 4)}g Carbs", "refeed_weekday_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 0.85 * 0.39 / 9)}g Fat", "refeed_weekday_fiber": "20g Fibre", - "refeed_weekend_calories": int(rmr_metrics.get("total_calories", 1600) * 1.375), + "refeed_weekend_calories": int( + rmr_metrics.get("total_calories", 1600) * 1.375 + ), "refeed_weekend_protein": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.22 / 4)}g Protein", "refeed_weekend_carbs": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.39 / 4)}g Carbs", "refeed_weekend_fat": f"{int(rmr_metrics.get('total_calories', 1600) * 1.375 * 0.39 / 9)}g Fat", @@ -1291,12 +1319,12 @@ class ContextGenerator: # Page 7 contexts["page_7"] = { - "peak_vt": f"{pnoe_metrics['peak_vt']:.2f}", - "peak_vt_bpm": f"{int(pnoe_metrics['peak_vt_hr'])}", - "fev1_percentage": f"{fev1_percentage:.1f}", - "lung_analysis_chart": graphs.get("spirometry_chart", ""), - "respiratory_analysis_chart": graphs.get("respiratory", ""), - } + "peak_vt": f"{pnoe_metrics['peak_vt']:.2f}", + "peak_vt_bpm": f"{int(pnoe_metrics['peak_vt_hr'])}", + "fev1_percentage": f"{fev1_percentage:.1f}", + "lung_analysis_chart": graphs.get("spirometry_chart", ""), + "respiratory_analysis_chart": graphs.get("respiratory", ""), + } # Page 8 contexts["page_8"] = { @@ -1562,7 +1590,11 @@ class ContextGenerator: } # For minimal reports, create combined context for page_19_20_minimal - if report_type == "minimal" and 19 in pages_to_generate and 20 in pages_to_generate: + if ( + report_type == "minimal" + and 19 in pages_to_generate + and 20 in pages_to_generate + ): contexts["page_19_20_minimal"] = { "patient_name": self.patient_info["name"], "body_fat_percentage_chart": graphs.get( diff --git a/notebooks/analysis.ipynb b/notebooks/analysis.ipynb index 37eff10..d51e5c2 100644 --- a/notebooks/analysis.ipynb +++ b/notebooks/analysis.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "b18c1027", "metadata": {}, "outputs": [], @@ -88,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "56a9d655", "metadata": {}, "outputs": [ @@ -104,7 +104,10 @@ ], "source": [ "import pandas as pd\n", - "spirometry_df = pd.read_csv(\"data/spirometry_data.csv\")\n", + "import os\n", + "\n", + "base_dir = os.path.dirname(os.path.abspath('.'))\n", + "spirometry_df = pd.read_csv(f\"{base_dir}/data/spirometry_data.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", @@ -122,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "990f4b4f", "metadata": {}, "outputs": [ @@ -136,7 +139,7 @@ } ], "source": [ - "df = pd.read_csv('data/Pnoe_20250729_1550-Moran_Keirstyn.csv', delimiter=';')\n", + "df = pd.read_csv(f'{base_dir}/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", @@ -146,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "id": "041cbc3d", "metadata": {}, "outputs": [ @@ -154,21 +157,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "Peak VT: 2.3770000000000002\n", - "HR at Peak VT: 171.525\n" + "Peak VT: 2.3844444444444446\n", + "HR at Peak VT: 172.80555555555554\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "/tmp/ipykernel_69398/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", + "/tmp/ipykernel_53922/361246798.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", + "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", @@ -176,7 +179,7 @@ "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", + "window_size = 9\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", @@ -195,7 +198,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "id": "de7cadd1", "metadata": {}, "outputs": [ @@ -203,7 +206,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Percent FEV: 72.91411042944786\n" + "Percent FEV: 73.14246762099523\n" ] } ], @@ -214,7 +217,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "id": "cb972ed3", "metadata": {}, "outputs": [ @@ -311,13 +314,13 @@ "[1 rows x 147 columns]" ] }, - "execution_count": 11, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "personal_df = pd.read_excel('data/SECA body comp for all patients.xlsx')\n", + "personal_df = pd.read_excel(f'{base_dir}/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" @@ -325,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "id": "98d9295a", "metadata": {}, "outputs": [ @@ -333,7 +336,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "VO2 Max: 47.906290322580645\n" + "VO2 Max: 48.19062126642772\n" ] } ], @@ -823,7 +826,7 @@ ], "metadata": { "kernelspec": { - "display_name": "report_generation", + "display_name": ".venv", "language": "python", "name": "python3" }, @@ -837,7 +840,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.12.6" } }, "nbformat": 4,