File size: 21,788 Bytes
a1a3e12
b9ea7ea
e95cc8d
 
b9ea7ea
 
f4bae97
f64a84a
b9ea7ea
e95cc8d
b9ea7ea
 
 
e95cc8d
b9ea7ea
 
 
 
 
cbd3079
b9ea7ea
 
 
 
 
 
cbd3079
 
 
b9ea7ea
 
 
 
 
cbd3079
 
b9ea7ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1a3e12
b9ea7ea
c7ce90c
1ae8e1f
a1a3e12
 
 
 
 
1ae8e1f
2366b13
 
 
 
 
 
 
 
 
 
 
f9c2e0a
c7ce90c
 
41763e1
f9c2e0a
1ae8e1f
 
2366b13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9c2e0a
13f7ecd
 
b9ea7ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1a3e12
1ae8e1f
a1a3e12
 
 
 
1ae8e1f
a1a3e12
 
 
 
 
1ae8e1f
 
 
 
a1a3e12
 
 
1ae8e1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1a3e12
1ae8e1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1a3e12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9ea7ea
 
 
 
 
 
 
 
4d75dac
a1a3e12
 
 
 
 
e95cc8d
 
b9ea7ea
a1a3e12
b9ea7ea
 
a1a3e12
b9ea7ea
 
a1a3e12
f9c2e0a
a1a3e12
f9c2e0a
b9ea7ea
a1a3e12
 
 
 
 
 
 
 
 
 
 
 
cbd3079
13c8514
a1a3e12
b9ea7ea
 
 
 
 
 
 
a1a3e12
aa653de
a1a3e12
f4bae97
a1a3e12
 
 
 
b9ea7ea
a1a3e12
b9ea7ea
2b5f33e
b9ea7ea
 
aa653de
b9ea7ea
 
a1a3e12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9ea7ea
 
f64a84a
a1a3e12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9ea7ea
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# app.py — Titanic Data Adventure (met uitgebreide introductie naast foto)

import gradio as gr
import pandas as pd
import numpy as np
import os
import plotly.express as px
import plotly.graph_objects as go  # voor de gauge

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.decomposition import PCA

# ======================================================
#  DATA LADEN
# ======================================================
REQUIRED = {"survived","pclass","sex","age","sibsp","parch","fare","embarked"}

def load_data(path="Titanic-Dataset.csv"):
    if not os.path.exists(path):
        raise FileNotFoundError("❌ Titanic-Dataset.csv niet gevonden in de rootmap.")
    df = pd.read_csv(path)
    df.columns = [c.lower().strip() for c in df.columns]
    missing = REQUIRED - set(df.columns)
    if missing:
        raise ValueError(f"Ontbrekende kolommen: {', '.join(sorted(missing))}")
    for c in df.columns:
        if df[c].isna().any():
            df[c] = df[c].fillna(df[c].mode()[0] if df[c].dtype=='O' else df[c].median())
    df["family_size"] = df["sibsp"] + df["parch"] + 1
    df["status"] = df["survived"].map({0:"Niet overleefd", 1:"Overleefd"})
    df["sex"] = df["sex"].astype(str).str.title()
    df["embarked"] = df["embarked"].astype(str).str.upper()
    return df

df = load_data()
MODEL = None
MODEL_ACC = None

# ======================================================
#  HULPFUNCTIES
# ======================================================
def hero_path():
    for n in ["titanic_bg.png","titanic_bg.jpg","titanic_bg.jpeg"]:
        if os.path.exists(n):
            return n
    return None

def make_plot(fig, title):
    fig.update_layout(
        title=title,
        paper_bgcolor="rgba(255,255,255,0)",
        plot_bgcolor="rgba(255,255,255,0)",
        font=dict(color="#0B1C3F"),
        title_font=dict(size=18, color="#1B4B91"),
        margin=dict(l=40, r=40, t=50, b=40),
        legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
    )
    return fig

# ======================================================
#  MODELTRAINING + 2D VISUALISATIE
# ======================================================
def train_and_embed_solid():
    global MODEL, MODEL_ACC
    features = ["pclass","sex","age","sibsp","parch","fare","embarked","family_size"]
    X = df[features].copy()
    y = df["survived"].astype(int)

    cat_cols = ["sex","embarked"]
    num_cols = [c for c in features if c not in cat_cols]

    pre = ColumnTransformer([
        ("num", StandardScaler(), num_cols),
        ("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols),
    ])

    pipe = Pipeline([
        ("prep", pre),
        ("clf", RandomForestClassifier(n_estimators=300, random_state=42))
    ])

    Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
    pipe.fit(Xtr, ytr)
    MODEL = pipe
    MODEL_ACC = pipe.score(Xte, yte)

    Z = pre.fit_transform(X)
    Z = Z.toarray() if hasattr(Z, "toarray") else Z
    emb = PCA(n_components=2, random_state=42).fit_transform(Z)

    dvis = pd.DataFrame({"x": emb[:,0], "y": emb[:,1]})
    dvis["Overleving"] = df["status"].values
    dvis["Geslacht"] = df["sex"].values
    dvis["Klasse"] = df["pclass"].values
    dvis["Leeftijd"] = df["age"].values
    dvis["Fare (£)"] = df["fare"].values
    dvis["Familie"] = df["family_size"].values
    for c in ["name","ticket","cabin"]:
        if c in df.columns:
            dvis[c.capitalize()] = df[c].values

    fig = px.scatter(
        dvis, x="x", y="y",
        color="Overleving", symbol="Klasse",
        hover_data=[col for col in dvis.columns if col not in ["x","y"]],
        color_discrete_map={"Overleefd":"#1B4B91","Niet overleefd":"#A3B1C6"},
        opacity=0.8
    )
    fig.update_traces(marker=dict(symbol="circle", size=8, line=dict(width=0.6, color="white")))
    fig = make_plot(fig, "2D-projectie (PCA) — elk bolletje is een passagier")

    status = f"✅ Model getraind (RandomForest) — nauwkeurigheid: **{MODEL_ACC:.2%}**. 2D-projectie gereed; hover voor details."
    return status, fig

# ======================================================
#  TEKSTBLOKKEN — ALLEEN BLAUW, GEEN ZWART/GEEN VET
# ======================================================
INTRO_MD = """
<span style='color:#1B4B91'>
April 1912.<br>
De RMS Titanic vertrekt richting New York: een drijvend paleis, gevuld met verwachtingen.<br>
Aan boord: industriëlen in avondkleding, jonge gezinnen met één koffer, bemanningsleden met routine.<br>
De zee is kalm; de toekomst lijkt maakbaar. Meer dan een eeuw later kijken wij mee — niet met verrekijkers of logboeken, maar met data.<br>
Elk record in deze dataset is een menselijk verhaal: iemand met een plek aan tafel, een ticket, een familie, een keuze.<br>
Door de gegevens te verkennen, begrijpen we beter wie overleefde — en waarom.

Waar de onderzoekers van toen vooral vertrouwden op statistiek, logboeken en getuigenissen, gebruiken we nu machine learning — een moderne vorm van datadenken.  
Waar een mens patronen zoekt met intuïtie, zoekt een algoritme met precisie. Het kan duizenden kleine verbanden herkennen tussen factoren die wij afzonderlijk misschien niet belangrijk vinden, maar die samen het verschil kunnen maken tussen leven en dood.  

Met supervised learning laten we een computer leren van bekende uitkomsten — in dit geval: wie overleefde en wie niet — om te begrijpen hoe die uitkomsten samenhangen met kenmerken als leeftijd, klasse, geslacht, familieomvang en ticketprijs.  
Door deze verbanden te modelleren ontstaat een nieuw soort blik op het verleden: één die niet alleen bevestigt wat we al vermoedden, maar ook onverwachte patronen blootlegt die vroeger verborgen bleven.  

Soms blijken ogenschijnlijk kleine factoren, zoals de combinatie van gezinssamenstelling en vertrekhaven, een verrassend grote rol te spelen in de overlevingskansen. Andere keren bevestigt het model juist de intuïtie van historici, maar met cijfers en waarschijnlijkheden in plaats van aannames.  

Zo wordt machine learning een hulpmiddel bij het herontdekken van oude verhalen — een digitale archeologie van data.  
Het is alsof we met een nieuwe bril naar de geschiedenis kijken: een bril die niet oordeelt, maar leert, vergelijkt en verbanden zichtbaar maakt die het menselijk oog destijds niet kon zien.
</span>
"""

EXPLAIN_MD_SIDE = """
<span style='color:#1B4B91'>
Bij het opstarten traint de computer een RandomForest-model dat leert wie op de Titanic overleefde – en waarom.<br>
Het kijkt naar klasse, geslacht, leeftijd, familieomvang, ticketprijs en haven van vertrek.<br>
Bij het opstarten traint de computer een RandomForest-model dat leert wie op de Titanic overleefde – en waarom.
Een RandomForest bestaat uit een heel bos van beslisbomen: kleine modellen die elk proberen een voorspelling te doen.

Elke boom stelt zijn eigen vragen aan de data.
Was deze passagier man of vrouw?
Hoe oud was hij of zij?
In welke klasse reisde men, en hoeveel familieleden waren er mee aan boord?
Wat kostte het ticket, en vanuit welke haven vertrok de persoon?

Elke boom komt op basis van die antwoorden tot een conclusie: overleefd, of niet overleefd.
Sommige bomen zijn optimistischer, andere voorzichtiger.
Wanneer al die beslisbomen samen worden genomen, stemt het hele bos:
de meerderheid beslist. Dat maakt het model robuust 

De nauwkeurigheid, bijvoorbeeld 74%, vertelt hoeveel van die voorspellingen juist zijn.
Een score van 74% betekent dus dat het model in 74 van de 100 gevallen correct inschat of iemand overleefde.
Geen perfecte voorspelling, maar wel een indrukwekkend resultaat voor een algoritme dat niets “weet” over de ramp, behalve wat de data het vertelt.

Het bijzondere aan deze methode is dat ze meer kan zien dan een mens.
Ze ontdekt kleine samenhangen die we zelf misschien over het hoofd zouden zien:
dat jonge vrouwen in de derde klasse andere kansen hadden dan oudere mannen in de tweede,
of dat gezinnen die vanuit Cherbourg vertrokken een iets hogere overlevingskans hadden dan die uit Southampton.

Met elke berekening wordt het model een beetje wijzer 
</span>
"""

# ======================================================
#  OVERIGE GRAFIEKEN
# ======================================================
def plot_age_hist(dfx):
    f = px.histogram(dfx, x="age", color="status", nbins=30, barmode="overlay", opacity=0.75,
                     color_discrete_map={"Overleefd":"#1B4B91","Niet overleefd":"#A3B1C6"})
    return make_plot(f, "Leeftijdsverdeling per overlevingsstatus")

def plot_gender(dfx):
    f = px.pie(dfx, names="sex", color="sex",
               color_discrete_map={"Male":"#A3B1C6","Female":"#1B4B91"}, hole=0.35)
    return make_plot(f, "Verdeling geslacht (alle passagiers)")

def plot_fare_box(dfx):
    f = px.box(dfx, x="pclass", y="fare", color="status",
               color_discrete_map={"Overleefd":"#1B4B91","Niet overleefd":"#A3B1C6"})
    return make_plot(f, "Ticketprijs per klasse (met overleving)")

# ======================================================
#  INTERACTIEVE VOORSPELLING — UITGEBREIDE TEKST & OPMAAK
# ======================================================
def predict_and_story(pclass, sex, age, sibsp, parch, fare, embarked):
    if MODEL is None:
        return "⏳ Het model initialiseert nog. Probeer het zo nog eens."

    X_row = pd.DataFrame([{
        "pclass": int(pclass), "sex": sex, "age": float(age),
        "sibsp": int(sibsp), "parch": int(parch), "fare": float(fare),
        "embarked": embarked, "family_size": int(sibsp)+int(parch)+1
    }])

    prob = float(MODEL.predict_proba(X_row)[0,1])
    pct = prob * 100

    klasse_txt = {1:"eerste",2:"tweede",3:"derde"}[int(pclass)]
    haven_txt = {"C":"Cherbourg","Q":"Queenstown","S":"Southampton"}[embarked]
    rol_txt = "vrouw" if sex.lower().startswith("v") else "man"
    familie_totaal = int(sibsp) + int(parch) + 1

    if pct >= 75:
        analyse = (
            "Je kansen zijn uitzonderlijk goed.<br>"
            "Het model ziet een combinatie van factoren die sterk wijzen op overleving — "
            "je positie aan boord, je profiel, en de omstandigheden rondom jouw reis. "
            "In dit scenario weegt geluk én voorrang zwaar mee: de uitkomst is gunstig."
        )
        avontuur = (
            "De nacht is helder en koud. Het dek glanst van het ijs. "
            "In de verte klinkt geroep; een sloep wordt neergelaten. "
            "Je ademt wolkjes van spanning terwijl je dichterbij sluipt. "
            "Er is nog plek — handen trekken je aan boord. "
            "Het schip helt achter je, maar jij leeft."
        )
    elif pct >= 50:
        analyse = (
            "Je kansen zijn redelijk goed.<br>"
            "Sommige kenmerken spelen in jouw voordeel, andere niet. "
            "Het model schat dat de balans neigt naar overleven — "
            "een situatie waarin oplettendheid, toeval en tijd samen het verschil kunnen maken."
        )
        avontuur = (
            "Het dek is onrustig; stemmen, touwen, water dat tegen de reling slaat. "
            "Je aarzelt even, zoekt naar je familie. "
            "In de chaos vind je een plek in een halfgevulde sloep. "
            "Je hoort geroep achter je, maar de boot drijft al weg. "
            "Je leeft — maar de stilte die volgt is zwaarder dan het lawaai."
        )
    elif pct >= 25:
        analyse = (
            "De kansen zijn fifty-fifty.<br>"
            "Het model weegt veel factoren die elkaar in evenwicht houden — "
            "je klasse, geslacht, leeftijd, familieomvang en vertrekhaven. "
            "Niets is beslissend, alles telt mee. "
            "De uitkomst lijkt even onzeker als die nacht zelf."
        )
        avontuur = (
            "De nacht is stil; fluiten, geroep, voetstappen echoën over het dek. "
            "Je klampt je vast aan je familie terwijl het schip verder helt. "
            "Het water glinstert als glas in het maanlicht. "
            "Op het laatste moment spring je — niet wetend of het water of de lucht je zal dragen. "
            "De nacht is lang, maar aan de horizon gloeit het eerste licht."
        )
    else:
        analyse = (
            "Het ziet er somber uit.<br>"
            "De omstandigheden zijn tegen je — klasse, positie, drukte bij de sloepen. "
            "Het model herkent een profiel dat destijds zelden overleefde. "
            "Toch blijft elke voorspelling slechts een kans; hoop is geen getal, maar een verhaal."
        )
        avontuur = (
            "Het geluid van brekend staal vult de lucht. "
            "Het dek helt scherp, water stroomt langs je voeten. "
            "Je klampt je vast aan een reling, voelt de kou door je heen snijden. "
            "In de verte hoor je stemmen, dan alleen nog de zee. "
            "De oceaan is meedogenloos — maar even, voor het verdwijnen, is alles stil."
        )

    return f"""
### 🔮 Jouw overlevingskans: **{pct:.1f}%**

**Situatie:**<br>
{rol_txt}, {klasse_txt} klasse, inscheping {haven_txt}.<br>
Leeftijd: {int(age)} jaar.<br>
Familie aan boord: {int(sibsp)} broers/zussen en {int(parch)} ouders/kinderen (totaal {familie_totaal}).<br>
Ticketprijs: £{float(fare):.2f}.<br><br>

**Analyse:**<br>
{analyse}<br><br>

**Avontuur:**<br>
{avontuur}
"""

# ======================================================
#  LIVE GAUGE VOOR JOUW SCENARIO (met kleurbanden + threshold)
# ======================================================
def live_viz(pclass, sex, age, sibsp, parch, fare, embarked):
    # Retourneer een gauge die live de kans toont (0–100%) met kleurbanden
    if MODEL is None:
        return make_plot(go.Figure(), "Jouw overlevingskans (live)")
    X_row = pd.DataFrame([{
        "pclass": int(pclass), "sex": sex, "age": float(age),
        "sibsp": int(sibsp), "parch": int(parch), "fare": float(fare),
        "embarked": embarked, "family_size": int(sibsp)+int(parch)+1
    }])
    prob = float(MODEL.predict_proba(X_row)[0,1]) * 100.0

    fig = go.Figure(go.Indicator(
        mode="gauge+number",
        value=prob,
        number={"suffix": "%", "valueformat": ".1f"},
        gauge={
            "axis": {"range": [0, 100]},
            "bar": {"thickness": 0.25},
            # Kleurbanden (0–25 rood, 25–50 oranje, 50–75 geel, 75–100 groen)
            "steps": [
                {"range": [0, 25], "color": "#FDECEC"},
                {"range": [25, 50], "color": "#FFF2E0"},
                {"range": [50, 75], "color": "#FFF9D6"},
                {"range": [75, 100], "color": "#E8F6EA"},
            ],
            # Threshold-lijn op actuele waarde
            "threshold": {
                "line": {"color": "#1B4B91", "width": 4},
                "thickness": 0.9,
                "value": prob
            },
        },
        title={"text": "Jouw overlevingskans (live)"}
    ))
    return make_plot(fig, "Jouw overlevingskans (live)")

# ======================================================
#  UI + LAYOUT
# ======================================================
CUSTOM_CSS = """
body { background:#FFFFFF; color:#0B1C3F; }
.gradio-container { background:#FFFFFF; }
h1, h2, h3, h4 { color:#1B4B91; }
.panel, .intro-card { background:#F9FBFF; border:1px solid #E0E6F3; border-radius:12px; padding:16px; }
.hero-img img { border-radius:12px; border:1px solid #E0E6F3; }
.kpi { display:flex; flex-direction:column; align-items:center; justify-content:center;
      background:#FFFFFF; border:1px solid #E0E6F3; border-radius:12px; padding:14px; }
.kpi .value { font-size:1.6rem; font-weight:800; color:#1B4B91; }
.kpi .label { font-size:.9rem; color:#3F557A; }
.explain-card { background:#EAF0FF; border-radius:12px; padding:18px; border:1px solid #D5E0FA; }
"""

with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Default(primary_hue="blue")) as demo:
    # Header-intro + foto
    with gr.Row():
        with gr.Column(scale=2, min_width=420):
            gr.Markdown(INTRO_MD, elem_classes=["intro-card"])
        with gr.Column(scale=1, min_width=320):
            hp = hero_path()
            if hp: 
                gr.Image(value=hp, interactive=False, show_label=False, elem_classes=["hero-img"])
            else: 
                gr.Markdown("⚠️ **Geen afbeelding gevonden.** Plaats `titanic_bg.png` of `titanic_bg.jpg` in de root.")

    # Passagierslijst — volledige dataset, scrollbaar
    with gr.Column(elem_classes=["panel"]):
        gr.Markdown("## 👥 Passagierslijst — volledige dataset (scrollbaar)")
        gr.DataFrame(
            value=df,               # volledige dataset
            wrap=True,
            interactive=False,      # alleen-lezen
            label="Titanic-passagiers",
            max_height=320          # vaste hoogte -> scroll binnen de tabel
        )

    # Spacer om overlap te voorkomen
    gr.Markdown("")

    # Panel: status + 2D-plot links en uitleg rechts
    with gr.Column(elem_classes=["panel"]):
        gr.Markdown("## 🔧 Initialisatie & Modeltraining")
        status_md = gr.Markdown("⏳ Initialiseren…")
        with gr.Row():
            with gr.Column(scale=2, min_width=420):
                train_plot = gr.Plot(label="2D-projectie — elk bolletje is een passagier")
            with gr.Column(scale=1, min_width=320):
                gr.Markdown(EXPLAIN_MD_SIDE, elem_classes=["explain-card"])

    # KPIs
    with gr.Row():
        gr.HTML(f"<div class='kpi'><div class='value'>{len(df):,}</div><div class='label'>Totaal passagiers</div></div>")
        gr.HTML(f"<div class='kpi'><div class='value'>{int(df['survived'].sum()):,}</div><div class='label'>Overlevenden</div></div>")
        gr.HTML(f"<div class='kpi'><div class='value'>{df['survived'].mean()*100:.1f}%</div><div class='label'>% Overleefd</div></div>")
        gr.HTML(f"<div class='kpi'><div class='value'>{', '.join(map(str, sorted(df['pclass'].unique())))}</div><div class='label'>Klassen</div></div>")

    # Overige visualisaties
    gr.Markdown("## 📊 Verken de data", elem_classes=["panel"])
    with gr.Row():
        g2 = gr.Plot(label="Leeftijdsverdeling per status")
        g3 = gr.Plot(label="Geslachtsverdeling")
    with gr.Row():
        g4 = gr.Plot(label="Ticketprijs per klasse")

    # Interactieve voorspelling
    with gr.Column(elem_classes=["panel"]):
        # Tekstblok + gauge naast elkaar
        with gr.Row():
            with gr.Column(scale=2, min_width=420):
                gr.Markdown("""## 🔮 Jouw scenario — bereken je overlevingskans en lees je scène
Hier kun je ontdekken **hoe groot jouw kans op overleving** zou zijn geweest aan boord van de *Titanic* — en meteen het **verhaal van jouw nacht** lezen.
1. **Kies je profiel**  
   - **Klasse:** 1e, 2e of 3e klasse (je reiscomfort en dekpositie).  
   - **Geslacht:** man of vrouw — dit had invloed op reddingsvoorrang.  
   - **Leeftijd:** jouw leeftijd in jaren.  
   - **Broers/zussen** en **ouders/kinderen**: hoeveel familieleden reisden met je mee.  
   - **Ticketprijs (£):** hoe duur je passage was.  
   - **Vertrekhaven:** Cherbourg (C), Queenstown (Q) of Southampton (S).
2. **Klik op de knop “🎲 Bereken én vertel mijn verhaal”**  
   Het model schat jouw **overlevingskans** op basis van historische patronen.
3. **Lees je persoonlijke scène**  
   Onder de knop verschijnt een korte beschrijving die je meeneemt naar die nacht —  
   gebaseerd op jouw ingevulde profiel en de berekende kans.
> 💡 *De voorspelling is een statistische schatting, geen oordeel.  
> Ze helpt je zien hoe factoren zoals klasse, geslacht en leeftijd destijds iemands lot konden bepalen.*""")
            with gr.Column(scale=1, min_width=320):
                viz_plot = gr.Plot(label="Jouw overlevingskans (live)")

        with gr.Row():
            ui_pclass = gr.Slider(1, 3, value=2, step=1, label="Klasse (1=1e, 3=3e)")
            ui_sex = gr.Radio(["Man","Vrouw"], value="Man", label="Geslacht")
            ui_age = gr.Slider(0, 80, value=30, label="Leeftijd")
        with gr.Row():
            ui_sibsp = gr.Slider(0, 8, value=1, step=1, label="Broers/Zussen aan boord")
            ui_parch = gr.Slider(0, 6, value=0, step=1, label="Ouders/Kinder(en) aan boord")
            ui_fare = gr.Slider(0, 600, value=50, label="Ticketprijs (£)")
            ui_emb = gr.Radio(["C","Q","S"], value="S", label="Vertrekhaven")
        btn = gr.Button("🎲 Bereken én vertel mijn verhaal", variant="primary")
        story_out = gr.Markdown()

    # Loads & acties
    demo.load(fn=train_and_embed_solid, inputs=[], outputs=[status_md, train_plot])
    demo.load(lambda: (plot_age_hist(df), plot_gender(df), plot_fare_box(df)), inputs=[], outputs=[g2, g3, g4])

    # Initiele gauge (na modeltraining): gebruik de default UI-waarden
    demo.load(lambda: live_viz(2, "Man", 30, 1, 0, 50, "S"), inputs=[], outputs=[viz_plot])

    # Live updates bij elke wijziging
    for comp in [ui_pclass, ui_sex, ui_age, ui_sibsp, ui_parch, ui_fare, ui_emb]:
        comp.change(live_viz,
                    inputs=[ui_pclass, ui_sex, ui_age, ui_sibsp, ui_parch, ui_fare, ui_emb],
                    outputs=viz_plot)

    # Ook updaten bij de knop
    btn.click(predict_and_story,
              inputs=[ui_pclass, ui_sex, ui_age, ui_sibsp, ui_parch, ui_fare, ui_emb],
              outputs=story_out)
    btn.click(live_viz,
              inputs=[ui_pclass, ui_sex, ui_age, ui_sibsp, ui_parch, ui_fare, ui_emb],
              outputs=viz_plot)

demo.launch()