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}
PC2: %{y:.2f}" + f"Klasse {lbl}" )) 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}
Score: %{y:.0f}
%{customdata}", 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}
Score: %{y:.0f}
%{customdata}", 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}
{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"{delta:+.2f}" if delta >= 0: return f"{delta:+.2f}" return f"{delta:+.2f}" 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 = """

Wat zien we hier

In dit onderdeel splitsen we de (synthetische) populatie in logische groepen en vergelijken we hun modeluitkomsten. Zo zie je snel waar de voorspelde risico’s hoger of lager liggen en hoeveel mensen dat betreft.

Hoe het werkt

Zo lees je dit

Let op: dit zijn didactische analyses op synthetische data met proxy-indicatoren. Niet bedoeld voor klinische besluitvorming.
""" 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}
PC2: %{y:.2f}What-if" )) 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}
PC2: %{y:.2f}Test set" )) 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("""
""") 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()