sentinel / app.py
DrKryptoMed's picture
Sentinel sepsis early-warning Gradio demo
4450072
Raw
History Blame Contribute Delete
3.93 kB
import os
os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE"
"""Sentinel — causal sepsis early-warning demo (Gradio)."""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import gradio as gr
from sentinel_core import SepsisPredictor, derive_alerts, LABEL
THRESHOLD, PERSISTENCE, REFRACTORY = 0.70, 2, 6
predictor = SepsisPredictor("sentinel_causal.pt", "scaler.csv", threshold=THRESHOLD)
EXAMPLES = {
"Septic patient (deteriorates to sepsis)": "examples/septic_patient.psv",
"Non-septic patient (stable)": "examples/nonseptic_patient.psv",
}
def _run(df):
onset = None
if LABEL in df.columns and (df[LABEL] == 1).any():
onset = int((df[LABEL] == 1).argmax()) + 6 # label shifted 6h pre-onset
feats = df.drop(columns=[LABEL]) if LABEL in df.columns else df
risk = predictor.predict(feats)
alert_hours, _ = derive_alerts(risk, THRESHOLD, PERSISTENCE, REFRACTORY)
return risk, alert_hours, onset
def _plot(risk, alert_hours, onset):
hours = list(range(len(risk)))
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(hours, risk, color="#BD3F36", lw=2, label="Sepsis risk")
ax.axhline(THRESHOLD, color="grey", ls="--", lw=1, label=f"Threshold ({THRESHOLD})")
if onset is not None and onset <= len(risk):
ax.axvline(onset, color="black", ls=":", lw=1.5, label=f"Onset (h{onset})")
if alert_hours:
ax.scatter(alert_hours, [risk[h] for h in alert_hours], color="#BD3F36",
s=90, marker="v", zorder=5, label="Alert fired")
ax.fill_between(hours, 0, risk, color="#BD3F36", alpha=0.08)
ax.set(xlabel="ICU hour", ylabel="Predicted sepsis risk", ylim=(0, 1),
title="Streaming sepsis risk (causal, real-time simulation)")
ax.legend(loc="upper left", fontsize=9)
fig.tight_layout()
return fig
def analyze(example_choice, uploaded):
if uploaded is not None:
df = pd.read_csv(uploaded.name, sep="|")
source = "uploaded patient"
else:
df = pd.read_csv(EXAMPLES[example_choice], sep="|")
source = example_choice
risk, alert_hours, onset = _run(df)
fig = _plot(risk, alert_hours, onset)
peak = float(np.max(risk))
first = alert_hours[0] if alert_hours else None
lines = [f"**{source}** — {len(risk)} ICU hours"]
lines.append(f"Peak risk: {peak:.2f}")
if onset is not None:
lines.append(f"Documented clinical onset: ~hour {onset}")
if first is not None:
lead = f", ~{onset - first}h before onset" if onset else ""
lines.append(f"First alert: hour {first}{lead}")
lines.append(f"Total alerts: {len(alert_hours)} (at hours {alert_hours})")
else:
lines.append("No alerts fired (risk stayed below the sustained-alert policy).")
return fig, "\n\n".join(lines)
with gr.Blocks(title="Sentinel — Sepsis Early Warning") as demo:
gr.Markdown(
"# Sentinel — Causal Sepsis Early Warning\n"
"Predicts hour-by-hour sepsis risk from streaming ICU vitals (PhysioNet 2019), "
"with a fatigue-aware alerting policy. Risk at each hour uses only data up to "
"that hour (a true causal simulation).\n\n"
"*Research demonstration on public data. **Not for clinical use.***"
)
with gr.Row():
with gr.Column(scale=1):
choice = gr.Dropdown(list(EXAMPLES.keys()), value=list(EXAMPLES.keys())[0],
label="Example patient")
upload = gr.File(label="…or upload a PhysioNet .psv", file_types=[".psv"])
btn = gr.Button("Analyze", variant="primary")
with gr.Column(scale=2):
plot = gr.Plot(label="Risk trajectory")
summary = gr.Markdown()
btn.click(analyze, [choice, upload], [plot, summary])
demo.load(analyze, [choice, upload], [plot, summary])
if __name__ == "__main__":
demo.launch()