Spaces:
Sleeping
Sleeping
| # 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 | |
| # ====================================================== | |
| # TEKST VOOR INTRODUCTIE (UITGEBREID) | |
| # ====================================================== | |
| INTRO_MD = """ | |
| # 🛳️ Titanic Data Adventure | |
| ### Een datagedreven reis door hoop, hiërarchie en toeval | |
| **April 1912.** | |
| De RMS *Titanic* vertrekt richting New York: een drijvend paleis, gevuld met verwachtingen. | |
| Aan boord: industriëlen in avondkleding, jonge gezinnen met één koffer, bemanningsleden met routine. | |
| De zee is kalm; de toekomst lijkt maakbaar. | |
| Meer dan een eeuw later kijken wij mee — niet met verrekijkers of logboeken, maar met **data**. | |
| Elk record in deze dataset is een menselijk verhaal: iemand met een plek aan tafel, een ticket, een familie, een keuze. | |
| Door de gegevens te verkennen, begrijpen we beter **wie overleefde — en waarom**. | |
| --- | |
| ### ⚓ De nacht die geschiedenis werd | |
| Op **14 april 1912**, net voor middernacht, raakte de *Titanic* een **ijsberg** in de ijskoude Noord-Atlantische Oceaan. | |
| Binnen drie uur zonk het schip – het “onzinkbare” symbool van vooruitgang bleek kwetsbaar. | |
| Meer dan **1.500 mensen** kwamen om, slechts een derde van de opvarenden vond redding in de sloepen. | |
| De verdeling tussen rijk en arm, man en vrouw, jong en oud bleek letterlijk van levensbelang. | |
| In deze applicatie duiken we opnieuw die nacht in – niet met reddingsvesten, maar met grafieken, modellen en cijfers | |
| die het menselijk verhaal achter de ramp zichtbaar maken. | |
| """ | |
| # ====================================================== | |
| # UITLEGTEKST NAAST DE 2D-PLOT | |
| # ====================================================== | |
| EXPLAIN_MD_SIDE = """ | |
| ### 📘 Wat je ziet | |
| Bij het opstarten traint de computer een **RandomForest-model** dat leert wie op de Titanic **overleefde** – en waarom. | |
| Het kijkt naar **klasse**, **geslacht**, **leeftijd**, **familieomvang**, **ticketprijs** en **haven van vertrek**. | |
| De nauwkeurigheid (bijv. *74%*) betekent: in 74 van de 100 gevallen voorspelt het model correct. | |
| --- | |
| ### 🗺️ Landkaart van passagiers | |
| Elk **bolletje** is één persoon. Met **PCA** brengen we veel kenmerken terug naar **2 dimensies**. | |
| - **Blauw** → overleefd | |
| - **Grijs** → niet overleefd | |
| - **Vorm** → klasse | |
| Dichter bij elkaar = vergelijkbare profielen. **Hover** voor details. | |
| --- | |
| ### 💬 Jij in dit verhaal | |
| Vul onderaan je **eigen profiel** in, ontdek jouw kans en lees je scène uit die nacht. | |
| """ | |
| # ====================================================== | |
| # 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 | |
| # ====================================================== | |
| 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" | |
| if pct>=75: | |
| tone, ending = ("Je kansen zijn uitzonderlijk goed.", | |
| "Je bereikt de sloep; het schip helt achter je, maar je leeft.") | |
| elif pct>=50: | |
| tone, ending = ("Je kansen zijn behoorlijk goed.", | |
| "In de chaos vind je een plek in een halfgevulde sloep.") | |
| elif pct>=25: | |
| tone, ending = ("De kansen zijn fifty-fifty.", | |
| "Op het laatste moment spring je; de nacht is lang, maar de horizon gloeit.") | |
| else: | |
| tone, ending = ("Het ziet er somber uit.", | |
| "Je klampt je vast terwijl de oceaan meedogenloos wordt.") | |
| return f"""### 🔮 Jouw overlevingskans: **{pct:.1f}%** | |
| **Situatie:** {rol_txt}, **{klasse_txt} klasse**, inscheping **{haven_txt}** — leeftijd **{int(age)}**, familie **{int(sibsp)}+{int(parch)}** (totaal {int(sibsp)+int(parch)+1}), ticket **£{float(fare):.2f}**. | |
| **Analyse:** {tone} Het model weegt o.a. klasse, geslacht, leeftijd en familieomvang mee. | |
| **Avontuur:** De nacht is stil; fluiten, geroep, voetstappen. {ending} | |
| """ | |
| # ====================================================== | |
| # 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.") | |
| # NIEUW PANEEL: 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 | |
| ) | |
| # Kleine 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() | |