Marcel0123 commited on
Commit
41021db
·
verified ·
1 Parent(s): d08a67b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +32 -366
app.py CHANGED
@@ -1,6 +1,4 @@
1
- from pathlib import Path
2
-
3
- full_app = r'''import gradio as gr
4
  import numpy as np
5
  import pandas as pd
6
  from pathlib import Path
@@ -14,125 +12,71 @@ from sklearn.mixture import GaussianMixture
14
 
15
  import plotly.graph_objects as go
16
 
17
- # ---------- UITLEGTEKSTEN ----------
18
 
19
  EXPLAIN_MD = """
20
- Wat test ik hier?
21
 
22
- We verkennen onbegeleide (unsupervised) structuur in data via clustering en dimensiereductie.
23
- Clustering: K-Means groepeert records in k clusters zonder labels.
24
- Dimensiereductie: PCA of t-SNE projecteert hoge-dimensiedata naar 2D/3D voor visuele inspectie.
25
- Hoe meet ik of dat gelukt is?
 
 
 
 
26
 
27
- Elbow-plot (inertia): helpt een redelijke k te kiezen.
28
- Silhouette-score: meet clustercompactheid en -scheiding (hoger is beter).
29
- Projecties: 2D/3D scatter met kleur per cluster, plus centroiden.
30
  Je kunt een eigen CSV uploaden of de synthetische demo gebruiken.
31
  """
32
 
33
  PSYCHIATRIE_MD = """
34
  ### Wat kun je hiermee in de psychiatrie?
35
 
36
- Stel: bij **Parnassia Groep** heb je een CSV-bestand met geanonimiseerde gegevens van patiënten, bijvoorbeeld scores op vragenlijsten (depressie, angst, slaap, stemming), aantal behandelsessies of leefstijlfactoren.
37
-
38
- Met deze app kun je **zonder labels** (dus zonder vooraf te zeggen “dit is diagnose X”) patronen laten zoeken in de data. Dat heet *unsupervised learning*.
39
-
40
- ---
41
-
42
- ### Wat levert dat op?
43
- - **Groepjes patiënten die op elkaar lijken**
44
- Het algoritme zet mensen met vergelijkbare patronen (bijvoorbeeld “hoge angst + slaapproblemen” of “lage stemming + weinig dagstructuur”) bij elkaar in clusters.
45
-
46
- - **Nieuwe inzichten in subgroepen**
47
- Misschien ontdek je dat er **3 duidelijke groepen** bestaan die niet netjes overeenkomen met de bestaande DSM-diagnoses, maar die wel iets zeggen over welke behandeling waarschijnlijk beter past.
48
-
49
- - **Visuele projecties**
50
- Door de data terug te brengen naar 2D of 3D kun je letterlijk zien: *“hier zit een wolk van patiënten die allemaal vergelijkbare profielen hebben, en daar zit een andere groep.”*
51
-
52
- ---
53
-
54
- ### Waarom is dit waardevol?
55
- - **Voor behandelaren:** het geeft extra handvatten om te zien of iemand lijkt op een groep patiënten die goed reageerde op een bepaalde interventie.
56
- - **Voor onderzoekers:** het helpt om nieuwe subtypen van psychische problematiek te ontdekken.
57
-
58
- 👉 Dit soort analyses zijn **niet bedoeld om diagnoses te vervangen**, maar juist om behandelaren te ondersteunen met extra inzichten.
59
  """
60
 
61
  ELBOW_HELP_MD = """
62
  **Wat zie je in de elbow-plot?**
63
- De elbow-plot laat zien hoe goed de data in groepen (clusters) past bij verschillende aantallen clusters (*k*).
64
- - Hoe meer clusters, hoe beter de data wordt opgesplitst.
65
- - Maar na een bepaald punt levert extra clusters bijna geen winst meer op.
66
-
67
- 👉 Het knikpunt in de grafiek — de “elleboog” — is vaak een goede keuze voor het aantal clusters.
68
  """
69
 
70
  PROJ_HELP_MD = """
71
- **Wat zie je in de 3D t-SNE plot?**
72
- Deze grafiek laat de data teruggebracht zien naar 3 dimensies met **t-SNE**.
73
  - Elk bolletje = één patiënt.
74
- - De kleur geeft aan in welk cluster de patiënt zit.
75
- - Patiënten die dicht bij elkaar liggen, hebben vergelijkbare kenmerken (bijv. hoge angst + slaapproblemen).
76
- - Beweeg met de muis over een bolletje om de gegevens van die patiënt te zien (zoals patiëntnummer en scores).
77
-
78
- Het is dus een **visuele kaart** van de data: groepjes bolletjes vormen de clusters die het algoritme heeft gevonden.
79
  """
80
 
81
  SETTINGS_HELP_MD = """
82
- **Wat betekenen deze instellingen?**
83
- - **Aantal clusters (k):** hoeveel groepen het algoritme maakt. Je kunt dit zelf kiezen of automatisch laten bepalen.
84
- - **Max k voor elbow:** tot welk aantal clusters de elbow-plot getest wordt.
85
- - **Standaardiseren:** zet alle variabelen op dezelfde schaal (aanraden!).
86
- - **Projectiemethode:** hoe de data naar 2D/3D wordt teruggebracht (PCA = sneller, t-SNE = vaak duidelijkere groepjes).
87
- - **Dimensies voor projectie:** of je de data in 2D of 3D wilt zien.
88
- - **Random seed:** bepaalt de “willekeur”. Zelfde seed = zelfde resultaat (handig om te herhalen).
89
- - **Auto k:** laat het algoritme automatisch het beste aantal clusters kiezen (via silhouette/BIC).
90
  """
91
 
92
  CENTERS_HELP_MD = """
93
  **Wat zijn clustercentra?**
94
  Elke cluster heeft een soort “gemiddelde patiënt” — dit noemen we het **clustercentrum**.
95
  - Voor elke gekozen eigenschap (bijv. depressie, angst, slaapduur) berekent het **algoritme** het gemiddelde van alle patiënten in dat cluster.
96
- - Dat gemiddelde is het **centrum van de groep**.
97
-
98
- Zo kun je zien wat typisch is voor een cluster. Bijvoorbeeld:
99
- - In **cluster 1** ligt het centrum bij “hogere depressiescores en lagere energie”.
100
- - In **cluster 2** ligt het centrum bij “lagere depressiescores en betere kwaliteit van leven”.
101
-
102
- Met deze tabel kun je dus begrijpen **wat de groepen van elkaar onderscheidt**.
103
  """
104
 
105
  CONCLUSIONS_MD = """
106
  **Wat levert dit nu op?**
 
107
 
108
- Met de synthetische demo-data zien we dat het algoritme **3 clusters** onderscheidt.
109
- De data is opgebouwd rond drie kunstmatige patiëntgroepen met 8 kenmerken (*slaapprobleem, depressie, angst, somatiek, kwaliteit van leven, slaapduur, stemming, energie*).
110
-
111
- De clusters verschillen ongeveer zo:
112
- - **Cluster 1:** lage scores op bijna alle klachten → een groep met **milde problematiek**.
113
- - **Cluster 2:** hogere scores op **slaapproblemen en depressie** → een groep die meer last heeft van **slaap en stemming**.
114
- - **Cluster 3:** hogere scores op **angst en somatiek**, gecombineerd met **lagere energie** → een groep met meer **lichamelijke klachten en angst**.
115
-
116
- 👉 Dit laat zien dat de methode automatisch **verschillende typen patiënten** kan onderscheiden, ook al was er geen label of diagnose meegegeven.
117
-
118
- ---
119
-
120
- **Waarom is dit waardevol voor Parnassia?**
121
- De demo is natuurlijk beperkt en synthetisch, dus we trekken hier **geen medische conclusies**. Maar stel je voor dat we dit doen met echte patiëntdata bij Parnassia, waarin veel meer kenmerken zitten: vragenlijsten, behandelgeschiedenis, leefstijl, medicatie, etc.
122
-
123
- Dan kan het algoritme helpen om:
124
- - **Nieuwe subgroepen te ontdekken** die niet netjes in DSM-diagnoses passen, maar wel klinisch herkenbaar en relevant zijn.
125
- - **Behandelaren extra handvatten te geven**, bijvoorbeeld: patiënten die sterk op elkaar lijken en goed reageerden op een bepaalde behandeling.
126
- - **Onderzoek te ondersteunen**: welke factoren hangen samen, welke profielen zie je steeds terug?
127
-
128
- Kortom: de synthetische data laat zien dát het werkt. Met echte datasets wordt het pas echt krachtig en waardevol voor zorg en behandeling.
129
  """
130
 
131
  DEFAULT_CSV = "demo_unsupervised_synthetic.csv"
132
- NUMERIC_HINT = "Tip: selecteer alleen numerieke kolommen voor clustering (categorische kolommen eerst encoderen of uitsluiten)."
133
- WHY_COLS_MD = "**Waarom kolommen kiezen?**\nMet de kolomselectie vertel je het algoritme: *“Let bij het groeperen op deze kenmerken.”* Zo kun je irrelevante kolommen weghalen of juist focussen op wat je wilt onderzoeken."
134
-
135
- # ---------- DATA HULPFUNCTIES ----------
136
 
137
  def ensure_demo_csv():
138
  p = Path(DEFAULT_CSV)
@@ -156,282 +100,4 @@ def ensure_demo_csv():
156
 
157
  def load_dataframe(file_obj, sep, decimal):
158
  if file_obj is None:
159
- path = ensure_demo_csv()
160
- df = pd.read_csv(path)
161
- source = f"Demo: {path}"
162
- else:
163
- df = pd.read_csv(file_obj.name, sep=sep, decimal=decimal)
164
- source = f"Upload: {Path(file_obj.name).name}"
165
- if "patient_id" not in df.columns:
166
- df.insert(0, "patient_id", np.arange(1, len(df) + 1))
167
- return df, source
168
-
169
- def _normalize_feature_list(feature_cols, df_columns):
170
- if isinstance(feature_cols, pd.DataFrame):
171
- values = feature_cols.iloc[:, 0].dropna().tolist()
172
- elif isinstance(feature_cols, list):
173
- if len(feature_cols) > 0 and isinstance(feature_cols[0], list):
174
- values = [row[0] for row in feature_cols if row and row[0] is not None]
175
- else:
176
- values = feature_cols
177
- else:
178
- values = []
179
- values = [str(v).strip() for v in values if str(v).strip() != ""]
180
- values = [c for c in values if c in list(df_columns)]
181
- return values
182
-
183
- def _make_hover_text(df, labels, features):
184
- rows = []
185
- for i in range(len(df)):
186
- parts = [f"Patiënt #{int(df.loc[i, 'patient_id'])} — cluster {int(labels[i])}"]
187
- for col in features:
188
- val = df.loc[i, col]
189
- try:
190
- parts.append(f"{col}: {float(val):.2f}")
191
- except Exception:
192
- parts.append(f"{col}: {val}")
193
- rows.append("<br>".join(parts))
194
- return rows
195
-
196
- # ---------- MODEL / METRIEKEN ----------
197
-
198
- def compute_kmeans(df, features, k, scale, seed):
199
- X = df[features].copy()
200
- if scale:
201
- scaler = StandardScaler().fit(X)
202
- Xs = pd.DataFrame(scaler.transform(X), columns=X.columns)
203
- else:
204
- scaler = None
205
- Xs = X
206
- km = KMeans(n_clusters=k, n_init=10, random_state=seed)
207
- labels = km.fit_predict(Xs)
208
- centers = pd.DataFrame(km.cluster_centers_, columns=Xs.columns)
209
- inertia = km.inertia_
210
- sil = silhouette_score(Xs, labels) if k > 1 and len(np.unique(labels)) > 1 else float("nan")
211
- return labels, centers, inertia, sil, scaler, Xs
212
-
213
- def elbow_curve(df, features, max_k, scale, seed):
214
- X = df[features].copy()
215
- if scale:
216
- X = pd.DataFrame(StandardScaler().fit_transform(X), columns=X.columns)
217
- inertias = []
218
- ks = list(range(1, max_k + 1))
219
- for k in ks:
220
- km = KMeans(n_clusters=k, n_init=10, random_state=seed)
221
- km.fit(X)
222
- inertias.append(km.inertia_)
223
- fig = go.Figure()
224
- fig.add_trace(go.Scatter(x=ks, y=inertias, mode="lines+markers", name="inertia"))
225
- fig.update_layout(title="Elbow-plot (inertia)", xaxis_title="k", yaxis_title="inertia", height=400)
226
- return fig
227
-
228
- def suggest_k(Xs, k_min, k_max, seed):
229
- # Silhouette (k>=2)
230
- sil_scores = {}
231
- for k in range(max(2, k_min), max(k_min, k_max) + 1):
232
- try:
233
- km = KMeans(n_clusters=k, n_init=10, random_state=seed).fit(Xs)
234
- sil = silhouette_score(Xs, km.labels_)
235
- sil_scores[k] = sil
236
- except Exception:
237
- continue
238
- k_sil = max(sil_scores, key=sil_scores.get) if sil_scores else None
239
-
240
- # BIC via Gaussian Mixture (k>=1) - lager is beter
241
- bic_scores = {}
242
- for k in range(max(1, k_min), max(k_min, k_max) + 1):
243
- try:
244
- gm = GaussianMixture(n_components=k, random_state=seed).fit(Xs)
245
- bic_scores[k] = gm.bic(Xs)
246
- except Exception:
247
- continue
248
- k_bic = min(bic_scores, key=bic_scores.get) if bic_scores else None
249
-
250
- k_final = k_sil or k_bic or None
251
- return k_final, k_sil, k_bic, sil_scores, bic_scores
252
-
253
- def projection_plot(df, features, labels, centers, method, dim, seed, scaler, hover_text):
254
- X = df[features].copy()
255
- if scaler is None:
256
- Xs = StandardScaler().fit_transform(X)
257
- centers_s = None
258
- else:
259
- Xs = scaler.transform(X)
260
- centers_s = centers.values if centers is not None and len(centers) > 0 else None
261
-
262
- if method == "PCA":
263
- pca = PCA(n_components=dim, random_state=seed)
264
- Z = pca.fit_transform(Xs)
265
- if centers_s is not None:
266
- cent = pca.transform(centers_s)
267
- else:
268
- cent = None
269
- expl = getattr(pca, "explained_variance_ratio_", None)
270
- if expl is not None:
271
- expl = expl[:dim]
272
- else:
273
- perplexity = max(5, min(30, len(Xs)-1))
274
- tsne = TSNE(n_components=dim, random_state=seed, init="pca", perplexity=perplexity)
275
- Z = tsne.fit_transform(Xs)
276
- cent = None
277
- expl = None
278
-
279
- if dim == 2:
280
- fig = go.Figure()
281
- fig.add_trace(go.Scatter(
282
- x=Z[:,0], y=Z[:,1], mode="markers",
283
- marker=dict(size=6),
284
- text=hover_text,
285
- hovertemplate="%{text}<extra></extra>",
286
- showlegend=False
287
- ))
288
- if cent is not None:
289
- fig.add_trace(go.Scatter(
290
- x=cent[:,0], y=cent[:,1], mode="markers+text",
291
- marker=dict(size=12, symbol="x"),
292
- text=[f"μ{j}" for j in range(len(cent))],
293
- textposition="top center",
294
- name="centroids"
295
- ))
296
- fig.update_layout(title=f"{method} projectie (2D) — kleur=cluster", height=500)
297
- else:
298
- fig = go.Figure(data=[go.Scatter3d(
299
- x=Z[:,0], y=Z[:,1], z=Z[:,2],
300
- mode="markers",
301
- marker=dict(size=3),
302
- text=hover_text,
303
- hovertemplate="%{text}<extra></extra>",
304
- )])
305
- if cent is not None:
306
- fig.add_trace(go.Scatter3d(
307
- x=cent[:,0], y=cent[:,1], z=cent[:,2],
308
- mode="markers+text",
309
- marker=dict(size=6, symbol="x"),
310
- text=[f"μ{j}" for j in range(len(cent))],
311
- name="centroids"
312
- ))
313
- fig.update_layout(title=f"{method} projectie (3D) — kleur=cluster", height=600)
314
-
315
- fig.update_traces(marker=dict(color=labels))
316
- if expl is not None:
317
- pct = " + ".join([f"{e*100:.1f}%" for e in expl])
318
- fig.add_annotation(
319
- xref="paper", yref="paper", x=0, y=-0.15, showarrow=False,
320
- text=f"Uitlegvariantie (componenten): {pct}"
321
- )
322
- return fig
323
-
324
- # ---------- UI CALLBACK ----------
325
-
326
- def ui_run(file_obj, sep, decimal, feature_cols, k, scale, reducer, dim, max_k, seed, auto_k):
327
- df, source = load_dataframe(file_obj, sep, decimal)
328
- features = _normalize_feature_list(feature_cols, df.columns)
329
- if len(features) == 0:
330
- return (gr.update(value="Selecteer minimaal één numerieke feature (kolomnamen links invullen)."),
331
- None, None, None, None)
332
-
333
- # Voor automatische k-suggestie
334
- X_raw = df[features].copy()
335
- X_for_k = StandardScaler().fit_transform(X_raw) if scale else X_raw.values
336
-
337
- k_suggested, k_sil, k_bic, sil_scores, bic_scores = suggest_k(X_for_k, k_min=2, k_max=max_k, seed=seed)
338
- k_used = int(k_suggested) if auto_k and k_suggested is not None else int(k)
339
-
340
- labels, centers, inertia, sil, scaler, Xs = compute_kmeans(df, features, k_used, scale, seed)
341
- hover_text = _make_hover_text(df.reset_index(drop=True), labels, features)
342
- fig_elbow = elbow_curve(df, features, max_k, scale, seed)
343
- fig_proj = projection_plot(df, features, labels, centers, reducer, dim, seed, scaler, hover_text)
344
-
345
- centers = centers[features]
346
- centers.index.name = "cluster"
347
- centers.reset_index(inplace=True)
348
-
349
- md = f"**Bron:** {source}\n\n"
350
- if auto_k and k_suggested is not None:
351
- md += f"- **Auto-k** actief → gebruikte k = **{k_used}** (silhouette-suggestie={k_sil}, BIC-suggestie={k_bic})\n"
352
- else:
353
- md += f"- Handmatige k = **{k_used}**\n"
354
- if k_suggested is not None:
355
- md += f" *(Tip: automatisch voorgesteld k = {k_suggested}; silhouette={k_sil}, BIC={k_bic})*\n"
356
- md += f"- Gekozen features: `{features}`\n"
357
- md += f"- inertia = **{inertia:.2f}**\n"
358
- md += f"- silhouette = **{sil:.3f}** *(NA bij k=1 of 1 cluster)*\n"
359
-
360
- # Vriendelijk leesbare labels-tabel
361
- patient_ids = df["patient_id"].astype(int)
362
- label_nums = pd.Series(labels).astype(int)
363
- labels_df = pd.DataFrame({
364
- "patiënt": patient_ids.map(lambda x: f"Patiënt #{x}"),
365
- "clusterbeschrijving": label_nums.map(lambda y: f"Patiënt past in cluster {y}")
366
- })
367
-
368
- return md, fig_elbow, fig_proj, labels_df, centers
369
-
370
- # ---------- UI ----------
371
-
372
- with gr.Blocks(title="Unsupervised Explorer") as demo:
373
- with gr.Row():
374
- with gr.Column(scale=2):
375
- file_in = gr.File(label="CSV upload (optioneel)")
376
- with gr.Accordion("Parser-instellingen", open=False):
377
- sep = gr.Dropdown([";", ",", "\\t"], value=",", label="Scheidingsteken (sep)")
378
- decimal = gr.Dropdown([",", "."], value=".", label="Decimaalteken")
379
-
380
- gr.Markdown(NUMERIC_HINT)
381
- feature_cols = gr.Dataframe(
382
- headers=["kolom"],
383
- datatype=["str"],
384
- row_count=(0, "dynamic"),
385
- col_count=(1, "fixed"),
386
- label="Welke kolommen gebruiken? (één per rij)",
387
- value=[[f] for f in ["slaapprobleem", "depressie", "angst", "somatiek", "kwaliteit_van_leven", "slaapduur", "stemming", "energie"]]
388
- )
389
- gr.Markdown(WHY_COLS_MD)
390
-
391
- with gr.Row():
392
- k = gr.Slider(1, 12, value=6, step=1, label="Aantal clusters (k)")
393
- max_k = gr.Slider(3, 20, value=10, step=1, label="Max k voor elbow")
394
- with gr.Row():
395
- scale = gr.Checkbox(True, label="Standaardiseren (aanraden)")
396
- reducer = gr.Dropdown(["PCA", "t-SNE"], value="t-SNE", label="Projectiemethode")
397
- dim = gr.Dropdown([2, 3], value=3, label="Dimensies voor projectie")
398
- seed = gr.Slider(0, 10_000, value=42, step=1, label="Random seed")
399
-
400
- auto_k = gr.Checkbox(True, label="Auto k (silhouette/BIC) toepassen")
401
- gr.Markdown(SETTINGS_HELP_MD)
402
-
403
- run_btn = gr.Button("Run clustering & visualisaties", variant="primary")
404
- gr.Markdown(EXPLAIN_MD)
405
- gr.Markdown(PSYCHIATRIE_MD)
406
-
407
- with gr.Column(scale=3):
408
- out_md = gr.Markdown()
409
- elbow_plot = gr.Plot()
410
- gr.Markdown(ELBOW_HELP_MD)
411
- proj_plot = gr.Plot()
412
- gr.Markdown(PROJ_HELP_MD)
413
- labels_df = gr.Dataframe(label="Clusterlabels per rij (met patient_id)")
414
- centers_df = gr.Dataframe(label="Clustercentra (feature-ruimte)")
415
- gr.Markdown(CENTERS_HELP_MD)
416
- gr.Markdown(CONCLUSIONS_MD)
417
-
418
- run_btn.click(
419
- fn=ui_run,
420
- inputs=[file_in, sep, decimal, feature_cols, k, scale, reducer, dim, max_k, seed, auto_k],
421
- outputs=[out_md, elbow_plot, proj_plot, labels_df, centers_df]
422
- )
423
-
424
- # Auto-run bij laden (met demo-data)
425
- demo.load(
426
- fn=ui_run,
427
- inputs=[file_in, sep, decimal, feature_cols, k, scale, reducer, dim, max_k, seed, auto_k],
428
- outputs=[out_md, elbow_plot, proj_plot, labels_df, centers_df]
429
- )
430
-
431
- if __name__ == "__main__":
432
- demo.launch()
433
- '''
434
-
435
- p = Path("/mnt/data/app_full_parnassia.py")
436
- p.write_text(full_app)
437
- str(p)
 
1
+ import gradio as gr
 
 
2
  import numpy as np
3
  import pandas as pd
4
  from pathlib import Path
 
12
 
13
  import plotly.graph_objects as go
14
 
15
+ APP_TITLE = "Unsupervised Explorer (Parnassia)"
16
 
17
  EXPLAIN_MD = """
18
+ **Wat test ik hier?**
19
 
20
+ We verkennen **onbegeleide (unsupervised)** structuur in data via clustering en dimensiereductie.
21
+ - **Clustering:** K-Means groepeert records in *k* clusters (zonder labels).
22
+ - **Dimensiereductie:** PCA of t-SNE projecteert hoge-dimensiedata naar 2D/3D voor visuele inspectie.
23
+
24
+ **Hoe meet ik of dat gelukt is?**
25
+ - **Elbow-plot (inertia):** helpt een redelijke *k* te kiezen.
26
+ - **Silhouette-score:** meet clustercompactheid en -scheiding (hoger = beter).
27
+ - **Projecties:** 2D/3D scatter met kleur per cluster + hover-informatie.
28
 
 
 
 
29
  Je kunt een eigen CSV uploaden of de synthetische demo gebruiken.
30
  """
31
 
32
  PSYCHIATRIE_MD = """
33
  ### Wat kun je hiermee in de psychiatrie?
34
 
35
+ Met deze app kun je **zonder labels** patronen laten zoeken (*unsupervised learning*).
36
+ Dat kan helpen om **subgroepen** te zien die niet netjes in DSM-5-categorieën vallen, maar wel klinisch herkenbaar zijn.
37
+ Dit is aanvullend aan, niet ter vervanging van, klinische diagnostiek.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  """
39
 
40
  ELBOW_HELP_MD = """
41
  **Wat zie je in de elbow-plot?**
42
+ De elbow-plot laat zien hoe goed de data in groepen (clusters) past bij verschillende aantallen clusters (*k*).
43
+ Na een bepaald punt levert extra clusters nauwelijks nog winst op: **het knikpunt (de elleboog)** is vaak een goede keuze.
 
 
 
44
  """
45
 
46
  PROJ_HELP_MD = """
47
+ **Wat zie je in de t-SNE/PCA plot?**
 
48
  - Elk bolletje = één patiënt.
49
+ - De kleur geeft het cluster aan.
50
+ - Dicht bij elkaar = vergelijkbare kenmerken.
51
+ - Beweeg met de muis over een bolletje om patiëntnummer en geselecteerde kenmerken te zien.
52
+ Je kunt inzoomen en ronddraaien (bij 3D).
 
53
  """
54
 
55
  SETTINGS_HELP_MD = """
56
+ **Instellingen (kort):**
57
+ - **Aantal clusters (k):** hoeveel groepen het algoritme maakt. Handmatig of automatisch (silhouette/BIC).
58
+ - **Max k voor elbow:** bereik voor de elbow-plot.
59
+ - **Standaardiseren:** alle variabelen op dezelfde schaal (aanraden).
60
+ - **Projectiemethode:** PCA (sneller) of t-SNE (vaak duidelijkere groepjes).
61
+ - **Dimensies:** 2D of 3D weergave.
 
 
62
  """
63
 
64
  CENTERS_HELP_MD = """
65
  **Wat zijn clustercentra?**
66
  Elke cluster heeft een soort “gemiddelde patiënt” — dit noemen we het **clustercentrum**.
67
  - Voor elke gekozen eigenschap (bijv. depressie, angst, slaapduur) berekent het **algoritme** het gemiddelde van alle patiënten in dat cluster.
68
+ - Dat gemiddelde is het **centrum van de groep**.
 
 
 
 
 
 
69
  """
70
 
71
  CONCLUSIONS_MD = """
72
  **Wat levert dit nu op?**
73
+ Met de demo-data zie je dat het algoritme **clusters** onderscheidt. Dit illustreert dat de methode automatisch **verschillende typen patiënten** kan onderscheiden, ook zonder labels.
74
 
75
+ **Waarom waardevol voor Parnassia?**
76
+ Met echte patiëntdata (meer kenmerken, behandelgeschiedenis) kan dit helpen om **subgroepen** te ontdekken, behandelkeuzes te ondersteunen en patronen zichtbaar te maken die je anders mist.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  """
78
 
79
  DEFAULT_CSV = "demo_unsupervised_synthetic.csv"
 
 
 
 
80
 
81
  def ensure_demo_csv():
82
  p = Path(DEFAULT_CSV)
 
100
 
101
  def load_dataframe(file_obj, sep, decimal):
102
  if file_obj is None:
103
+ path