""" 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." )