{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "151efa84",
   "metadata": {},
   "source": [
    "# Class 2｜鐵達尼號：訓練一個會估計生還機率的神經網路\n",
    "\n",
    "**目標**：用課堂教過的方法，把驗證 accuracy 一步一步往上推，最後預測「如果你在船上」的生還機率。\n",
    "\n",
    "**只改標著 🎛️ 的格子**。其他格子直接按 ▶ 執行。\n",
    "\n",
    "| 投影片 | Colab | 做什麼 |\n",
    "|---|---|---|\n",
    "| P66–P72、P62 | A0 | 資料處理練習：老師示範一行、換你寫一行（清洗、補值、標準化、歸一化、one-hot、sklearn） |\n",
    "| P74 實作① | A1–A3 | 看資料、切分、基準（全猜未生還）vs Logistic |\n",
    "| P87 實作② | A5 | 手算一步訓練；🎛️ 控制台做挑戰任務；🏆 自己處理資料，把損失壓到目標線以下 |\n",
    "| P100 實作③ | A4 → A7 → A6 | 門檻、最後測試（只看一次）、如果你在船上 |\n",
    "\n",
    "神經網路是用 NumPy 自己寫的（前向 → 損失 → 反向傳播 → 更新），和講義的計算圖同一套步驟；準備格可以展開看。\n",
    "上傳到 Colab 後先「另存副本」，從上往下執行。CPU 就夠，資料已內附。重跑 A2 會清空排行榜。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0945b284",
   "metadata": {},
   "outputs": [],
   "source": [
    "#@title 準備工具與內附資料（執行一次，不用改；想看怎麼寫可以展開）\n",
    "\"\"\"鐵達尼號實作工具。神經網路用 NumPy 自己寫（前向、反向、更新），和講義的計算圖同一套步驟。\n",
    "所有補值、標準化只從訓練集學；驗證集用來比較設定，測試集最後只看一次。\"\"\"\n",
    "import io, gzip, base64, copy, time, warnings\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "from IPython.display import display, HTML\n",
    "from sklearn import __version__ as sklearn_version\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.svm import SVC\n",
    "from sklearn.metrics import confusion_matrix\n",
    "warnings.filterwarnings('ignore')\n",
    "SEED = 42\n",
    "NAVY, ORANGE, GRAY, GREEN = '#073F81', '#ED802C', '#8796A5', '#2E8B57'\n",
    "plt.rcParams.update({'font.size': 12, 'axes.labelsize': 13, 'lines.linewidth': 2.4,\n",
    "                     'figure.dpi': 110, 'axes.spines.top': False, 'axes.spines.right': False})\n",
    "\n",
    "def compact_table(df, mark=None):\n",
    "    df = df.copy()\n",
    "    if mark is not None: df.insert(0, '', np.where(df['回合'] == mark, '👉', ''))\n",
    "    display(HTML('<div style=\"max-height:320px;overflow:auto\">' + df.to_html(index=False, na_rep='', float_format=lambda v: f'{v:.3g}' if abs(v) < .01 else f'{v:.3f}') + '</div>'))\n",
    "\n",
    "# ---------- 資料與特徵 ----------\n",
    "BASE = ['sex', 'pclass', 'age', 'fare', 'sibsp', 'parch', 'embarked']\n",
    "EXTRA = ['family_size', 'is_child', 'age_missing', 'birth_month']            # 用現有欄位自己造的\n",
    "MORE = ['who', 'adult_male', 'alone', 'deck', 'class', 'embark_town']       # 資料表裡原本就有、但還沒用的\n",
    "ALL_FEATURES = BASE + EXTRA + MORE\n",
    "CATEGORY = ['sex', 'pclass', 'embarked', 'who', 'deck', 'class', 'embark_town']\n",
    "\n",
    "class Stop(Exception):\n",
    "    \"\"\"停下這一格，只顯示一句話（不顯示一長串錯誤訊息）。\"\"\"\n",
    "    def _render_traceback_(self): return [str(self)]\n",
    "\n",
    "def t_derive(df):\n",
    "    \"\"\"自己造的特徵：只用現有欄位算得出來的資訊。\"\"\"\n",
    "    df = df.copy()\n",
    "    df['family_size'] = df['sibsp'] + df['parch'] + 1        # 家庭人數（含自己）\n",
    "    df['is_child'] = (df['age'] < 16).astype(int)             # 未滿 16 歲\n",
    "    df['age_missing'] = df['age'].isna().astype(int)          # 年齡是否缺值（缺值本身也是訊息）\n",
    "    if 'birth_month' not in df:                               # 資料沒有生日：用固定規則模擬，當「無關特徵」對照\n",
    "        df['birth_month'] = (np.asarray(df.index) * 7919) % 12 + 1\n",
    "    if 'who' not in df: df['who'] = np.where(df['age'] < 16, 'child', np.where(df['sex'] == 'male', 'man', 'woman'))\n",
    "    if 'adult_male' not in df: df['adult_male'] = df['who'] == 'man'\n",
    "    if 'alone' not in df: df['alone'] = df['family_size'] == 1\n",
    "    if 'class' not in df: df['class'] = df['pclass'].map({1: 'First', 2: 'Second', 3: 'Third'})\n",
    "    if 'embark_town' not in df: df['embark_town'] = df['embarked'].map({'S': 'Southampton', 'C': 'Cherbourg', 'Q': 'Queenstown'})\n",
    "    if 'deck' not in df: df['deck'] = 'missing'\n",
    "    df['adult_male'] = df['adult_male'].astype(int); df['alone'] = df['alone'].astype(int)\n",
    "    return df\n",
    "\n",
    "def t_preprocess(cols, impute='median', scale=True):\n",
    "    num = [c for c in cols if c not in CATEGORY]\n",
    "    cat = [c for c in cols if c in CATEGORY]\n",
    "    fill = SimpleImputer(strategy='constant', fill_value=0) if impute == 'zero' else SimpleImputer(strategy=impute)\n",
    "    parts = []\n",
    "    if num: parts.append(('num', Pipeline([('fill', fill)] + ([('scale', StandardScaler())] if scale else [])), num))\n",
    "    if cat: parts.append(('cat', Pipeline([('str', FunctionTransformer(lambda d: d.astype(object).where(d.notna(), 'missing').astype(str))),\n",
    "                                           ('encode', OneHotEncoder(handle_unknown='ignore', sparse_output=False))]), cat))\n",
    "    return ColumnTransformer(parts)\n",
    "\n",
    "# ---------- NumPy 神經網路 ----------\n",
    "def sigmoid(z): return 1 / (1 + np.exp(-np.clip(z, -60, 60)))\n",
    "ACT = {'relu':    (lambda a: np.maximum(0, a), lambda a, h: (a > 0).astype(float)),\n",
    "       'sigmoid': (sigmoid,                    lambda a, h: h * (1 - h)),\n",
    "       'tanh':    (np.tanh,                    lambda a, h: 1 - h ** 2),\n",
    "       'none':    (lambda a: a,                lambda a, h: np.ones_like(a))}\n",
    "\n",
    "def loss_value(kind, y, p):\n",
    "    p = np.clip(p, 1e-7, 1 - 1e-7)\n",
    "    if kind == 'BCE': return float(np.mean(-(y * np.log(p) + (1 - y) * np.log(1 - p))))\n",
    "    if kind == 'MSE': return float(np.mean((p - y) ** 2))\n",
    "    return float(np.mean(np.abs(p - y)))\n",
    "\n",
    "def loss_grad_z(kind, y, p):\n",
    "    \"\"\"損失對輸出分數 z 的梯度（講義 P44：BCE 接 sigmoid 剛好是 p − y）。\"\"\"\n",
    "    if kind == 'BCE': return p - y\n",
    "    if kind == 'MSE': return 2 * (p - y) * p * (1 - p)\n",
    "    return np.sign(p - y) * p * (1 - p)\n",
    "\n",
    "class TinyNet:\n",
    "    def __init__(self, d, hidden, activation, seed):\n",
    "        self.rng = np.random.default_rng(seed); self.activation = activation\n",
    "        sizes = [d] + list(hidden) + [1]; self.W, self.b = [], []\n",
    "        for i, (m, n) in enumerate(zip(sizes[:-1], sizes[1:])):\n",
    "            gain = 2 if (activation == 'relu' and i < len(hidden)) else 1\n",
    "            self.W.append(self.rng.normal(0, np.sqrt(gain / m), (m, n))); self.b.append(np.zeros(n))\n",
    "    def forward(self, X, dropout=0., train=False):\n",
    "        f, _ = ACT[self.activation]; cache = []; h = X\n",
    "        for W, b in zip(self.W[:-1], self.b[:-1]):\n",
    "            a = h @ W + b; act = f(a); mask = None                 # a：加權分數；act：激活後\n",
    "            if train and dropout > 0:                              # Dropout：訓練時隨機關掉隱藏神經元\n",
    "                mask = (self.rng.random(act.shape) >= dropout) / (1 - dropout)\n",
    "            cache.append((h, a, act, mask)); h = act if mask is None else act * mask\n",
    "        z = (h @ self.W[-1] + self.b[-1])[:, 0]\n",
    "        return sigmoid(z), cache + [h]\n",
    "    def backward(self, cache, dz):\n",
    "        _, df = ACT[self.activation]; g = dz[:, None] / len(dz)\n",
    "        gW = [None] * len(self.W); gb = [None] * len(self.b)\n",
    "        gW[-1] = cache[-1].T @ g; gb[-1] = g.sum(0); g = g @ self.W[-1].T\n",
    "        for i in range(len(self.W) - 2, -1, -1):                   # 沿同一張圖往回乘（鏈式法則）\n",
    "            h, a, act, mask = cache[i]\n",
    "            if mask is not None: g = g * mask\n",
    "            g = g * df(a, act)\n",
    "            gW[i] = h.T @ g; gb[i] = g.sum(0); g = g @ self.W[i].T\n",
    "        return gW, gb\n",
    "    def predict_proba(self, X):\n",
    "        p = self.forward(X)[0]; return np.c_[1 - p, p]\n",
    "\n",
    "class Optimizer:\n",
    "    def __init__(self, kind, params, beta=.9):\n",
    "        self.kind, self.beta, self.t = kind, beta, 0\n",
    "        self.v = [np.zeros_like(p) for p in params]; self.s = [np.zeros_like(p) for p in params]\n",
    "    def step(self, params, grads, lr):\n",
    "        self.t += 1\n",
    "        for i, (p, g) in enumerate(zip(params, grads)):\n",
    "            if self.kind == 'SGD':\n",
    "                p -= lr * g                                        # w ← w − η·梯度\n",
    "            elif self.kind == 'Momentum':\n",
    "                self.v[i] = self.beta * self.v[i] + g              # 速度：累積過去的方向\n",
    "                p -= lr * self.v[i]\n",
    "            else:                                                  # Adam\n",
    "                self.v[i] = .9 * self.v[i] + .1 * g; self.s[i] = .999 * self.s[i] + .001 * g ** 2\n",
    "                vh = self.v[i] / (1 - .9 ** self.t); sh = self.s[i] / (1 - .999 ** self.t)\n",
    "                p -= lr * vh / (np.sqrt(sh) + 1e-8)\n",
    "\n",
    "# ---------- 訓練一回合 ----------\n",
    "def _check(name, ok, msg):\n",
    "    if not ok: raise ValueError(f'{name}：{msg}')\n",
    "\n",
    "def t_run(model='NN', features=None, impute='median', scale=True,\n",
    "          hidden_layers=1, neurons=16, activation='relu', loss='BCE',\n",
    "          optimizer='SGD', lr=.01, momentum=.9, batch=32, epochs=20, lr_step_every=0,\n",
    "          l2=0., dropout=0., early_stop=False, patience=10,\n",
    "          svm_kernel='rbf', svm_C=1., svm_gamma='scale', seed=SEED, note='', show=True, record=True, prepare=None):\n",
    "    if prepare is not None:\n",
    "        return _run_matrix(prepare, locals())\n",
    "    cols = list(features or BASE)\n",
    "    if {'alive', 'survived'} & set(cols):\n",
    "        raise Stop('⛔ alive／survived 就是答案本身：放進來等於考試偷看答案，這叫資料洩漏（P55）。把它拿掉。')\n",
    "    bad = [c for c in cols if c not in ALL_FEATURES]\n",
    "    if bad: raise Stop(f'⛔ 沒有這個欄位：{bad}。可以用的有：{ALL_FEATURES}')\n",
    "    _check('特徵', len(cols) > 0, '至少選一個。')\n",
    "    _check('IMPUTE', impute in ('median', 'mean', 'zero'), \"選 'median'、'mean' 或 'zero'。\")\n",
    "    prep = t_preprocess(cols, impute, scale)\n",
    "    Xa = prep.fit_transform(t_train[cols]); Xb = prep.transform(t_valid[cols])   # 規則只從訓練集學\n",
    "    return _fit(Xa, Xb, prep, cols, False, locals())\n",
    "\n",
    "def _fit(Xa, Xb, prep, cols, custom, cfg):\n",
    "    g = {k: v for k, v in cfg.items() if k in _KNOBS}\n",
    "    model, impute, scale, hidden_layers, neurons, activation, loss, optimizer, lr, momentum, batch, epochs, lr_step_every, \\\n",
    "        l2, dropout, early_stop, patience, svm_kernel, svm_C, svm_gamma, seed, note, show, record = [g[k] for k in _KNOBS]\n",
    "    ya = t_train.survived.to_numpy(float); yb = t_valid.survived.to_numpy(float)\n",
    "    hist, info = [], dict(steps=0, best_epoch=None, stop_epoch=None, diverged=False)\n",
    "    t0 = time.time()\n",
    "    if model == 'NN':\n",
    "        _check('ACTIVATION', activation in ACT, \"選 'relu'、'sigmoid'、'tanh' 或 'none'。\")\n",
    "        _check('LOSS', loss in ('BCE', 'MSE', 'MAE'), \"選 'BCE'、'MSE' 或 'MAE'。\")\n",
    "        _check('OPTIMIZER', optimizer in ('SGD', 'Momentum', 'Adam'), \"選 'SGD'、'Momentum' 或 'Adam'。\")\n",
    "        _check('LR', lr > 0, '要大於 0。'); _check('EPOCHS', epochs >= 1, '至少 1。')\n",
    "        _check('BATCH', batch >= 1, '至少 1。'); _check('DROPOUT', 0 <= dropout < 1, '介於 0～0.9。')\n",
    "        hidden = [neurons] * hidden_layers\n",
    "        net = TinyNet(Xa.shape[1], hidden, activation, seed)\n",
    "        opt = Optimizer(optimizer, net.W + net.b, momentum)\n",
    "        rng = np.random.default_rng(seed); best = (np.inf, None, 0); wait = 0\n",
    "        for e in range(1, epochs + 1):\n",
    "            lr_now = lr * (.5 ** ((e - 1) // lr_step_every)) if lr_step_every else lr\n",
    "            order = rng.permutation(len(ya))\n",
    "            for s in range(0, len(ya), batch):                     # 一個 batch 更新一次＝一個 step\n",
    "                idx = order[s:s + batch]\n",
    "                p, cache = net.forward(Xa[idx], dropout, train=True)\n",
    "                gW, gb = net.backward(cache, loss_grad_z(loss, ya[idx], p))\n",
    "                gW = [g + 2 * l2 * W for g, W in zip(gW, net.W)]    # L2：損失多加 λ·Σw²，梯度多 2λw\n",
    "                opt.step(net.W + net.b, gW + gb, lr_now); info['steps'] += 1\n",
    "            pa, pb = net.predict_proba(Xa)[:, 1], net.predict_proba(Xb)[:, 1]\n",
    "            if not (np.isfinite(pa).all() and all(np.isfinite(W).all() for W in net.W)) or np.abs(net.W[-1]).max() > 1e6:\n",
    "                info['diverged'] = True; break\n",
    "            vl = loss_value(loss, yb, pb)\n",
    "            hist.append([e, loss_value(loss, ya, pa), vl, np.mean((pa >= .5) == ya), np.mean((pb >= .5) == yb), loss_value('BCE', yb, pb)])\n",
    "            if vl < best[0] - 1e-5: best = (vl, copy.deepcopy((net.W, net.b)), e); wait = 0\n",
    "            else: wait += 1\n",
    "            if early_stop and wait >= patience: break\n",
    "        info['stop_epoch'] = hist[-1][0] if hist else 0\n",
    "        if early_stop and best[1] is not None:                     # 早停：退回驗證損失最低的那一刻\n",
    "            net.W, net.b = best[1]; info['best_epoch'] = best[2]\n",
    "        clf = net\n",
    "        arch = 'Logistic（0 層）' if hidden_layers == 0 else f\"{'×'.join(map(str, hidden))} {activation}\"\n",
    "    elif model == 'SVM':\n",
    "        _check('SVM_KERNEL', svm_kernel in ('rbf', 'linear', 'poly'), \"選 'rbf'、'linear' 或 'poly'。\")\n",
    "        gamma = svm_gamma if svm_gamma == 'scale' else float(svm_gamma)\n",
    "        clf = SVC(C=svm_C, kernel=svm_kernel, gamma=gamma, probability=True, random_state=seed).fit(Xa, ya)\n",
    "        arch = f'SVM {svm_kernel} C={svm_C} γ={svm_gamma}'\n",
    "    else:\n",
    "        raise ValueError(\"MODEL 選 'NN' 或 'SVM'。\")\n",
    "    pa = clf.predict_proba(Xa)[:, 1] if not info['diverged'] else np.full(len(ya), .5)\n",
    "    pb = clf.predict_proba(Xb)[:, 1] if not info['diverged'] else np.full(len(yb), .5)\n",
    "    run = dict(id=len(t_runs), note=note, model=model, arch=arch, features=cols, custom=custom, impute=impute, scale=scale,\n",
    "               hidden_layers=hidden_layers, activation=activation, loss=loss if model == 'NN' else 'hinge',\n",
    "               optimizer=optimizer if model == 'NN' else '—', lr=lr if model == 'NN' else None,\n",
    "               momentum=momentum if optimizer == 'Momentum' else None, batch=batch if model == 'NN' else None,\n",
    "               epochs=epochs if model == 'NN' else None, lr_step_every=lr_step_every, l2=l2, dropout=dropout,\n",
    "               early_stop=early_stop, prep=prep, clf=clf, prob=pb, history=hist, seconds=time.time() - t0,\n",
    "               train_acc=float(np.mean((pa >= .5) == ya)), valid_acc=float(np.mean((pb >= .5) == yb)),\n",
    "               valid_bce=loss_value('BCE', yb, pb), after_test=t_state['locked'], **info)\n",
    "    if not record: return run\n",
    "    t_runs.append(run)\n",
    "    if show:\n",
    "        t_report(run)\n",
    "    return run['id']\n",
    "\n",
    "# ---------- 報告、診斷、排行榜 ----------\n",
    "def t_best():\n",
    "    ok = [r for r in t_runs if not r['after_test']] or t_runs\n",
    "    return max(ok, key=lambda r: (round(r['valid_acc'], 4), -r['valid_bce']))['id']\n",
    "\n",
    "def _rid(run_id):\n",
    "    if not t_runs: raise RuntimeError('還沒有任何回合，先執行控制台。')\n",
    "    return t_best() if run_id in (None, -1) else run_id\n",
    "\n",
    "def t_report(run):\n",
    "    rest = [r for r in t_runs if r['id'] != run['id']]\n",
    "    best_before = max((r['valid_acc'] for r in rest), default=None)\n",
    "    if run['history']:\n",
    "        h = np.array(run['history'])\n",
    "        fig, ax = plt.subplots(1, 2, figsize=(11, 4), layout='constrained')\n",
    "        ax[0].plot(h[:, 0], h[:, 1], color=NAVY, label='Train'); ax[0].plot(h[:, 0], h[:, 2], color=ORANGE, label='Validation')\n",
    "        if run['loss'] == 'BCE': ax[0].axhline(MEDALS[0][1], color='goldenrod', ls='--', label=f'Gold target {MEDALS[0][1]}')\n",
    "        ax[0].set(xlabel='Epoch', ylabel=f\"{run['loss']} loss\", title='Loss (lower is better)')\n",
    "        ax[1].plot(h[:, 0], h[:, 3], color=NAVY, label='Train'); ax[1].plot(h[:, 0], h[:, 4], color=ORANGE, label='Validation')\n",
    "        ax[1].axhline(BASELINE_ACC, color=GRAY, ls=':', label='Baseline (all die)')\n",
    "        if best_before is not None: ax[1].axhline(best_before, color=GREEN, ls='--', label='Best so far')\n",
    "        ax[1].axhline(MEDALS[0][2], color='goldenrod', ls='--', label=f'Gold target {MEDALS[0][2]}')\n",
    "        ax[1].set(xlabel='Epoch', ylabel='Accuracy', title='Accuracy (higher is better)', ylim=(.5, 1))\n",
    "        if run['best_epoch']:\n",
    "            for a in ax: a.axvline(run['best_epoch'], color='k', lw=1, ls='--')\n",
    "        for a in ax: a.grid(alpha=.2); a.legend(fontsize=9)\n",
    "        plt.show()\n",
    "    elif not run['diverged']:\n",
    "        fig, ax = plt.subplots(figsize=(8, 3.8), layout='constrained')\n",
    "        for lab, col in [(0, GRAY), (1, NAVY)]:\n",
    "            ax.hist(run['prob'][t_valid.survived.to_numpy() == lab], bins=np.linspace(0, 1, 11), alpha=.6, color=col, label=f'Actual {lab}')\n",
    "        ax.axvline(.5, color=ORANGE, ls='--', label='Threshold 0.5')\n",
    "        ax.set(xlabel='Estimated survival probability (validation)', ylabel='Passengers'); ax.legend(); plt.show()\n",
    "    extra = f\"｜更新次數 steps={run['steps']}\" if run['model'] == 'NN' else ''\n",
    "    print(f\"回合 {run['id']}｜{run['arch']}｜訓練 accuracy={run['train_acc']:.3f}｜驗證 accuracy={run['valid_acc']:.3f}\"\n",
    "          f\"｜驗證 BCE={run['valid_bce']:.3f}{extra}｜{run['seconds']:.1f} 秒\")\n",
    "    print(t_goal(run))\n",
    "    for tip in t_diagnose(run, best_before): print(tip)\n",
    "    t_board(top=5, mark=run['id'])\n",
    "\n",
    "# 目標：驗證損失（BCE）要低於、驗證正確率要高於（兩個都達到才拿牌）\n",
    "MEDALS = [('🥇', .38, .84), ('🥈', .40, .83), ('🥉', .42, .80)]\n",
    "def conf(loss):\n",
    "    \"\"\"平均把握 = e^(−損失)：模型給「正確答案」的機率，取幾何平均。\"\"\"\n",
    "    return float(np.exp(-loss))\n",
    "def t_medal(bce, acc):\n",
    "    return next((m for m, b, a in MEDALS if bce <= b and acc >= a), '')\n",
    "\n",
    "def t_goal(run):\n",
    "    bce, acc = run['valid_bce'], run['valid_acc']; got = t_medal(bce, acc)\n",
    "    head = f'🎯 目標看板：驗證損失 {bce:.3f}（平均把握 {conf(bce):.1%}）｜驗證正確率 {acc:.1%}｜' + (f'拿到 {got}' if got else '還沒拿牌')\n",
    "    n_ok = int(round(acc * len(t_valid)))\n",
    "    head += f'\\n   算式：L = (1/N) Σ −ln qᵢ = {bce:.3f}　平均把握 = e^(−L) = e^(−{bce:.3f}) = {conf(bce):.3f}　A = {n_ok}/{len(t_valid)} = {acc:.3f}'\n",
    "    nxt = [(m, b, a) for m, b, a in MEDALS[::-1] if not (bce <= b and acc >= a)]\n",
    "    if not nxt: return head + '　最高等級！換 SEED_RUN 確認不是運氣。'\n",
    "    m, b, a = nxt[0]\n",
    "    need = [f'損失再降 {max(bce - b, .001):.3f}（把握再升 {max(conf(b) - conf(bce), .001):.1%}）' if bce > b else '損失 ✓', f'正確率再升 {a - acc:.1%}' if acc < a else '正確率 ✓']\n",
    "    return head + f'\\n   下一個 {m}（損失 ≤ {b}，即平均把握 ≥ {conf(b):.1%}；正確率 ≥ {a:.0%}）：' + '、'.join(need)\n",
    "\n",
    "def t_diagnose(run, best_before=None):\n",
    "    tips = []\n",
    "    if run['diverged']:\n",
    "        return ['🔴 損失爆掉（出現無限大）：學習率太大，一步跨太遠（講義 P20）。把 LR 調小 10 倍再試。']\n",
    "    h = np.array(run['history']) if run['history'] else None\n",
    "    if h is not None and len(h) >= 5:\n",
    "        tl, vl = h[:, 1], h[:, 2]\n",
    "        if tl[0] - tl.min() < .03 * tl[0] or tl[-1] > tl[0]:\n",
    "            if run['lr'] >= .3:\n",
    "                tips.append('🔴 訓練損失降不下來：學習率太大，每一步都跨過谷底來回彈（P20）。把 LR 調小。')\n",
    "            else:\n",
    "                tips.append('🟡 訓練損失幾乎沒降：步伐太小或走太少步。試：調大 LR、增加 EPOCHS、改 Momentum／Adam（P20、P52）。')\n",
    "        elif np.mean(np.diff(tl) > .02 * tl[:-1]) > .2:\n",
    "            tips.append('🟡 訓練損失上下跳：步伐偏大或 batch 太小，每步方向很吵。試：調小 LR 或加大 BATCH（P19、P20）。')\n",
    "        k = int(np.argmin(vl))\n",
    "        if k < .7 * (len(vl) - 1) and vl[-1] > vl[k] * 1.05 and not run['best_epoch']:\n",
    "            tips.append(f'🟠 驗證損失在第 {int(h[k, 0])} 個 epoch 最低，之後回升：開始過擬合。試：EARLY_STOP、L2、DROPOUT，或減少 NEURONS（P82–P84）。')\n",
    "        elif vl[-1] < vl[-5] - .005 and not run['best_epoch']:\n",
    "            tips.append('🟡 最後幾個 epoch 驗證損失還在下降：還沒學完。試：增加 EPOCHS、調大 LR，或改 Momentum／Adam。')\n",
    "    gap = run['train_acc'] - run['valid_acc']\n",
    "    if gap > .05:\n",
    "        tips.append(f'🟠 訓練比驗證高 {gap:.1%}：模型在背訓練集。泛化工具：L2、DROPOUT、EARLY_STOP、減少神經元或特徵（P78）。')\n",
    "    if run['valid_acc'] < BASELINE_ACC + .03:\n",
    "        tips.append('🔴 幾乎沒贏過「全猜未生還」：模型沒學到。檢查 LR、EPOCHS，以及有沒有勾到 sex、pclass。')\n",
    "    if run['model'] == 'NN' and run['activation'] == 'none' and run['hidden_layers'] > 0:\n",
    "        tips.append('ℹ️ 沒有激活函數：疊幾層都還是一條直線，等於 Logistic（P48）。和 relu 比一比。')\n",
    "    if run['model'] == 'NN' and run['loss'] != 'BCE':\n",
    "        tips.append(f\"ℹ️ {run['loss']} 配 sigmoid：錯得很離譜時推力反而變小，通常學得比 BCE 慢（P43）。\")\n",
    "    if run['model'] == 'NN' and run['best_epoch']:\n",
    "        tips.append(f\"ℹ️ 早停：第 {run['stop_epoch']} 個 epoch 停下，退回第 {run['best_epoch']} 個 epoch 的權重（圖上虛線）。\")\n",
    "    if run['after_test']:\n",
    "        tips.append('ℹ️ 這是看過測試集之後的回合，只能當探索，不會被選為「第一名」。')\n",
    "    if not [t for t in tips if t[0] in '🔴🟠🟡']:\n",
    "        tips.append('🟢 曲線正常。下一步：換一個旋鈕，想辦法超越排行榜第一名。')\n",
    "    if best_before is not None and run['valid_acc'] > best_before and not run['after_test']:\n",
    "        tips.append(f'🏆 新紀錄！驗證 accuracy 從 {best_before:.3f} 進步到 {run[\"valid_acc\"]:.3f}。')\n",
    "    if best_before is not None and abs(run['valid_acc'] - best_before) < .015:\n",
    "        tips.append(f'ℹ️ 和第一名只差 {abs(run[\"valid_acc\"] - best_before) * len(t_valid):.0f} 位乘客：驗證集只有 {len(t_valid)} 人，這麼小的差距可能只是運氣。換 SEED 再跑一次看看（P85）。')\n",
    "    return tips\n",
    "\n",
    "def t_board(top=None, mark=None):\n",
    "    if not t_runs: print('還沒有回合。'); return\n",
    "    rows = []\n",
    "    for r in sorted(t_runs, key=lambda r: (-round(r['valid_acc'], 4), r['valid_bce'])):\n",
    "        rows.append({'回合': r['id'], '牌': t_medal(r['valid_bce'], r['valid_acc']), '備註': r['note'], '結構': r['arch'], '特徵數': len(r['features']),\n",
    "                     '損失': r['loss'], '最佳化器': r['optimizer'], 'LR': r['lr'], 'Batch': r['batch'],\n",
    "                     'Epoch': (f\"{r['stop_epoch']}\" + (f\"→{r['best_epoch']}\" if r['best_epoch'] else '')) if r['model'] == 'NN' else None,\n",
    "                     'Steps': r['steps'] or None, 'L2': r['l2'] or None, 'Dropout': r['dropout'] or None,\n",
    "                     '訓練acc': r['train_acc'], '驗證acc': r['valid_acc'], '驗證BCE': r['valid_bce'], '平均把握': f\"{conf(r['valid_bce']):.1%}\",\n",
    "                     '看過測試後': '是' if r['after_test'] else ''})\n",
    "    df = pd.DataFrame(rows)\n",
    "    print(f'排行榜（依驗證 accuracy；共 {len(df)} 回合）' + ('，顯示前幾名與本回合' if top else ''))\n",
    "    if top:\n",
    "        df = pd.concat([df.head(top), df[df['回合'] == mark]]).drop_duplicates('回合')\n",
    "    compact_table(df, mark)\n",
    "\n",
    "def t_compare(ids):\n",
    "    ids = [_rid(i) for i in ids]\n",
    "    fig, ax = plt.subplots(1, 2, figsize=(11, 4), layout='constrained')\n",
    "    for i, c in zip(ids, [NAVY, ORANGE, GREEN, 'purple', 'brown', GRAY]):\n",
    "        r = t_runs[i]\n",
    "        if not r['history']: print(f'回合 {i} 不是神經網路，沒有曲線。'); continue\n",
    "        h = np.array(r['history']); lab = f\"Run {i}\"\n",
    "        ax[0].plot(h[:, 0], h[:, 5], color=c, label=lab); ax[0].plot(h[:, 0], h[:, 1] if r['loss'] == 'BCE' else np.nan * h[:, 0], color=c, ls=':', lw=1.4)\n",
    "        ax[1].plot(h[:, 0], h[:, 4], color=c, label=lab)\n",
    "    ax[0].set(xlabel='Epoch', ylabel='Validation BCE', title='Validation loss (dotted = train)')\n",
    "    ax[1].set(xlabel='Epoch', ylabel='Validation accuracy', title='Validation accuracy', ylim=(.5, 1))\n",
    "    for a in ax: a.grid(alpha=.2); a.legend(fontsize=9)\n",
    "    plt.show()\n",
    "    for i in ids: print(f\"Run {i}：{t_runs[i]['note'] or ''}｜{t_runs[i]['arch']}｜驗證 accuracy={t_runs[i]['valid_acc']:.3f}\")\n",
    "\n",
    "# ---------- 門檻、最後測試、如果你在船上 ----------\n",
    "def t_threshold(run_id=-1, threshold=.5):\n",
    "    run = t_runs[_rid(run_id)]; y = t_valid.survived.to_numpy(); pred = (run['prob'] >= threshold).astype(int)\n",
    "    cm = confusion_matrix(y, pred, labels=[0, 1]); tn, fp, fn, tp = cm.ravel()\n",
    "    fig, ax = plt.subplots(1, 2, figsize=(11, 4), layout='constrained', gridspec_kw={'width_ratios': [1.6, 1]})\n",
    "    for lab, col in [(0, GRAY), (1, NAVY)]:\n",
    "        ax[0].hist(run['prob'][y == lab], bins=np.linspace(0, 1, 21), alpha=.6, color=col, label=f'Actual {lab}')\n",
    "    ax[0].axvline(threshold, color=ORANGE, ls='--', lw=3, label=f'Threshold {threshold:.2f}')\n",
    "    ax[0].set(xlabel='Estimated survival probability', ylabel='Passengers (validation)'); ax[0].legend()\n",
    "    ax[1].imshow(cm, cmap='Blues')\n",
    "    for (i, j), v, name in zip([(0, 0), (0, 1), (1, 0), (1, 1)], cm.ravel(), ['TN', 'FP', 'FN', 'TP']):\n",
    "        ax[1].text(j, i, f'{name}\\n{v}', ha='center', va='center', fontsize=18, color='white' if v > cm.max() / 2 else NAVY)\n",
    "    ax[1].set(xticks=[0, 1], yticks=[0, 1], xlabel='Predicted', ylabel='Actual'); plt.show()\n",
    "    prec = tp / (tp + fp) if tp + fp else 0; rec = tp / (tp + fn) if tp + fn else 0\n",
    "    print(f\"回合 {run['id']}（{run['arch']}）｜門檻 {threshold:.2f}\")\n",
    "    print(f'Accuracy={(tp + tn) / cm.sum():.3f}｜Precision={prec:.3f}（說會活的人，真的活了幾成）｜Recall={rec:.3f}（真的活下來的人，抓到幾成）')\n",
    "    print(f'FP（說會活、其實沒活）={fp}｜FN（說不會活、其實活了）={fn}')\n",
    "\n",
    "def t_finish(run_id=-1, threshold=.5, confirm=False):\n",
    "    if not confirm: print('先在驗證集選定回合與門檻，再把 CONFIRM 改成 True。'); return\n",
    "    if t_state['locked']:\n",
    "        print(f\"已鎖定：回合 {t_state['selected']}、門檻 {t_state['threshold']}。以下是同一份測試結果。\")\n",
    "        compact_table(t_state['result']); return\n",
    "    rid = _rid(run_id); run = t_runs[rid]\n",
    "    prob = run['clf'].predict_proba(_X(run, t_test))[:, 1]\n",
    "    y = t_test.survived.to_numpy(); pred = (prob >= threshold).astype(int); tn, fp, fn, tp = confusion_matrix(y, pred, labels=[0, 1]).ravel()\n",
    "    acc = (tp + tn) / len(y); all_die = float(np.mean(y == 0)); gender = float(np.mean((t_test.sex == 'female').astype(int).to_numpy() == y))\n",
    "    result = pd.DataFrame([{'回合': '基準', '結構': '全猜未生還', '門檻': None, '驗證acc': None, '測試acc': all_die, 'Precision': None, 'Recall': None},\n",
    "                           {'回合': '基準', '結構': 'Kaggle 範例：女性全活', '門檻': None, '驗證acc': None, '測試acc': gender, 'Precision': None, 'Recall': None},\n",
    "                           {'回合': rid, '結構': run['arch'], '門檻': threshold, '驗證acc': run['valid_acc'],\n",
    "                            '測試acc': acc, 'Precision': tp / (tp + fp) if tp + fp else 0, 'Recall': tp / (tp + fn) if tp + fn else 0}])\n",
    "    t_state.update(locked=True, selected=rid, threshold=threshold, result=result)\n",
    "    compact_table(result)\n",
    "    print(f'Kaggle 測試集 {len(y)} 人：答對 {tp + tn} 人，正確率 = {tp + tn}/{len(y)} = {acc:.3f}。')\n",
    "    print(f'比 Kaggle 官方範例（女性全活 {gender:.3f}）' + (f'高 {acc - gender:.1%}，贏了！' if acc > gender else f'低 {gender - acc:.1%}：還沒贏過最簡單的規則。'))\n",
    "    print(f\"驗證 {run['valid_acc']:.3f} → 測試 {acc:.3f}：差 {run['valid_acc'] - acc:+.1%}。驗證集被反覆用來挑設定，測試才是真正沒看過的資料（P85）。\")\n",
    "    print('測試集已開啟並鎖定。之後再訓練的回合會標成「看過測試後」，只能當探索。')\n",
    "\n",
    "def t_person(person, run_id=-1, threshold=.5):\n",
    "    _check('sex', person['sex'] in ('male', 'female'), '選 male 或 female。')\n",
    "    _check('pclass', person['pclass'] in (1, 2, 3), '選 1、2、3。'); _check('embarked', person['embarked'] in ('S', 'C', 'Q'), '選 S、C、Q。')\n",
    "    rid = _rid(run_id); run = t_runs[rid]\n",
    "    def prob(d, r=run):\n",
    "        row = t_derive(pd.DataFrame([d])); return float(r['clf'].predict_proba(_X(r, row))[0, 1])\n",
    "    other = 'female' if person['sex'] == 'male' else 'male'\n",
    "    names = {1: '1st class', 2: '2nd class', 3: '3rd class'}\n",
    "    age_alt = ('Age - 20', {**person, 'age': person['age'] - 20}) if person['age'] >= 40 else ('Age + 20', {**person, 'age': person['age'] + 20})\n",
    "    alts = [('You', person)] + [(names[c], {**person, 'pclass': c}) for c in (1, 2, 3) if c != person['pclass']] + \\\n",
    "           [(f'sex = {other}', {**person, 'sex': other}), age_alt,\n",
    "            ('+2 family', {**person, 'parch': person['parch'] + 2}), ('Born in Dec', {**person, 'birth_month': 12})]\n",
    "    vals = [prob(d) for _, d in alts]; p0 = vals[0]\n",
    "    fig, ax = plt.subplots(figsize=(8.5, 4.3), layout='constrained')\n",
    "    ax.barh([a for a, _ in alts][::-1], vals[::-1], color=[GRAY] * (len(alts) - 1) + [ORANGE])\n",
    "    ax.axvline(threshold, color=NAVY, ls='--'); ax.text(threshold, len(alts) - .45, f' Threshold {threshold:.0%}', color=NAVY, va='bottom')\n",
    "    for i, v in enumerate(vals[::-1]): ax.text(v + .01, i, f'{v:.0%}', va='center')\n",
    "    ax.set(xlim=(0, 1.1), ylim=(-.6, len(alts) - .1), xlabel='Estimated survival probability (change one thing at a time)'); plt.show()\n",
    "    print(f\"用回合 {rid}（{run['arch']}）預測：你的生還機率約 {p0:.0%}，門檻 {threshold:.0%} → 判定：\" + ('生還' if p0 >= threshold else '未生還'))\n",
    "    print(f'意思是：1912 年那艘船上，和你條件相似的乘客，每 10 人約有 {round(p0 * 10)} 人生還；不是「你有 {p0:.0%} 的機會活下來」。')\n",
    "    print('只改一個條件看到的變化是「關聯」不是「因果」；這份歷史資料不能預測你今天的命運。')\n",
    "    if 'birth_month' not in run['features']: print('（這個模型沒有用 birth_month，所以改出生月份不會變。）')\n",
    "    others = sorted(t_runs, key=lambda r: -r['valid_acc'])[:5]\n",
    "    compact_table(pd.DataFrame([{'回合': r['id'], '結構': r['arch'], '驗證acc': r['valid_acc'], '你的生還機率': f'{prob(person, r):.0%}'} for r in others]), rid)\n",
    "    print('同一個你，不同模型給的機率不一樣：機率是模型的估計，不是事實。')\n",
    "\n",
    "\n",
    "_KNOBS = ['model', 'impute', 'scale', 'hidden_layers', 'neurons', 'activation', 'loss', 'optimizer', 'lr', 'momentum', 'batch',\n",
    "          'epochs', 'lr_step_every', 'l2', 'dropout', 'early_stop', 'patience', 'svm_kernel', 'svm_C', 'svm_gamma', 'seed', 'note', 'show', 'record']\n",
    "\n",
    "def _X(run, df):\n",
    "    return run['prep'].transform(df if run['custom'] else df[run['features']])\n",
    "\n",
    "# ---------- 自己寫資料處理：只要 sklearn 吃得下就行 ----------\n",
    "class _Custom:\n",
    "    \"\"\"把學生的 my_prepare(df, train) 包成和 sklearn 前處理器一樣的 transform()。\"\"\"\n",
    "    def __init__(self, fn, columns): self.fn, self.columns = fn, columns\n",
    "    def transform(self, df):\n",
    "        X = self.fn(t_derive(df).copy(), t_train.copy())\n",
    "        X = pd.DataFrame(X).astype(float) if not isinstance(X, pd.DataFrame) else X.astype(float)\n",
    "        return X.reindex(columns=self.columns, fill_value=0).fillna(0).to_numpy(float)\n",
    "\n",
    "def t_check_X(X, name='train', ref=None, quiet=False):\n",
    "    \"\"\"檢查 my_prepare 的輸出能不能直接餵給模型；不行就說哪一欄、怎麼修。\"\"\"\n",
    "    base = t_train if name == 'train' else t_valid\n",
    "    if isinstance(X, np.ndarray): X = pd.DataFrame(X, index=base.index, columns=[f'x{i}' for i in range(X.shape[1])] if X.ndim == 2 else None)\n",
    "    if not isinstance(X, pd.DataFrame): raise Stop('⛔ my_prepare 要 return 一張表（DataFrame），例如 return X。')\n",
    "    if len(X) != len(base): raise Stop(f'⛔ {name} 的列數從 {len(base)} 變成 {len(X)}：不要在 my_prepare 裡刪列（dropna），缺值請用補的。')\n",
    "    if X.shape[1] == 0: raise Stop('⛔ 一個特徵都沒有：至少放一欄。')\n",
    "    bad = [c for c in X.columns if any(w in str(c).lower() for w in ('survived', 'alive'))]\n",
    "    if bad: raise Stop(f'⛔ {bad} 就是答案：放進來等於偷看答案，這叫資料洩漏（P55）。')\n",
    "    X = X.copy()\n",
    "    for c in X.columns:\n",
    "        if X[c].dtype == bool: X[c] = X[c].astype(int)\n",
    "    text = [c for c in X.columns if not pd.api.types.is_numeric_dtype(X[c])]\n",
    "    if text: raise Stop(f'⛔ 這幾欄還是文字，模型吃不下：{text}。用 one-hot（pd.get_dummies）或 (df[\"欄\"] == \"值\").astype(int) 轉成數字（P60–P61）。')\n",
    "    nan = [c for c in X.columns if X[c].isna().any()]\n",
    "    if nan: raise Stop(f'⛔ 這幾欄還有缺值：{nan}。用 fillna 補，補的數字只能從 train 算（P56）。')\n",
    "    if not np.isfinite(X.to_numpy(float)).all(): raise Stop('⛔ 出現無限大：是不是除以 0 了？')\n",
    "    if name == 'train':\n",
    "        y = t_train.survived.to_numpy(float)\n",
    "        leak = [c for c in X.columns if X[c].std() > 0 and abs(np.corrcoef(X[c], y)[0, 1]) > .95]\n",
    "        if leak: raise Stop(f'⛔ {leak} 幾乎等於答案（相關 > 0.95）：這是資料洩漏，不是好特徵（P55）。')\n",
    "        big = [c for c in X.columns if X[c].std() > 10]\n",
    "        if big and not quiet: print(f'⚠️ 這幾欄的數字尺度很大：{big}。神經網路會比較難學，要不要標準化或歸一化（P62）？')\n",
    "    if ref is not None:\n",
    "        miss = [c for c in ref if c not in X.columns]\n",
    "        X = X.reindex(columns=ref, fill_value=0)\n",
    "        if miss and not quiet: print(f'（驗證集少了 {miss}，已補 0：通常是 one-hot 時驗證集沒出現那個類別。）')\n",
    "    return X.astype(float)\n",
    "\n",
    "def _run_matrix(prepare, cfg):\n",
    "    Xa = t_check_X(prepare(t_train.copy(), t_train.copy()), 'train', quiet=not cfg['show'])\n",
    "    Xb = t_check_X(prepare(t_valid.copy(), t_train.copy()), 'valid', list(Xa.columns), quiet=not cfg['show'])\n",
    "    return _fit(Xa.to_numpy(float), Xb.to_numpy(float), _Custom(prepare, list(Xa.columns)), list(Xa.columns), True, cfg)\n",
    "\n",
    "GOALS = [('🥇', .38), ('🥈', .39), ('🥉', .40)]\n",
    "CHALLENGE_SEEDS = (42, 1, 2)                       # 每次用 3 個種子取平均，避免靠運氣過關\n",
    "tries = []\n",
    "def t_challenge(prepare, note='', prep=None, **model):\n",
    "    \"\"\"用你的 my_prepare 訓練（3 個種子取平均），看驗證損失有沒有低於目標線。\"\"\"\n",
    "    cfg = dict(optimizer='Momentum', lr=.01, epochs=60, hidden_layers=1, neurons=16); cfg.update(model)\n",
    "    Xa = t_check_X(prepare(t_train.copy(), t_train.copy()), 'train')\n",
    "    print(f'✅ 資料格式正確：訓練集 {Xa.shape[0]} 列 × {Xa.shape[1]} 個特徵 → {list(Xa.columns)}')\n",
    "    rid = t_run(prepare=prepare, seed=CHALLENGE_SEEDS[0], note=note or '挑戰', show=False, **cfg)\n",
    "    runs = [t_runs[rid]] + [t_run(prepare=prepare, seed=s, show=False, record=False, **cfg) for s in CHALLENGE_SEEDS[1:]]\n",
    "    bce = float(np.mean([r['valid_bce'] for r in runs])); acc = float(np.mean([r['valid_acc'] for r in runs]))\n",
    "    medal = next((m for m, g in GOALS if bce <= g), '')\n",
    "    prev = tries[-1] if tries else None\n",
    "    tries.append(dict(no=len(tries) + 1, cols=list(Xa.columns), bce=bce, acc=acc, medal=medal, run=rid, arch=t_runs[rid]['arch'], cfg=dict(cfg, **(prep or {}))))\n",
    "    fig, ax = plt.subplots(figsize=(9, 3.6), layout='constrained')\n",
    "    ys = [t['bce'] for t in tries]\n",
    "    ax.bar([f\"Try {t['no']}\" for t in tries], ys, color=[GRAY] * (len(ys) - 1) + [ORANGE])\n",
    "    for (m, g), c, lab in zip(GOALS, ['goldenrod', 'silver', '#CD7F32'], ['Gold', 'Silver', 'Bronze']):\n",
    "        ax.axhline(g, color=c, ls='--', lw=2, label=f'{lab} {g}')\n",
    "    for i, v in enumerate(ys): ax.text(i, v + .002, f'{v:.3f}\\n({conf(v):.1%})', ha='center', fontsize=9, bbox=dict(fc='white', ec='none', pad=1))\n",
    "    ax.set(ylim=(min(ys + [.36]) - .01, max(ys + [.42]) + .015), ylabel='Validation loss (3-seed mean)', title='Challenge: get below the lines')\n",
    "    ax.legend(fontsize=9, loc='upper right'); plt.show()\n",
    "    print(f\"第 {len(tries)} 次｜{t_runs[rid]['arch']}｜{Xa.shape[1]} 個特徵｜驗證損失＝{bce:.3f}（平均把握 {conf(bce):.1%}）｜驗證正確率＝{acc:.1%}｜\" + (f'拿到 {medal}' if medal else '還沒拿牌'))\n",
    "    parts = ' + '.join(f\"{r['valid_bce']:.3f}\" for r in runs)\n",
    "    print(f'   算式：L = (L₁ + L₂ + L₃)/3 = ({parts})/3 = {bce:.3f}　平均把握 = e^(−{bce:.3f}) = {conf(bce):.3f}')\n",
    "    if prev:\n",
    "        add = [c for c in Xa.columns if c not in prev['cols']]; rm = [c for c in prev['cols'] if c not in Xa.columns]\n",
    "        d = bce - prev['bce']\n",
    "        names = dict(hidden_layers='隱藏層', neurons='神經元', activation='激活', optimizer='最佳化器', lr='LR', epochs='Epoch', l2='L2', dropout='Dropout', FILL='補值', NUM_SCALE='縮放')\n",
    "        changed = [f\"{names.get(k, k)} {prev['cfg'].get(k)} → {v}\" for k, v in tries[-1]['cfg'].items() if prev['cfg'].get(k) != v]\n",
    "        print(f\"和上一次比：加入 {add or '—'}、移除 {rm or '—'}\" + (f\"、設定 {'、'.join(changed)}\" if changed else '') +\n",
    "              f\" → 損失{'下降' if d < 0 else '上升'} {abs(d):.3f}\" + ('　👍' if d < -.002 else ''))\n",
    "    nxt = [(m, g) for m, g in GOALS[::-1] if bce > g]\n",
    "    print(f'下一個目標 {nxt[0][0]}：損失 ≤ {nxt[0][1]}（平均把握 ≥ {conf(nxt[0][1]):.1%}），還差 {max(bce - nxt[0][1], .001):.3f}' if nxt else '🥇 達成！想一想：哪一個改動貢獻最大？')\n",
    "    print(f'（這次的模型記在排行榜第 {rid} 回合；A6 可以用它預測你。）')\n",
    "\n",
    "# ---------- 練習檢查器：參考答案已編碼（避免直接看到答案）；老師需要時執行 answer(\"E5\") ----------\n",
    "import zlib\n",
    "_checker = (\n",
    "    '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')\n",
    "exec(zlib.decompress(base64.b64decode(_checker)).decode('utf-8')); del _checker\n",
    "\n",
    "raw=pd.read_csv(io.BytesIO(gzip.decompress(base64.b64decode('H4sIAAAAAAAC/71d2Y7dxhF9z7dcEKxe2OSjZ6zxs6C8B7I0iYTIkqElTv4+3Hov3q6qHhsGDCMZ8pLs7lpOnTr17cfX/3z8z/P72+/vPr399u327fm/t7f/er59+/jrt99vv7/9+u7D7Z9vvz7fnn/79e3Xf69/ePzdHx++3N6+//Hp+z9+e/vp+fb++d2/zz/5x/cvf3y+vf203nX995fPz38bb/q2/5VSw3iD23hzg7K3N7e/f/j49f36f32+/f3rj+fb7c2XH98/vP3t9+9fPt8+f7k9vf307flvsF7xz+f9ej3762FQs9a3x9vTx6/fvt/++LLdZP/79X97/PD89dcvP77+6/a/52/hLtrfRU3rXcb9KZbkMdJ7ZE+y3WR7vuxJ7PkkVg+w3gJ7jPIex4OEj7HfYvtnfSnCx9ifIFx8XmjsrG+vq0tf/3h+/vxtX4d4JRxXWnP+rIVhnvb3Px4+XP7q/k9va6jXuykYRhcf/N2Hj5/eY58vWci4BG5/CHUDGECvC0laBH8b5W8D5lwFPa7PMs7bR39+9+Vz8TSN/XDcZP1nGhz6Or9cPknYD3Y+v+q6uawlbojiu44d+0Ev+0vYm15PhiOfrTF+BTDhVMzWKNK6nqcirIf1Wxq2I/bGL0b7ZGWby2ybaxlg3ZuvsYfId3fcFPFggE5/Hv8G2W/7Uw3nfoJ5v0FjT2I7+/yEan34R+z6fC+eD6AKi6Cm9vOnn/+42i/gxdv/fGXU4hZILJJaVPPrZ4bxWD5/DNZXQSzLT80VmF/AuJxeYj8LenaWY1xyA5uvY1yHuIiVeYVlfwO1ruGxDMUneKTuoNklK5A9ObYC5XPPi53JNuR8dOMN0GqdndqtafHwyIsnNvA4OWY1gOnF6aM/tEzx+fTOct783P+Td+sw7nuPdHrGbOduLzCrAVz75f128d/ORzZWYat+vej5ZluX/N5uQ+wlyJyGP/HbW6sugxd88OrKlQEXXqDpgpPn2LfedpNlMJQDmyyAymKK7R77N2k7n8wJVmeHcugTz6f331Y3A4N1+yoKApHddvSfflE8ua/gZrRb0eTLnFn/s+pYr8k1dj4asfgjC+v3Yu2Z89fdGWyswdM0s7xNff7SR2g57GAxzeLzmTX0PA5/bTR/vtg2ceP7uyh61FWYr+nw/HCbYFhc8iDhVR6wxQh+/47Pf7wKO+LzA2q4OVHjvF5ItKFY2HoeXrXv4jfo0W2EC7C9g90MwDQsoo2kwkZivEKeGe+Hf7OkqOtNv5v3Wj6LnfVpd2nRSnhq44Mdx3zrdN/wwo1osGAzWMo0jPX1kbm/5Z4aey4Y63mALQunHv5ovg7zozgwRLlpptN9z8M0KUbW51MGJQqZ4itM2K6nee3CfGreA8TXP4LOwWw5K8mDhC0U3t5Og2nHy4jZseHMTsjHfyJAOMxIXejmQ444DutO3b7+fgA4li7Nr3yeAIoWrSFfzn/6JQk72ycnP7vLYKlLhoQrMy9gKRNc4xLcr/310zMvcXXp59e70d3DtVlmN+A4OHrDDDQPLRLuv+ColEbzYxZyqkwHUqaWnotHHCSjZtjeYK1hFjgK9IoZ/D1UWfePYOFszDCU/bOtTnht5638ut2mw05XwBAe3cQtswenetCbq60ub4bIJzYGx714IXIKrwEdnKvSlWA6Cpt/z12+GLZzOlq4uXXlZ2zr/dzYOlqHczNxtr6/3J2hSh7tUDfu1ccjR1tJojbxaj75x3fOsuME3fvwx74za6BJclklNuWdDo5NPTajPLP6KizMusA27iQ3MpRKRXRoVpaZ6Vee1xxVswY6X5t+QKuGxO3vxpChOkut2Klbkd8Dy/JA5i1X27duBIsmWg8NpEWdmUpRXKIkemmgr+iBfmfAkWY4Fq8QNnKE6DrUuXJw6TpeXZ3/vOTKN77B+IDqAVkvYLq7uw+qYGsNeLdfFkTcmydfTYC+OnZP95/c+DRrTTIdvzKqc+NLq+bU8Z7EcBkXE1TLgtdfBNxTRZpKKYvW1sd/v606eIAt909QGfGfQMkW8R+kjTcoyNlAKLXDCBdkoAqmkDQqmBw7fAqHYL2+aT/LbXAwHQqs6trv1GyJiA06buzibxGqNPKkcQnHkO3CwEdAq/2y7C0Iy8nQ0FMG0FLLa8rF78fEec7Pt+zeS5X5MsMBro9u/SnADuFT43qjeg6xhQjZWGXp3ALI9uCeuk7DJEvbrT09qSTzNuP5/TJTyoof3KBBsZNXC7E0otHSSOP0QqSbuY3qxK+S6bGfKUbDh4tL5/V7T8tBaOJ9cONDnvXtgc70U1lB+PQ6TlgUSr47P2wI+YLs04G4tBisVnCb43lkG3dAeWn7Guy0LOzIvrpzZP1CHCUik5d4KKlXVuKxZ9JEwUvrpJ8G0FdHdwq4k8Yqkw8NiyOmosWj75mFBcuRU1kU5p3eevlDsNqQ6Qh/CdBb3Mbz+QZUvLO0uzuXyw5mosRccef4wmxgJM2Dg52KdZ/vW+d/o8ht6uC0j6tHcpkiDftq88kojYWY29Ii7jG1G2eyrrmmR0XrpQ7rdVElemqXiYy3X2vkwLNfue2wI4Za/UTCzDJGSxsyg6wwXtN5qYDfSWcCDqOm3nRMtDBFLMYGYnH1/KoI9TmnpiQyQV0mIbK7E0YVFPEybQ9Ggo25oDTeYyWWoW/gdrO4jSh0T0+cdBb2w8b1H+kGuJ9SmeaNjL107cOX4a+MPwMZmhk5RMjJMhnAkPlMYrSO5Zp+xWFc3eaF4fyl9foKTffJCXOg0s5HoZxESr3IeRgbDyoq8s7IvxuzIIG/4YN2qfkMth+kTQxKUmV9kYjltLqOzuOEuvlh/WXlLIuMWHb28JurcqQDDps9MsAeqPyfmwa17GB5+0WuFoFBjIIKbORZAFWgVaINYKEXK2CGHXDLW/OWsV1kxNKcyAjSbFoTLIzlQjuvrPi8RmiTuVzeP2MpEqVEUPciFixOHqdmEaElquCVgOadl4SBy8pZsDKH6SMznm9PzNYKmyUihyROZ05qJFwKsuQQ1G5HR4RYOdH3wygCjCqHJGHLa+SSxS8dNvCxPj0m1G1mu9i+cZQ3m69rs3mVKnp3a9Fj29y5UIX4AVmnVxihKM7BxjCxR8J3t8CNLKAV06uyDioJu8RXl8qAO/0CvzRQzkndBwweCVsIjsZZEDTvGohgsQJlZa1cykPOxFrlFeJclElb/a+pKwuVjmnAHdkDrTX/aHBXg77qqsEfRBXII0jK7p2EC93jCz3sqzxuuxx0NyrPtMy/0GDugehKCalf7YemHkO+R95Y3YWc+dqAXNGYfldKABv2p709ocdj2D6G9b8mfUGceGxwj2IWTSWrl80NbewbO0DGl01hYXqVnPIxHhUYV4MAj614QoY/nFdP2m+ENZE9+ns4rJW8fkbnblTnIVkC3lFw/eWrM7GA3avISCOTxYAgGvaZcn4FjOOE8mO5PSawiDL4rOQ50csWKMl64gKPwXkyGmOwEJhZdeDTU1CCkhIjhpWsz3xgnojTb8UOoXCxbPoTF6HDwwVdsfJgajiw01qJ4y5dLGlvma2gTSDBPyWtAuyezPjDmt+Gm1R71dnbAhbSGDq1WI/XYEji7UZWHzLUMV9JlP6zNSwg74fXg2rYynv0vJHRUqeyh97i5e2HUYrzi+oOhAUbh0XxFv066YA1ZJyvVMEaiQcEpu44DwtDVyyHjM8+jZFMM68zHzutn+TC8rxqvEW0HPpoSqa8R/Up5pPsqNakVl/UAa5tYGqNgZ5AvVTLT4TkkgKy4pFGa504ehUbRVf6+NtBsW2XWuFTN7UHB2Ayw3zAzKKMJNIa9AYVUXboHZUDfvOl6yVGJI3LV4buwj6rggcLRQc1Cxhgl7XTxZzKrJB8xmNQDqE4rrSTlEinJiugrTamIWYXlpdepNZqOp22dblyCc9n136X02Dn2QK7DgkZ+4OsOxABDHjdDZWME31THZ5vTT4mYtx8RziJ21mxYLzytlwaFEDDyDBKecxlbE+uF67G1W/uZ03qJThOynR01KvC18pahLoajC6MCdkYEShKT224ahRy5qoCVgrgk0yZzvluYClSOlibnOpirIc+r2WvYkmSRm15/cVRdPKgGGdJI7252GIyYNzCEblwJ+e1ld00Qmq+qjSfmNy8Xq3JF9OqPOPJoreAKl4UOyMDVkHJuWpsFthV61rD2f0FmntowVmw+SafqTo7KCHNC4N52un2C2HzweKvQbTmUXpzi7GePmMAqS000pigPEhll15hNRI1FaWuMoCW4Uq0KoUau3nEWEqlEsrEGN7LpAnCLZcQV3tPKa08VDd2r1sJuLYfJFypNGQNZAHl9tSHDlhlrUVnqd7JBAEiTZLRWIRK4KsLH/ZnpA0l604kHCcUa+RlwI2Ci99AqdB2gyCAyIipFGuk4J3x6XfUVREK/EiwmjSlOZFy3ixRAyplxPrJyoH6lEr/X/N+7iNajtdokKNzdJSxRjongWw8ou/AojqrStZq3YsTW9MuDi+gWqI7jb5CdSHoUBfKtcNZ3RalwBGSutH3QFQnAwl9zgLGUqAOHvCZ6yyWz03tiROsoZBE/WJanNO67tRJLKkL1Slp/jWK3NznvXLpIVA06AvOXBrCZyeOQzrVfujKLq/EI52qrFK1m2+5qt/Yx96H0UehuzqbQPhZrk0EuK4EkVyTas1LrKbvDN9yZ8mRC63lJWX1z6L8lj5LVJRLd9+Cul1BCMhmiN6X03t1zXTIPTaJq/yCSKfKEiBhUbTU5+BR9j0/dowIwEKmx6ZlJ4NG0Om5f2iTA09VvEpXiKcQdrQQ6NL/svT/jUfiWFNrohmFviDSWLxYzxJDH+WdxizaL7oIc0f9IPWnAIqhag23TGphhiMOwbbjTwSLplHOLlFxJu+7ZgknFJ1g6ngEleoFEuaopNx3Ng8sL0ch0yDIbmm3jcuwMCVP8nDk+PEWqFXsAg+Iz8sAIyaz/EiQiGSxuJBgYiHMYyhkehpiG69aSNRhyYtxhhSedt3w0TGEhO3N/bLNnNdH1MFMB5gctIE1Sl141UAe5q5edfGww1lSdRET6kspJYppu2qtdooR7J1bc9z2lMgmdrXHFXNlOEXlNNHTuDnlBYwRpNKDO1r6Uc7RBeuprDksw6x53RiZIJHiMPtfBqBRUVFnF2Fl66dHmHJnNvP1031ooLyEMZ+hsEhnJPX2wliBgGLy7lOkRtjZcScG5wFm2ViAasiiycILCgVkGg00opA3gsFZoT1ZD/cACy1chbB3uugxsrisxloEyaZlBKTXwzPW8M6iUR0JJBLLgZR6+UTV59DP7eZBbZ+cXqApft9SOqKrXhKLN5Ncb3Zk0Y20tNOpIFqPrGB10pYsi0kR2DlYZcoxzR4m/j0JG7rjQN0Z7cxotYHmAUgtvMyRN+hQAoxYm8pIztTEapKNKMMyM0foKXqhrh6dhMr4wINX7W4sb0DsMhg6xQQ7vRLt2JgYrfc5IqbWzAa0xtE3ZPD4iNngFypHQE/dGKsW9oTWTU2L5p7+rrZUKbuzVo6U9BWM3LHOV2LnPDjhJaCMzOCt/sdtb84WM1lEisUpH0EdoslWIqXUJtWTZ79z1w0czmWmSsV7vVxNGFCE5ghaSiRIvp/rkKIiyuc+kA0HjKvjYzDkqthVTs5We8TN6icqu7TUEb3TDF+1mIt86lKsT4P0JpANiaUqM2EpY9A2KtayRdVMa1PXigrNkZtLT6032Uv6wJfvsswRdFbjXW4EexaE4ZDRJ5yaDPgTXTVINwsDpSQlj2FWMBwYTEdsKDZfhTbl5jkRybgl69Vk+vrkZdMjQk3JTxdfPvfEOU2YOhN8QUWgKF0maZvmFNtrjEgDRqx9PXY1gKclWfuXFCfkzKg8a1Eh6Bc3FrDD5VwCzpTNAXwuHWOKPZJutPV+si6+/LCyVGDTlFOfH9/CYNLIl9onXyW/mpf89om2C9SHr1sbWKETkvKyaRCl2Sq4OcyxNVGMlDTq82oyPIscmeYgns+iRwHoVNrvu8ImDddtjch1l3t5tcWAeaC702POXeiAn34Haiffd8XqtEsmF3J0M9IuOSCM/LvXFtAxsktQGLcCUlxqAFU/aG+cTOIjTYGpgHfJQnNdGrI6SNZwfG8poqSlDUFmEQ0bkuJcOWWoVB26HLVwRT+EQq7/L5KPOIyT2iNTFh/DGEkxEopzdqQF9+JyJCOcYreIZLLJKAVl81QOL0a2PZtXaTMw2AMXbXef4UpCh2lmiaRWJapj/9oS3uCHqxJegEyxvDMr0laycUs9KnWw7mRlAYNR/xndThMmds3iPBvxgLKYHnYJDatksnMmHdDUe8sJ8GzBdMjozicjIusAvrJC15QGibpquJymWH41mHFT6KYX12JU63AlL2rjVXx5lrrv5YDRjtoUvXcn7J55jDa8yUSrnztp9hvpKx+dX8avbgbGiL8eyThugYbrg0co7NkWc/T7WubzJgm+ImEt828reXOOv5OMGcgbLFgWo1+3o5iNJtoE6ZkTcCm6mbg52xuO4VQOjbgb+ZUM4oN6IKwdLHeyWj0ih84f1ZGAyoPiO2ZTlZ2icDyGrL0oMulMOsuXHi0iZRASqpe/w9ZsLBmsEJBlomLGi3RIpxPZqnmo9MQ+pX8LiPuhTbjQirgS5C4bikavrpgrJLYroHUhgz8gKJTerGfhEXs06nGyhnN28jCTDoWWmIySbbs+acEixOKPfzdaBg9kHUmKybIu9QihynBY4zMA0+e55mAUX96NwpbOoiGUMw+gn+kfdYEkq67MybuS9QlA1ASct4yM5+X8yh+EgSnvkqDx/NMDD/X8I0avwdmMqhmepoaYyCFONY5RokTpd4Dp7DKZ8KPHslgoQNaUJYzPj40wYmhKpKMAd4U27jecJcz3tHzjlRU3cTatnWV1yENtwhxXpiobQr8plg8GJscJ2DtShrEaoiTw/NMo6jdURRe0iP0gQ8er8nUKbRBC3bwoAUS+Ur39TUzTJ+Tpn5q7N8iD3NHne7yf56UlAhYFvo74O/RJ0/GQm0diasRMIfajJfxl7LcIas+lKkS9hxltd0b1NyJEvc3rVqKWtsOZ+2RDFe/jtBhsUnaAERT/CR0sj/cBgwBY4GEkfbDwMpiZnDejdkwQDi59lkDPBKXOx8YsyTjQ+5KP/4oCfpCkgi/mifGUSsOj+1AyL2pzuPRhsA4VbC5tiOnRlrejSFseiiKP1Ttc2jQdV4CpuHl+3PHOsmmcKCtfOmMem0wVjctsTkVOhSoBBFbjcrAhVTjJ6SWCkJPOgzsms7Wk5nuz0j78qu/q2IFo+YU6XTCYtUSiqgyF6AlpL/TbhRoXs+RGSnEWoc1BNYqQMdUkKINJhnClXGJutSdyIEEmEAkJfLTT54CFPqUk1nr0KJ2LpUdxsQ9ueeMIbv1/bvF0gD35tCA4QFWvIHV9+CFPUbDDXKAITQemdYekmu6c1nVaHgFTJaCQ47Cm/CdTRapMO8tEAEqdcmI3Y7+0UykvK2u+F3bRxRQcREzUutLcbCNAKeS4vjm1/y6n0QtF/i10DZDOnFDaPEQd8oC2EFPUH2IBg9Z4ftU0Pg9aMobIyE2OEQ6Mjp3T3Eytn4irCr0D3VGiTmFbK6UjIQv+1BAos5GMZajSUBdcKI56XlowQbFmynlPyzcatzp3sOLLMbIZYE2zO1DgJZTQEzu3attBhk8sVj2zjFKgu6dyVhSpGfyQuWPefRgKsn+99exzBc6gmMTQ7hnhnL+7vA6YhL25Hh6op5FS2GRFkjQdsp8cZSrb1d0ZsGUusuOtxhJ5FXvnA4MBCsj0jnmYUnSTQypkBDu4o8e6Vihr2AOxpakWiMWbY486iPW0wwEeB7OzdNa1sLJaX0Bqp2Fx/EEkWjbFAoodPXLkJTET6pxohEjniQrDLLJ8mdq3WTGqZ34IE+tNtAau4v2tsCEgwcrFch15zC5R8r3eOg/Npm+NQj2MreMtwNaBd1SoWJyHwLaaSIFLAeuOUlJw2fcIHJ2qK9NBwvoRyHC+d/qb7FqHoz2809dPPPHGk1unYpAsKqqZn4RX7T0mx4OhkJcO0wzp7rZw6M7B5/fZiwYiwlMHdGvWvYOXxFJXKbwnaB+CqCutBcCWZ4jMq6s5ePjtqd/XfG7JuGSxkG3AR9htCFktH5XffaTx6kTdKzCJ+KC1pNfuLNh6XtpTM9evpp0V6LLJ1zrhE1n+RqnzK3Krr6WPVbiSvylr9/QRi+goCYryhI5DSfriIWf6psIlmtEZ86Y1WrzWr92domFCuCrTkmRIkGHSgIELtmwNj6yxS1BN2YHJDPOhg02e2hrbIUA0r6Ieu6mrNiq6uqtcWhN8M8eaI4yz5o8dhzB5lKVPmWuCCZGHftxGFTC7MOc3Siz0k/Oi4JTWO7YBVUogb3LYvenovSk5cY4baRnYbjz0wMMAaTsYq7Y4derrGq83WraRs2pMoUxoedlj3p+0kFVL0EF8hjjIuDaMOMW6Bdpj3QazEai8Ly/RBC5bfuud070w+LEp4BK4+nKhaN0rO5dKtwFxvsWV3J9Iz0FY/EgTkr3tJK8+MFocXR/6BmjZiFjrjxFfWTxi0LyCNdufgJmbJJ311ArI/wGFXbiVut4AAA=='))))\n",
    "kaggle_test = pd.read_csv(io.BytesIO(gzip.decompress(base64.b64decode('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'))))\n",
    "print('工具與內附資料已載入：練習用 891 位乘客，最後測試用 Kaggle 測試集 418 位乘客。sklearn', sklearn_version)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "396e5409",
   "metadata": {},
   "source": [
    "## A0｜資料處理練習：老師示範一行，換你寫一行（P66–P72、P62）\n",
    "\n",
    "每一格上面是 👀 示範，下面的 `___` 換成你的程式，再按 ▶。\n",
    "✅ 才能進到下一格；❌ 會告訴你哪裡不對。\n",
    "用 `df = raw.copy()` 練習，不會改到後面用的原始資料。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "149f5545",
   "metadata": {},
   "source": [
    "### A0-①　讀進來、列出來（P66）\n",
    "\n",
    "先看清楚這張表：幾列、幾欄、哪裡有缺值。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ee7e817b",
   "metadata": {},
   "outputs": [],
   "source": [
    "df = raw.copy()                          # 👀 示範：複製一份來練習，不動到 raw\n",
    "print(df.shape)\n",
    "display(df.head(3))\n",
    "\n",
    "# ✏️ 換你：age 欄有幾格缺值？\n",
    "n_missing_age = ___\n",
    "check(\"E1\", n_missing_age)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d96b869",
   "metadata": {},
   "source": [
    "### A0-②　提取：條件篩選（P67）\n",
    "\n",
    "和上週的 H[列, 欄] 一樣：先說要哪些列。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c3e32e85",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E1\")\n",
    "men50 = df.loc[(df[\"sex\"] == \"male\") & (df[\"age\"] >= 50)]   # 👀 示範：50 歲以上的男性\n",
    "print(\"50 歲以上男性：\", len(men50))\n",
    "\n",
    "# ✏️ 換你：選出一等艙（pclass == 1）的女性\n",
    "women1 = ___\n",
    "check(\"E2\", women1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "714d305e",
   "metadata": {},
   "source": [
    "### A0-③　清洗：拿掉不能用的欄（P68）\n",
    "\n",
    "會洩漏答案的、缺太多的，都先拿掉。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b6f40ac8",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E2\")\n",
    "df = raw.drop(columns=[\"alive\"])         # 👀 示範：alive 是答案的文字版，會洩漏\n",
    "print(df.isna().sum()[lambda s: s > 0])\n",
    "\n",
    "# ✏️ 換你：deck 缺了 688 格，把它也拿掉\n",
    "df = ___\n",
    "check(\"E3\", df)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a64b0363",
   "metadata": {},
   "source": [
    "### A0-④　先切分（P71、P54）\n",
    "\n",
    "補值、標準化之前要先切分：訓練集學規則，驗證、測試集只套用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a20d56ae",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E3\")\n",
    "train, rest = train_test_split(df, test_size=.4, stratify=df.survived, random_state=SEED)   # 👀 示範：60% 訓練\n",
    "\n",
    "# ✏️ 換你：把 rest 對半切成 valid 和 test\n",
    "valid, test = ___\n",
    "check(\"E4\", (valid, test))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e2cb909",
   "metadata": {},
   "source": [
    "### A0-⑤　補數據（P69、P56）\n",
    "\n",
    "補的數字只能從訓練集算。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "134d9d89",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E4\")\n",
    "age_median = train[\"age\"].median()          # 👀 示範：中位數只從訓練集算\n",
    "train_age = train[\"age\"].fillna(age_median)\n",
    "print(\"訓練集中位數：\", age_median, \"｜補完還缺：\", train_age.isna().sum())\n",
    "\n",
    "# ✏️ 換你：把驗證集的 age 也補起來\n",
    "valid_age = ___\n",
    "check(\"E5\", valid_age)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "86b86502",
   "metadata": {},
   "source": [
    "### A0-⑥　新增欄位：自己造特徵（P70）\n",
    "\n",
    "整欄一起算，不用寫迴圈。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1bcd33a8",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E5\")\n",
    "df[\"family_size\"] = df[\"sibsp\"] + df[\"parch\"] + 1    # 👀 示範：家庭人數\n",
    "print(df[\"family_size\"].value_counts().head())\n",
    "\n",
    "# ✏️ 換你：is_child＝未滿 16 歲是 1、否則 0\n",
    "is_child = ___\n",
    "check(\"E6\", is_child)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d8594d6",
   "metadata": {},
   "source": [
    "### A0-⑦　分組統計（P70）\n",
    "\n",
    "先看出資料裡的規律，再交給模型。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8cc7b001",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E6\")\n",
    "print(df.groupby(\"sex\")[\"survived\"].mean().round(3))   # 👀 示範：各性別的生還率\n",
    "\n",
    "# ✏️ 換你：各艙等（pclass）的生還率\n",
    "rate_by_class = ___\n",
    "check(\"E7\", rate_by_class)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f34f996",
   "metadata": {},
   "source": [
    "### A0-⑧　標準化（P62）\n",
    "\n",
    "標準化＝(x − 平均) ÷ 標準差，平均和標準差只從訓練集算。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "38c6255c",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E7\")\n",
    "age_mean, age_std = train_age.mean(), train_age.std()      # 👀 示範：訓練集的平均、標準差\n",
    "train_age_z = (train_age - age_mean) / age_std\n",
    "print(\"訓練集標準化後：平均\", round(train_age_z.mean(), 3), \"標準差\", round(train_age_z.std(), 3))\n",
    "\n",
    "# ✏️ 換你：驗證集的 valid_age 也標準化\n",
    "valid_age_z = ___\n",
    "check(\"E8\", valid_age_z)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a73b0c40",
   "metadata": {},
   "source": [
    "### A0-⑨　歸一化（P62）\n",
    "\n",
    "歸一化＝(x − 最小) ÷ (最大 − 最小)，把數字壓到 0～1。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1da484a7",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E8\")\n",
    "fare_min, fare_max = train[\"fare\"].min(), train[\"fare\"].max()   # 👀 示範：最小、最大只從訓練集算\n",
    "print(\"票價範圍：\", fare_min, \"～\", fare_max)\n",
    "\n",
    "# ✏️ 換你：把訓練集票價歸一化到 0～1\n",
    "train_fare_01 = ___\n",
    "check(\"E9\", train_fare_01)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84da398e",
   "metadata": {},
   "source": [
    "### A0-⑩　One-hot（P60–P61）\n",
    "\n",
    "文字類別變成幾欄 0／1；類別之間沒有大小，不能直接編成 1、2、3。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a02a32ec",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E9\")\n",
    "sex_oh = pd.get_dummies(train[\"sex\"], prefix=\"sex\", dtype=int)   # 👀 示範：性別變兩欄\n",
    "display(sex_oh.head(3))\n",
    "\n",
    "# ✏️ 換你：embarked（S、C、Q）也做 one-hot\n",
    "emb_oh = ___\n",
    "check(\"E10\", emb_oh)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c8b4380",
   "metadata": {},
   "source": [
    "### A0-⑪　拼成矩陣 X（P71）\n",
    "\n",
    "處理好的欄位左右拼起來，就是 Class 1 的矩陣 X（N, d）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fd144c2b",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E10\")\n",
    "X_part = pd.concat([train_age_z, train_fare_01, sex_oh], axis=1)   # 👀 示範：左右拼起來\n",
    "print(X_part.shape)\n",
    "\n",
    "# ✏️ 換你：再把 emb_oh 也拼上去\n",
    "X_train = ___\n",
    "check(\"E11\", X_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "391a3b30",
   "metadata": {},
   "source": [
    "### A0-⑫　sklearn 一次做完（P72、P65）\n",
    "\n",
    "E5～E11 可以包成一個前處理器：訓練集 fit（學規則），驗證集只 transform（套用）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04d3b8cc",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"E11\")\n",
    "cols = [\"age\", \"fare\", \"sex\", \"embarked\", \"pclass\"]\n",
    "prep = t_preprocess(cols)                  # 👀 示範：補值、標準化、one-hot 包在一起\n",
    "X_tr = prep.fit_transform(train[cols])      # 訓練集：fit ＋ transform\n",
    "print(X_tr.shape)\n",
    "\n",
    "# ✏️ 換你：驗證集只套用規則\n",
    "X_va = ___\n",
    "check(\"E12\", X_va)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "070967d7",
   "metadata": {},
   "source": [
    "# 實作①　先建立基準（P74）\n",
    "\n",
    "## A1｜看資料\n",
    "\n",
    "一列是一位乘客，一欄是一項資訊。`survived`：0 未生還、1 生還。`alive` 是答案的文字版，不能當輸入。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e1a12840",
   "metadata": {},
   "outputs": [],
   "source": [
    "print('列、欄：', raw.shape)\n",
    "display(raw.head())\n",
    "print(raw[['age', 'embarked']].isna().sum())\n",
    "print(raw.groupby('sex')['survived'].mean().round(3))\n",
    "print(raw.groupby('pclass')['survived'].mean().round(3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f6251575",
   "metadata": {},
   "source": [
    "## A2｜先切分，補值只從訓練集學\n",
    "\n",
    "891 位乘客切成：60% 訓練（拿來學）、20% 驗證（拿來比較設定）；剩下 20% 這一版不用。\n",
    "**最後測試用 Kaggle 鐵達尼號比賽的測試集 418 人**（A7）：和全世界參賽者同一份考卷，答案已內附，請不要偷看，A7 之前也不要用它調參。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "857137cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "data = t_derive(raw).drop(columns=['alive'])                 # alive 是答案的文字版，不放進來\n",
    "t_train, remain = train_test_split(data, test_size=.4, stratify=data.survived, random_state=SEED)\n",
    "t_valid, _unused = train_test_split(remain, test_size=.5, stratify=remain.survived, random_state=SEED)\n",
    "t_test = t_derive(kaggle_test).drop(columns=['alive'])         # 最後測試：Kaggle 測試集 418 人\n",
    "t_runs, t_state = [], {'locked': False}                      # 排行榜、測試鎖\n",
    "print('訓練／驗證／測試：', len(t_train), len(t_valid), len(t_test))\n",
    "print('訓練集年齡中位數（補值用）：', t_train['age'].median(), '｜訓練集年齡缺值：', t_train['age'].isna().sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9fc70517",
   "metadata": {},
   "source": [
    "## A3｜基準 vs Logistic\n",
    "\n",
    "基準：永遠猜訓練集的多數類（未生還）。\n",
    "Logistic＝**沒有隱藏層**的神經網路，只有一個 sigmoid 神經元（P30）。它會是排行榜的第 0 回合。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5527d3ef",
   "metadata": {},
   "outputs": [],
   "source": [
    "BASELINE_ACC = float((t_valid.survived == t_train.survived.mode()[0]).mean())\n",
    "print(f'基準（全猜未生還）驗證 accuracy：{BASELINE_ACC:.3f}')\n",
    "logit_id = t_run(hidden_layers=0, optimizer='Adam', lr=.01, epochs=100, note='A3 Logistic')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6f9f1ea3",
   "metadata": {},
   "source": [
    "# 實作②　訓練你的神經網路（P87）\n",
    "\n",
    "## A5-1｜親手走一遍：前向 → 損失 → 反向 → 更新\n",
    "\n",
    "一位乘客 x=(1, 0.5)、真的生還（y=1），走過 2 → 3 → 1 的小網路；順序和講義計算圖相同（P51、P16）。\n",
    "一樣是老師示範一行、換你寫一行。控制台裡的網路做的是同一件事，只是一次算一批乘客、重複幾百到幾千步。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1be37a3e",
   "metadata": {},
   "source": [
    "### N1｜隱藏層：加權分數 → ReLU"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13fbb5dd",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = np.array([1.0, 0.5]); y = 1            # 一位乘客：兩個特徵，真的生還（y=1）\n",
    "W1 = np.array([[1.0, -1.0], [0.5, 2.0], [-1.0, 1.0]]); b1 = np.array([0.0, -0.5, 0.2])\n",
    "w2 = np.array([1.5, -1.0, 0.8]); b2 = -0.2\n",
    "\n",
    "a = W1 @ x + b1                            # 👀 示範：3 個隱藏神經元的加權分數\n",
    "print(\"a =\", a)\n",
    "\n",
    "# ✏️ 換你：ReLU\n",
    "h = ___\n",
    "check(\"N1\", h)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ab4f970",
   "metadata": {},
   "source": [
    "### N2｜輸出層：分數 → sigmoid 機率"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2a5bbbb7",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"N1\")\n",
    "z = w2 @ h + b2                            # 👀 示範：輸出分數\n",
    "print(\"z =\", round(float(z), 3))\n",
    "\n",
    "# ✏️ 換你：sigmoid 變成機率\n",
    "p = ___\n",
    "check(\"N2\", p)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f3cb94f2",
   "metadata": {},
   "source": [
    "### N3｜損失：交叉熵"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b675026b",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"N2\")\n",
    "print(\"p =\", round(float(p), 3))\n",
    "loss_if_y0 = -np.log(1 - p)                # 👀 示範：如果 y=0，損失是 −log(1−p)\n",
    "\n",
    "# ✏️ 換你：這位乘客 y=1，損失是多少？\n",
    "loss = ___\n",
    "check(\"N3\", loss)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e8505e0",
   "metadata": {},
   "source": [
    "### N4｜反向：算梯度"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aaa306bd",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"N3\")\n",
    "dz = p - y                                 # 👀 示範：BCE 接 sigmoid，損失對 z 的梯度＝p − y\n",
    "db2 = dz                                   #    z = w2·h + b2，z 對 b2 的斜率是 1\n",
    "\n",
    "# ✏️ 換你：對 w2 的梯度（z 對 w2 的斜率是 h）\n",
    "dw2 = ___\n",
    "check(\"N4\", dw2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "070de105",
   "metadata": {},
   "source": [
    "### N5｜更新：梯度下降"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4c19d985",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"N4\")\n",
    "lr = 0.5\n",
    "b2_new = b2 - lr * db2                     # 👀 示範：更新 b2\n",
    "\n",
    "# ✏️ 換你：更新 w2\n",
    "w2_new = ___\n",
    "check(\"N5\", w2_new)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fe4692a6",
   "metadata": {},
   "source": [
    "### N6｜驗收：損失有沒有變小"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39572c1e",
   "metadata": {},
   "outputs": [],
   "source": [
    "need(\"N5\")\n",
    "p_new = 1 / (1 + np.exp(-(w2_new @ h + b2_new)))   # 👀 示範：用新參數再算一次 p（這裡先不更新 W1）\n",
    "print(\"p：\", round(float(p), 3), \"→\", round(float(p_new), 3))\n",
    "\n",
    "# ✏️ 換你：新的損失\n",
    "loss_new = ___\n",
    "check(\"N6\", loss_new)\n",
    "print(\"損失：\", round(float(loss), 3), \"→\", round(float(loss_new), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b4b08ed",
   "metadata": {},
   "source": [
    "## A5-2｜🎛️ 訓練控制台\n",
    "\n",
    "預設是一個**還沒練好**的網路（SGD、20 個 epoch），驗證 accuracy 約 0.75。你的任務是用課堂教過的方法把它推高。\n",
    "\n",
    "| 區塊 | 旋鈕 | 講義 | 改了會看到什麼 |\n",
    "|---|---|---|---|\n",
    "| ② 特徵 | 勾選 `use_…` | P26、P59 | 拿掉 sex 會掉很多；加 birth_month（無關）通常沒幫助 |\n",
    "| ③ 前處理 | `IMPUTE`、`SCALE` | P58、P62 | 關掉標準化：fare 的尺度很大，梯度下降變難走 |\n",
    "| ④ 結構 | `HIDDEN_LAYERS`、`NEURONS`、`ACTIVATION` | P48、P49 | 0 層＝Logistic；`none` 疊再多層也只是一條直線 |\n",
    "| ⑤ 損失 | `LOSS` | P41、P43 | MSE、MAE 配 sigmoid 推力小，學得慢 |\n",
    "| ⑥ 梯度下降 | `OPTIMIZER`、`LR`、`MOMENTUM`、`BATCH`、`EPOCHS`、`LR_STEP_EVERY` | P16、P19、P20、P52 | 太小不動、太大亂跳；steps＝epoch × 每輪 batch 數 |\n",
    "| ⑦ 泛化 | `L2`、`DROPOUT`、`EARLY_STOP` | P78、P82–P84 | 訓練／驗證曲線分岔時，把它們拉回來 |\n",
    "| ⑧ SVM | `SVM_KERNEL`、`SVM_C`、`SVM_GAMMA` | P114、P125 | C、γ 太大：訓練很高、驗證下降 |\n",
    "| ⑨ 種子 | `SEED_RUN` | P85 | 同設定換種子，分數也會差 1～2%：小差距可能只是運氣 |\n",
    "\n",
    "每跑一次會看到：①損失與 accuracy 曲線（訓練 vs 驗證）；②🎯 目標看板；③🔴🟠🟡🟢 診斷，告訴你下一步可以改哪裡；④排行榜（👉＝這一回合）。\n",
    "\n",
    "**🎯 目標看板**：損失和正確率兩個都達標才拿牌。\n",
    "\n",
    "| 牌 | 驗證損失 | ＝平均把握 | 驗證正確率 |\n",
    "|---|---|---|---|\n",
    "| 🥉 | ≤ 0.42 | ≥ 65.7% | ≥ 80% |\n",
    "| 🥈 | ≤ 0.40 | ≥ 67.0% | ≥ 83% |\n",
    "| 🥇 | ≤ 0.38 | ≥ 68.4% | ≥ 84% |\n",
    "\n",
    "**三個數字怎麼算**（驗證集 N 位乘客；$y_i$ 是答案，$p_i$ 是模型給的生還機率）\n",
    "\n",
    "① 模型給「正確答案」的機率：\n",
    "\n",
    "$$q_i=\\begin{cases}p_i, & y_i=1\\\\ 1-p_i, & y_i=0\\end{cases}$$\n",
    "\n",
    "② 驗證損失（交叉熵）：\n",
    "\n",
    "$$L=\\frac{1}{N}\\sum_{i=1}^{N}-\\big[y_i\\ln p_i+(1-y_i)\\ln(1-p_i)\\big]=\\frac{1}{N}\\sum_{i=1}^{N}\\left(-\\ln q_i\\right)$$\n",
    "\n",
    "③ 平均把握：兩邊乘 −1 再取 $e$ 的次方，就是所有 $q_i$ 的幾何平均：\n",
    "\n",
    "$$e^{-L}=e^{\\frac{1}{N}\\sum_i \\ln q_i}=\\left(q_1\\,q_2\\cdots q_N\\right)^{1/N}$$\n",
    "\n",
    "例：$L=0.38 \\Rightarrow e^{-0.38}=0.684$，也就是平均把握 68.4%。\n",
    "\n",
    "④ 驗證正確率：只數 $q_i\\ge 0.5$（判對）的人數：\n",
    "\n",
    "$$A=\\frac{1}{N}\\sum_{i=1}^{N}\\mathbb{1}\\left[q_i\\ge 0.5\\right]$$\n",
    "\n",
    "$L$ 和 $A$ 用的是同一組 $q_i$：$L$ 看每一題有多少把握、錯得多離譜；$A$ 只看判對幾題。所以兩者不能互相換算，獎牌才要求兩個都達標。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cc4870c5",
   "metadata": {},
   "outputs": [],
   "source": [
    "#@title 🎛️ 訓練控制台：改這裡，再按 ▶（每按一次，排行榜多一個回合）\n",
    "#@markdown **這一回合你改了什麼？**（會記在排行榜，方便回頭比較）\n",
    "NOTE = '預設設定'  #@param {type:\"string\"}\n",
    "#@markdown ---\n",
    "#@markdown ### ① 模型（P49；SVM 見 P106）\n",
    "MODEL = 'NN'  #@param ['NN', 'SVM']\n",
    "#@markdown ### ② 特徵：給模型看哪些欄位（P26、P55）\n",
    "#@markdown sex 性別、pclass 艙等、age 年齡、fare 票價、sibsp 同行兄弟姊妹／配偶數、parch 同行父母／子女數、embarked 登船港口\n",
    "use_sex = True  #@param {type:\"boolean\"}\n",
    "use_pclass = True  #@param {type:\"boolean\"}\n",
    "use_age = True  #@param {type:\"boolean\"}\n",
    "use_fare = True  #@param {type:\"boolean\"}\n",
    "use_sibsp = True  #@param {type:\"boolean\"}\n",
    "use_parch = True  #@param {type:\"boolean\"}\n",
    "use_embarked = True  #@param {type:\"boolean\"}\n",
    "#@markdown 自己造的特徵：family_size 家庭人數、is_child 是否未滿 16 歲、age_missing 年齡是否缺值（P59）、birth_month 出生月份（模擬的無關特徵）\n",
    "use_family_size = False  #@param {type:\"boolean\"}\n",
    "use_is_child = False  #@param {type:\"boolean\"}\n",
    "use_age_missing = False  #@param {type:\"boolean\"}\n",
    "use_birth_month = False  #@param {type:\"boolean\"}\n",
    "#@markdown 資料表裡原本就有、還沒用的欄位：who（man／woman／child）、adult_male、alone、deck（缺七成）\n",
    "use_who = False  #@param {type:\"boolean\"}\n",
    "use_adult_male = False  #@param {type:\"boolean\"}\n",
    "use_alone = False  #@param {type:\"boolean\"}\n",
    "use_deck = False  #@param {type:\"boolean\"}\n",
    "#@markdown ### ③ 資料前處理：補值（P58）、標準化（P62）\n",
    "IMPUTE = 'median'  #@param ['median', 'mean', 'zero']\n",
    "SCALE = True  #@param {type:\"boolean\"}\n",
    "#@markdown ### ④ 網路結構：隱藏層數（0 層＝Logistic）、每層神經元、激活函數（P48）\n",
    "HIDDEN_LAYERS = 1  #@param {type:\"slider\", min:0, max:3, step:1}\n",
    "NEURONS = 16  #@param [2, 4, 8, 16, 32, 64, 128, 256] {type:\"raw\"}\n",
    "ACTIVATION = 'relu'  #@param ['relu', 'sigmoid', 'tanh', 'none']\n",
    "#@markdown ### ⑤ 損失函數（P41、P43）\n",
    "LOSS = 'BCE'  #@param ['BCE', 'MSE', 'MAE']\n",
    "#@markdown ### ⑥ 梯度下降：最佳化器（P52）、學習率（P20）、Batch 與 Epoch（P19）\n",
    "OPTIMIZER = 'SGD'  #@param ['SGD', 'Momentum', 'Adam']\n",
    "LR = 0.01  #@param [0.0001, 0.001, 0.01, 0.03, 0.1, 0.3, 1.0, 3.0, 10.0] {type:\"raw\"}\n",
    "MOMENTUM = 0.9  #@param {type:\"slider\", min:0, max:0.99, step:0.01}\n",
    "BATCH = 32  #@param [1, 8, 32, 128, 534] {type:\"raw\"}\n",
    "EPOCHS = 20  #@param {type:\"slider\", min:1, max:300, step:1}\n",
    "#@markdown MOMENTUM 只在 OPTIMIZER＝Momentum 時有用；BATCH＝534 是整個訓練集一次算（full batch）。\n",
    "#@markdown LR_STEP_EVERY：每幾個 epoch 把學習率減半（step 衰減）；0＝不減。\n",
    "LR_STEP_EVERY = 0  #@param {type:\"slider\", min:0, max:100, step:5}\n",
    "#@markdown ### ⑦ 泛化：L2（P82）、Dropout（P83）、早停（P84）\n",
    "L2 = 0  #@param [0, 0.0001, 0.001, 0.01, 0.1] {type:\"raw\"}\n",
    "DROPOUT = 0.0  #@param {type:\"slider\", min:0, max:0.8, step:0.1}\n",
    "EARLY_STOP = False  #@param {type:\"boolean\"}\n",
    "PATIENCE = 10  #@param {type:\"slider\", min:1, max:50, step:1}\n",
    "#@markdown ### ⑧ SVM（MODEL＝SVM 時才用）：核函數（P121）、C（P114）、γ（P125）\n",
    "SVM_KERNEL = 'rbf'  #@param ['rbf', 'linear', 'poly']\n",
    "SVM_C = 1.0  #@param [0.01, 0.1, 1.0, 10.0, 100.0] {type:\"raw\"}\n",
    "SVM_GAMMA = 'scale'  #@param ['scale', '0.01', '0.1', '1', '10']\n",
    "#@markdown ### ⑨ 亂數種子：初始權重與洗牌順序（P85）\n",
    "SEED_RUN = 42  #@param {type:\"integer\"}\n",
    "\n",
    "FEATURES_USED = [name for name, on in [\n",
    "    ('sex', use_sex), ('pclass', use_pclass), ('age', use_age), ('fare', use_fare), ('sibsp', use_sibsp),\n",
    "    ('parch', use_parch), ('embarked', use_embarked), ('family_size', use_family_size),\n",
    "    ('is_child', use_is_child), ('age_missing', use_age_missing), ('birth_month', use_birth_month),\n",
    "    ('who', use_who), ('adult_male', use_adult_male), ('alone', use_alone), ('deck', use_deck)] if on]\n",
    "run_id = t_run(model=MODEL, features=FEATURES_USED, impute=IMPUTE, scale=SCALE,\n",
    "               hidden_layers=HIDDEN_LAYERS, neurons=NEURONS, activation=ACTIVATION, loss=LOSS,\n",
    "               optimizer=OPTIMIZER, lr=LR, momentum=MOMENTUM, batch=BATCH, epochs=EPOCHS, lr_step_every=LR_STEP_EVERY,\n",
    "               l2=L2, dropout=DROPOUT, early_stop=EARLY_STOP, patience=PATIENCE,\n",
    "               svm_kernel=SVM_KERNEL, svm_C=SVM_C, svm_gamma=SVM_GAMMA, seed=SEED_RUN, note=NOTE)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6472c847",
   "metadata": {},
   "source": [
    "### A5-3｜挑戰任務：一次只改一個旋鈕\n",
    "\n",
    "每個任務只改表中那一個旋鈕，其他維持不變；把改了什麼寫在 `NOTE`，跑完看圖和診斷訊息。\n",
    "建議順序：先做任務 1–3，把找到的好設定固定下來，再做 4–9。\n",
    "\n",
    "| 任務 | 改哪裡 | 講義 | 想一想 |\n",
    "|---|---|---|---|\n",
    "| 1 學習率 | `LR`：0.0001 → 0.01 → 1.0 → 10.0 | P20 | 太小和太大，損失曲線各長什麼樣？ |\n",
    "| 2 Step 與 Batch | `EPOCHS` 20 → 100；`BATCH` 1、32、534 | P19 | 印出的 steps 差幾倍？曲線哪個比較抖？ |\n",
    "| 3 最佳化器 | `OPTIMIZER`：SGD → Momentum → Adam | P52 | 同樣 20 個 epoch，誰先把損失降下來？ |\n",
    "| 4 損失函數 | 固定 `OPTIMIZER`=SGD、`EPOCHS`=60，`LOSS`：BCE → MSE → MAE | P43 | 哪個學得最慢？和講義的「推力」圖對得上嗎？換成 Momentum 後差距還在嗎？ |\n",
    "| 5 ReLU 與層數 | `HIDDEN_LAYERS` 0 vs 1；`ACTIVATION` relu vs none | P48 | 沒有激活函數的一層網路，和 Logistic 差多少？ |\n",
    "| 6 製造過擬合，再救回來 | 先 `NEURONS`=256、`HIDDEN_LAYERS`=2、`OPTIMIZER`=Adam、`EPOCHS`=200；再分別開 `L2`=0.01、`DROPOUT`=0.5、`EARLY_STOP` | P78、P82–P84 | 訓練和驗證差距怎麼變？哪個工具最有效？ |\n",
    "| 7 特徵 | 取消 `use_sex`；勾 `use_family_size`、`use_is_child`、`use_who`、`use_deck`、`use_birth_month` | P26、P59 | birth_month 和生還無關，為什麼分數還可能變高一點？（提示：任務 10 換種子） |\n",
    "| 8 標準化 | 取消 `SCALE` | P62 | 為什麼梯度下降變難走？ |\n",
    "| 9 換成 SVM | `MODEL`=SVM；`SVM_C` 0.1 → 100；`SVM_GAMMA` 0.01 → 10；`SVM_KERNEL` linear | P114、P125 | 和你最好的神經網路比，誰贏？ |\n",
    "| 10 自由挑戰 | 任意組合 | P85 | 衝上排行榜第一；再換 `SEED_RUN` 驗證是不是運氣 |\n",
    "\n",
    "驗證集只有 178 人，1 位乘客＝0.56%。反覆挑最高分，也會慢慢「背」驗證集（P85），所以最後要用測試集驗收。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8e551f90",
   "metadata": {},
   "source": [
    "## A5-4｜🏆 終極挑戰：自己處理資料，把驗證損失壓到目標線以下\n",
    "\n",
    "A0 學過的寫法都能用，**不必照 A0 的步驟**：只要 `my_prepare` 最後交出一張「全是數字、沒有缺值、列數不變」的表，模型就吃得下。格式不對時會告訴你哪一欄、怎麼修。\n",
    "\n",
    "**目標**（驗證損失，3 個亂數種子平均，避免靠運氣）：🥉 ≤ 0.400（平均把握 ≥ 67.0%）　🥈 ≤ 0.390（≥ 67.7%）　🥇 ≤ 0.380（≥ 68.4%）\n",
    "\n",
    "平均把握 $=e^{-L}=\\left(q_1\\,q_2\\cdots q_N\\right)^{1/N}$，算法見 A5-2。\n",
    "\n",
    "**可以動的**：①勾選特徵（15 個，起點只勾 sex、pclass、age）；②數字欄的補值方式與標準化／歸一化；③模型：隱藏層數、神經元、最佳化器、L2、Dropout…；④勾選裡沒有的特徵，直接寫進 `my_prepare` 最下面。每個勾選框實際做了什麼，都寫在 `my_prepare` 裡，展開程式就看得到。\n",
    "\n",
    "**可用欄位**：`sex`、`pclass`、`age`、`fare`、`sibsp`、`parch`、`embarked`、`who`（man／woman／child）、`adult_male`、`alone`、`deck`、`class`、`embark_town`，以及已經幫你造好的 `family_size`、`is_child`、`age_missing`、`birth_month`（模擬的無關特徵）。\n",
    "\n",
    "想一想：`class` 和 `pclass` 有什麼不同？`deck` 缺了七成，還能用嗎？`alive` 放進去會發生什麼事？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "28644d5e",
   "metadata": {},
   "outputs": [],
   "source": [
    "#@title 🏆 我的資料處理＋模型（勾選特徵、選處理方式，再按 ▶；想自己造特徵，改下面的 my_prepare）\n",
    "#@markdown ### ① 勾選要放進去的特徵\n",
    "#@markdown sex 性別、pclass 艙等、age 年齡、fare 票價、sibsp 同行兄弟姊妹／配偶數、parch 同行父母／子女數、embarked 登船港口\n",
    "F_sex = True  #@param {type:\"boolean\"}\n",
    "F_pclass = True  #@param {type:\"boolean\"}\n",
    "F_age = True  #@param {type:\"boolean\"}\n",
    "F_fare = False  #@param {type:\"boolean\"}\n",
    "F_sibsp = False  #@param {type:\"boolean\"}\n",
    "F_parch = False  #@param {type:\"boolean\"}\n",
    "F_embarked = False  #@param {type:\"boolean\"}\n",
    "#@markdown 自己造的：family 家庭人數、child 是否未滿 16 歲、who 男／女／兒童、alone 獨自一人、adult_male 成年男性、age_missing 年齡是否缺值、deck 甲板（缺七成）、birth_month 出生月份（模擬的無關特徵）\n",
    "F_family = False  #@param {type:\"boolean\"}\n",
    "F_child = False  #@param {type:\"boolean\"}\n",
    "F_who = False  #@param {type:\"boolean\"}\n",
    "F_alone = False  #@param {type:\"boolean\"}\n",
    "F_adult_male = False  #@param {type:\"boolean\"}\n",
    "F_age_missing = False  #@param {type:\"boolean\"}\n",
    "F_deck = False  #@param {type:\"boolean\"}\n",
    "F_birth_month = False  #@param {type:\"boolean\"}\n",
    "#@markdown ### ② 數字欄（age、fare、family）怎麼處理\n",
    "FILL = 'median'  #@param ['median', 'mean', 'zero']\n",
    "NUM_SCALE = 'standard'  #@param ['standard', 'minmax', 'none']\n",
    "#@markdown FILL：缺值補中位數／平均／0；NUM_SCALE：標準化 (x − 平均) ÷ 標準差／歸一化到 0～1／不處理。數字都只從訓練集算。\n",
    "\n",
    "def my_prepare(df, train):\n",
    "    # df：要處理的表（訓練集或驗證集）；train：訓練集，補值、平均、最大最小都只能從這裡算\n",
    "    X = pd.DataFrame(index=df.index)\n",
    "    def num(name, col, ref):                                   # 數字欄：補值 → 縮放\n",
    "        fill = {'median': ref.median(), 'mean': ref.mean(), 'zero': 0}[FILL]\n",
    "        col, ref = col.fillna(fill), ref.fillna(fill)\n",
    "        if NUM_SCALE == 'standard': col = (col - ref.mean()) / ref.std()\n",
    "        if NUM_SCALE == 'minmax': col = (col - ref.min()) / (ref.max() - ref.min())\n",
    "        X[name] = col\n",
    "    def onehot(col, prefix):                                   # 類別欄：one-hot\n",
    "        return pd.concat([X, pd.get_dummies(col, prefix=prefix, dtype=int)], axis=1)\n",
    "    fam, fam_train = df[\"sibsp\"] + df[\"parch\"] + 1, train[\"sibsp\"] + train[\"parch\"] + 1\n",
    "    if F_sex: X[\"female\"] = (df[\"sex\"] == \"female\").astype(int)        # 文字 → 0／1\n",
    "    if F_pclass: X = onehot(df[\"pclass\"], \"class\")\n",
    "    if F_age: num(\"age\", df[\"age\"], train[\"age\"])\n",
    "    if F_fare: num(\"fare\", df[\"fare\"], train[\"fare\"])\n",
    "    if F_sibsp: X[\"sibsp\"] = df[\"sibsp\"]\n",
    "    if F_parch: X[\"parch\"] = df[\"parch\"]\n",
    "    if F_embarked: X = onehot(df[\"embarked\"], \"port\")\n",
    "    if F_family: num(\"family\", fam, fam_train)\n",
    "    if F_child: X[\"child\"] = (df[\"age\"] < 16).astype(int)\n",
    "    if F_who: X = onehot(df[\"who\"], \"who\")\n",
    "    if F_alone: X[\"alone\"] = (fam == 1).astype(int)\n",
    "    if F_adult_male: X[\"adult_male\"] = (df[\"who\"] == \"man\").astype(int)\n",
    "    if F_age_missing: X[\"age_missing\"] = df[\"age\"].isna().astype(int)\n",
    "    if F_deck: X = onehot(df[\"deck\"].fillna(\"unknown\"), \"deck\")\n",
    "    if F_birth_month: X[\"birth_month\"] = df[\"birth_month\"]\n",
    "    # ✏️ 想加勾選裡沒有的特徵，寫在這裡，例如：\n",
    "    # X[\"fare_per_person\"] = df[\"fare\"] / fam                    # 每人分攤的票價\n",
    "    # X[\"old\"] = (df[\"age\"] >= 60).astype(int)                   # 是否 60 歲以上\n",
    "    return X\n",
    "\n",
    "#@markdown ### ③ 模型設定（隱藏層數 0＝Logistic）\n",
    "MY_LAYERS = 1  #@param {type:\"slider\", min:0, max:3, step:1}\n",
    "MY_NEURONS = 16  #@param [2, 4, 8, 16, 32, 64, 128] {type:\"raw\"}\n",
    "MY_ACTIVATION = 'relu'  #@param ['relu', 'sigmoid', 'tanh', 'none']\n",
    "MY_OPTIMIZER = 'Momentum'  #@param ['SGD', 'Momentum', 'Adam']\n",
    "MY_LR = 0.01  #@param [0.001, 0.01, 0.03, 0.1] {type:\"raw\"}\n",
    "MY_EPOCHS = 60  #@param {type:\"slider\", min:10, max:200, step:10}\n",
    "MY_L2 = 0  #@param [0, 0.0001, 0.001, 0.01, 0.1] {type:\"raw\"}\n",
    "MY_DROPOUT = 0.0  #@param {type:\"slider\", min:0, max:0.8, step:0.1}\n",
    "MY_NOTE = '起點：sex、pclass、age'  #@param {type:\"string\"}\n",
    "t_challenge(my_prepare, note=MY_NOTE, prep=dict(FILL=FILL, NUM_SCALE=NUM_SCALE), hidden_layers=MY_LAYERS, neurons=MY_NEURONS, activation=MY_ACTIVATION,\n",
    "            optimizer=MY_OPTIMIZER, lr=MY_LR, epochs=MY_EPOCHS, l2=MY_L2, dropout=MY_DROPOUT)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9833da2d",
   "metadata": {},
   "source": [
    "### 🎛️ 把幾個回合疊在一起比較\n",
    "\n",
    "填回合編號（排行榜第一欄）。實線＝驗證，虛線＝訓練（只有用 BCE 訓練的回合才畫）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0fb1cfc9",
   "metadata": {},
   "outputs": [],
   "source": [
    "COMPARE = [0, 1]   # 🎛️ 改這裡：想比較的回合編號，-1＝目前第一名\n",
    "t_compare(COMPARE)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a3af319f",
   "metadata": {},
   "source": [
    "### 完整排行榜"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ad9ac794",
   "metadata": {},
   "outputs": [],
   "source": [
    "t_board()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b9625db",
   "metadata": {},
   "source": [
    "# 實作③　門檻、最後測試、如果你在船上（P100）\n",
    "\n",
    "## A4｜🎛️ 門檻與混淆矩陣\n",
    "\n",
    "門檻只改「判定」，不重新訓練（P92）。把 `THRESHOLD` 拉到 0.3、0.7，看 TP、FP、FN 怎麼變（P90、P91）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef462444",
   "metadata": {},
   "outputs": [],
   "source": [
    "#@title 🎛️ 門檻\n",
    "RUN = -1  #@param {type:\"integer\"}\n",
    "#@markdown RUN：回合編號，-1＝排行榜第一名\n",
    "THRESHOLD = 0.5  #@param {type:\"slider\", min:0.05, max:0.95, step:0.05}\n",
    "t_threshold(RUN, THRESHOLD)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "145218f0",
   "metadata": {},
   "source": [
    "## A7｜最後測試：Kaggle 測試集 418 人，只看一次\n",
    "\n",
    "選你在驗證集上最有把握的回合（不一定是第一名，也可以選差距小但比較簡單的模型），固定門檻，再把 `CONFIRM` 改成 True。測試結果出來後，不再回頭調參。\n",
    "\n",
    "這 418 人就是 Kaggle「Titanic - Machine Learning from Disaster」比賽要預測的乘客，分數可以直接和 Kaggle 上的成績比較：\n",
    "- 全猜未生還：0.622\n",
    "- Kaggle 官方範例「女性全活、男性全死」：0.766（要先贏過它）\n",
    "- 誠實訓練的模型通常在 0.76～0.80。排行榜上的 1.0 多半是抄網路上的真實名單（資料洩漏），不是模型做出來的。\n",
    "\n",
    "測試分數通常比驗證低幾個百分點：驗證集被你反覆用來挑設定，已經不算「沒看過的資料」了（P85）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a0ccf066",
   "metadata": {},
   "outputs": [],
   "source": [
    "SELECTED_RUN = -1        # 🎛️ 回合編號，-1＝排行榜第一名\n",
    "FINAL_THRESHOLD = 0.5\n",
    "CONFIRM = False\n",
    "t_finish(SELECTED_RUN, FINAL_THRESHOLD, confirm=CONFIRM)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d0ec6e2",
   "metadata": {},
   "source": [
    "## A6｜🎛️ 如果我在船上？（P102–P103）\n",
    "\n",
    "填你的資料（也可以是虛構人物）；male／female 是這份歷史資料的編碼限制。\n",
    "會看到：①你的生還機率；②只改一個條件（艙等、性別、年齡、家人、出生月份）時機率怎麼變；③不同模型給你的機率。\n",
    "想一想：如果模型說 30%，代表什麼？不代表什麼？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f69e64e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "#@title 🎛️ 填入你的資料\n",
    "sex = 'female'  #@param ['male', 'female']\n",
    "age = 25  #@param {type:\"slider\", min:1, max:80, step:1}\n",
    "pclass = 2  #@param [1, 2, 3] {type:\"raw\"}\n",
    "fare = 30  #@param {type:\"number\"}\n",
    "sibsp = 0  #@param {type:\"slider\", min:0, max:8, step:1}\n",
    "parch = 0  #@param {type:\"slider\", min:0, max:6, step:1}\n",
    "embarked = 'S'  #@param ['S', 'C', 'Q']\n",
    "birth_month = 5  #@param {type:\"slider\", min:1, max:12, step:1}\n",
    "USE_RUN = -1  #@param {type:\"integer\"}\n",
    "#@markdown USE_RUN：用哪個回合的模型預測，-1＝排行榜第一名\n",
    "me = dict(sex=sex, age=age, pclass=pclass, fare=fare, sibsp=sibsp, parch=parch, embarked=embarked, birth_month=birth_month)\n",
    "t_person(me, run_id=USE_RUN)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ca471fb",
   "metadata": {},
   "source": [
    "### A6-2｜加入你自己的特徵，再預測一次\n",
    "\n",
    "回到 A5-2 控制台，勾選 `use_family_size`、`use_is_child`（或 `use_birth_month`），`NOTE` 寫「加特徵」，按 ▶。\n",
    "再回到這裡，把 `USE_RUN` 改成新的回合編號重跑：你的機率變了嗎？出生月份那一條，模型有沒有當真？\n",
    "（看過測試集之後的回合會標「看過測試後」，只能當探索。）"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69cf5d34",
   "metadata": {},
   "source": [
    "## A8｜反思與繳交\n",
    "\n",
    "保留圖、排行榜與文字反思，繳交可檢視的分享連結。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "06074ec4",
   "metadata": {},
   "outputs": [],
   "source": [
    "best_setting = \"請填寫\"          # 你的第一名用了什麼設定？比預設進步多少，主要靠哪個旋鈕？挑戰區拿到哪面牌？\n",
    "worst_knob = \"請填寫\"            # 哪個旋鈕讓結果變最差？用講義的觀念說明為什麼\n",
    "generalization = \"請填寫\"        # 過擬合時你用了哪個泛化工具？訓練／驗證差距怎麼變？\n",
    "threshold_note = \"請填寫\"        # 降低門檻時哪種錯誤變多？\n",
    "my_probability = \"請填寫\"        # 模型說你的生還機率幾 %？代表什麼、不代表什麼？\n",
    "print(best_setting, worst_knob, generalization, threshold_note, my_probability, sep='\\n')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7aea7628",
   "metadata": {},
   "source": [
    "## 資料來源\n",
    "\n",
    "- https://github.com/mwaskom/seaborn-data/blob/master/titanic.csv（891 位乘客，非完整航船名單）"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
