File size: 7,025 Bytes
33b26a5
 
 
 
 
ed87042
 
 
31d95af
ed87042
31d95af
ed87042
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31d95af
33b26a5
 
 
 
 
 
 
ed87042
33b26a5
 
 
ed87042
33b26a5
 
 
 
ed87042
 
 
 
 
 
 
33b26a5
ed87042
 
 
 
33b26a5
76fd913
33b26a5
31d95af
ed87042
 
 
33b26a5
76fd913
ed87042
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33b26a5
ed87042
 
 
33b26a5
76fd913
33b26a5
ed87042
 
 
 
 
 
33b26a5
ed87042
33b26a5
 
76fd913
ed87042
 
 
 
 
 
 
 
 
88307cb
 
ed87042
 
 
 
 
 
31d95af
ed87042
88307cb
ed87042
 
 
88307cb
 
ed87042
88307cb
ed87042
 
88307cb
 
 
ed87042
88307cb
 
 
ed87042
 
88307cb
 
33b26a5
ed87042
00682d2
33b26a5
ed87042
 
 
00682d2
ed87042
 
 
 
33b26a5
 
ed87042
 
 
33b26a5
364bef6
00682d2
 
33b26a5
52acace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import gradio as gr
import joblib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import json
from catboost import CatBoostRegressor, Pool
from huggingface_hub import hf_hub_download
import spaces                                       # GPU

REPO_ID = "Elegbede/stunting-catboost"

def _dl(fname):
    return hf_hub_download(repo_id=REPO_ID, filename=fname)

model = CatBoostRegressor()
model.load_model(_dl("catboost_final.cbm"))

features     = json.load(open(_dl("features.json")))
cat_features = json.load(open(_dl("cat_features.json")))
breslow      = joblib.load(_dl("breslow_cat.pkl"))
risk_centre  = float(np.load(_dl("risk_centre.npy"))[0])

wealth_map  = {"poorest": 0, "poorer": 1, "middle": 2, "richer": 3, "richest": 4}
mat_edu_map = {"no education": 0, "primary": 1, "secondary": 2, "higher": 3}


def create_inputs():
    return [
        gr.Number(label="Birth Order", value=1, minimum=1, maximum=20),
        gr.Number(label="Maternal Age (years)", value=25, minimum=12, maximum=49),
        gr.Dropdown(
            choices=[("Poorest", "poorest"), ("Poorer", "poorer"), ("Middle", "middle"),
                     ("Richer", "richer"), ("Richest", "richest")],
            label="Wealth Index", value="middle"),
        gr.Dropdown(
            choices=[("No Education", "no education"), ("Primary", "primary"),
                     ("Secondary", "secondary"), ("Higher", "higher")],
            label="Maternal Education", value="primary"),
        gr.Dropdown(choices=[("No", 0), ("Yes", 1)], label="Multiple Birth", value=0),
        gr.Dropdown(choices=["male", "female"], label="Child Sex", value="male"),
        gr.Dropdown(
            choices=["islam", "catholic", "other christian", "traditionalist", "other"],
            label="Religion", value="islam"),
        gr.Dropdown(choices=["Improved", "Unimproved"],
                    label="Water Source", value="Improved"),
        gr.Dropdown(choices=["Improved", "Unimproved"],
                    label="Sanitation", value="Improved"),
        gr.Dropdown(choices=["no education", "primary", "secondary", "higher"],
                    label="Partner Education", value="primary"),
        gr.Dropdown(
            choices=["North West", "North East", "North Central",
                     "South West", "South East", "South South"],
            label="Region", value="North West"),
        gr.Dropdown(choices=["rural", "urban"], label="Residence", value="rural"),
    ]


@spaces.GPU                                          # GPU
def predict_stunting(birth_order, mat_age, wealth_label, mat_edu_label,
                     multiple_birth_binary, child_sex, religion,
                     water_improved, sanitation_improved,
                     partner_edu, region, residence):

    row = pd.DataFrame([{
        "birth_order":           float(birth_order),
        "mat_age":               float(mat_age),
        "wealth_ordinal":        float(wealth_map[wealth_label]),
        "mat_edu_ordinal":       float(mat_edu_map[mat_edu_label]),
        "multiple_birth_binary": float(multiple_birth_binary),
        "child_sex":             str(child_sex),
        "religion":              str(religion),
        "water_improved":        str(water_improved),
        "sanitation_improved":   str(sanitation_improved),
        "partner_edu":           str(partner_edu),
        "region":                str(region),
        "residence":             str(residence),
    }])[features]

    pool = Pool(row, cat_features=cat_features)
    log_hazard = float(model.predict(pool, prediction_type="RawFormulaVal")[0])
    log_hazard -= risk_centre

    times = np.array([6, 12, 18, 24, 36, 48])
    surv_fn = breslow.get_survival_function(np.array([log_hazard]))[0]
    domain_min, domain_max = surv_fn.domain
    surv_probs = surv_fn(np.clip(times, domain_min, domain_max))
    stunting_risks = 1 - surv_probs

    fig, ax = plt.subplots(figsize=(8, 4))
    ax.plot(times, stunting_risks, marker="o", linestyle="-",
            color="steelblue", linewidth=2, markersize=8)
    ax.axhline(y=0.20, color="red", linestyle="--", label="20% Risk Threshold")
    ax.set_xlabel("Age (months)")
    ax.set_ylabel("Stunting Risk")
    ax.set_title("Stunting Risk by Age")
    ax.set_ylim(0, 1)
    ax.grid(True, linestyle=":", alpha=0.7)
    ax.legend()
    ax.set_xticks(times)

    THRESH = 0.20
    age_lines = "\n".join(f"- **{int(a)} months:** {r:.1%} estimated risk"
                          for a, r in zip(times, stunting_risks))
    crossing = next((int(a) for a, r in zip(times, stunting_risks) if r >= THRESH), None)
    threshold_text = (
        f"Predicted stunting risk reaches the {THRESH:.0%} threshold by about "
        f"**{crossing} months**."
        if crossing else
        f"Predicted stunting risk does not reach {THRESH:.0%} within 48 months."
    )

    flagged = []
    if wealth_map[wealth_label] <= 1:        flagged.append("lower household wealth quintile")
    if mat_edu_map[mat_edu_label] == 0:      flagged.append("mother has no formal education")
    if water_improved == "Unimproved":       flagged.append("unimproved water source")
    if sanitation_improved == "Unimproved":  flagged.append("unimproved sanitation")
    if multiple_birth_binary == 1:           flagged.append("multiple birth")
    if mat_age < 18 or mat_age > 40:         flagged.append("maternal age outside 18-40")
    if birth_order >= 5:                     flagged.append("birth order 5th or later")

    factors_text = ("Factors flagged as elevated-risk in this input:\n"
                    + "\n".join(f"- {f}" for f in flagged)) if flagged else \
                   "No major elevated-risk factors flagged."

    summary_md = f"""
## Stunting Risk Prediction (CatBoost Cox model)

### Predicted risk over time
{age_lines}

{threshold_text}

### Factors noted
{factors_text}

---
⚠️ **Research prototype only** — external validation not performed.
Do not use for clinical or policy decisions.
"""
    return summary_md, fig


demo = gr.Interface(
    fn=predict_stunting,
    inputs=create_inputs(),
    outputs=[gr.Markdown(label="Risk Summary"), gr.Plot(label="Risk Curve")],
    title="Stunting Risk Predictor — CatBoost",
    description="""
    Predicts stunting risk at ages 6, 12, 18, 24, 36, and 48 months using a
    **CatBoost Cox model** trained on 42,981 Nigerian children.

    ⚠️ **Research prototype** — external validation not performed.
    """,
    examples=[
        [1, 25, "middle",  "primary",       0, "male",   "islam", "Improved",   "Improved",   "primary",      "North West", "rural"],
        [4, 18, "poorest", "no education",  0, "female", "catholic", "Unimproved", "Unimproved", "no education", "South East", "rural"],
        [2, 35, "richest", "higher",        1, "male",   "other christian", "Improved", "Improved", "secondary", "South West", "urban"],
    ],
    flagging_mode="never",
)

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=7860)