Lars Masanneck
Adding explanaotry notes to app and exports
a3309b8
"""
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."
)