ElMETRICO commited on
Commit
b703a33
·
verified ·
1 Parent(s): 03289af

Upload 5 files

Browse files
Files changed (3) hide show
  1. app.py +168 -90
  2. find_threshold_recall.py +79 -0
  3. nhanes_sleep_extended_v2.csv +0 -0
app.py CHANGED
@@ -1,106 +1,184 @@
1
  import joblib
 
2
  import pandas as pd
3
- import gradio as gr
4
-
5
- # ---------------- Load model bundle ----------------
6
- bundle = joblib.load("best_model_v2_calibrated.joblib")
7
- model = bundle["model"]
8
- threshold = float(bundle["threshold"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- # ---------------- Output formatting ----------------
11
- def format_output(lang, label, proba):
12
  if lang == "বাংলা":
13
- risk_text = "উচ্চ ঝুঁকি" if label == "High Risk" else "কম ঝুঁকি"
14
- return (
15
- f"### ফলাফল: **{risk_text}**\n"
16
- f"- ঝুঁকির সম্ভবনা (Probability): **{proba:.3f}**\n"
17
- f"- Threshold: **{threshold:.2f}**\n\n"
18
- "⚠️ এটি একটি **স্ক্রিনিং টল**, চিকিৎসা নির্ণয় নয়। সম থাকলে ডাক্তারের পরামর্শ িন।"
19
- )
 
 
 
 
 
20
  else:
21
- return (
22
- f"### Result: **{label}**\n"
23
- f"- Risk probability: **{proba:.3f}**\n"
24
- f"- Threshold: **{threshold:.2f}**\n\n"
25
- "⚠️ This is a **screening tool**, not a medical diagnosis. If you have symptoms, consult a clinician."
26
- )
27
-
28
- def web_speak_html(text, lang):
29
- voice_lang = "bn-BD" if lang == "বাংলা" else "en-US"
30
- safe = text.replace("`", "").replace("\\", "\\\\").replace("'", "\\'")
31
- return f"""
32
- <div style="display:flex;gap:10px;align-items:center;">
33
- <button onclick="(function(){{
34
- const msg = new SpeechSynthesisUtterance('{safe}');
35
- msg.lang = '{voice_lang}';
36
- window.speechSynthesis.cancel();
37
- window.speechSynthesis.speak(msg);
38
- }})()" style="padding:10px 14px;border-radius:10px;border:1px solid #ccc;cursor:pointer;">
39
- 🔊 Speak / শোনান
40
- </button>
41
- <span style="opacity:0.7;">(Voice depends on browser installed voices)</span>
42
- </div>
43
- """
44
-
45
- # ---------------- Prediction ----------------
46
- def predict(lang, age, gender, bmi, bp_sys, bp_dia, phys_days, dpq, smoking, alcohol, diabetes, cycle):
47
- sample = pd.DataFrame([{
48
  "Age": float(age),
49
- "Gender": gender,
50
  "BMI": float(bmi),
51
  "BP_SYS": float(bp_sys),
52
  "BP_DIA": float(bp_dia),
53
  "Phys_Activity_Days": float(phys_days),
54
  "DPQ_Score": float(dpq),
55
- "Smoking_Indicator": float(smoking),
56
  "Alcohol_Feature": float(alcohol),
57
- "Diabetes_Indicator": float(diabetes),
58
- "Cycle": cycle
59
- }])
60
-
61
- proba = float(model.predict_proba(sample)[:, 1][0])
62
- pred = int(proba >= threshold)
63
- label = "High Risk" if pred == 1 else "Low Risk"
64
-
65
- md = format_output(lang, label, proba)
66
- speak = web_speak_html(md, lang)
67
- return md, speak
68
-
69
- # ---------------- UI ----------------
70
- with gr.Blocks(title="SleepGuardAI – Sleep Risk Screening") as demo:
71
- gr.Markdown("# SleepGuardAI – Sleep Risk Screening (NHANES-based)")
72
- gr.Markdown("Fill the form → get risk score. Bilingual output + Speak button included.")
73
-
74
- lang = gr.Radio(["English", "বাংলা"], value="English", label="Language / ভাষা")
75
-
76
- with gr.Row():
77
- age = gr.Slider(10, 90, value=30, label="Age / বয়স")
78
- gender = gr.Dropdown(["Male", "Female"], value="Male", label="Gender")
79
-
80
- with gr.Row():
81
- bmi = gr.Slider(10, 50, value=25, label="BMI")
82
- bp_sys = gr.Slider(80, 220, value=120, label="Systolic BP")
83
- bp_dia = gr.Slider(40, 140, value=80, label="Diastolic BP")
84
-
85
- phys = gr.Slider(0, 7, value=3, step=1, label="Physical Activity Days/Week")
86
-
87
- gr.Markdown("### Optional (improves accuracy if known)")
88
- with gr.Row():
89
- dpq = gr.Slider(0, 27, value=0, step=1, label="DPQ Depression Score (0–27)")
90
- smoking = gr.Dropdown([1, 2], value=2, label="Smoking (1=Yes, 2=No)")
91
- diabetes = gr.Dropdown([1, 2], value=2, label="Diabetes (1=Yes, 2=No)")
92
- alcohol = gr.Slider(0, 30, value=0, step=1, label="Alcohol feature (proxy)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- cycle = gr.Dropdown(["G", "H", "I", "J"], value="J", label="NHANES Cycle")
95
 
96
- btn = gr.Button("Predict / ফলাফল দেখুন")
97
- out_md = gr.Markdown()
98
- out_speak = gr.HTML()
 
 
 
 
 
 
 
99
 
100
- btn.click(
101
- predict,
102
- inputs=[lang, age, gender, bmi, bp_sys, bp_dia, phys, dpq, smoking, alcohol, diabetes, cycle],
103
- outputs=[out_md, out_speak]
104
- )
105
 
106
- demo.launch()
 
 
 
1
  import joblib
2
+ import numpy as np
3
  import pandas as pd
4
+ import streamlit as st
5
+
6
+ # =========================
7
+ # CONFIG / CONSTANTS
8
+ # =========================
9
+
10
+ # These thresholds decide the UI label.
11
+ # You can change them anytime.
12
+ TH_LOW = 0.20 # below this => Low Risk
13
+ TH_HIGH = 0.40 # above this => High Risk
14
+ # between TH_LOW and TH_HIGH => Moderate Risk
15
+
16
+ MODEL_PATH = "best_model_v2_calibrated.joblib"
17
+
18
+ FEATURE_ORDER = [
19
+ "Age",
20
+ "Gender",
21
+ "BMI",
22
+ "BP_SYS",
23
+ "BP_DIA",
24
+ "Phys_Activity_Days",
25
+ "DPQ_Score",
26
+ "Smoking_Indicator",
27
+ "Alcohol_Feature",
28
+ "Diabetes_Indicator",
29
+ "Cycle",
30
+ ]
31
+
32
+ # =========================
33
+ # HELPERS
34
+ # =========================
35
+
36
+ def label_from_probability(prob: float) -> str:
37
+ """3-level risk label based on thresholds."""
38
+ if prob < TH_LOW:
39
+ return "Low Risk"
40
+ elif prob < TH_HIGH:
41
+ return "Moderate Risk"
42
+ else:
43
+ return "High Risk"
44
 
45
+ def get_texts(lang: str):
46
+ """Simple bilingual text dictionary."""
47
  if lang == "বাংলা":
48
+ return {
49
+ "title": "SleepGuardAI – Sleep Risk Screening (NHANES-based)",
50
+ "subtitle": "র্ম পূরণ করুন → ঝুঁকির স্কোর দেখুন। (এটি মেডিকেয়াগনসিস নয়)",
51
+ "predict_btn": "Predict / ফলফল দেখুন",
52
+ "result": "ফলাফল",
53
+ "prob": "ঝকি সম্",
54
+ "thresholds": "থ্রেশহোল্ড",
55
+ "warning": "⚠️ এটি একটি স্ক্রিনিং টুল, মেডিকেল ডায়াগনসিস নয়। লক্ষণ থাকলে ডাক্তার দেখান।",
56
+ "low": "কম ঝুঁকি",
57
+ "mod": "মাঝারি ঝুঁকি",
58
+ "high": "উচ্চ ঝুঁকি",
59
+ }
60
  else:
61
+ return {
62
+ "title": "SleepGuardAI – Sleep Risk Screening (NHANES-based)",
63
+ "subtitle": "Fill the form → get a risk score. (This is NOT a medical diagnosis)",
64
+ "predict_btn": "Predict / ফলাফল দেখুন",
65
+ "result": "Result",
66
+ "prob": "Risk probability",
67
+ "thresholds": "Thresholds",
68
+ "warning": "⚠️ This is a screening tool, not a medical diagnosis. If you have symptoms, consult a clinician.",
69
+ "low": "Low Risk",
70
+ "mod": "Moderate Risk",
71
+ "high": "High Risk",
72
+ }
73
+
74
+ @st.cache_resource
75
+ def load_model():
76
+ return joblib.load(MODEL_PATH)
77
+
78
+ def build_feature_row(
79
+ age, gender, bmi, bp_sys, bp_dia, phys_days, dpq, smoking, alcohol, diabetes, cycle
80
+ ) -> pd.DataFrame:
81
+ """Build one-row DataFrame in the exact feature order used by training."""
82
+ row = {
 
 
 
 
 
83
  "Age": float(age),
84
+ "Gender": str(gender), # keep as string (your pipeline encodes it)
85
  "BMI": float(bmi),
86
  "BP_SYS": float(bp_sys),
87
  "BP_DIA": float(bp_dia),
88
  "Phys_Activity_Days": float(phys_days),
89
  "DPQ_Score": float(dpq),
90
+ "Smoking_Indicator": int(smoking),
91
  "Alcohol_Feature": float(alcohol),
92
+ "Diabetes_Indicator": int(diabetes),
93
+ "Cycle": str(cycle),
94
+ }
95
+ df = pd.DataFrame([row])[FEATURE_ORDER]
96
+ return df
97
+
98
+ # =========================
99
+ # UI
100
+ # =========================
101
+
102
+ st.set_page_config(page_title="SleepGuardAI", layout="wide")
103
+
104
+ lang = st.radio("Language / ভাষা", ["English", "বাংলা"], horizontal=True)
105
+ T = get_texts(lang)
106
+
107
+ st.title(T["title"])
108
+ st.caption(T["subtitle"])
109
+
110
+ # Load model
111
+ try:
112
+ bundle = load_model()
113
+ except FileNotFoundError:
114
+ st.error(
115
+ f"Model file not found: '{MODEL_PATH}'. Put it in the same folder as app.py."
116
+ )
117
+ st.stop()
118
+
119
+ # We support two possible save formats:
120
+ # 1) bundle is a dict with {"model":..., "calibrator":..., ...}
121
+ # 2) bundle is directly a sklearn Pipeline/Calibrated model with predict_proba
122
+ model = bundle.get("model") if isinstance(bundle, dict) and "model" in bundle else bundle
123
+ calibrator = bundle.get("calibrator") if isinstance(bundle, dict) and "calibrator" in bundle else None
124
+
125
+ colA, colB = st.columns(2)
126
+
127
+ with colA:
128
+ age = st.slider("Age / বয়স", 10, 90, 30)
129
+ gender = st.selectbox("Gender", ["Male", "Female"])
130
+
131
+ bmi = st.slider("BMI", 10.0, 50.0, 25.0, 0.1)
132
+ bp_sys = st.slider("Systolic BP", 80, 220, 120)
133
+ bp_dia = st.slider("Diastolic BP", 40, 140, 80)
134
+
135
+ phys_days = st.slider("Physical Activity Days/Week", 0, 7, 3)
136
+
137
+ with colB:
138
+ st.markdown("### Optional (improves accuracy if known)")
139
+ dpq = st.slider("DPQ Depression Score (0–27)", 0, 27, 5)
140
+ smoking = st.selectbox("Smoking (1=Yes, 2=No)", [2, 1], index=0)
141
+ diabetes = st.selectbox("Diabetes (1=Yes, 2=No)", [2, 1], index=0)
142
+ alcohol = st.slider("Alcohol feature (proxy)", 0.0, 30.0, 2.0, 0.5)
143
+ cycle = st.selectbox("NHANES Cycle", ["G", "H", "I", "J"])
144
+
145
+ st.divider()
146
+
147
+ if st.button(T["predict_btn"], use_container_width=True):
148
+ X = build_feature_row(age, gender, bmi, bp_sys, bp_dia, phys_days, dpq, smoking, alcohol, diabetes, cycle)
149
+
150
+ # Get probability
151
+ # If you saved a separate calibrator, run model -> probs -> calibrator
152
+ # Otherwise assume model already outputs calibrated proba.
153
+ try:
154
+ raw_proba = model.predict_proba(X)[:, 1]
155
+ except Exception as e:
156
+ st.error(f"Model could not compute predict_proba. Error: {e}")
157
+ st.stop()
158
+
159
+ if calibrator is not None:
160
+ # calibrator usually expects 2D array
161
+ prob = float(calibrator.predict(raw_proba.reshape(-1, 1))[0])
162
+ else:
163
+ prob = float(raw_proba[0])
164
 
165
+ label = label_from_probability(prob)
166
 
167
+ # Display with bilingual mapping
168
+ if label == "Low Risk":
169
+ pretty_label = T["low"]
170
+ st.success(f"{T['result']}: {pretty_label}")
171
+ elif label == "Moderate Risk":
172
+ pretty_label = T["mod"]
173
+ st.warning(f"{T['result']}: {pretty_label}")
174
+ else:
175
+ pretty_label = T["high"]
176
+ st.error(f"{T['result']}: {pretty_label}")
177
 
178
+ st.write(f"**{T['prob']}:** {prob:.3f}")
179
+ st.write(f"**{T['thresholds']}:** Low<{TH_LOW:.2f} | Moderate {TH_LOW:.2f}–{TH_HIGH:.2f} | High≥{TH_HIGH:.2f}")
180
+ st.caption(T["warning"])
 
 
181
 
182
+ # Debug view if you want to show professor:
183
+ with st.expander("Show input feature row (for debugging/paper)"):
184
+ st.dataframe(X)
find_threshold_recall.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import joblib
2
+ import numpy as np
3
+ import pandas as pd
4
+ from sklearn.model_selection import train_test_split
5
+ from sklearn.metrics import recall_score, fbeta_score, roc_auc_score
6
+
7
+ MODEL_PATH = "best_model_v2_calibrated.joblib"
8
+ CSV_PATH = "nhanes_sleep_extended_v2.csv"
9
+
10
+ TARGET = "Sleep_Risk"
11
+
12
+ FEATURES = [
13
+ "Age",
14
+ "Gender",
15
+ "BMI",
16
+ "BP_SYS",
17
+ "BP_DIA",
18
+ "Phys_Activity_Days",
19
+ "DPQ_Score",
20
+ "Smoking_Indicator",
21
+ "Alcohol_Feature",
22
+ "Diabetes_Indicator",
23
+ "Cycle",
24
+ ]
25
+
26
+ def main():
27
+ df = pd.read_csv(CSV_PATH)
28
+
29
+ # drop rows with missing values in required columns
30
+ df = df.dropna(subset=FEATURES + [TARGET]).copy()
31
+
32
+ X = df[FEATURES]
33
+ y = df[TARGET].astype(int)
34
+
35
+ # Train/test split (keep same logic as training if possible)
36
+ X_train, X_test, y_train, y_test = train_test_split(
37
+ X, y, test_size=0.20, random_state=42, stratify=y
38
+ )
39
+
40
+ bundle = joblib.load(MODEL_PATH)
41
+ model = bundle.get("model") if isinstance(bundle, dict) and "model" in bundle else bundle
42
+ calibrator = bundle.get("calibrator") if isinstance(bundle, dict) and "calibrator" in bundle else None
43
+
44
+ # Get probabilities
45
+ p_test_raw = model.predict_proba(X_test)[:, 1]
46
+ if calibrator is not None:
47
+ p_test = calibrator.predict(p_test_raw.reshape(-1, 1))
48
+ else:
49
+ p_test = p_test_raw
50
+
51
+ # AUC (threshold-free)
52
+ auc = roc_auc_score(y_test, p_test)
53
+ print(f"TEST AUC: {auc:.4f}")
54
+
55
+ # Sweep thresholds
56
+ thresholds = np.round(np.arange(0.05, 0.90, 0.01), 2)
57
+
58
+ best_f2 = (-1, None)
59
+ th_recall_085 = None
60
+
61
+ for th in thresholds:
62
+ pred = (p_test >= th).astype(int)
63
+ rec = recall_score(y_test, pred)
64
+ f2 = fbeta_score(y_test, pred, beta=2)
65
+
66
+ if f2 > best_f2[0]:
67
+ best_f2 = (f2, th)
68
+
69
+ if th_recall_085 is None and rec >= 0.85:
70
+ th_recall_085 = th
71
+
72
+ print(f"Best threshold by F2: {best_f2[1]} (F2={best_f2[0]:.4f})")
73
+ if th_recall_085 is not None:
74
+ print(f"Threshold achieving recall ≥ 0.85 : {th_recall_085}")
75
+ else:
76
+ print("No threshold achieved recall ≥ 0.85 in tested range.")
77
+
78
+ if __name__ == "__main__":
79
+ main()
nhanes_sleep_extended_v2.csv ADDED
The diff for this file is too large to render. See raw diff