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