Marcel0123's picture
Update app.py
47c408d verified
import time
import json
import os
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import gradio as gr
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import SGDClassifier, LogisticRegression, LinearRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.metrics import (
accuracy_score, f1_score, roc_auc_score,
confusion_matrix, roc_curve, auc,
precision_recall_curve, precision_score, recall_score
)
# =========================
# Ingebouwde dataset
# =========================
def load_builtin_dataset(n=1000, seed=42):
rng = np.random.default_rng(seed)
age = rng.integers(18, 75, size=n)
gender = rng.choice([0, 1], size=n) # dummy
sleep_quality = np.clip(rng.normal(6.5, 1.5, size=n), 1, 10)
energy = np.clip(rng.normal(6.0, 1.7, size=n), 1, 10)
anhedonia = np.clip(rng.normal(3.5, 1.8, size=n), 1, 10)
stress = np.clip(rng.normal(4.5, 2.0, size=n), 1, 10)
social_support = np.clip(rng.normal(6.0, 1.8, size=n), 1, 10)
activity = np.clip(rng.normal(3.0 + 0.4*energy - 0.2*stress, 1.5, size=n), 0, 10)
phq9 = np.clip(
0.8*anhedonia + 0.7*stress - 0.5*sleep_quality - 0.4*energy
+ rng.normal(0, 1.2, size=n) + 5, 0, 27
)
# label
logit = (
+ 0.65*anhedonia + 0.55*stress
- 0.45*sleep_quality - 0.40*energy
- 0.30*social_support - 0.20*activity
+ 0.01*(age - 40) + 0.05*gender
+ rng.normal(0, 0.6, size=n)
)
logit -= np.median(logit)
prob = 1 / (1 + np.exp(-logit))
depressed = (prob > 0.5).astype(int)
df = pd.DataFrame({
"age": age, "gender": gender, "sleep_quality": sleep_quality, "energy": energy,
"anhedonia": anhedonia, "stress": stress, "social_support": social_support,
"activity": activity, "phq9": phq9, "depressed": depressed
})
return df, "depressed"
# =========================
# NL-weergave helpers
# =========================
COLMAP_NL = {
"age": "Leeftijd",
"gender": "Geslacht",
"sleep_quality": "Slaapkwaliteit",
"energy": "Energie",
"anhedonia": "Anhedonie",
"stress": "Stress",
"social_support": "Sociale steun",
"activity": "Activiteit",
"phq9": "PHQ-9",
"depressed": "Depressie",
"prediction": "Voorspelling",
"confidence": "Zekerheid (%)",
}
GENDER_MAP = {0: "Vrouw", 1: "Man"}
DEPRESSED_MAP = {0: "Nee", 1: "Ja"}
def df_to_nl_display(df: pd.DataFrame, include_percent_confidence: bool = False) -> pd.DataFrame:
disp = df.copy()
if "gender" in disp.columns:
disp["gender"] = disp["gender"].map(GENDER_MAP).fillna("Onbekend")
if "depressed" in disp.columns:
disp["depressed"] = disp["depressed"].map(DEPRESSED_MAP).fillna("Onbekend")
if include_percent_confidence and "confidence" in disp.columns:
disp["confidence"] = (pd.to_numeric(disp["confidence"], errors="coerce") * 100.0).round(1).map(
lambda x: f"{x:.1f}%" if pd.notnull(x) else "—"
)
return disp.rename(columns={k: v for k, v in COLMAP_NL.items() if k in disp.columns})
# =========================
# Helpers – PCA/DB
# =========================
def ensure_min_classes(y):
if len(np.unique(y)) < 2:
raise gr.Error("Label heeft minder dan 2 unieke klassen.")
def make_base_fig(coords, y, title):
palette = ["#2563eb", "#ef4444", "#10b981", "#f59e0b", "#a855f7", "#06b6d4", "#f97316", "#22c55e"]
fig = go.Figure()
fig.update_layout(
title=title, xaxis_title="PC1", yaxis_title="PC2",
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
margin=dict(l=10, r=10, t=60, b=10),
template=None, plot_bgcolor="#ffffff", paper_bgcolor="#ffffff", height=520
)
labels = pd.Series(y).astype(str).values
uniq = list(np.unique(labels))
for i, lbl in enumerate(uniq):
mask = labels == lbl
color = palette[i % len(palette)]
fig.add_trace(go.Scatter(
x=coords[mask, 0], y=coords[mask, 1],
mode="markers", name=f"Klasse {lbl}",
marker=dict(size=10, opacity=0.95, color=color, line=dict(width=1, color="#111")),
hovertemplate="PC1: %{x:.2f}<br>PC2: %{y:.2f}<extra>" + f"Klasse {lbl}</extra>"
))
return fig
def draw_decision_boundary(fig, clf2d, scaler2d, pca2d, X_scaled):
coords = pca2d.transform(X_scaled)
x_min, x_max = coords[:, 0].min() - 0.5, coords[:, 0].max() + 0.5
y_min, y_max = coords[:, 1].min() - 0.5, coords[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200), np.linspace(y_min, y_max, 200))
grid_2d = np.c_[xx.ravel(), yy.ravel()]
coords_grid_s = scaler2d.transform(grid_2d)
if hasattr(clf2d, "predict_proba"):
Z = clf2d.predict_proba(coords_grid_s)[:, -1]
else:
dec = clf2d.decision_function(coords_grid_s)
Z = (dec - np.nanmin(dec)) / (np.nanmax(dec) - np.nanmin(dec) + 1e-9)
Z = np.nan_to_num(Z, nan=0.5, posinf=1.0, neginf=0.0).reshape(xx.shape)
fig.add_trace(go.Contour(
x=np.linspace(x_min, x_max, 200),
y=np.linspace(y_min, y_max, 200),
z=Z,
showscale=False,
contours=dict(coloring="lines", showlines=True),
line=dict(width=1),
opacity=0.8,
name="Beslissingslijnen"
))
return fig
def get_model(model_name, params):
if model_name == "SGDClassifier (realtime)":
return SGDClassifier(
loss=params.get("sgd_loss", "log_loss"),
alpha=params.get("sgd_alpha", 1e-4),
learning_rate=params.get("sgd_lr", "optimal"),
max_iter=1, random_state=42
)
elif model_name == "Logistic Regression":
return LogisticRegression(max_iter=300)
elif model_name == "Random Forest":
return RandomForestClassifier(
n_estimators=int(params.get("rf_n", 250)),
max_depth=int(params.get("rf_depth", 8)) if params.get("rf_depth", None) else None,
random_state=42
)
elif model_name == "SVM (RBF)":
return SVC(probability=True, gamma="scale", C=params.get("svm_c", 1.0), random_state=42)
return LogisticRegression(max_iter=300)
# =========================
# Regressievisual (robust)
# =========================
def _safe_split_masks(n, stratify_y=None, test_size=0.25, seed=42):
try:
idx = np.arange(n)
idx_tr, idx_te = train_test_split(idx, test_size=test_size, random_state=seed, stratify=stratify_y)
except Exception:
idx = np.arange(n)
idx_tr, idx_te = train_test_split(idx, test_size=test_size, random_state=seed)
train_mask = np.zeros(n, dtype=bool); train_mask[idx_tr] = True
test_mask = np.zeros(n, dtype=bool); test_mask[idx_te] = True
return train_mask, test_mask
def make_activity_regression_fig(df, train_mask, test_mask, epoch_title=""):
n = len(df)
if (train_mask is None) or (test_mask is None) or (len(train_mask) != n) or (len(test_mask) != n):
strat = df["depressed"].values if "depressed" in df.columns else None
train_mask, test_mask = _safe_split_masks(n, stratify_y=strat)
rng = np.random.default_rng(0)
minutes = (
df["activity"].to_numpy(dtype=float) * 100.0
+ df["energy"].to_numpy(dtype=float) * 20.0
- df["stress"].to_numpy(dtype=float) * 10.0
+ rng.normal(0.0, 30.0, n)
)
minutes = np.clip(minutes, 0.0, 1200.0).astype(float)
functioning = (
20.0
+ 6.0*df["energy"].to_numpy(dtype=float)
+ 5.0*df["sleep_quality"].to_numpy(dtype=float)
- 4.0*df["stress"].to_numpy(dtype=float)
+ 3.0*df["social_support"].to_numpy(dtype=float)
+ 5.0*df["activity"].to_numpy(dtype=float)
+ rng.normal(0.0, 5.0, n)
)
functioning = np.clip(functioning, 0.0, 100.0).astype(float)
have_line = False
try:
X = minutes.reshape(-1, 1)
y = functioning
if X[train_mask].shape[0] < 2:
train_mask, test_mask = _safe_split_masks(n, stratify_y=None)
reg = LinearRegression().fit(X[train_mask], y[train_mask])
x_line = np.linspace(minutes.min(), minutes.max(), 200).reshape(-1, 1)
y_line = reg.predict(x_line)
have_line = True
except Exception:
have_line = False
def hover_for(idx):
row = df.iloc[idx]
return f"id={idx} • leeftijd={int(row['age'])} • steun={row['social_support']:.1f} • stress={row['stress']:.1f}"
train_idx = np.where(train_mask)[0]
test_idx = np.where(test_mask)[0]
train_hover = np.array([hover_for(i) for i in train_idx], dtype=object)
test_hover = np.array([hover_for(i) for i in test_idx], dtype=object)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=minutes[train_mask], y=functioning[train_mask],
mode="markers", name="train",
marker=dict(size=8, opacity=0.9),
hovertemplate="Min/wk: %{x:.0f}<br>Score: %{y:.0f}<br>%{customdata}<extra></extra>",
customdata=train_hover
))
fig.add_trace(go.Scatter(
x=minutes[test_mask], y=functioning[test_mask],
mode="markers", name="test",
marker=dict(size=9, symbol="x", line=dict(width=2)),
hovertemplate="Min/wk: %{x:.0f}<br>Score: %{y:.0f}<br>%{customdata}<extra></extra>",
customdata=test_hover
))
if have_line:
fig.add_trace(go.Scatter(
x=x_line.ravel(), y=y_line,
mode="lines", name="model", line=dict(width=3)
))
fig.update_layout(
title=f"Bewegen (min/week) → Functioneringsscore — {epoch_title}",
xaxis_title="Minuten bewegen per week",
yaxis_title="Functioneringsscore (0–100)",
template=None, plot_bgcolor="#ffffff", paper_bgcolor="#ffffff",
height=460, margin=dict(l=10, r=10, t=60, b=10),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
return fig
# =========================
# Visuals (testset)
# =========================
def fig_confusion_matrix(y_true, y_pred, title="Confusion matrix (testset)"):
cm = confusion_matrix(y_true, y_pred, labels=[0, 1]).astype(int)
fig = go.Figure(data=go.Heatmap(
z=cm, x=["Pred 0", "Pred 1"], y=["True 0", "True 1"],
colorscale="Blues", showscale=False
))
N = len(y_true)
for i_y, row in enumerate(["True 0", "True 1"]):
for i_x, col in enumerate(["Pred 0", "Pred 1"]):
val = cm[i_y, i_x]; pct = 100.0 * val / max(1, N)
fig.add_annotation(x=col, y=row, text=f"{val}<br>{pct:.1f}%", showarrow=False)
fig.update_layout(title=title, plot_bgcolor="#fff", paper_bgcolor="#fff",
height=360, margin=dict(l=10, r=10, t=60, b=10))
return fig
def fig_roc(y_true, proba, thr=None, title="ROC-curve (testset)"):
fpr, tpr, thresholds = roc_curve(y_true, proba)
roc_auc = auc(fpr, tpr)
fig = go.Figure()
fig.add_trace(go.Scatter(x=fpr, y=tpr, mode="lines", name=f"ROC (AUC={roc_auc:.3f})"))
fig.add_trace(go.Scatter(x=[0,1], y=[0,1], mode="lines", name="Random", line=dict(dash="dash")))
if thr is not None and thresholds is not None:
idx = int(np.argmin(np.abs(thresholds - thr)))
fig.add_trace(go.Scatter(x=[fpr[idx]], y=[tpr[idx]], mode="markers", name=f"Threshold={thr:.2f}",
marker=dict(size=12, symbol="diamond-open")))
fig.update_layout(title=title, xaxis_title="False Positive Rate", yaxis_title="True Positive Rate",
plot_bgcolor="#fff", paper_bgcolor="#fff",
height=360, margin=dict(l=10, r=10, t=60, b=10))
return fig
def fig_pr(y_true, proba, thr=None, title="Precision–Recall (testset)"):
prec, rec, thr_pr = precision_recall_curve(y_true, proba)
fig = go.Figure()
fig.add_trace(go.Scatter(x=rec, y=prec, mode="lines", name="PR-curve"))
if thr is not None and thr_pr is not None and len(thr_pr) > 0:
idx = int(np.argmin(np.abs(thr_pr - thr)))
fig.add_trace(go.Scatter(x=[rec[idx]], y=[prec[idx]], mode="markers", name=f"Threshold={thr:.2f}",
marker=dict(size=12, symbol="diamond-open")))
fig.update_layout(title=title, xaxis_title="Recall", yaxis_title="Precision",
plot_bgcolor="#fff", paper_bgcolor="#fff",
height=360, margin=dict(l=10, r=10, t=60, b=10))
return fig
def fig_threshold_sweep(y_true, proba, title="Drempel-sweep (testset)"):
thr = np.linspace(0, 1, 101)
P, R, F1 = [], [], []
for t in thr:
y_pred = (proba >= t).astype(int)
P.append(precision_score(y_true, y_pred, zero_division=0))
R.append(recall_score(y_true, y_pred, zero_division=0))
F1.append(f1_score(y_true, y_pred))
fig = go.Figure()
fig.add_trace(go.Scatter(x=thr, y=P, mode="lines", name="Precision"))
fig.add_trace(go.Scatter(x=thr, y=R, mode="lines", name="Recall"))
fig.add_trace(go.Scatter(x=thr, y=F1, mode="lines", name="F1"))
fig.update_layout(title=title, xaxis_title="Drempel", yaxis_title="Score (0–1)",
plot_bgcolor="#fff", paper_bgcolor="#fff",
height=360, margin=dict(l=10, r=10, t=60, b=10))
return fig
def fig_prob_hist(y_true, proba, thr=None, title="Verdeling voorspelde kansen P(y=1) – testset"):
fig = go.Figure()
fig.add_trace(go.Histogram(x=proba[y_true == 0], name="Ware klasse 0", opacity=0.7, nbinsx=30))
fig.add_trace(go.Histogram(x=proba[y_true == 1], name="Ware klasse 1", opacity=0.7, nbinsx=30))
shapes = []
if thr is not None:
shapes.append(dict(type="line", x0=thr, x1=thr, y0=0, y1=1, xref="x", yref="paper",
line=dict(width=2, dash="dash")))
fig.update_layout(barmode="overlay", title=title, xaxis_title="P(y=1)", yaxis_title="Frequentie",
plot_bgcolor="#fff", paper_bgcolor="#fff",
height=360, margin=dict(l=10, r=10, t=60, b=10),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
shapes=shapes)
return fig
def testset_visuals(model_name_v, sgd_loss_v, sgd_alpha_v, sgd_lr_v, rf_n_v, rf_depth_v, svm_c_v, test_size_v, threshold_v):
df, ycol = load_builtin_dataset()
X = df.drop(columns=[ycol]).values
y = df[ycol].values
X_train, X_test, y_train, y_test, idx_tr, idx_te = _split_with_indices(X, y, float(test_size_v))
scaler = StandardScaler().fit(X_train)
X_train_s = scaler.transform(X_train)
X_test_s = scaler.transform(X_test)
base = get_model(model_name_v, dict(
sgd_loss=sgd_loss_v, sgd_alpha=float(sgd_alpha_v), sgd_lr=sgd_lr_v,
rf_n=int(rf_n_v), rf_depth=None if int(rf_depth_v) == 0 else int(rf_depth_v),
svm_c=float(svm_c_v)
))
clf = LogisticRegression(max_iter=300) if isinstance(base, SGDClassifier) else base
clf.fit(X_train_s, y_train)
if hasattr(clf, "predict_proba"):
proba = clf.predict_proba(X_test_s)[:, 1]
elif hasattr(clf, "decision_function"):
dec = clf.decision_function(X_test_s)
proba = (dec - dec.min()) / (dec.max() - dec.min() + 1e-9)
else:
proba = clf.predict(X_test_s).astype(float)
thr = float(threshold_v)
y_pred_thr = (proba >= thr).astype(int)
cm_fig = fig_confusion_matrix(y_test, y_pred_thr)
roc_fig = fig_roc(y_test, proba, thr=thr)
pr_fig = fig_pr(y_test, proba, thr=thr)
sweep_fig = fig_threshold_sweep(y_test, proba)
hist_fig = fig_prob_hist(y_test, proba, thr=thr)
return cm_fig, roc_fig, pr_fig, sweep_fig, hist_fig
# =========================
# Één rij & Batch & Segmenten — helpers
# =========================
def _fmt_pct(x, max_val):
try:
return f"{(float(x) / float(max_val) * 100):.1f}%"
except Exception:
return "—"
def _gender_to_text(g):
return {0: "Vrouw", 1: "Man"}.get(int(g), "Onbekend")
def _bool01_to_text(b):
return {0: "Nee", 1: "Ja"}.get(int(b), "Onbekend")
# Norm, delta, oordeel
def _norm_spec(key):
if key in {"sleep_quality", "energy", "social_support", "activity"}:
return 7.0, 10.0, +1
if key in {"anhedonia", "stress"}:
return 3.0, 10.0, -1
if key == "phq9":
return 4.0, 27.0, -1
return None, None, None
def _format_delta(delta):
if delta is None or pd.isna(delta):
return "—"
if abs(delta) < 0.05:
return f"<span style='color:#6b7280'>{delta:+.2f}</span>"
if delta >= 0:
return f"<span style='color:#16a34a'>{delta:+.2f}</span>"
return f"<span style='color:#dc2626'>{delta:+.2f}</span>"
def _deviation_vs_norm(key, val):
norm, _, direction = _norm_spec(key)
try:
x = float(val)
except Exception:
x = None
if norm is None or x is None:
return "—"
raw_delta = (x - norm) if direction == +1 else (norm - x)
return _format_delta(raw_delta)
def _judge_feature(key, val):
try:
x = float(val)
except Exception:
x = None
higher_better = {"sleep_quality", "energy", "social_support", "activity"}
lower_better = {"anhedonia", "stress"}
if key in higher_better and x is not None:
if x >= 7: return "Goed"
if x >= 4: return "Gemiddeld"
return "Slecht"
if key in lower_better and x is not None:
if x <= 3: return "Goed"
if x <= 6: return "Gemiddeld"
return "Slecht"
if key == "phq9" and x is not None:
pct = (x / 27.0) * 100.0
if pct <= 15: return "Goed"
if pct <= 40: return "Gemiddeld"
return "Slecht"
return "—"
def _row_markdown(record: dict, pred=None, proba=None):
labels = {
"age": "Leeftijd (jaar)",
"gender": "Geslacht",
"sleep_quality": "Slaapkwaliteit",
"energy": "Energie",
"anhedonia": "Anhedonie",
"stress": "Stress",
"social_support": "Sociale steun",
"activity": "Activiteit",
"phq9": "PHQ-9 (0–27)",
"depressed": "Depressie (waar label)"
}
rows = []
rows.append([labels["age"], f"{int(record['age'])} jaar", "—", "—", "—"])
rows.append([labels["gender"], _gender_to_text(record["gender"]), "—", "—", "—"])
for k in ["sleep_quality", "energy", "anhedonia", "stress", "social_support", "activity"]:
val = float(record[k])
rows.append([
labels[k],
f"{val:.2f} / 10",
_fmt_pct(val, 10),
_deviation_vs_norm(k, val),
_judge_feature(k, val),
])
phq = float(record["phq9"])
rows.append([
labels["phq9"],
f"{phq:.2f} / 27",
_fmt_pct(phq, 27),
_deviation_vs_norm("phq9", phq),
_judge_feature("phq9", phq),
])
dep = int(record["depressed"])
rows.append([labels["depressed"], _bool01_to_text(dep), "—", "—", "—"])
md = ["### Gekozen patiënt — overzicht (NL & %)\n",
"| Kenmerk | Waarde | Percentage | Afwijking t.o.v. norm | Oordeel |",
"|---|---:|:---:|:---:|:---:|"]
for r in rows:
md.append(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]} | {r[4]} |")
if pred is not None:
md.append("\n**Modelvoorspelling:** " + ("Positief (depressie=1)" if int(pred)==1 else "Negatief (depressie=0)"))
if proba is not None:
md.append(f"\n**Zekerheid (max. klasse-prob):** {proba:.3f} ({proba*100:.1f}%)")
return "\n".join(md)
# ---------- predict_row / predict_batch / population_segments ----------
def predict_row(model_name_v, sgd_loss_v, sgd_alpha_v, sgd_lr_v, rf_n_v, rf_depth_v, svm_c_v, row_idx):
df, ycol = load_builtin_dataset()
Xdf = df.drop(columns=[ycol])
y = df[ycol]
idx = int(row_idx)
if idx < 0 or idx >= len(df):
raise gr.Error("Ongeldige rij-index.")
scaler = StandardScaler().fit(Xdf.values)
Xs = scaler.transform(Xdf.values)
base = get_model(model_name_v, dict(
sgd_loss=sgd_loss_v, sgd_alpha=float(sgd_alpha_v), sgd_lr=sgd_lr_v,
rf_n=int(rf_n_v), rf_depth=None if int(rf_depth_v) == 0 else int(rf_depth_v),
svm_c=float(svm_c_v)
))
clf = LogisticRegression(max_iter=300) if isinstance(base, SGDClassifier) else base
clf.fit(Xs, y.values)
x_row = Xs[idx].reshape(1, -1)
pred = clf.predict(x_row)[0]
proba = clf.predict_proba(x_row)[0].max() if hasattr(clf, "predict_proba") else None
record = df.iloc[idx].to_dict()
return _row_markdown(record, pred=pred, proba=proba)
def predict_batch(model_name_v, sgd_loss_v, sgd_alpha_v, sgd_lr_v, rf_n_v, rf_depth_v, svm_c_v, scope, n_rows):
df, ycol = load_builtin_dataset()
Xdf = df.drop(columns=[ycol]); y = df[ycol]
scaler = StandardScaler().fit(Xdf.values); Xs = scaler.transform(Xdf.values)
base = get_model(model_name_v, dict(
sgd_loss=sgd_loss_v, sgd_alpha=float(sgd_alpha_v), sgd_lr=sgd_lr_v,
rf_n=int(rf_n_v), rf_depth=None if int(rf_depth_v) == 0 else int(rf_depth_v),
svm_c=float(svm_c_v)
))
clf = LogisticRegression(max_iter=300) if isinstance(base, SGDClassifier) else base
clf.fit(Xs, y.values)
if scope == "Eerste N rijen":
k = max(1, min(int(n_rows), len(df)))
Xb = Xs[:k]; batch_df = df.iloc[:k].copy()
else:
Xb = Xs; batch_df = df.copy()
preds = clf.predict(Xb)
probas = clf.predict_proba(Xb).max(axis=1) if hasattr(clf, "predict_proba") else np.full(len(preds), np.nan)
out = batch_df.copy()
out["prediction"] = preds
out["confidence"] = np.round(probas, 4)
display_out = df_to_nl_display(out, include_percent_confidence=True)
os.makedirs("/tmp", exist_ok=True)
out_path = "/tmp/batch_voorspellingen_nl.csv"
display_out.to_csv(out_path, index=False)
return display_out, out_path
def population_segments(model_name_v, sgd_loss_v, sgd_alpha_v, sgd_lr_v, rf_n_v, rf_depth_v, svm_c_v,
thr_pred=0.50, thr_more_treat=0.70, high_support_cut=6.0, high_activity_cut=5.0):
df, ycol = load_builtin_dataset()
Xdf = df.drop(columns=[ycol]); y = df[ycol].values
scaler = StandardScaler().fit(Xdf.values); Xs = scaler.transform(Xdf.values)
base = get_model(model_name_v, dict(
sgd_loss=sgd_loss_v, sgd_alpha=float(sgd_alpha_v), sgd_lr=sgd_lr_v,
rf_n=int(rf_n_v), rf_depth=None if int(rf_depth_v) == 0 else int(rf_depth_v),
svm_c=float(svm_c_v)
))
clf = LogisticRegression(max_iter=300) if isinstance(base, SGDClassifier) else base
clf.fit(Xs, y)
if hasattr(clf, "predict_proba"):
proba = clf.predict_proba(Xs)[:, 1]
elif hasattr(clf, "decision_function"):
dec = clf.decision_function(Xs)
proba = (dec - dec.min()) / (dec.max() - dec.min() + 1e-9)
else:
proba = clf.predict(Xs).astype(float)
thr_pred = float(thr_pred); thr_more_treat = float(thr_more_treat)
pred_dep = (proba >= thr_pred).astype(int)
need_more_treat = (proba >= thr_more_treat).astype(int)
high_risk_cut = np.quantile(proba, 0.80)
high_risk = (proba >= high_risk_cut).astype(int)
segs = {
"Totaal": np.ones(len(df), dtype=bool),
"Hoge sociale steun (≥ {:.1f})".format(high_support_cut): (df["social_support"] >= high_support_cut).values,
"Lage sociale steun (< {:.1f})".format(high_support_cut): (df["social_support"] < high_support_cut).values,
"Hoge activiteit (≥ {:.1f})".format(high_activity_cut): (df["activity"] >= high_activity_cut).values,
"Lage activiteit (< {:.1f})".format(high_activity_cut): (df["activity"] < high_activity_cut).values,
}
rows = []
for name, m in segs.items():
N = int(m.sum())
if N == 0:
rows.append([name, 0, 0, 0, 0, 0.0, 0.0, 0.0]); continue
pct_pred_dep = 100.0 * pred_dep[m].mean()
pct_more_treat = 100.0 * need_more_treat[m].mean()
pct_high_risk = 100.0 * high_risk[m].mean()
prevalence = 100.0 * y[m].mean()
rows.append([
name, N, prevalence, pct_pred_dep, pct_more_treat,
pct_high_risk, float(proba[m].mean()), float(np.median(proba[m]))
])
out = pd.DataFrame(rows, columns=[
"Segment", "N", "Ware prevalentie (%)",
f"Voorspeld depressed ≥ {thr_pred:.2f} (%)",
f"Meer behandeling nodig ≥ {thr_more_treat:.2f} (%)",
"Hoog-risico (top 20%) (%)",
"Gem. voorspelde kans", "Mediaan voorspelde kans"
])
fig_bar = go.Figure(go.Bar(
x=out["Segment"],
y=out[f"Voorspeld depressed ≥ {thr_pred:.2f} (%)"],
text=[f"{v:.1f}%" for v in out[f"Voorspeld depressed ≥ {thr_pred:.2f} (%)"]],
textposition="outside"
))
fig_bar.update_layout(
title="Percentage voorspeld depressed per segment",
yaxis_title="%", xaxis_tickangle=20,
template=None, plot_bgcolor="#fff", paper_bgcolor="#fff",
height=420, margin=dict(l=10, r=10, t=60, b=120)
)
fig_stack = go.Figure()
fig_stack.add_trace(go.Bar(
x=out["Segment"], y=out[f"Meer behandeling nodig ≥ {thr_more_treat:.2f} (%)"],
name="Meer behandeling nodig (%)"
))
fig_stack.add_trace(go.Bar(
x=out["Segment"], y=out["Hoog-risico (top 20%) (%)"],
name="Hoog-risico (proxy) (%)"
))
fig_stack.update_layout(
barmode="group",
title="Andere indicatoren per segment",
yaxis_title="%", xaxis_tickangle=20,
template=None, plot_bgcolor="#fff", paper_bgcolor="#fff",
height=420, margin=dict(l=10, r=10, t=60, b=120)
)
# Uitleg in twee kolommen (HTML)
md_html = """
<style>
.seg-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
width: 100%;
}
@media (max-width: 900px) { .seg-grid { grid-template-columns: 1fr; } }
.seg-col h3 { margin: 0 0 8px 0; }
.seg-col p { margin: 0 0 12px 0; }
.seg-col ul { margin: 0 0 12px 18px; }
.seg-note {
margin-top: 8px; padding: 8px 12px;
border-left: 4px solid #e5e7eb; background: #f9fafb;
}
</style>
<div class="seg-grid">
<div class="seg-col">
<h3>Wat zien we hier</h3>
<p>In dit onderdeel splitsen we de (synthetische) populatie in logische groepen en vergelijken we hun modeluitkomsten. Zo zie je snel <strong>waar</strong> de voorspelde risico’s hoger of lager liggen en <strong>hoeveel</strong> mensen dat betreft.</p>
<h3>Hoe het werkt</h3>
<ul>
<li><strong>Segmenten</strong>: we verdelen de populatie o.a. op <strong>sociale steun</strong> (hoog/laag) en <strong>activiteit</strong> (hoog/laag). Je kunt de grenzen zelf instellen met de schuifjes.</li>
<li><strong>Drempels</strong>:
<ul>
<li><em>Voorspeld depressie</em>: aandeel personen met P(y=1) ≥ ingestelde drempel.</li>
<li><em>Meer behandeling nodig</em>: strengere drempel (hogere P(y=1)) als proxy voor zwaardere zorgbehoefte.</li>
<li><em>Hoog-risico</em>: top 20% hoogste kansen in de populatie (proxy-indicator).</li>
</ul>
</li>
<li><strong>Tabel</strong>: per segment zie je omvang (N), <strong>ware prevalentie</strong> (labels) en de drie indicatoren, plus gemiddelde/mediaan van de voorspelde kans.</li>
<li><strong>Grafieken</strong>: percentage voorspeld depressie per segment, en daarnaast “meer behandeling nodig” en “hoog-risico”.</li>
</ul>
</div>
<div class="seg-col">
<h3>Zo lees je dit</h3>
<ul>
<li>Kijk eerst naar <strong>N</strong>: zijn de segmenten voldoende groot voor zinnige uitspraken?</li>
<li>Vergelijk <strong>ware prevalentie</strong> met <strong>voorspeld depressie</strong>: onderschat of overschat het model binnen een segment?</li>
<li>Let op <strong>verschillen tussen segmenten</strong>: waar is het aandeel hoog (signaal) of laag (kans op minder intensieve interventie)?</li>
<li>Speel met de <strong>drempels</strong> om te zien hoe strenger/soepeler instellen de percentages beïnvloedt (trade-off precisie vs. recall).</li>
<li>Gebruik <strong>gemiddelde/mediaan kans</strong> als context: brede, hoge kansen kunnen op risico-concentraties wijzen.</li>
</ul>
<div class="seg-note">
<strong>Let op:</strong> dit zijn didactische analyses op synthetische data met proxy-indicatoren. Niet bedoeld voor klinische besluitvorming.
</div>
</div>
</div>
"""
return md_html, out, fig_bar, fig_stack
# =========================
# What-if panel
# =========================
def whatif_update(model_name_v, sgd_loss_v, sgd_alpha_v, sgd_lr_v, rf_n_v, rf_depth_v, svm_c_v,
age_v, gender_v, sleep_v, energy_v, anhedonia_v, stress_v, support_v, activity_v, phq9_v):
df, ycol = load_builtin_dataset()
Xdf = df.drop(columns=[ycol]); y = df[ycol].values
scaler = StandardScaler().fit(Xdf.values)
Xs = scaler.transform(Xdf.values)
base = get_model(model_name_v, dict(
sgd_loss=sgd_loss_v, sgd_alpha=float(sgd_alpha_v), sgd_lr=sgd_lr_v,
rf_n=int(rf_n_v), rf_depth=None if int(rf_depth_v) == 0 else int(rf_depth_v),
svm_c=float(svm_c_v)
))
clf = LogisticRegression(max_iter=300) if isinstance(base, SGDClassifier) else base
clf.fit(Xs, y)
x_new = np.array([[age_v, gender_v, sleep_v, energy_v, anhedonia_v, stress_v, support_v, activity_v, phq9_v]], dtype=float)
x_new_s = scaler.transform(x_new)
if hasattr(clf, "predict_proba"):
p1 = float(clf.predict_proba(x_new_s)[0,1])
elif hasattr(clf, "decision_function"):
dec = clf.decision_function(x_new_s)
p1 = float((dec - dec.min()) / (dec.max() - dec.min() + 1e-9))
else:
p1 = float(clf.predict(x_new_s))
pred = int(p1 >= 0.5)
gauge = go.Figure(go.Indicator(
mode="gauge+number",
value=p1*100.0,
number={'suffix': '%'},
gauge={'axis': {'range': [0, 100]},
'bar': {'thickness': 0.3},
'steps': [
{'range': [0, 25], 'color': '#e5e7eb'},
{'range': [25, 50], 'color': '#d1d5db'},
{'range': [50, 75], 'color': '#fecaca'},
{'range': [75, 100], 'color': '#fca5a5'},
]},
title={'text': "P(y=1) kansmeter"}
))
gauge.update_layout(height=240, margin=dict(l=10, r=10, t=40, b=10), paper_bgcolor="#fff")
badge = f"**Voorspelling:** {'Depressie (1)' if pred==1 else 'Geen depressie (0)'} — **{p1*100:.1f}%**"
pca = PCA(n_components=2, random_state=42).fit(Xs)
coords = pca.transform(Xs)
base_fig = make_base_fig(coords, y, title="PCA – populatie + jouw punt")
coord_new = pca.transform(x_new_s)[0]
base_fig.add_trace(go.Scatter(
x=[coord_new[0]], y=[coord_new[1]],
mode="markers", name="What-if punt",
marker=dict(size=16, symbol="star", line=dict(width=2, color="#111")),
hovertemplate="PC1: %{x:.2f}<br>PC2: %{y:.2f}<extra>What-if</extra>"
))
return gauge, badge, base_fig
# =========================
# Threshold theater
# =========================
def threshold_theater_update(model_name_v, sgd_loss_v, sgd_alpha_v, sgd_lr_v, rf_n_v, rf_depth_v, svm_c_v, test_size_v, thr_v):
df, ycol = load_builtin_dataset()
X = df.drop(columns=[ycol]).values
y = df[ycol].values
X_train, X_test, y_train, y_test, _, _ = _split_with_indices(X, y, float(test_size_v))
scaler = StandardScaler().fit(X_train)
X_train_s = scaler.transform(X_train)
X_test_s = scaler.transform(X_test)
base = get_model(model_name_v, dict(
sgd_loss=sgd_loss_v, sgd_alpha=float(sgd_alpha_v), sgd_lr=sgd_lr_v,
rf_n=int(rf_n_v), rf_depth=None if int(rf_depth_v) == 0 else int(rf_depth_v),
svm_c=float(svm_c_v)
))
clf = LogisticRegression(max_iter=300) if isinstance(base, SGDClassifier) else base
clf.fit(X_train_s, y_train)
if hasattr(clf, "predict_proba"):
proba = clf.predict_proba(X_test_s)[:, 1]
elif hasattr(clf, "decision_function"):
dec = clf.decision_function(X_test_s)
proba = (dec - dec.min()) / (dec.max() - dec.min() + 1e-9)
else:
proba = clf.predict(X_test_s).astype(float)
thr = float(thr_v)
y_pred = (proba >= thr).astype(int)
cm = fig_confusion_matrix(y_test, y_pred, title=f"Confusion matrix @ thr={thr:.2f}")
roc = fig_roc(y_test, proba, thr=thr, title="ROC met drempel-marker")
prc = fig_pr(y_test, proba, thr=thr, title="PR met drempel-marker")
hist = fig_prob_hist(y_test, proba, thr=thr, title="Histogram P(y=1) met drempel")
acc = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred)
prec = precision_score(y_test, y_pred, zero_division=0)
rec = recall_score(y_test, y_pred, zero_division=0)
try:
auc_val = roc_auc_score(y_test, proba)
except Exception:
auc_val = np.nan
md = (
f"**Metrics @ drempel {thr:.2f}** \n"
f"- Accuracy: **{acc:.3f}** \n"
f"- Precision: **{prec:.3f}** \n"
f"- Recall: **{rec:.3f}** \n"
f"- F1: **{f1:.3f}** \n"
f"- ROC-AUC (threshold-onafhankelijk): **{auc_val:.3f}**"
)
return cm, roc, prc, hist, md
# =========================
# Train & Stream
# =========================
def _split_with_indices(X, y, test_size):
indices = np.arange(len(X))
idx_tr, idx_te = train_test_split(indices, test_size=test_size, random_state=42, stratify=y)
return X[idx_tr], X[idx_te], y[idx_tr], y[idx_te], idx_tr, idx_te
def _init_plot():
df, ycol = load_builtin_dataset()
X = df.drop(columns=[ycol]).values
y = df[ycol].values
Xs = StandardScaler().fit_transform(X)
coords = PCA(n_components=2, random_state=42).fit_transform(Xs)
fig = make_base_fig(coords, y, title="Init – wacht op training…")
return fig
def train_and_stream(test_size, model_name, params, epochs, pause_s, visual_mode):
df_all, ycol = load_builtin_dataset()
X = df_all.drop(columns=[ycol]).values
y = df_all[ycol].values
ensure_min_classes(y)
X_train, X_test, y_train, y_test, idx_tr, idx_te = _split_with_indices(X, y, float(test_size))
train_mask = np.zeros(len(X), dtype=bool); train_mask[idx_tr] = True
test_mask = np.zeros(len(X), dtype=bool); test_mask[idx_te] = True
scaler = StandardScaler().fit(X_train)
X_train_s = scaler.transform(X_train)
X_test_s = scaler.transform(X_test)
pca = PCA(n_components=2, random_state=42).fit(X_train_s)
coords_train = pca.transform(X_train_s)
coords_test = pca.transform(X_test_s)
clf = get_model(model_name, params)
if model_name == "SGDClassifier (realtime)":
classes = np.unique(y_train)
for e in range(1, int(epochs) + 1):
clf.partial_fit(X_train_s, y_train, classes=classes)
y_pred = clf.predict(X_test_s)
acc = accuracy_score(y_test, y_pred)
f1 = f1_score(y_test, y_pred, average="weighted")
title = f"Epoch {e}/{epochs} • Acc {acc:.2f} • F1 {f1:.2f}"
if visual_mode == "Bewegen→Functioneren (regressie)":
fig_epoch = make_activity_regression_fig(df_all, train_mask, test_mask, epoch_title=title)
else:
scaler2d = StandardScaler().fit(coords_train)
coords_train_s = scaler2d.transform(coords_train)
clf2d = LogisticRegression(max_iter=200).fit(coords_train_s, y_train)
fig_epoch = make_base_fig(coords_train, y_train, title=title)
fig_epoch = draw_decision_boundary(fig_epoch, clf2d, scaler2d, pca, X_train_s)
fig_epoch.add_trace(go.Scatter(
x=coords_test[:, 0], y=coords_test[:, 1],
mode="markers", name="Test set",
marker=dict(size=10, symbol="circle-open", line=dict(width=2, color="#111")),
hovertemplate="PC1: %{x:.2f}<br>PC2: %{y:.2f}<extra>Test set</extra>"
))
yield fig_epoch
if pause_s and float(pause_s) > 0:
time.sleep(float(pause_s))
return
else:
clf.fit(X_train_s, y_train)
if visual_mode == "Bewegen→Functioneren (regressie)":
fig = make_activity_regression_fig(df_all, train_mask, test_mask, epoch_title=f"Model: {model_name}")
else:
fig = make_base_fig(coords_train, y_train, title=f"Model: {model_name}")
scaler2d = StandardScaler().fit(coords_train)
coords_train_s = scaler2d.transform(coords_train)
clf2d = LogisticRegression(max_iter=200).fit(coords_train_s, y_train)
fig = draw_decision_boundary(fig, clf2d, scaler2d, pca, X_train_s)
fig.add_trace(go.Scatter(
x=coords_test[:, 0], y=coords_test[:, 1],
mode="markers", name="Test set",
))
yield fig
return
# =========================
# UI – één pagina
# =========================
DESCRIPTION = """
# Machinelearning Supervised learning – Depressie - predictions (synthetisch 1000 patiënten) by Marcel Ooms 2025
"""
# ========= Uitleg voor ROC (Markdown + LaTeX) =========
ROC_INFO_MD = r"""
### ROC-curve — uitleg
Een **ROC-curve** (Receiver Operating Characteristic-curve) is een grafiek die gebruikt wordt om de prestaties van een classificatiemodel te evalueren. Het wordt vooral toegepast bij **binaire classificatie** (bijvoorbeeld: ziek vs. niet ziek, fraude vs. geen fraude).
### Wat staat er op de ROC-curve?
* **X-as (False Positive Rate, FPR)**: het aandeel negatieve gevallen dat ten onrechte als positief wordt voorspeld.
$$\mathrm{FPR} \;=\; \frac{\text{False Positives}}{\text{False Positives} + \text{True Negatives}}$$
* **Y-as (True Positive Rate, TPR)**: ook wel *gevoeligheid* of *recall*. Het aandeel positieve gevallen dat correct als positief wordt voorspeld.
$$\mathrm{TPR} \;=\; \frac{\text{True Positives}}{\text{True Positives} + \text{False Negatives}}$$
Door voor verschillende drempelwaarden (thresholds) de TPR en FPR te berekenen, krijg je een curve die laat zien hoe het model presteert bij strengere of lossere beslissingsgrenzen.
### Belangrijke inzichten
* **De diagonaal (45°-lijn)**: stelt willekeurige gokjes voor. Een model dat hierop ligt heeft geen voorspellende kracht.
* **Hoe meer de curve naar linksboven buigt, hoe beter het model**: dit betekent hoge TPR bij een lage FPR.
* **AUC (Area Under the Curve)**: de oppervlakte onder de ROC-curve. Deze waarde ligt tussen 0 en 1:
* 0,5 = willekeurig model (geen voorspellende waarde)
* 1,0 = perfect model
* Hoe hoger, hoe beter.
### Voorbeeld
* Stel je hebt een medisch testmodel:
* Een **hoge TPR** betekent dat bijna alle zieke patiënten worden herkend.
* Een **lage FPR** betekent dat gezonde mensen zelden ten onrechte als ziek worden bestempeld.
De ROC-curve helpt om te kiezen welke balans tussen TPR en FPR passend is.
"""
# ========= Uitleg voor PCA (Markdown) =========
PCA_INFO_MD = r"""
### PCA
**Principal Component Analysis (PCA)** is een slimme manier om gegevens met veel variabelen overzichtelijker te maken.
Het zoekt nieuwe “assen” die zoveel mogelijk van de verschillen in de data uitleggen.
Vaak kun je met maar **2 of 3** van die assen (de hoofdcomponenten) al een groot deel van de informatie behouden.
Zo kun je data beter tekenen en patronen sneller zien.
#### Het “What if”-punt
In deze demo kun je een extra punt verplaatsen:
- Sleep het punt rond om te zien waar het terechtkomt in de PCA-weergave.
- Zo ontdek je hoe PCA data “samenknijpt” tot minder dimensies, en toch de belangrijkste patronen behoudt.
"""
# ========= NIEUW: Uitlegtekst voor Segmentgrafiek (Markdown) =========
SEGMENT_INFO_MD = r"""
### Percentage voorspeld “depressief” per segment
Dit overzicht laat zien welk deel van de mensen binnen elk segment door het model als depressief wordt voorspeld.
Elk segment staat voor een groep met een gemeenschappelijke eigenschap (bijv. leeftijdsgroep, geslacht, regio of andere categorie).
Het percentage geeft aan hoeveel procent van de mensen in dat segment volgens het model een verhoogde kans heeft op depressie.
Door de segmenten te vergelijken zie je verschillen tussen groepen, bijvoorbeeld welke segmenten een hoger of lager risico laten zien.
> ⚠️ **Let op:** dit gaat om voorspellingen van het model, niet om een medische diagnose. De cijfers laten trends en patronen zien, geen definitieve uitspraken over individuen.
"""
# ========= NIEUW: Precision & Drempels (onder PCA) =========
PRECISION_INFO_MD = r"""
### Precision (Pr) & drempels
Goede vraag! Ik neem aan dat je met **Pr** doelt op **Precision (positieve voorspellende waarde)** en met **drempels/marker** op het verschuiven van de beslissingsdrempel (*threshold*) in een model, bijvoorbeeld bij een classificatie zoals *depressief* vs. *niet depressief*.
#### Precision en drempels
**Precision (Pr)** vertelt je: *van alle voorspellingen die het model “positief” noemt, hoeveel zijn er echt positief?*
$$
\mathrm{Precision} \;=\; \frac{\mathrm{True\ Positives}}{\mathrm{True\ Positives} + \mathrm{False\ Positives}}
$$
- Een **hoge precision** betekent weinig vals positieven (bijna alle voorspelde positieven zijn correct).
- Een **lage precision** betekent dat het model vaak onterecht “positief” roept.
#### Drempels en markers
Een model voorspelt vaak eerst een **kans** (bijv. 0,8 kans op depressief). Pas daarna leg je een **drempel (threshold)** vast:
- Als de kans **boven** de drempel ligt → het model zegt *positief*.
- Ligt de kans **eronder** → het model zegt *negatief*.
Met een **drempelmarker** (bijvoorbeeld een schuif in een grafiek) kun je verkennen:
- **Hogere drempel** → model is strenger, **minder vals positieven** → precision stijgt, maar je mist meer echte positieven (**recall daalt**).
- **Lagere drempel** → model is soepeler, je vangt meer positieven (**recall stijgt**), maar er sluipen meer vals positieven in → **precision daalt**.
#### Wat je ermee ziet
Door een drempelmarker interactief te verschuiven kun je **balans zoeken tussen precision en recall**, afhankelijk van wat belangrijker is in jouw toepassing:
- Liever **weinig vals positieven** (hoge precision)
- of liever **zoveel mogelijk echte positieven vinden** (hoge recall).
"""
with gr.Blocks(theme=gr.themes.Soft(primary_hue="orange", neutral_hue="slate")) as demo:
gr.Markdown(DESCRIPTION)
# Video + tekst naast elkaar: video links 25%, tekst rechts 75%
gr.Markdown("## Video")
with gr.Row():
with gr.Column(scale=1, min_width=260):
gr.HTML("""
<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.08);">
<iframe
src="https://www.youtube-nocookie.com/embed/wK8cc5Cp5kc"
title="YouTube video"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
style="position:absolute;top:0;left:0;width:100%;height:100%;"
></iframe>
</div>
""")
with gr.Column(scale=3, min_width=360):
# Uitleg-tekst naast de video
gr.Markdown("""### Wat zien we hier
Ontdek de kracht van AI in de GGZ – bekijk de video
In slechts vijf minuten krijg je in deze video een rondleiding door de Space. Op het eerste gezicht lijkt het misschien wat ingewikkeld, maar geloof me: dat is het absoluut niet.
**Een korte inleiding**
Stel je voor: je hebt de beschikking over gelabelde data van 1000 GGZ-patiënten. Daarmee kun je een krachtig onderdeel van kunstmatige intelligentie inzetten: machine learning.
Nog specifieker gaat het hier om supervised learning. Wat houdt dat in? Je gebruikt bestaande gegevens (data met labels, bijvoorbeeld uitslagen van een depressietest of behandeluitkomsten) om een model te trainen. Dat model leert hierdoor alles over deze specifieke cliëntengroep.
Het mooie: een AI-model kan patronen ontdekken die voor mensen niet zichtbaar zijn. Na de training kan dit model vervolgens voorspellingen doen voor een nieuwe groep patiënten. Zo wordt het een slimme assistent die professionals kan ondersteunen met inzichten en voorspellingen.
**Klaar om te kijken?**
De video start niet automatisch. Klik op Play wanneer jij er klaar voor bent. Wil je de rondleiding groter in beeld zien? Rechtsboven kun je eenvoudig naar fullscreen schakelen.
👉 Bekijk de video en ontdek hoe AI een waardevolle rol kan spelen in de GGZ.
""")
# Sectie: Model & Visualisatie (2 kolommen)
gr.Markdown("## Dashboard")
with gr.Row():
with gr.Column(scale=1, min_width=360):
gr.Markdown("### Instellingen")
model_choice = gr.Radio(
label="Model",
choices=["SGDClassifier (realtime)", "Logistic Regression", "Random Forest", "SVM (RBF)"],
value="SGDClassifier (realtime)"
)
visual_mode = gr.Radio(
label="Visualisatie",
choices=["PCA classificatie", "Bewegen→Functioneren (regressie)"],
value="PCA classificatie"
)
gr.Markdown("#### Hyperparameters")
sgd_loss = gr.Radio(["log_loss", "hinge", "modified_huber"], value="log_loss", label="SGD loss")
sgd_alpha = gr.Slider(1e-6, 1e-2, value=1e-4, step=1e-6, label="SGD alpha (L2)")
sgd_lr = gr.Radio(["optimal", "invscaling", "constant", "adaptive"], value="optimal", label="SGD learning rate")
rf_n = gr.Slider(50, 500, value=250, step=10, label="RandomForest n_estimators")
rf_depth = gr.Slider(0, 20, value=8, step=1, label="RandomForest max_depth (0 = None)")
svm_c = gr.Slider(0.1, 5.0, value=1.0, step=0.1, label="SVM C")
test_size = gr.Slider(0.1, 0.5, value=0.25, step=0.05, label="Testset proportie")
with gr.Row():
epochs = gr.Slider(1, 30, value=12, step=1, label="Epochs (alleen realtime SGD)")
pause_s = gr.Slider(0.0, 1.0, value=0.15, step=0.05, label="Pauze per epoch (s)")
btn_train = gr.Button("Train & Visualiseer", variant="primary")
with gr.Column(scale=2):
gr.Markdown("### Visualisatie")
main_plot = gr.Plot(label="Visualisatie")
gr.Markdown("""**Wat zien we hier**
Deze Space laat stap voor stap zien hoe een (didactisch) machine-learning-model werkt op een synthetische dataset van 1.000 “patiënten”. Je kunt een model kiezen, parameters aanpassen en direct zien wat dat doet met de grafieken.
- Model & instellingen – kies tussen SGD (realtime), Logistic Regression, Random Forest of SVM.
- Visualisatie –
- PCA classificatie: elke stip is een persoon; kleuren geven de klasse weer (depressie: ja/nee). Lijnen zijn beslissingsgrenzen.
- Bewegen → Functioneren: x-as = minuten bewegen per week; y-as = functioneringsscore (0–100). De lijn geeft de trend.
Let op: dit is synthetische oefendata; geen medisch advies.
- Één-rij voorspelling – compacte samenvatting met voorspelling en (indien beschikbaar) zekerheid.
- Histogram (P(y=1)) – kansverdeling van de testset: hoe vaak hoge of lage kansen?
- Dataset – preview van de ruwe features (leeftijd, energie, slaapkwaliteit, stress, sociale steun, activiteit, PHQ-9, label).
- Batch voorspellen – meerdere rijen tegelijk scoren en downloaden als CSV.
- Populatie & Segmenten – vergelijk groepen en speel met drempels en cut-offs om verschillen te zien.""")
# Downloadknop NA de bedoelde tekst in de Visualisatie-kolom
pdf_path = "Gebruikershandleiding-ML-Space.pdf"
if os.path.exists(pdf_path):
gr.DownloadButton(
label="📄 Download de gebruikershandleiding (PDF)",
value=pdf_path
)
else:
gr.Markdown("> ⚠️ Handleiding niet gevonden. Voeg **Gebruikershandleiding-ML-Space.pdf** toe aan de repo.")
# Voorspellen & Histogram
with gr.Row():
with gr.Column(scale=1, min_width=360):
gr.Markdown("## Voorspellen (één rij)")
row_index = gr.Slider(0, 999, value=0, step=1, label="Kies rij-index")
btn_predict = gr.Button("Voorspel voor gekozen rij", variant="primary")
pred_md = gr.Markdown(label="Uitkomst", show_label=False)
with gr.Column(scale=1):
gr.Markdown("## Histogram testset (P(y=1))")
btn_hist = gr.Button("Genereer histogram", variant="primary")
hist_plot = gr.Plot(label="", show_label=False)
gr.Markdown("""Wat laat het histogram zien?
Het histogram toont voor de testset de verdeling van de voorspelde kansen P(y=1). Je ziet twee overlappende balkensets:
Ware klasse 0 (geen depressie in de labels) en Ware klasse 1 (wel depressie in de labels).
Zo lees je het:
- X-as = P(y=1): 0 (laag) tot 1 (hoog).
- Y-as = frequentie: hoeveel personen vallen in die kans-bucket.
Overlap toont twijfel; weinig overlap betekent betere scheiding.
Waarom nuttig?
- Zie of voorspellingen scherp (pieken bij 0/1) of onzeker (heuvel rond 0.5) zijn.
- Begrijp drempelkeuze: hoger = vaak hogere precisie, lager = vaak hogere recall.
- Helpt bij risico-stratificatie: waar zitten hoge-kans gevallen en hoeveel zijn dat er?""")
# What-if & Threshold theater
gr.Markdown("---")
gr.Markdown("## Experimenteer: What-if & Threshold theater")
with gr.Row():
with gr.Column(scale=1, min_width=360):
gr.Markdown("""**What-if & Threshold theater – wat is het en wat heb je eraan?**
**What-if panel (links)**
- Nabootsen van één persoon met schuifjes (leeftijd, slaap, energie, stress, etc.).
- Live feedback:
- Kansmeter P(y=1)
- Voorspelling (wel/geen depressie)
- PCA-positie t.o.v. de populatie
- Handig om te zien hoe het aanpassen van eigenschappen de kans beïnvloedt.""")
with gr.Column(scale=1, min_width=360):
gr.Markdown("""**Threshold theater (rechts)**
- Met een drempel-slider kies je vanaf welke kans het model “positief” zegt.
- Je ziet live:
- Confusion matrix,
- ROC- en PR-curve met marker,
- Histogram met drempel-lijn.
- Handig om de trade-off te begrijpen: hogere drempel → minder positieven (vaak hogere precisie); lagere drempel → meer positieven (vaak hogere recall).""")
with gr.Row():
# What-if panel
with gr.Column(scale=1, min_width=360):
gr.Markdown("### What-if panel")
wi_age = gr.Slider(18, 75, value=40, step=1, label="Leeftijd")
wi_gender = gr.Radio([0,1], value=0, label="Geslacht (0=Vrouw, 1=Man)")
wi_sleep = gr.Slider(0.0, 10.0, value=6.5, step=0.1, label="Slaapkwaliteit")
wi_energy = gr.Slider(0.0, 10.0, value=6.0, step=0.1, label="Energie")
wi_anhedonia = gr.Slider(0.0, 10.0, value=3.5, step=0.1, label="Anhedonie")
wi_stress = gr.Slider(0.0, 10.0, value=4.5, step=0.1, label="Stress")
wi_support = gr.Slider(0.0, 10.0, value=6.0, step=0.1, label="Sociale steun")
wi_activity = gr.Slider(0.0, 10.0, value=5.0, step=0.1, label="Activiteit")
wi_phq9 = gr.Slider(0.0, 27.0, value=7.0, step=0.1, label="PHQ-9")
wi_btn = gr.Button("Bereken What-if", variant="primary")
# >>> Toegevoegde uitlegtekst boven de kansmeter <<<
gr.Markdown("""**Kansmeter (P(y=1))**
Deze meter visualiseert de **voorspelde kans** dat het label 1 is voor jouw ingevoerde kenmerken.
- **0–25%**: laag signaal — model verwacht meestal geen depressie.
- **25–50%**: twijfelgebied — kleine wijzigingen in input of drempel tellen.
- **50–75%**: verhoogd signaal — model neigt naar positief.
- **75–100%**: sterk signaal — model verwacht vaak positief.
De **drempel** bepaalt alleen de binaire uitspraak (positief/negatief); de kans is **continu** en geeft nuance. Gebruik dit om **trade-offs** te begrijpen en scenario’s te vergelijken.""")
wi_gauge = gr.Plot(label="Kansmeter")
wi_badge = gr.Markdown()
# >>> Uitleg boven de PCA-plot <<<
gr.Markdown(PCA_INFO_MD)
wi_pca = gr.Plot(label="PCA met What-if punt")
# >>> NIEUW: jouw tekst als Markdown onder de PCA-plot <<<
gr.Markdown(PRECISION_INFO_MD)
# Threshold theater
with gr.Column(scale=1, min_width=360):
gr.Markdown("### Threshold theater")
thr_slider = gr.Slider(0.0, 1.0, value=0.50, step=0.01, label="Drempel (thr)")
thr_btn = gr.Button("Update drempel-metrics", variant="primary")
thr_metrics = gr.Markdown()
# Uitleg bij de Confusion Matrix (toegevoegd)
gr.Markdown("""Confusion matrix (uitleg, kort):
De confusion matrix geeft een overzicht van de juiste en foutieve voorspellingen van het model. De matrix bestaat uit vier vakken:
- True Positives (TP): correcte voorspellingen van de positieve klasse.
- False Positives (FP): onterecht als positief voorspeld (vals alarm).
- True Negatives (TN): correcte voorspellingen van de negatieve klasse.
- False Negatives (FN): onterecht als negatief voorspeld (gemist geval).
Samen laat dit zien waar het model goed presteert en waar fouten ontstaan.""")
thr_cm = gr.Plot(label="Confusion matrix")
# Uitleg boven de ROC-plot
gr.Markdown(ROC_INFO_MD)
thr_roc = gr.Plot(label="ROC")
thr_pr = gr.Plot(label="Precision–Recall")
thr_hist = gr.Plot(label="Histogram P(y=1)")
# Dataset
gr.Markdown("---")
gr.Markdown("## Dataset")
with gr.Row():
ds_preview = gr.Dataframe(label="Voorbeeld van de data (eerste 10 rijen)")
with gr.Row():
btn_preview = gr.Button("Ververs dataset-preview", variant="primary")
# Batch
gr.Markdown("---")
gr.Markdown("## Batch voorspellen")
with gr.Row():
batch_scope = gr.Radio(["Eerste N rijen", "Volledige dataset"], value="Eerste N rijen", label="Bereik")
batch_n = gr.Slider(10, 1000, value=100, step=10, label="N (bij 'Eerste N rijen')")
btn_predict_batch = gr.Button("Voorspel batch", variant="primary")
with gr.Row():
batch_df = gr.Dataframe(label="Voorspellingen (voorbeeld/weergave)")
with gr.Row():
batch_file = gr.File(label="Download voorspellingen (CSV)")
# Segmenten
gr.Markdown("---")
gr.Markdown("## Populatie & Segmenten")
with gr.Row():
seg_thr_pred = gr.Slider(0.0, 1.0, value=0.50, step=0.01, label="Drempel: voorspeld depressed (prob ≥ drempel)")
seg_thr_more = gr.Slider(0.0, 1.0, value=0.70, step=0.01, label="Drempel: 'meer behandeling nodig' (prob ≥ drempel)")
seg_support_cut = gr.Slider(1.0, 10.0, value=6.0, step=0.5, label="Cut-off: hoge sociale steun (≥)")
seg_activity_cut = gr.Slider(0.0, 10.0, value=5.0, step=0.5, label="Cut-off: hoge activiteit (≥)")
btn_segments = gr.Button("Bereken segmentpercentages", variant="primary")
with gr.Row():
seg_md = gr.HTML()
with gr.Row():
seg_table = gr.Dataframe(label="Segmentoverzicht")
with gr.Row():
# >>> Uitleg boven de eerste segmentgrafiek <<<
gr.Markdown(SEGMENT_INFO_MD)
seg_chart1 = gr.Plot(label="Voorspeld depressed per segment")
seg_chart2 = gr.Plot(label="Andere indicatoren per segment")
# =========================
# Preloads (auto-laden)
# =========================
demo.load(lambda: _init_plot(), inputs=None, outputs=[main_plot])
demo.load(lambda: df_to_nl_display(load_builtin_dataset()[0].head(10)), inputs=None, outputs=[ds_preview])
def _proxy_train(test_size_v, model_name_v,
sgd_loss_v, sgd_alpha_v, sgd_lr_v, rf_n_v, rf_depth_v, svm_c_v,
epochs_v, pause_v, visual_mode_v):
params = dict(
sgd_loss=sgd_loss_v, sgd_alpha=float(sgd_alpha_v), sgd_lr=sgd_lr_v,
rf_n=int(rf_n_v), rf_depth=None if int(rf_depth_v) == 0 else int(rf_depth_v),
svm_c=float(svm_c_v),
)
for fig_epoch in train_and_stream(test_size_v, model_name_v, params, epochs_v, pause_v, visual_mode_v):
yield fig_epoch
# Hoofdvisualisatie
demo.load(
_proxy_train,
inputs=[test_size, model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, epochs, pause_s, visual_mode],
outputs=[main_plot]
)
# Histogram
demo.load(
lambda *args: testset_visuals(*args, threshold_v=float(0.50))[-1],
inputs=[model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, test_size],
outputs=[hist_plot]
)
# Een-rij voorspelling
demo.load(
predict_row,
inputs=[model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, row_index],
outputs=[pred_md]
)
# Batch voorspellen
demo.load(
predict_batch,
inputs=[model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, batch_scope, batch_n],
outputs=[batch_df, batch_file]
)
# Populatie & Segmenten
demo.load(
population_segments,
inputs=[model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c,
seg_thr_pred, seg_thr_more, seg_support_cut, seg_activity_cut],
outputs=[seg_md, seg_table, seg_chart1, seg_chart2]
)
# What-if panel auto-load
wi_inputs = [model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c,
wi_age, wi_gender, wi_sleep, wi_energy, wi_anhedonia, wi_stress, wi_support, wi_activity, wi_phq9]
demo.load(whatif_update, inputs=wi_inputs, outputs=[wi_gauge, wi_badge, wi_pca])
# Threshold theater auto-load
thr_inputs = [model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, test_size, thr_slider]
demo.load(threshold_theater_update, inputs=thr_inputs, outputs=[thr_cm, thr_roc, thr_pr, thr_hist, thr_metrics])
# =========================
# Buttons & live interactie
# =========================
btn_train.click(
_proxy_train,
inputs=[test_size, model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, epochs, pause_s, visual_mode],
outputs=[main_plot]
)
btn_preview.click(lambda: df_to_nl_display(load_builtin_dataset()[0].head(10)), inputs=None, outputs=[ds_preview])
btn_predict.click(
predict_row,
inputs=[model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, row_index],
outputs=[pred_md]
)
btn_predict_batch.click(
predict_batch,
inputs=[model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, batch_scope, batch_n],
outputs=[batch_df, batch_file]
)
btn_segments.click(
population_segments,
inputs=[model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c,
seg_thr_pred, seg_thr_more, seg_support_cut, seg_activity_cut],
outputs=[seg_md, seg_table, seg_chart1, seg_chart2]
)
btn_hist.click(
lambda *args: testset_visuals(*args, threshold_v=float(0.50))[-1],
inputs=[model_choice, sgd_loss, sgd_alpha, sgd_lr, rf_n, rf_depth, svm_c, test_size],
outputs=[hist_plot]
)
# What-if: op knop en live sliders
wi_btn.click(whatif_update, inputs=wi_inputs, outputs=[wi_gauge, wi_badge, wi_pca])
for c in [wi_age, wi_gender, wi_sleep, wi_energy, wi_anhedonia, wi_stress, wi_support, wi_activity, wi_phq9]:
c.change(whatif_update, inputs=wi_inputs, outputs=[wi_gauge, wi_badge, wi_pca])
# Threshold theater: op knop en live slider
thr_btn.click(threshold_theater_update, inputs=thr_inputs, outputs=[thr_cm, thr_roc, thr_pr, thr_hist, thr_metrics])
thr_slider.change(threshold_theater_update, inputs=thr_inputs, outputs=[thr_cm, thr_roc, thr_pr, thr_hist, thr_metrics])
if __name__ == "__main__":
demo.launch()