Spaces:
Sleeping
Sleeping
Replace streamlit app file
Browse files- src/streamlit_app.py +104 -0
src/streamlit_app.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import pandas as pd, joblib, json
|
| 3 |
+
|
| 4 |
+
st.set_page_config(page_title="VOβ Max & Training Readiness", page_icon="π", layout="centered")
|
| 5 |
+
st.title("π VOβ Max & Training Readiness (Synthetic, Demo)")
|
| 6 |
+
st.caption("CPU-only β’ Synthetic data β’ Not medical advice.")
|
| 7 |
+
|
| 8 |
+
MODEL_PATH = "model/vo2_predictor.joblib"
|
| 9 |
+
DATA_PATH = "assets/vo2max_synthetic.csv"
|
| 10 |
+
|
| 11 |
+
@st.cache_resource
|
| 12 |
+
def load_model():
|
| 13 |
+
return joblib.load(MODEL_PATH)
|
| 14 |
+
|
| 15 |
+
@st.cache_data
|
| 16 |
+
def load_sample():
|
| 17 |
+
try:
|
| 18 |
+
df = pd.read_csv(DATA_PATH)
|
| 19 |
+
return df
|
| 20 |
+
except Exception:
|
| 21 |
+
return pd.DataFrame()
|
| 22 |
+
|
| 23 |
+
pipe = load_model()
|
| 24 |
+
df = load_sample()
|
| 25 |
+
|
| 26 |
+
with st.expander("Sample data (first 50 rows)"):
|
| 27 |
+
if not df.empty:
|
| 28 |
+
st.dataframe(df.head(50), use_container_width=True)
|
| 29 |
+
else:
|
| 30 |
+
st.info("Sample CSV not found.")
|
| 31 |
+
|
| 32 |
+
st.subheader("Enter runner metrics")
|
| 33 |
+
cols = st.columns(2)
|
| 34 |
+
|
| 35 |
+
sex = cols[0].selectbox("Sex (0=female, 1=male)", [0,1], index=1)
|
| 36 |
+
age = cols[1].slider("Age", 18, 70, 35)
|
| 37 |
+
height_cm = cols[0].slider("Height (cm)", 150, 200, 172)
|
| 38 |
+
weight_kg = cols[1].slider("Weight (kg)", 45, 120, 74)
|
| 39 |
+
|
| 40 |
+
resting_hr = cols[0].slider("Resting HR (bpm)", 40, 100, 60)
|
| 41 |
+
max_hr = cols[1].slider("Max HR (bpm)", 150, 205, 185)
|
| 42 |
+
avg_hr_during_run = cols[0].slider("Avg HR During Run (bpm)", 95, 190, 150)
|
| 43 |
+
hr_recovery_1min = cols[1].slider("HR Recovery 1 min (bpm drop)", 10, 55, 30)
|
| 44 |
+
|
| 45 |
+
distance_km = cols[0].slider("Distance of last run (km)", 1, 30, 5)
|
| 46 |
+
duration_min = cols[1].slider("Duration of last run (min)", 10, 180, 30)
|
| 47 |
+
pace_min_per_km = max(3.5, min(12.0, duration_min / max(distance_km, 0.5)))
|
| 48 |
+
avg_speed_kmh = 60.0 / pace_min_per_km
|
| 49 |
+
elevation_gain_m = cols[0].slider("Elevation Gain (m)", 0, 600, 50)
|
| 50 |
+
|
| 51 |
+
training_hours_week = cols[1].slider("Training Hours / Week", 0, 12, 4)
|
| 52 |
+
avg_intensity = cols[0].slider("Avg Intensity (1β10)", 1, 10, 6)
|
| 53 |
+
rest_days = cols[1].slider("Rest Days (last 7d)", 0, 4, 1)
|
| 54 |
+
|
| 55 |
+
sleep_hours_last_night = cols[0].slider("Sleep Hours Last Night", 3, 10, 7)
|
| 56 |
+
avg_sleep_hours_week = cols[1].slider("Avg Sleep Hours / Week", 4, 9, 7)
|
| 57 |
+
sleep_quality_score = cols[0].slider("Sleep Quality (0β100)", 25, 95, 72)
|
| 58 |
+
resting_hr_delta = resting_hr - 60
|
| 59 |
+
|
| 60 |
+
temperature_C = cols[1].slider("Temperature (Β°C)", 0, 35, 18)
|
| 61 |
+
humidity_pct = cols[0].slider("Humidity (%)", 15, 95, 55)
|
| 62 |
+
altitude_m = cols[1].slider("Altitude (m)", 0, 2200, 200)
|
| 63 |
+
|
| 64 |
+
hr_ratio = avg_hr_during_run / max_hr if max_hr>0 else 0
|
| 65 |
+
training_load_index = distance_km * (avg_hr_during_run/100.0) / (duration_min/60.0 + 0.1)
|
| 66 |
+
speed_per_kg = avg_speed_kmh / (weight_kg/70.0)
|
| 67 |
+
|
| 68 |
+
features = {
|
| 69 |
+
"sex":sex,"age":age,"height_cm":height_cm,"weight_kg":weight_kg,
|
| 70 |
+
"bmi": weight_kg/((height_cm/100.0)**2),
|
| 71 |
+
"resting_hr":resting_hr,"max_hr":max_hr,"avg_hr_during_run":avg_hr_during_run,
|
| 72 |
+
"hr_recovery_1min":hr_recovery_1min,
|
| 73 |
+
"distance_km":distance_km,"duration_min":duration_min,
|
| 74 |
+
"pace_min_per_km":pace_min_per_km,"avg_speed_kmh":avg_speed_kmh,
|
| 75 |
+
"elevation_gain_m":elevation_gain_m,
|
| 76 |
+
"training_hours_week":training_hours_week,"avg_intensity":avg_intensity,"rest_days":rest_days,
|
| 77 |
+
"sleep_hours_last_night":sleep_hours_last_night,"avg_sleep_hours_week":avg_sleep_hours_week,
|
| 78 |
+
"sleep_quality_score":sleep_quality_score,"resting_hr_delta":resting_hr_delta,
|
| 79 |
+
"temperature_C":temperature_C,"humidity_pct":humidity_pct,"altitude_m":altitude_m,
|
| 80 |
+
"hr_ratio":hr_ratio,"training_load_index":training_load_index,"speed_per_kg":speed_per_kg
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
def coaching_tip(vo2, sleep_quality, training_hours, hrr):
|
| 84 |
+
tips = []
|
| 85 |
+
if vo2 < 38: tips.append("Prioritize easy aerobic runs (Zone 2) 3β4x/week.")
|
| 86 |
+
elif vo2 < 48: tips.append("Add 1 weekly interval (e.g., 4Γ4min hard, full recovery).")
|
| 87 |
+
else: tips.append("Maintain with polarized training; 80% easy, 20% hard.")
|
| 88 |
+
|
| 89 |
+
if sleep_quality < 60: tips.append("Boost sleep hygiene to 7β8h; reduce late caffeine/screens.")
|
| 90 |
+
if training_hours > 8: tips.append("High load detected β deload 10β20% this week.")
|
| 91 |
+
if hrr < 20: tips.append("Low HRR β keep intensities sub-threshold for 2β3 days.")
|
| 92 |
+
return " ".join(tips) or "Keep up the good work!"
|
| 93 |
+
|
| 94 |
+
if st.button("Predict VOβ max"):
|
| 95 |
+
x = pd.DataFrame([features])
|
| 96 |
+
pipe = load_model()
|
| 97 |
+
vo2 = float(pipe.predict(x)[0])
|
| 98 |
+
tip = coaching_tip(vo2, sleep_quality_score, training_hours_week, hr_recovery_1min)
|
| 99 |
+
|
| 100 |
+
st.metric("Estimated VOβ max (ml/kg/min)", f"{vo2:.1f}")
|
| 101 |
+
st.success(tip)
|
| 102 |
+
|
| 103 |
+
st.divider()
|
| 104 |
+
st.caption("Limitations: Synthetic data; not a medical device; demo use only.")
|