| """ |
| 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 |
|
|
| |
| 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", |
| ) |
|
|
| |
| 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" |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| 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") |
| |
| |
| 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("---") |
|
|
| |
| 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 |
| } |
| |
| |
| 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!") |
| |
| |
| 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") |
| |
| |
| 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] |
| |
| |
| higher_is_better = biomarker in HIGHER_IS_BETTER |
| |
| if higher_is_better: |
| |
| 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: |
| |
| 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}") |
| |
| |
| 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}**." |
| ) |
| |
| |
| 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.") |
|
|
| |
| 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.* |
| """) |
|
|
| |
| 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.* |
| """) |
|
|
| |
| 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." |
| ) |
|
|
|
|