File size: 11,503 Bytes
ae420f7
 
 
 
 
 
 
 
 
 
 
523afc6
ae420f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee7a14d
523afc6
 
 
 
 
 
ee7a14d
523afc6
ee7a14d
523afc6
 
 
 
 
ae420f7
523afc6
 
 
ee7a14d
523afc6
ee7a14d
523afc6
 
 
 
 
ae420f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a3309b8
ae420f7
 
 
 
a3309b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae420f7
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
"""
PDF Report Generator page for Smartwatch Normative Z-Score Calculator.

Generate downloadable PDF reports for individual patients.
"""
import streamlit as st
import sys
import os

# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from batch_utils import generate_pdf_report, BIOMARKER_LABELS, AVAILABLE_BIOMARKERS, HIGHER_IS_BETTER
import normalizer_model

st.set_page_config(
    page_title="PDF Report - Smartwatch Z-Score Calculator",
    page_icon="📄",
    layout="wide",
)

# Load normative data
DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Table_1_summary_measure.csv")

@st.cache_data
def get_normative_data():
    try:
        return normalizer_model.load_normative_table(DATA_PATH)
    except Exception as e:
        st.error(f"Could not load normative data: {e}")
        return None

normative_df = get_normative_data()

st.title("📄 PDF Report Generator")
st.markdown("**Generate a professional smartwatch biomarker report for download**")

st.info(
    "Enter patient information and biomarker measurements below to generate a downloadable PDF report "
    "with z-scores, percentiles, and visual gauges."
)

col1, col2 = st.columns(2)

with col1:
    st.subheader("👤 Patient Information")
    
    patient_name = st.text_input(
        "Patient Name/ID (optional)", 
        placeholder="e.g., John Doe or P001"
    )
    
    # Region with default Western Europe
    if normative_df is not None:
        regions = sorted(normative_df["area"].unique())
        if "Western Europe" in regions:
            default_region_idx = regions.index("Western Europe")
        else:
            default_region_idx = 0
    else:
        regions = ["Western Europe", "Southern Europe", "North America", "Japan"]
        default_region_idx = 0
    
    region = st.selectbox(
        "Region",
        regions,
        index=default_region_idx
    )
    
    # Gender
    if normative_df is not None:
        genders = sorted(normative_df["gender"].unique())
    else:
        genders = ["Man", "Woman"]
    
    gender = st.selectbox("Gender", genders)
    
    age = st.number_input(
        "Age (years)",
        min_value=0,
        max_value=120,
        value=45
    )
    
    bmi = st.number_input(
        "BMI",
        min_value=10.0,
        max_value=60.0,
        value=24.0,
        step=0.1,
        format="%.1f"
    )

with col2:
    st.subheader("📊 Biomarker Measurements")
    st.caption("Select which biomarkers to include in the report")
    
    # Biomarker inputs with checkboxes
    measurements = {}
    
    include_steps = st.checkbox("Include Number of Steps", value=True)
    if include_steps:
        measurements['nb_steps'] = st.number_input(
            "Number of Steps",
            min_value=0.0,
            max_value=50000.0,
            value=6500.0,
            step=100.0
        )
    
    include_sleep = st.checkbox("Include Sleep Duration", value=True)
    if include_sleep:
        measurements['sleep_duration'] = st.number_input(
            "Sleep Duration (hours)",
            min_value=0.0,
            max_value=24.0,
            value=7.5,
            step=0.1,
            format="%.1f"
        )
    
    include_hr = st.checkbox("Include Average Night Heart Rate", value=True)
    if include_hr:
        measurements['avg_night_hr'] = st.number_input(
            "Average Night Heart Rate (bpm)",
            min_value=30.0,
            max_value=150.0,
            value=62.0,
            step=1.0
        )
    
    include_active = st.checkbox("Include Mean Active Time", value=False)
    if include_active:
        measurements['mean_active_time'] = st.number_input(
            "Mean Active Time (minutes)",
            min_value=0.0,
            max_value=1440.0,
            value=45.0,
            step=1.0
        )
    
    include_moderate = st.checkbox("Include Moderate Active Minutes", value=False)
    if include_moderate:
        measurements['nb_moderate_active_minutes'] = st.number_input(
            "Moderate Active Minutes",
            min_value=0.0,
            max_value=1440.0,
            value=30.0,
            step=1.0
        )

st.markdown("---")

# Generate Report Button
if st.button("📄 Generate PDF Report", type="primary"):
    if not measurements:
        st.error("Please include at least one biomarker measurement.")
    elif normative_df is None:
        st.error("Normative data not loaded. Cannot generate report.")
    else:
        patient_info = {
            'name': patient_name if patient_name else 'Not specified',
            'age': age,
            'gender': gender,
            'region': region,
            'bmi': bmi
        }
        
        # Calculate z-scores for each included biomarker
        z_scores = {}
        errors = []
        
        for biomarker, value in measurements.items():
            try:
                result = normalizer_model.compute_normative_position(
                    value=value,
                    biomarker=biomarker,
                    age_group=age,
                    region=region,
                    gender=gender,
                    bmi=bmi,
                    normative_df=normative_df
                )
                z_scores[biomarker] = result
            except Exception as e:
                errors.append(f"{BIOMARKER_LABELS.get(biomarker, biomarker)}: {str(e)}")
        
        if errors:
            for err in errors:
                st.warning(f"Z-score calculation note: {err}")
        
        if z_scores:
            with st.spinner("Generating PDF report..."):
                pdf_buffer = generate_pdf_report(patient_info, measurements, z_scores)
            
            st.success("✅ PDF report generated successfully!")
            
            # Report Preview
            st.subheader("Report Preview")
            
            with st.expander("View Report Contents", expanded=True):
                st.markdown("### Demographics")
                st.markdown(f"- **Age:** {age} years")
                st.markdown(f"- **Gender:** {gender}")
                st.markdown(f"- **Region:** {region}")
                st.markdown(f"- **BMI:** {bmi}")
                
                st.markdown("### Measurements & Z-Scores")
                
                # Create columns for z-score display
                num_scores = len(z_scores)
                if num_scores > 0:
                    cols = st.columns(min(num_scores, 3))
                    
                    for idx, (biomarker, data) in enumerate(z_scores.items()):
                        with cols[idx % 3]:
                            label = BIOMARKER_LABELS.get(biomarker, biomarker)
                            z = data['z_score']
                            pct = data['percentile']
                            value = measurements[biomarker]
                            
                            # Context-aware interpretation (Average = -0.5 to 0.5)
                            higher_is_better = biomarker in HIGHER_IS_BETTER
                            
                            if higher_is_better:
                                # For steps, sleep, activity: high is good
                                if z < -2:
                                    interp = "Very Low ⚠️"
                                elif z < -0.5:
                                    interp = "Below Average"
                                elif z < 0.5:
                                    interp = "Average"
                                elif z < 2:
                                    interp = "Above Average ✓"
                                else:
                                    interp = "Excellent ✓✓"
                            else:
                                # For HR: low is good
                                if z < -2:
                                    interp = "Very Low ✓✓"
                                elif z < -0.5:
                                    interp = "Below Average ✓"
                                elif z < 0.5:
                                    interp = "Average"
                                elif z < 2:
                                    interp = "Above Average"
                                else:
                                    interp = "Elevated ⚠️"
                            
                            st.metric(
                                label,
                                f"Z = {z:.2f}",
                                f"{pct:.1f}th percentile"
                            )
                            st.caption(f"Value: {value} | {interp}")
                
                # Cohort info
                age_group_str = normalizer_model._categorize_age(age, normative_df)
                bmi_cat = normalizer_model.categorize_bmi(bmi)
                st.markdown("### Reference Population")
                st.markdown(
                    f"Z-scores calculated from normative data: **{region}**, "
                    f"**{gender}**, age group **{age_group_str}**, BMI category **{bmi_cat}**."
                )
            
            # Download button
            filename = f"smartwatch_report_{patient_name.replace(' ', '_') if patient_name else 'patient'}.pdf"
            
            st.download_button(
                label="⬇️ Download PDF Report",
                data=pdf_buffer,
                file_name=filename,
                mime="application/pdf"
            )
        else:
            st.error("Could not calculate z-scores for any biomarkers. Please check your inputs.")

# Information section
st.markdown("---")

st.markdown("### Report Contents")
st.markdown("""
The generated PDF report includes:

1. **Patient Demographics** - Age, gender, region, BMI
2. **Biomarker Measurements** - All selected smartwatch metrics
3. **Z-Score Analysis** - Comparison to normative population data
   - Z-scores and percentiles for each biomarker
   - Visual gauge charts showing position in distribution
   - Interpretation (Very Low → Average → Very High)
4. **Reference Population Info** - Details about the comparison cohort
5. **Classification Guide** - Explanation of z-score interpretation

*All reports include a disclaimer noting educational/research purpose.*
""")

# Z-Score Classification Guide
with st.expander("📊 Z-Score Classification Guide"):
    st.markdown("""
    **How to interpret Z-Scores:**
    
    | Z-Score Range | Classification | Percentile Range |
    |:-------------:|:--------------:|:----------------:|
    | z < -2.0 | Very Low | < 2.3% |
    | -2.0 ≤ z < -0.5 | Below Average | 2.3% - 30.9% |
    | **-0.5 ≤ z < 0.5** | **Average** | **30.9% - 69.1%** |
    | 0.5 ≤ z < 2.0 | Above Average | 69.1% - 97.7% |
    | z ≥ 2.0 | Very High | > 97.7% |
    
    **Context matters:**
    - For **steps, sleep duration, and active minutes**: Higher values are generally better ✓
    - For **heart rate**: Lower resting values are generally better ✓
    
    *A z-score of 0 means you are exactly at the population average for your demographic group.*
    """)

# Footer
st.markdown("---")
st.markdown(
    "*PDF reports are for educational and research purposes. "
    "For detailed questions regarding personal health data, contact your healthcare professionals.*"
)
st.markdown(
    "Built with ❤️ in Düsseldorf. © Lars Masanneck 2026."
)