{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "4b503f88",
   "metadata": {},
   "source": [
    "# 第 3 章　大數據收集與資料前處理實戰\n",
    "\n",
    "這是「醫學生的機器學習入門」第 3 章的配套 notebook。建議搭配網站章節一起讀：網站負責講直覺與觀念，這份 notebook 負責讓你親手跑一次。\n",
    "\n",
    "- **在 Colab 執行**：上方選單「執行階段 → 全部執行」即可，不需要安裝額外套件（`requests`、`beautifulsoup4` 都已預裝）。\n",
    "- **網路**：第一部分會連到 UCI、WHO 與 OpenML。若網路或對方伺服器暫時失敗，程式會自動改用**離線備用資料**並清楚印出提示，後面的儲存格仍然可以跑。\n",
    "- **圖上文字用英文**：Colab 預設沒有中文字型，中文會變成方塊。\n",
    "\n",
    "> 本 notebook 的醫學資料是公開教學資料集，僅供學習，不構成臨床建議。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19dbf3dd",
   "metadata": {},
   "source": [
    "## 0. 環境與版本\n",
    "\n",
    "先印出版本。本教材以 Colab 現況（scikit-learn 1.6 / pandas 2.2）為底線，也在較新的 pandas 3 / scikit-learn 1.9 測過。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "856e33a9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:24.312585Z",
     "iopub.status.busy": "2026-09-29T20:04:24.312381Z",
     "iopub.status.idle": "2026-09-29T20:04:25.298799Z",
     "shell.execute_reply": "2026-09-29T20:04:25.298463Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Python 3.12.2\n",
      "numpy 2.1.3 | pandas 2.2.3 | scikit-learn 1.6.1\n"
     ]
    }
   ],
   "source": [
    "import io\n",
    "import sys\n",
    "import warnings\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import requests\n",
    "import sklearn\n",
    "\n",
    "print(\"Python\", sys.version.split()[0])\n",
    "print(\"numpy\", np.__version__, \"| pandas\", pd.__version__, \"| scikit-learn\", sklearn.__version__)\n",
    "\n",
    "# scikit-learn 1.6 + SciPy 1.16 (the Colab combo) prints a harmless solver-option notice\n",
    "warnings.filterwarnings(\"ignore\", message=\"Unknown solver options\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f645829b",
   "metadata": {},
   "source": [
    "## 1. 資料從哪裡來\n",
    "\n",
    "### 1-1 直接讀 CSV 網址\n",
    "\n",
    "UCI Machine Learning Repository 的 Chronic Kidney Disease（慢性腎臟病）資料集（id 336，CC BY 4.0）有一個直接的 CSV 網址。我們先用 `requests` 下載（設定 `timeout`，避免網路卡住時整本 notebook 停住），再交給 pandas。\n",
    "\n",
    "下一格是**離線備用資料產生器**：只有在下載失敗時才會用到。它產生欄位相同的**模擬資料**，數字不是真的病人，只讓後面的程式碼能繼續練習。可以先略過不讀。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "7f50a9ba",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:25.299977Z",
     "iopub.status.busy": "2026-09-29T20:04:25.299871Z",
     "iopub.status.idle": "2026-09-29T20:04:25.303638Z",
     "shell.execute_reply": "2026-09-29T20:04:25.303390Z"
    }
   },
   "outputs": [],
   "source": [
    "def make_fake_ckd(n_ckd=250, n_healthy=150, seed=42):\n",
    "    \"\"\"Synthetic stand-in with the same columns as UCI CKD (offline practice only).\"\"\"\n",
    "    g = np.random.default_rng(seed)\n",
    "    y = np.array([\"ckd\"] * n_ckd + [\"notckd\"] * n_healthy)\n",
    "    sick = y == \"ckd\"\n",
    "    n = len(y)\n",
    "    # column: (mean if CKD, mean if healthy, sd)\n",
    "    spec = {\"age\": (55, 46, 15), \"bp\": (80, 71, 10), \"bgr\": (170, 110, 60),\n",
    "            \"bu\": (75, 33, 40), \"sod\": (133, 141, 6), \"pot\": (4.8, 4.3, 0.8),\n",
    "            \"hemo\": (10.6, 15.2, 2.0), \"pcv\": (32, 46, 6), \"wbcc\": (8900, 7700, 2500),\n",
    "            \"rbcc\": (3.9, 5.4, 0.8)}\n",
    "    df = pd.DataFrame({c: np.round(np.where(sick, g.normal(a, s, n), g.normal(b, s, n)), 1)\n",
    "                       for c, (a, b, s) in spec.items()})\n",
    "    df[\"sg\"] = np.where(sick, g.choice([1.005, 1.010, 1.015], n), g.choice([1.020, 1.025], n))\n",
    "    df[\"al\"] = np.where(sick, g.integers(0, 5, n), 0).astype(float)\n",
    "    df[\"su\"] = np.where(sick, g.integers(0, 3, n), 0).astype(float)\n",
    "    df[\"sc\"] = np.round(np.where(sick, g.lognormal(1.0, 0.8, n), g.normal(0.9, 0.2, n)), 1)\n",
    "    # column: (P(abnormal) if CKD, P(abnormal) if healthy, abnormal label, normal label)\n",
    "    cats = {\"rbc\": (0.3, 0.0, \"abnormal\", \"normal\"), \"pc\": (0.35, 0.0, \"abnormal\", \"normal\"),\n",
    "            \"pcc\": (0.15, 0.0, \"present\", \"notpresent\"), \"ba\": (0.08, 0.0, \"present\", \"notpresent\"),\n",
    "            \"htn\": (0.6, 0.0, \"yes\", \"no\"), \"dm\": (0.55, 0.0, \"yes\", \"no\"),\n",
    "            \"cad\": (0.13, 0.0, \"yes\", \"no\"), \"appet\": (0.3, 0.0, \"poor\", \"good\"),\n",
    "            \"pe\": (0.3, 0.0, \"yes\", \"no\"), \"ane\": (0.25, 0.0, \"yes\", \"no\")}\n",
    "    for c, (p1, p0, bad, ok) in cats.items():\n",
    "        p = np.where(sick, p1, p0)\n",
    "        df[c] = np.where(g.random(n) < p, bad, ok).astype(object)\n",
    "    df[\"class\"] = y.astype(object)\n",
    "    # more missing values among CKD patients, like the real data\n",
    "    for c in df.columns.drop(\"class\"):\n",
    "        mask = g.random(n) < np.where(sick, 0.14, 0.03)\n",
    "        df.loc[mask, c] = np.nan\n",
    "    # the same dirty values the real file has\n",
    "    df.loc[3, \"dm\"] = \"\\tno\"\n",
    "    df.loc[5, \"class\"] = \"ckd\\t\"\n",
    "    df.loc[10, \"sod\"] = 4.5\n",
    "    df.loc[11, \"pot\"] = 47.0\n",
    "    order = [\"age\", \"bp\", \"sg\", \"al\", \"su\", \"rbc\", \"pc\", \"pcc\", \"ba\", \"bgr\", \"bu\", \"sc\", \"sod\",\n",
    "             \"pot\", \"hemo\", \"pcv\", \"wbcc\", \"rbcc\", \"htn\", \"dm\", \"cad\", \"appet\", \"pe\", \"ane\", \"class\"]\n",
    "    return df[order]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "976422c6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:25.304561Z",
     "iopub.status.busy": "2026-09-29T20:04:25.304504Z",
     "iopub.status.idle": "2026-09-29T20:04:26.032638Z",
     "shell.execute_reply": "2026-09-29T20:04:26.032106Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Loaded UCI CKD: (400, 25)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>age</th>\n",
       "      <th>bp</th>\n",
       "      <th>sg</th>\n",
       "      <th>al</th>\n",
       "      <th>su</th>\n",
       "      <th>rbc</th>\n",
       "      <th>pc</th>\n",
       "      <th>pcc</th>\n",
       "      <th>ba</th>\n",
       "      <th>bgr</th>\n",
       "      <th>...</th>\n",
       "      <th>pcv</th>\n",
       "      <th>wbcc</th>\n",
       "      <th>rbcc</th>\n",
       "      <th>htn</th>\n",
       "      <th>dm</th>\n",
       "      <th>cad</th>\n",
       "      <th>appet</th>\n",
       "      <th>pe</th>\n",
       "      <th>ane</th>\n",
       "      <th>class</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>48.0</td>\n",
       "      <td>80.0</td>\n",
       "      <td>1.020</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>normal</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>121.0</td>\n",
       "      <td>...</td>\n",
       "      <td>44.0</td>\n",
       "      <td>7800.0</td>\n",
       "      <td>5.2</td>\n",
       "      <td>yes</td>\n",
       "      <td>yes</td>\n",
       "      <td>no</td>\n",
       "      <td>good</td>\n",
       "      <td>no</td>\n",
       "      <td>no</td>\n",
       "      <td>ckd</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>7.0</td>\n",
       "      <td>50.0</td>\n",
       "      <td>1.020</td>\n",
       "      <td>4.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>normal</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>38.0</td>\n",
       "      <td>6000.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>no</td>\n",
       "      <td>no</td>\n",
       "      <td>no</td>\n",
       "      <td>good</td>\n",
       "      <td>no</td>\n",
       "      <td>no</td>\n",
       "      <td>ckd</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>62.0</td>\n",
       "      <td>80.0</td>\n",
       "      <td>1.010</td>\n",
       "      <td>2.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>normal</td>\n",
       "      <td>normal</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>423.0</td>\n",
       "      <td>...</td>\n",
       "      <td>31.0</td>\n",
       "      <td>7500.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>no</td>\n",
       "      <td>yes</td>\n",
       "      <td>no</td>\n",
       "      <td>poor</td>\n",
       "      <td>no</td>\n",
       "      <td>yes</td>\n",
       "      <td>ckd</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>48.0</td>\n",
       "      <td>70.0</td>\n",
       "      <td>1.005</td>\n",
       "      <td>4.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>normal</td>\n",
       "      <td>abnormal</td>\n",
       "      <td>present</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>117.0</td>\n",
       "      <td>...</td>\n",
       "      <td>32.0</td>\n",
       "      <td>6700.0</td>\n",
       "      <td>3.9</td>\n",
       "      <td>yes</td>\n",
       "      <td>no</td>\n",
       "      <td>no</td>\n",
       "      <td>poor</td>\n",
       "      <td>yes</td>\n",
       "      <td>yes</td>\n",
       "      <td>ckd</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>51.0</td>\n",
       "      <td>80.0</td>\n",
       "      <td>1.010</td>\n",
       "      <td>2.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>normal</td>\n",
       "      <td>normal</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>notpresent</td>\n",
       "      <td>106.0</td>\n",
       "      <td>...</td>\n",
       "      <td>35.0</td>\n",
       "      <td>7300.0</td>\n",
       "      <td>4.6</td>\n",
       "      <td>no</td>\n",
       "      <td>no</td>\n",
       "      <td>no</td>\n",
       "      <td>good</td>\n",
       "      <td>no</td>\n",
       "      <td>no</td>\n",
       "      <td>ckd</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 25 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "    age    bp     sg   al   su     rbc        pc         pcc          ba  \\\n",
       "0  48.0  80.0  1.020  1.0  0.0     NaN    normal  notpresent  notpresent   \n",
       "1   7.0  50.0  1.020  4.0  0.0     NaN    normal  notpresent  notpresent   \n",
       "2  62.0  80.0  1.010  2.0  3.0  normal    normal  notpresent  notpresent   \n",
       "3  48.0  70.0  1.005  4.0  0.0  normal  abnormal     present  notpresent   \n",
       "4  51.0  80.0  1.010  2.0  0.0  normal    normal  notpresent  notpresent   \n",
       "\n",
       "     bgr  ...   pcv    wbcc  rbcc  htn   dm  cad  appet   pe  ane class  \n",
       "0  121.0  ...  44.0  7800.0   5.2  yes  yes   no   good   no   no   ckd  \n",
       "1    NaN  ...  38.0  6000.0   NaN   no   no   no   good   no   no   ckd  \n",
       "2  423.0  ...  31.0  7500.0   NaN   no  yes   no   poor   no  yes   ckd  \n",
       "3  117.0  ...  32.0  6700.0   3.9  yes   no   no   poor  yes  yes   ckd  \n",
       "4  106.0  ...  35.0  7300.0   4.6   no   no   no   good   no   no   ckd  \n",
       "\n",
       "[5 rows x 25 columns]"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "CKD_URL = \"https://archive.ics.uci.edu/static/public/336/data.csv\"\n",
    "\n",
    "def load_ckd(url=CKD_URL):\n",
    "    \"\"\"Download UCI CKD (id 336); fall back to synthetic data if offline.\"\"\"\n",
    "    try:\n",
    "        r = requests.get(url, timeout=30)\n",
    "        r.raise_for_status()\n",
    "        df = pd.read_csv(io.StringIO(r.text))\n",
    "        print(\"Loaded UCI CKD:\", df.shape)\n",
    "        return df\n",
    "    except Exception as e:\n",
    "        print(\"Download failed:\", type(e).__name__, \"-> using SYNTHETIC fallback (not real patients)\")\n",
    "        return make_fake_ckd()\n",
    "\n",
    "ckd = load_ckd()\n",
    "ckd.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9a89e8aa",
   "metadata": {},
   "source": [
    "### 1-2 開放資料 API：WHO Global Health Observatory\n",
    "\n",
    "API 就像一張檢驗申請單：你照規定格式填好「要什麼、篩選條件」，對方回傳結構化的結果。這裡查 WHO GHO 的出生時平均餘命（指標代碼 `WHOSIS_000001`），只取「男女合計」與四個國家。`$filter` 是這個 API（OData 格式）的篩選語法。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "0f4cfe27",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.034704Z",
     "iopub.status.busy": "2026-09-29T20:04:26.034547Z",
     "iopub.status.idle": "2026-09-29T20:04:26.373739Z",
     "shell.execute_reply": "2026-09-29T20:04:26.373361Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Rows from WHO API: 88\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th>SpatialDim</th>\n",
       "      <th>IND</th>\n",
       "      <th>JPN</th>\n",
       "      <th>KOR</th>\n",
       "      <th>USA</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>TimeDim</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>2019</th>\n",
       "      <td>70.7</td>\n",
       "      <td>84.5</td>\n",
       "      <td>83.7</td>\n",
       "      <td>78.7</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2020</th>\n",
       "      <td>70.2</td>\n",
       "      <td>84.7</td>\n",
       "      <td>83.8</td>\n",
       "      <td>76.9</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2021</th>\n",
       "      <td>67.3</td>\n",
       "      <td>84.5</td>\n",
       "      <td>83.8</td>\n",
       "      <td>76.4</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "SpatialDim   IND   JPN   KOR   USA\n",
       "TimeDim                           \n",
       "2019        70.7  84.5  83.7  78.7\n",
       "2020        70.2  84.7  83.8  76.9\n",
       "2021        67.3  84.5  83.8  76.4"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "GHO = \"https://ghoapi.azureedge.net/api/\"\n",
    "params = {\"$filter\": \"Dim1 eq 'SEX_BTSX' and SpatialDim in ('JPN','KOR','USA','IND')\"}\n",
    "\n",
    "try:\n",
    "    r = requests.get(GHO + \"WHOSIS_000001\", params=params, timeout=30)\n",
    "    r.raise_for_status()\n",
    "    life = pd.DataFrame(r.json()[\"value\"])[[\"SpatialDim\", \"TimeDim\", \"NumericValue\"]]\n",
    "    print(\"Rows from WHO API:\", len(life))\n",
    "except Exception as e:\n",
    "    print(\"WHO API unavailable:\", type(e).__name__, \"-> using a small offline sample\")\n",
    "    life = pd.DataFrame({\n",
    "        \"SpatialDim\": [\"IND\", \"JPN\", \"KOR\", \"USA\"] * 2,\n",
    "        \"TimeDim\": [2019] * 4 + [2021] * 4,\n",
    "        \"NumericValue\": [70.7, 84.5, 83.7, 78.7, 67.3, 84.5, 83.8, 76.4]})\n",
    "\n",
    "wide = life.pivot_table(index=\"TimeDim\", columns=\"SpatialDim\", values=\"NumericValue\").round(1)\n",
    "wide.tail(3)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e03a084",
   "metadata": {},
   "source": [
    "### 1-3 爬蟲之前：先看 robots.txt\n",
    "\n",
    "網站用 `robots.txt` 告訴自動程式哪些路徑不歡迎抓取。Python 內建 `urllib.robotparser` 可以讀它。WHO 網站的 robots.txt 封鎖了一長串特定爬蟲，但沒有限制一般使用者代理（`*`）。\n",
    "\n",
    "注意：robots.txt 只是「最低門檻」，網站的服務條款、著作權與個資規範一樣要遵守。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "1dcf1296",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.375156Z",
     "iopub.status.busy": "2026-09-29T20:04:26.374983Z",
     "iopub.status.idle": "2026-09-29T20:04:26.450137Z",
     "shell.execute_reply": "2026-09-29T20:04:26.449814Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "         * may fetch /news ? True\n",
      " AhrefsBot may fetch /news ? False\n"
     ]
    }
   ],
   "source": [
    "from urllib import robotparser\n",
    "\n",
    "ROBOTS_URL = \"https://www.who.int/robots.txt\"\n",
    "rp = robotparser.RobotFileParser()\n",
    "try:\n",
    "    r = requests.get(ROBOTS_URL, timeout=15)\n",
    "    r.raise_for_status()\n",
    "    rp.parse(r.text.splitlines())\n",
    "except Exception as e:\n",
    "    print(\"Could not fetch robots.txt:\", type(e).__name__, \"-> using an example file\")\n",
    "    rp.parse([\"User-agent: AhrefsBot\", \"Disallow: /\"])\n",
    "\n",
    "for agent in [\"*\", \"AhrefsBot\"]:\n",
    "    print(f\"{agent:>10} may fetch /news ?\", rp.can_fetch(agent, \"https://www.who.int/news\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7eed6f77",
   "metadata": {},
   "source": [
    "### 1-4 極小的解析示範（不連網）\n",
    "\n",
    "真的需要從網頁表格取資料時，流程是「確認可以抓 → 下載一頁 → 解析 HTML」。為了不對任何網站造成負擔，這裡直接解析一段寫在程式裡的 HTML，示範 BeautifulSoup 怎麼把表格轉成 DataFrame。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "3265cc3a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.451382Z",
     "iopub.status.busy": "2026-09-29T20:04:26.451317Z",
     "iopub.status.idle": "2026-09-29T20:04:26.466853Z",
     "shell.execute_reply": "2026-09-29T20:04:26.466436Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>test</th>\n",
       "      <th>value</th>\n",
       "      <th>unit</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Creatinine</td>\n",
       "      <td>1.4</td>\n",
       "      <td>mg/dL</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Creatinine</td>\n",
       "      <td>124.0</td>\n",
       "      <td>umol/L</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Hemoglobin</td>\n",
       "      <td>11.2</td>\n",
       "      <td>g/dL</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "         test  value    unit\n",
       "0  Creatinine    1.4   mg/dL\n",
       "1  Creatinine  124.0  umol/L\n",
       "2  Hemoglobin   11.2    g/dL"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from bs4 import BeautifulSoup\n",
    "\n",
    "html = \"\"\"\n",
    "<table id=\"labs\">\n",
    "  <tr><th>test</th><th>value</th><th>unit</th></tr>\n",
    "  <tr><td>Creatinine</td><td>1.4</td><td>mg/dL</td></tr>\n",
    "  <tr><td>Creatinine</td><td>124</td><td>umol/L</td></tr>\n",
    "  <tr><td>Hemoglobin</td><td>11.2</td><td>g/dL</td></tr>\n",
    "</table>\n",
    "\"\"\"\n",
    "soup = BeautifulSoup(html, \"html.parser\")\n",
    "rows = [[td.get_text(strip=True) for td in tr.find_all(\"td\")]\n",
    "        for tr in soup.find(\"table\", id=\"labs\").find_all(\"tr\")[1:]]\n",
    "labs = pd.DataFrame(rows, columns=[\"test\", \"value\", \"unit\"])\n",
    "labs[\"value\"] = pd.to_numeric(labs[\"value\"])\n",
    "labs"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7974dfb",
   "metadata": {},
   "source": [
    "若真的要連網抓頁面，請至少做到下面這個模板的四件事：檢查 robots.txt、表明身分（User-Agent）、設定 timeout、每次請求之間暫停。這一格只**定義**函式，不會實際去抓任何網站。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "87bfd329",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.468177Z",
     "iopub.status.busy": "2026-09-29T20:04:26.468105Z",
     "iopub.status.idle": "2026-09-29T20:04:26.470279Z",
     "shell.execute_reply": "2026-09-29T20:04:26.469920Z"
    }
   },
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "def polite_get(url, rp, agent=\"med-ml-course-demo\", pause=2.0):\n",
    "    \"\"\"Fetch one page only if robots.txt allows it, then wait.\"\"\"\n",
    "    if not rp.can_fetch(agent, url):\n",
    "        raise PermissionError(f\"robots.txt disallows {url}\")\n",
    "    r = requests.get(url, headers={\"User-Agent\": agent}, timeout=15)\n",
    "    r.raise_for_status()\n",
    "    time.sleep(pause)\n",
    "    return r.text"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fca64ac",
   "metadata": {},
   "source": [
    "## 2. 先看資料再動手\n",
    "\n",
    "形狀、型別、遺漏值比例是拿到資料後的三個第一眼。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "6bdd1afc",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.471199Z",
     "iopub.status.busy": "2026-09-29T20:04:26.471150Z",
     "iopub.status.idle": "2026-09-29T20:04:26.474662Z",
     "shell.execute_reply": "2026-09-29T20:04:26.473984Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(400, 25)\n",
      "float64    14\n",
      "object     11\n",
      "Name: count, dtype: int64\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "rbc     0.38\n",
       "rbcc    0.33\n",
       "wbcc    0.26\n",
       "pot     0.22\n",
       "sod     0.22\n",
       "pcv     0.18\n",
       "pc      0.16\n",
       "hemo    0.13\n",
       "dtype: float64"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "print(ckd.shape)\n",
    "print(ckd.dtypes.value_counts())\n",
    "missing_rate = ckd.isna().mean().sort_values(ascending=False)\n",
    "missing_rate.head(8).round(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9efc36f3",
   "metadata": {},
   "source": [
    "### 2-1 髒字串：肉眼看不到的 tab\n",
    "\n",
    "類別欄位的 `unique()` 會暴露問題：有些值前後多了一個 tab 字元（`\\t`），電腦會把 `\"ckd\"` 和 `\"ckd\\t\"` 當成兩個不同的類別。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "55698947",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.475762Z",
     "iopub.status.busy": "2026-09-29T20:04:26.475697Z",
     "iopub.status.idle": "2026-09-29T20:04:26.477587Z",
     "shell.execute_reply": "2026-09-29T20:04:26.477305Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "dm ['yes' 'no' '\\tno' nan]\n",
      "class ['ckd' 'ckd\\t' 'notckd']\n"
     ]
    }
   ],
   "source": [
    "for col in [\"dm\", \"class\"]:\n",
    "    print(col, ckd[col].unique())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c8c8dcb",
   "metadata": {},
   "source": [
    "用 `.str.strip()` 把所有文字欄位的前後空白去掉。注意寫法是 `ckd[col] = ...`，**把結果指派回去**，不用 `inplace=True`（pandas 3 之後很多 inplace 寫法會靜默失效）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "ffb4c76e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.478545Z",
     "iopub.status.busy": "2026-09-29T20:04:26.478491Z",
     "iopub.status.idle": "2026-09-29T20:04:26.481786Z",
     "shell.execute_reply": "2026-09-29T20:04:26.481408Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "class\n",
      "ckd       250\n",
      "notckd    150\n",
      "Name: count, dtype: int64\n"
     ]
    }
   ],
   "source": [
    "text_cols = ckd.select_dtypes(exclude=\"number\").columns\n",
    "for col in text_cols:\n",
    "    ckd[col] = ckd[col].str.strip()\n",
    "\n",
    "print(ckd[\"class\"].value_counts())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6a971c6",
   "metadata": {},
   "source": [
    "### 2-2 遺漏值本身可能是訊號\n",
    "\n",
    "比較兩組病人平均每列缺了幾格。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "19aaee5b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.482770Z",
     "iopub.status.busy": "2026-09-29T20:04:26.482716Z",
     "iopub.status.idle": "2026-09-29T20:04:26.485448Z",
     "shell.execute_reply": "2026-09-29T20:04:26.485143Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "class\n",
       "ckd       3.63\n",
       "notckd    0.69\n",
       "dtype: float64"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "row_missing = ckd.isna().sum(axis=1)\n",
    "row_missing.groupby(ckd[\"class\"]).mean().round(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ced4491e",
   "metadata": {},
   "source": [
    "CKD 組平均缺的格數遠多於非 CKD 組。如果只拿「哪些格子是空的」當特徵，模型能猜得多準？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "a9ad9592",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.486409Z",
     "iopub.status.busy": "2026-09-29T20:04:26.486351Z",
     "iopub.status.idle": "2026-09-29T20:04:26.626267Z",
     "shell.execute_reply": "2026-09-29T20:04:26.625833Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Accuracy using ONLY missingness pattern: 0.81\n"
     ]
    }
   ],
   "source": [
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.model_selection import cross_val_score\n",
    "\n",
    "X_na = ckd.drop(columns=\"class\").isna().astype(int)\n",
    "y_all = (ckd[\"class\"] == \"ckd\").astype(int)\n",
    "acc_na = cross_val_score(LogisticRegression(max_iter=1000), X_na, y_all, cv=5)\n",
    "print(\"Accuracy using ONLY missingness pattern:\", acc_na.mean().round(3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c02a3f1a",
   "metadata": {},
   "source": [
    "完全沒看任何檢驗數值，準確率就遠高於「全部猜 CKD」的 0.625。這代表兩組資料的**收集方式不同**，模型可能學到「資料收集流程」而不是「疾病」。這種模型換一家醫院就可能失靈。\n",
    "\n",
    "### 2-3 另一種遺漏值：用 0 假裝\n",
    "\n",
    "Pima Indians Diabetes 資料集（OpenML 37）裡，血壓、BMI、胰島素等欄位出現 0，生理上不可能，其實是「沒測」。這種情況要先換成 `NaN` 才能正確處理。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "5fdc12fb",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.627337Z",
     "iopub.status.busy": "2026-09-29T20:04:26.627222Z",
     "iopub.status.idle": "2026-09-29T20:04:26.668576Z",
     "shell.execute_reply": "2026-09-29T20:04:26.668249Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Zeros per column: {'plas': 5, 'pres': 35, 'skin': 227, 'insu': 374, 'mass': 11}\n",
      "Missing after replace: {'plas': 5, 'pres': 35, 'skin': 227, 'insu': 374, 'mass': 11}\n"
     ]
    }
   ],
   "source": [
    "from sklearn.datasets import fetch_openml\n",
    "\n",
    "try:\n",
    "    pima = fetch_openml(data_id=37, as_frame=True).data\n",
    "    zero_cols = [\"plas\", \"pres\", \"skin\", \"insu\", \"mass\"]\n",
    "    print(\"Zeros per column:\", (pima[zero_cols] == 0).sum().to_dict())\n",
    "    pima[zero_cols] = pima[zero_cols].replace(0, np.nan)\n",
    "    print(\"Missing after replace:\", pima[zero_cols].isna().sum().to_dict())\n",
    "except Exception as e:\n",
    "    print(\"OpenML unavailable:\", type(e).__name__, \"- skip this optional example\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "08e27021",
   "metadata": {},
   "source": [
    "## 3. 離群值與不可能值\n",
    "\n",
    "先用 IQR 規則（低於 Q1 − 1.5×IQR 或高於 Q3 + 1.5×IQR）數一數各欄「統計上偏離」的值有幾個。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "756205a4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.669800Z",
     "iopub.status.busy": "2026-09-29T20:04:26.669722Z",
     "iopub.status.idle": "2026-09-29T20:04:26.674016Z",
     "shell.execute_reply": "2026-09-29T20:04:26.673721Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "su     61\n",
       "sc     51\n",
       "bu     38\n",
       "bp     36\n",
       "bgr    34\n",
       "sod    16\n",
       "dtype: int64"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "num_cols = ckd.select_dtypes(\"number\").columns\n",
    "q1 = ckd[num_cols].quantile(0.25)\n",
    "q3 = ckd[num_cols].quantile(0.75)\n",
    "iqr = q3 - q1\n",
    "is_out = (ckd[num_cols] < q1 - 1.5 * iqr) | (ckd[num_cols] > q3 + 1.5 * iqr)\n",
    "is_out.sum().sort_values(ascending=False).head(6)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67f751c7",
   "metadata": {},
   "source": [
    "IQR 抓到的多半是**真實但極端**的病人（例如末期腎病的高 BUN），不能一律刪掉。真正該處理的是**生理上不可能**的值，例如血鈉 4.5 mEq/L、血鉀 47 mEq/L（多半是小數點打錯）。我們把它們改成遺漏值，交給後面的插補處理。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "13e53d45",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.675214Z",
     "iopub.status.busy": "2026-09-29T20:04:26.675155Z",
     "iopub.status.idle": "2026-09-29T20:04:26.677614Z",
     "shell.execute_reply": "2026-09-29T20:04:26.677318Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sod implausible values: [4.5]\n",
      "pot implausible values: [39.0, 47.0]\n"
     ]
    }
   ],
   "source": [
    "limits = {\"sod\": (100, 180), \"pot\": (1.5, 10)}\n",
    "for col, (lo, hi) in limits.items():\n",
    "    bad = ckd[col].notna() & ~ckd[col].between(lo, hi)\n",
    "    print(col, \"implausible values:\", ckd.loc[bad, col].tolist())\n",
    "    ckd.loc[bad, col] = np.nan"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7652985b",
   "metadata": {},
   "source": [
    "## 4. 單位不一：人工注入的教學情境\n",
    "\n",
    "**以下是刻意改造的資料，原始 UCI 檔案沒有這個問題。** 假設 20% 的病人來自醫院 B，B 的肌酸酐（serum creatinine, `sc`）用 µmol/L 回報（1 mg/dL = 88.4 µmol/L）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "7927b3c2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.678590Z",
     "iopub.status.busy": "2026-09-29T20:04:26.678537Z",
     "iopub.status.idle": "2026-09-29T20:04:26.682252Z",
     "shell.execute_reply": "2026-09-29T20:04:26.681879Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "site\n",
       "A      1.20\n",
       "B    123.76\n",
       "Name: sc, dtype: float64"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ckd[\"site\"] = \"A\"\n",
    "site_b = ckd.sample(frac=0.2, random_state=42).index\n",
    "ckd.loc[site_b, \"site\"] = \"B\"\n",
    "ckd.loc[site_b, \"sc\"] = ckd.loc[site_b, \"sc\"] * 88.4  # hospital B reports umol/L\n",
    "\n",
    "ckd.groupby(\"site\")[\"sc\"].median().round(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ecfbfa9",
   "metadata": {},
   "source": [
    "兩家醫院的中位數差了將近 90 倍，這不是病人差異，而是單位差異。畫在對數刻度上會看到兩團分開的山。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "58c29fb9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.683191Z",
     "iopub.status.busy": "2026-09-29T20:04:26.683138Z",
     "iopub.status.idle": "2026-09-29T20:04:26.833528Z",
     "shell.execute_reply": "2026-09-29T20:04:26.833200Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAmEAAAFRCAYAAAA8Z3p7AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAMQBJREFUeJzt3Qd4VFXe+PFfKAk1NCHUACtNuqJCRBAwgLy+KAKirCJtcVdZBLIIYgFBJZaXokhRpCjCooCg8AqCLE16X7GAsCAooRtCD4T7Pr/z/Gf+mZAymZQzmfl+nueSzL137j33njvML6eGOI7jCAAAAHJVvtw9HQAAABRBGAAAgAUEYQAAABYQhAEAAFhAEAYAAGABQRgAAIAFBGEAAAAWEIQBAABYUEAC3I0bN+TYsWNSvHhxCQkJsZ0cAAAQwBzHkfPnz0vFihUlX758wR2EaQBWpUoV28kAAABB5OjRo1K5cuXgDsK0BMx1M8LDw20nBwAABLCEhART+OOKP4I6CHNVQWoARhAGAABygzdNoGiYDwAAYAFBGAAAgAUEYQAAABYEfJswAACCdaiE69evS1JSku2kBJyCBQtK/vz5s3wcgjAAAAJMYmKixMXFyaVLl2wnJWAb3VeuXFmKFSuWpeMQhAEAEGCDlB86dMiU1OiAoaGhoQxWns0ljKdOnZLffvtNatasmaUSMYIwAAACrBRMAzEdq6pIkSK2kxOQypYtK4cPH5Zr165lKQijYT4AAAEooylz4LvsmgaRHAIAALCA6kgEpI4LZ3u135IuPXI8LQAApIYgDACAIOHtH6jZJTv/0O3Vq5fEx8fL4sWLJVBQHQkAAPzeu+++K7NmzXK/btWqlQwaNChbz1GnTh0JCwuT48ePS24gCAMAAH6vRIkSUrJkyRw7/nfffSeXL1+Wrl27yscffyy5gSAMAAD4hQULFkiDBg2kcOHCUqZMGYmOjpaLFy+6qyM7derk/n3t2rWmdEx7KuqiQ0aovXv3SocOHcxAqhEREdKjRw85ffp0hueePn26/PnPfzb7z5gxQ3IDQRgAALAuLi5OunfvLn369JGffvpJ1qxZI507dzaDo6akwVdUVJT069fPvE8XHRdN24y1adNGbr/9dtm+fbssX75cTpw4Id26dUv33OfPn5f58+fLk08+KW3btpVz587J+vXrJafRMB8AAFgXFxdn5rrUwKtq1apmnZaKpVU1qTMB6GC05cuXd69///33TQA2ZswY9zot1dIAbf/+/VKrVq1Ujzdv3jwz+n29evXM68cff9yUjLVo0UJyEiVhAADAukaNGsn9999vAq9HH31Upk2bJn/88UemjrFnzx5ZvXq1qYp0LdrYXh08eDDN92mgpqVgLvq7loxpCVlOIggDAADW5c+fX1auXCnLli2TunXrysSJE6V27dpmHkxvXbhwQTp27Ci7d+/2WH755Rdp2bJlqu/58ccfZfPmzTJ06FApUKCAWZo1a2YmP9cSspxEEAYAAPxCSEiING/eXEaNGiW7du0yVY6LFi1KdV/dlpSU5LHujjvukB9++EGqVasmNWrU8FiKFi2a6nG02lEDNC1FSx64xcTEmG05iSAMAABYt2XLFtOWSxvUHzlyRL744gs5deqU3Hbbbanur4GWvkd7RWrvR520vH///nL27FnTwH/btm2mCvKbb76R3r173xSwKZ2Ae/bs2Wb/+vXreyx/+ctfzPE1qMspNMwHACBI+PNUbeHh4bJu3TqZMGGCJCQkmMb5Y8eONcNNpGbIkCHSs2dPU3Wp43tptaUGZhs2bJBhw4ZJu3bt5OrVq+Y4DzzwQKoTmn/11Vdy5swZeeSRR27apsGfLloaNm7cuBy55hAntb6fAUQzUntRaHdTzWAEB+aOBBCsrly5YgKS6tWrS6FChWwnJ+jucUIm4g6qIwEAACwgCAMAALCAIAwAAMACgjAAAAALCMIAAAAsIAgDAACwgCAMAAAg2IKwV1991UxRkHxxTbTpGodDR78tU6aMmYSzS5cucuLECZtJBgAACIySsHr16klcXJx7+e6779zbBg8eLEuWLDEzma9du1aOHTsmnTt3tppeAACQ+3r16iWdOnUKqFtvfdoina28fPnyN63XkWZ1qoC5c+dKmzZtzLqZM2eaKQR0tnOd4RwAAHjvypg+uXq7Cr04I9uO9e6770rySX5atWoljRs3NtMcZcWaNWukdevW7tc6Av6f/vQnGThwoDz99NMS0CVhv/zyi1SsWNFc8BNPPGEm7VQ7duwwE2tGR0e799WqysjISNm0aVOax9N5onTKgOQLAADI20qUKCElS5bMsePv27fP1Mj9+OOP8te//lWeeeYZWbVqlQRsENa0aVOZNWuWLF++XKZMmWLmYWrRooWc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V6lSxWzXz8z06dPNNr1Xffv2dd+H2rVry7vvvpvuNXhz79q2bStnz56VtWvXZuFuIehYnT4cyCXHjh1zChQo4IwbN845dOiQ8+9//9uZNGmSc/78ebP9008/dSpUqOAsXLjQ+c9//mN+li5d2pk1a5bZru/Rj0u1atXc++gxR44c6TRq1MjjXOPHj3eqVq3qft2zZ0+nePHiTv/+/Z2ff/7ZmT59ujlW+/btnTfeeMPZv3+/89prrzkFCxZ0jh49muY1DB061ClVqpRJ04EDB5z169c706ZNSzd9GV2X0nNv2LDBHOOrr75yIiIinLfeestsu3TpkvOPf/zDqVevnhMXF2cWXaf0fIsWLfI4f506dZylS5c6+/btc7p27Wruw7Vr18w+M2fOdEqUKOE+r967YsWKOZ07d3a+//57Z926dU758uWdF1980b3P66+/bo65fPly5+DBg+YYYWFhzpo1a9K8Tw899JDzt7/9zWPdb7/95rzzzjvOrl27zHHee+89J3/+/M6WLVu8ej5SWr16tbnehg0bOitWrDD5cebMmQzTq+nQ+6/XvG3bNnOfZsyYYZ4LpecPDw93/vnPf5p1muf6XOgzkl4+a37ps6HrfvzxR6dv377mmXv44Ye9vpdHjhwxr2NiYsy59dnRZ0HP98cffzje0uc9+Xmfe+45p2LFis7XX3/t/PDDD2a7plXvl9Jr0GscMmSIOa9ee6VKldI97/z588190mP++uuvJh8//PBD9/Zu3bo5VapUcb744gtzrd9++60zb948sy0xMdEZMWKEuf96br3OIkWKOJ999lma1+Dtc9i0aVPzXAPeIghDUNixY4f5T/3w4cOpbr/11luduXPneqzT4CQqKsrjy2/ChAke+3gbhOnrpKQk97ratWs7LVq0cL++fv26U7RoUfMFlJqEhATzn74r6EoprfRldF2p0WClSZMm6V5jWkHYRx995N6uX7i67qeffkozCNMvP702l+eff958kakrV66Y7Rs3bvQ4rwYY3bt3TzP9mtbRo0c7GXnwwQdNgOnN85FWELZ48WL3Om/SO3z4cKd69eomEEiNBisamCd31113Oc8++2y6+ayB9ttvv+1+rYFv5cqV3YGEt2mrW7eux/Zhw4ZlKQi7cOGCCbDmzJnj3q7XrtfpSq+eo379+h7HeOmll9I979ixY51atWql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",
      "text/plain": [
       "<Figure size 700x350 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = plt.subplots(figsize=(7, 3.5))\n",
    "bins = np.logspace(-1, 4, 50)\n",
    "for site, color in [(\"A\", \"#00897B\"), (\"B\", \"#F4511E\")]:\n",
    "    ax.hist(ckd.loc[ckd[\"site\"] == site, \"sc\"].dropna(), bins=bins, alpha=0.7,\n",
    "            color=color, label=f\"site {site}\")\n",
    "ax.set_xscale(\"log\")\n",
    "ax.set_xlabel(\"serum creatinine (as recorded, log scale)\")\n",
    "ax.set_ylabel(\"patients\")\n",
    "ax.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "163fa226",
   "metadata": {},
   "source": [
    "既然知道醫院 B 用 µmol/L，就依來源換算回 mg/dL，再把 `site` 欄位拿掉（它只是我們注入的輔助欄，不該進模型）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "d960f5ca",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.834743Z",
     "iopub.status.busy": "2026-09-29T20:04:26.834672Z",
     "iopub.status.idle": "2026-09-29T20:04:26.837534Z",
     "shell.execute_reply": "2026-09-29T20:04:26.837259Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sc median after conversion: 1.3\n"
     ]
    }
   ],
   "source": [
    "is_b = ckd[\"site\"] == \"B\"\n",
    "ckd.loc[is_b, \"sc\"] = ckd.loc[is_b, \"sc\"] / 88.4\n",
    "ckd = ckd.drop(columns=\"site\")\n",
    "\n",
    "print(\"sc median after conversion:\", round(ckd[\"sc\"].median(), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5c4499ce",
   "metadata": {},
   "source": [
    "## 5. 先切分，再前處理\n",
    "\n",
    "從這裡開始，所有「會從資料學東西」的步驟（插補的中位數、標準化的平均與標準差、one-hot 的類別清單）都只能從**訓練集**學。所以第一步是切分。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "3cb80b8c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.838629Z",
     "iopub.status.busy": "2026-09-29T20:04:26.838572Z",
     "iopub.status.idle": "2026-09-29T20:04:26.841675Z",
     "shell.execute_reply": "2026-09-29T20:04:26.841221Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(300, 24) (100, 24) | CKD share in train: 0.627\n"
     ]
    }
   ],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "X = ckd.drop(columns=\"class\")\n",
    "y = (ckd[\"class\"] == \"ckd\").astype(int)\n",
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    X, y, test_size=0.25, stratify=y, random_state=42)\n",
    "print(X_train.shape, X_test.shape, \"| CKD share in train:\", y_train.mean().round(3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "66fecf9d",
   "metadata": {},
   "source": [
    "### 5-1 標準化 vs 正規化\n",
    "\n",
    "血紅素（hemo）大約 3–18，白血球（wbcc）大約 2,000–26,000。兩種常見縮放：\n",
    "\n",
    "- **標準化**（`StandardScaler`）：減平均、除以標準差，變成 z 分數。\n",
    "- **正規化**（`MinMaxScaler`）：壓到 0 到 1 之間。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "d75acef7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.842660Z",
     "iopub.status.busy": "2026-09-29T20:04:26.842604Z",
     "iopub.status.idle": "2026-09-29T20:04:26.848317Z",
     "shell.execute_reply": "2026-09-29T20:04:26.848042Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "          raw: {'hemo': [12.86, 3.01, 3.1, 17.8], 'wbcc': [8401.37, 3083.18, 2200.0, 26400.0]}\n",
      " standardized: {'hemo': [0.0, 1.0, -3.25, 1.65], 'wbcc': [-0.0, 1.0, -2.02, 5.85]}\n",
      "      min-max: {'hemo': [0.66, 0.2, 0.0, 1.0], 'wbcc': [0.26, 0.13, 0.0, 1.0]}\n"
     ]
    }
   ],
   "source": [
    "from sklearn.preprocessing import MinMaxScaler, StandardScaler\n",
    "\n",
    "demo = X_train[[\"hemo\", \"wbcc\"]].dropna()\n",
    "scaled = {\n",
    "    \"raw\": demo,\n",
    "    \"standardized\": pd.DataFrame(StandardScaler().fit_transform(demo), columns=demo.columns),\n",
    "    \"min-max\": pd.DataFrame(MinMaxScaler().fit_transform(demo), columns=demo.columns),\n",
    "}\n",
    "for name, d in scaled.items():\n",
    "    print(f\"{name:>13}:\", d.agg([\"mean\", \"std\", \"min\", \"max\"]).round(2).to_dict(\"list\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "84290f49",
   "metadata": {},
   "source": [
    "### 5-2 把前處理裝進 Pipeline\n",
    "\n",
    "`ColumnTransformer` 讓數值欄與類別欄各走各的處理，`Pipeline` 把前處理和模型串成一個整體。`fit` 的時候整條管線只看訓練集。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "2bbf7398",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.849370Z",
     "iopub.status.busy": "2026-09-29T20:04:26.849296Z",
     "iopub.status.idle": "2026-09-29T20:04:26.914707Z",
     "shell.execute_reply": "2026-09-29T20:04:26.914320Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test accuracy: 0.98\n"
     ]
    }
   ],
   "source": [
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder\n",
    "\n",
    "num_cols = X.select_dtypes(\"number\").columns.tolist()\n",
    "cat_cols = X.select_dtypes(exclude=\"number\").columns.tolist()\n",
    "\n",
    "numeric = Pipeline([(\"impute\", SimpleImputer(strategy=\"median\")),\n",
    "                    (\"scale\", StandardScaler())])\n",
    "categorical = Pipeline([(\"impute\", SimpleImputer(strategy=\"most_frequent\")),\n",
    "                        (\"onehot\", OneHotEncoder(handle_unknown=\"ignore\", sparse_output=False))])\n",
    "prep = ColumnTransformer([(\"num\", numeric, num_cols), (\"cat\", categorical, cat_cols)])\n",
    "\n",
    "model = Pipeline([(\"prep\", prep), (\"clf\", LogisticRegression(max_iter=1000))])\n",
    "model.fit(X_train, y_train)\n",
    "print(\"Test accuracy:\", round(model.score(X_test, y_test), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0bf87e9f",
   "metadata": {},
   "source": [
    "看看前處理之後的欄位：數值欄前綴 `num__`，類別欄被 one-hot 展開、前綴 `cat__`。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "f91efdc9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.915842Z",
     "iopub.status.busy": "2026-09-29T20:04:26.915755Z",
     "iopub.status.idle": "2026-09-29T20:04:26.917850Z",
     "shell.execute_reply": "2026-09-29T20:04:26.917462Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "34 features\n",
      "['num__age' 'num__bp' 'num__sg' 'num__al' 'num__su'] ... ['cat__pe_no' 'cat__pe_yes' 'cat__ane_no' 'cat__ane_yes']\n"
     ]
    }
   ],
   "source": [
    "feature_names = model.named_steps[\"prep\"].get_feature_names_out()\n",
    "print(len(feature_names), \"features\")\n",
    "print(feature_names[:5], \"...\", feature_names[-4:])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2bb1f6f5",
   "metadata": {},
   "source": [
    "### 5-3 測試集的混淆矩陣"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "d197fc82",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.918758Z",
     "iopub.status.busy": "2026-09-29T20:04:26.918694Z",
     "iopub.status.idle": "2026-09-29T20:04:26.952846Z",
     "shell.execute_reply": "2026-09-29T20:04:26.952501Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiIAAAG2CAYAAABCq+3iAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAN4xJREFUeJzt3Qd4VGXa8PH7DJBCSQIICSU0Db2IqNSlGcBGERRlYUVEeJXeiywdxFdWKUpRpFhgERVUcMUPQxVDF19YAWlCKAEUQghsQiDzXc8DM8skEWcyk5yTyf/HdUzmnDPnPBkH5s59P8Ww2+12AQAAMIHNjJsCAAAoBCIAAMA0BCIAAMA0BCIAAMA0BCIAAMA0BCIAAMA0BCIAAMA0BCIAAMA0BCIAAMA0BCIAAMA0BCIAACBTp0+flm7duknx4sUlODhYatWqJbt27XIeV6vEjBs3TkqVKqWPR0dHy+HDh8UTBCIAACCDS5cuSePGjaVAgQLyzTffyM8//yxvvvmmFC1a1HnOG2+8IbNnz5b58+fL9u3bpVChQtKmTRtJTk4WdxksegcAANIbNWqUbN26VbZs2SKZUdmQ0qVLy9ChQ2XYsGF63+XLlyU8PFyWLFkizz33nLiDQMREaWlpcubMGSlSpIgYhmFmUwAAWaA+jK9cuaI/kG227CkyJCcny/Xr133W3vSfN4GBgXpLr3r16jq7cerUKdm0aZOUKVNG+vTpI7169dLHjx07Jvfee6/8+OOPcv/99zuf16xZM/141qxZbrUpv9c/FbJMBSGRkZG8ggCQy8XFxUnZsmWzJQgJDi0kcj3NJ9crXLiwJCUluewbP368TJgwIcO5KtCYN2+eDBkyRF599VXZuXOnDBgwQAICAqR79+4SHx+vz1MZkDupx45j7iAQMZHKhCgPz3lK8gcXMLMpQLZZ1cG934qA3OhK4hW5r0Jl57/nvnZdZUJUENIkQiS/l5nzG3ZJ+j5eB00hISHO3ZllQxxZ+wcffFBee+01/bhu3bqyf/9+3R9EBSK+QiBiIkd6TAUh+QsSiMA/3fkPHuCvsr28XsAmkt/L0o+R5vw76c7fSzUSRpVn7lStWjX5/PPP9fcRERH667lz5/S5DurxnaWaP8OoGQAArM7mo80DasTMoUOHXPb98ssvUr58ef19xYoVdTASExPjPJ6YmKhHzzRs2NDt+5ARAQDA6gzj1ubtNTwwePBgadSokS7NdO7cWXbs2CHvvfee3m5dzpBBgwbJlClTJCoqSgcmY8eO1R13O3To4PZ9CEQAAEAGDz30kKxatUpGjx4tkyZN0oHGzJkzpWvXrs5zRowYIVevXpXevXtLQkKCNGnSRNauXStBQUHiLobvmkilsEJDQ6XRos70EYHf+vbpW789Af7673h4sVJ6/ozs6A+VePtzQqLL3Oon4o3UNJHvTmdbW7OKjAgAAFZn5HxpJqfQWRUAAJiGjAgAAFZn80HqwKKpBwIRAACszqA0AwAA4HNkRAAAsDrj9ubtNSyIQAQAAKuzGbc2b69hQRbtugIAAPICMiIAAFidQWkGAACYxfDfUTNkRAAAsDrDfzMi9BEBAACmISMCAIDV2fx31AyBCAAAVmdQmgEAAPA5MiIAAFidwagZAABgFpv/9hFh1AwAADANpRkAAKzO8N/OqgQiAADkikDE8P4aFkRpBgAAmIaMCAAAuYEhfolABAAAq/PjUTMEIgAAWJ3hv51V6SMCAABMQ0YEAACrM5hZFQAAmFm/sPngGhZk0WYBAIC8gNIMAABWZ1CaAQAAZjEYNQMAAOBzlGYAALA6g9IMAAAwi41RMwAAAD5HaQYAAKszKM0AAACzGP47aoaMCAAAVmfz39V3mVkVAACYhowIAABWZ9BHBAAAmMXw3z4ilGYAAIBpKM0AAGB5hhiqPOMFu0VTIgQiAABYnGF4H4iofiZ2sR5KMwAAwDRkRAAAyAODZsRQ5RnrIRABAMDibD4ozdgNQ9LEeijNAAAA05ARAQAgj3RWtSICEQAALM4gEAEAAGYx/DgQoY8IAAAwDYEIAAC5ZPiu4eXmiQkTJjgzMY6tatWqzuPJycnSt29fKV68uBQuXFg6deok586d8/hnIxABAMDi0gcEWd08VaNGDTl79qxz+/77753HBg8eLKtXr5ZPP/1UNm3aJGfOnJGOHTt6fA86qwIAgEzlz59fIiIiMuy/fPmyLFy4UJYtWyYtW7bU+xYvXizVqlWTbdu2SYMGDcRdZEQAAMhDGZHExESXLSUl5Q/ve/jwYSldurRUqlRJunbtKidPntT7d+/eLampqRIdHe08V5VtypUrJ7GxsR79bAQiAABYnOGjP0pkZKSEhoY6t2nTpmV6z/r168uSJUtk7dq1Mm/ePDl+/Lj85S9/kStXrkh8fLwEBARIWFiYy3PCw8P1MU9QmgEAIA+Ji4uTkJAQ5+PAwMBMz3vsscec39euXVsHJuXLl5cVK1ZIcHCwz9pDRgQAgDxUmgkJCXHZ/igQSU9lPypXrixHjhzR/UauX78uCQkJLueoUTOZ9Sm5GwIRAAAszjBh+G56SUlJcvToUSlVqpTUq1dPChQoIDExMc7jhw4d0n1IGjZs6NF1Kc0AAIAMhg0bJm3bttXlGDU0d/z48ZIvXz7p0qWL7lvSs2dPGTJkiBQrVkxnVvr376+DEE9GzCgEIgAAWJxNZzS8S2nYPXz6qVOndNDx+++/S4kSJaRJkyZ6aK76XpkxY4bYbDY9kZkaedOmTRuZO3eux+0iEAEAwOIME9aaWb58+V2PBwUFyZw5c/TmDQIRAAAszmDROwAAAN8jIwIAgNUZ3o968bSPSE4hEAEAIA+UZgxvI5lswjwiAADANGREAACwOH/OiBCIAABgcYb4IBC5veid1VCaAQAApiEjAgCAxRmUZgAAgFkMHwzftWgXEUozAADAPJRmAACwOIPSDAAAMItBIAIAAMxiMwy9+WMnEYbvAgAA09BHBAAAizP8eNQMgQgAABZn+HEfEUozAADANGRE4HeerNRMnqjUTMILFdePTySekaUHvpZd8fv146KBIfJS7aflgfBqUjB/kMRdOSfLD/5Lvj+9x+SWA96Z/9XHMuOzhXLu0gWpVamqvNVnrDxUpQ4vq7+sNSOsNWNJEyZMkPvvv9+tcxMTE2XMmDFStWpVCQoKkoiICImOjpaVK1eK3W7X5zRv3lwGDRrk8rxZs2ZJYGCgLF++3HmOI02m9pcpU0batm2rrwPzXfjPJVm0f6X0i5kq/WOmyk/nD8mERn2kfEgpfXz4wy9KZJFwmbB1jvzPuomy9fQeebVBb7k3LNLspgNZ9ummr2Xkgmkypls/iX3nC6ldqaq0G9NTzif8zqvqB4zbnzneblaUZ0ozCQkJ0qhRI/nwww9l9OjRsmfPHtm8ebM8++yzMmLECLl8+XKmzxs/fry8+uqr8uWXX8pzzz3n3N+rVy85e/asHD16VD7//HOpXr26Pt67d+8c/KmQme1n/092xu+XM0nn5XTSeVny7y8k+UaKVC1WSR+vXrySfHlkgxy69KvEX/1N/nnwX3L1+jWJCivPC4pca/bKxdLj0c7yfOtOUq38ffJ2/0kSHBgkH3z7mdlNA6xbmlGZhdq1a+vsxPvvvy8BAQHy8ssv6yyHw8mTJ6V///4SExMjNptNHn30UXn77bclPDxclixZIhMnTtTnOSK9xYsXywsvvJDhXiqY+PXXX+WXX36R0qVLO/dXrlxZunTpottwJ5UhGTBggHz88ceybt06HcTcqWDBgjqjopQtW1YaNGigMy0vvviidO7cWWdaYD6bGPKXsg9KYL4AOfD7Mb3v59+PSbPIB2XH2f+TpNT/SNOyD0pAvgLyfxcOmd1cIEuup16XHw//W4Y/+z/Oferfy5Z1G8mOA3t5Vf2A4cedVU3vI/LBBx/IkCFDZPv27RIbG6uDiMaNG0urVq0kLS1N2rdvL4ULF5ZNmzbJjRs3pG/fvjqLsXHjRv11//79snbtWvnuu+/09UJDQzPcQ11HlVW6du3qEoQ4qOvfSd2nW7dusn79en1fFSy5o3v37jJ06FBdoiEQMVeFkDIys+VICbAVkP/cSJFJsfPk5JWz+tjUbe/Kq/V7y2ftZ8qNtJuScvO6TIydJ2euXjC51UDW/JZ4SW6m3ZSSYfe47FePD8XdCsCRuxkM380+6kNelT+UqKgoeeedd3T2QwUi6uu+ffvk+PHjEhl5q36vSis1atSQnTt3ykMPPaSDiPz58zuzE5n57bff5NKlSzpj4Y4FCxborz/99JPbz3H8BqIyLCrzkpmUlBS93dlnBdnj1JV46bNushQsECx/KVtPhj3UQ4Zv/IcORrrXaC+FAwrKyM1vSWJKkjQsfb+Mqd9bhm6cLr8mnuZ/CQDkpT4i6bMNpUqVkvPnz+vvDxw4oAMQRxCiqL4YYWFh+pi7HB1R3dWkSRMd4IwdO1ZnRzyh7vVH6a9p06bpjI1ju/Pngm/dsN/UGY4jCSdl8f5VcjzhlHSIekRKFSoh7e9rKW/tWiJ7zx+UY5dPydIDa+TwpRPS7t7m/G9ArnRPSFHJZ8sn5xN+c9mvHkcULWFau+A7Bp1Vs0+BAgUyvNiqlOJLJUqU0MHLwYMH3Tq/Vq1aOhuzYcMGXf5xNxi5efOmHD58WCpWrJjpcdVJVnWKdWxxcXEe/RzIOvW+KmDLr/uKKGnpgtOb9jTL1k+BPxNQIEDqRtWQDXtjnfvUv6Pq8cPV3BtVCGszCETMUa1aNf1hfecH9s8//6xHwKjMiKI6uKoA4M9KJmpEy9KlS+XMmTMZjiclJWUINtSQYBWMqJE1qvNpamqqW/1dVAmoU6dOmR5XQ31DQkJcNvhej5pPSc17oiS8YHHdV0Q9rl2ismw4uV3irsTL6SvnZOAD3aRK0Qo6Q9IpqpWeU+SHM3TqQ+41oGMPWfzNCvl43Uo5ePKIDHh7vFxL/o8eRQM/YPhg6K5Ff9kyvbPq3agOnyo7oTqZzpw5UwcLffr0kWbNmsmDDz6oz6lQoYLuQ7J37149eqVIkSL6Az+9qVOn6g6u9evX19+r56tszJYtW3TJRPU5UVmTO9WpU0d3WH3kkUd0MLJixQpnBufatWsSHx+v23Tq1ClZtWqVzJgxQ1555RVp0aJFDr1CyExYYBEZ/lAPKRYUKtdS/yPHL5+WMVtmyZ7zt8p5f9/6tvSs2VEmNu4nwfkD9TDff+xcoof8ArnVM82ekN8uX5RJH83WE5rVrlRNvpyyUMKLunZgBazG0oGIiuDU/B1q+G7Tpk1dhu86qOyDGqWiPvxVpuSPhu8WK1ZMtm3bJq+//rpMmTJFTpw4IUWLFtWBzvTp0zMdbaOo445g5JlnntHBiKNDq9pURqZ48eJSr149+eSTT+Spp57KxlcE7pix+8O7HleBx+Rt83kx4Xdeafc3vcH/GH48asawe9qTEz6jRs2oAKjRos6Sv6BrXxnAX3z79HtmNwHI1n/Hw4uV0v3+sqPcnnj7c+K+adGSL8i73MHN5BtyZPR32dbWXDtqBgAA5F2WLs0AAAC5XZrxdmZVa76SBCIAAFic4cdTvFOaAQAApiEjAgCAxRk+KK1YMx9CIAIAgOVRmgEAAMgGlGYAALA4w487qxKIAABgcQaBCAAAMIvhx1O8M3wXAACYhtIMAAAWZ1CaAQAApjH8tzZDaQYAAJiG0gwAABZnUJoBAABmMfy3MkNpBgAAmIfSDAAAFmdQmgEAAGYx/DgQYdQMAAAwDYEIAAAW58iIeLtl1euvv66fP2jQIOe+5ORk6du3rxQvXlwKFy4snTp1knPnznl8bQIRAAByyagZw8stK3bu3Cnvvvuu1K5d22X/4MGDZfXq1fLpp5/Kpk2b5MyZM9KxY0ePr08gAgCAxRkmZUSSkpKka9eusmDBAilatKhz/+XLl2XhwoXy1ltvScuWLaVevXqyePFi+eGHH2Tbtm0e3YNABACAPCQxMdFlS0lJ+cNzVenliSeekOjoaJf9u3fvltTUVJf9VatWlXLlyklsbKxH7SEQAQDA6gwfZENuZ0QiIyMlNDTUuU2bNi3TWy5fvlz27NmT6fH4+HgJCAiQsLAwl/3h4eH6mCeYRwQAgDw0fDcuLk5CQkKc+wMDAzOcq84ZOHCgrFu3ToKCgiQ7kREBACAPCQkJcdkyC0RU6eX8+fPywAMPSP78+fWmOqTOnj1bf68yH9evX5eEhASX56lRMxERER61h4wIAAAWZ+TwhGaPPPKI7Nu3z2Vfjx49dD+QkSNH6vJOgQIFJCYmRg/bVQ4dOiQnT56Uhg0betQuAhEAACzOyOFF74oUKSI1a9Z02VeoUCE9Z4hjf8+ePWXIkCFSrFgxnVnp37+/DkIaNGjgUbsIRAAAgMdmzJghNptNZ0TUyJs2bdrI3LlzPb4OgQgAABZniA9KM+Ld8zdu3OjyWHVinTNnjt68QSACAIDFGSx6BwAA4HtkRAAAsDjDjzMiBCIAAFickcOjZnISgQgAABbnzxkRZlYFAACmISMCAIDVGT6orVgzIUIgAgCA1RmUZgAAAHyP0gwAABZnM25t3l7DighEAACwOIPSDAAAgO+REQEAwOJshqE3b69hRQQiAABYnOHHpRkCEQAALM7mgxlIrTqDqVXbBQAA8gAyIgAAWJzhgz4ilGYAAECWgwjDTwMRSjMAAMA0lGYAALA4hu8CAADTGJRmAAAAfI/SDAAAFmfz43lE3ApEvvrqK7cv2K5dO2/aAwAA0snzfUQ6dOgg7tawbt686da5AAAAbmVE0tLSeKUAADCJ4cedVb3qI5KcnCxBQUG+aw0AAMhTpRmP+66o0svkyZOlTJkyUrhwYTl27JjeP3bsWFm4cGF2tBEAgDzN8NHmF4HI1KlTZcmSJfLGG29IQECAc3/NmjXl/fff93X7AACAH/M4EPnwww/lvffek65du0q+fPmc++vUqSMHDx70dfsAAMjzbLdLM95uftFH5PTp03Lfffdl2qE1NTXVV+0CAAC32cQHfUQsWpzxOCNSvXp12bJlS4b9n332mdStW9dX7QIAAHmAxxmRcePGSffu3XVmRGVBVq5cKYcOHdIlmzVr1mRPKwEAyMMMPx6+63FGpH379rJ69Wr57rvvpFChQjowOXDggN7XqlWr7GklAAB5mOGD/iF+NY/IX/7yF1m3bp3vWwMAAPKULE9otmvXLp0JcfQbqVevni/bBQAAbvPFPCCGvwQip06dki5dusjWrVslLCxM70tISJBGjRrJ8uXLpWzZstnRTgAA8iwbM6v+10svvaSH6apsyMWLF/WmvlcdV9UxAACAbMuIbNq0SX744QepUqWKc5/6/u2339Z9RwAAgG/Z/Dgj4nEgEhkZmenEZWoNmtKlS/uqXQAA4DYVQ3g/fFf8Y/ju9OnTpX///rqzqoP6fuDAgfKPf/zD1+0DACDPs+X1Kd6LFi3qEoldvXpV6tevL/nz33r6jRs39PcvvviidOjQIftaCwAA/IpbgcjMmTOzvyUAACBTeX74rprSHQAAmMNGZ9XMJScny/Xr1132hYSE5Mj/FAAAkPt5PGpG9Q8ZOXKkrFixQn7//fdMR88AAADfsflxRsTjUTMjRoyQ9evXy7x58yQwMFDef/99mThxoh66q1bgBQAA2bP6ruHl5hcZEbXKrgo4mjdvLj169NCTmN13331Svnx5Wbp0qXTt2jV7WgoAAPyOxxkRNaV7pUqVnP1B1GOlSZMmsnnzZt+3EACAPM7mo82KPG6XCkKOHz+uv69ataruK+LIlDgWwQMAAD5k+KAsY9HSjMeBiCrH/PTTT/r7UaNGyZw5cyQoKEgGDx4sw4cPz442AgAAP+VxIKICjgEDBujvo6Oj5eDBg7Js2TL58ccf9TTvAAAg90/xPm/ePKldu7buhqG2hg0byjfffOMyhUffvn2lePHiUrhwYenUqZOcO3cu+zurpqc6qaoNAAD4z/DdsmXLyuuvvy5RUVFit9vlgw8+kPbt2+vEQ40aNXRi4uuvv5ZPP/1UQkNDpV+/ftKxY0fZunWr7wOR2bNnu31BR7YEAAD4huGD4beePr9t27Yuj6dOnaqzJNu2bdNBysKFC3VFpGXLlvr44sWLpVq1avp4gwYNfBuIzJgxw+0fkkAEAADrSkxMdHms5gRT292oyUpV5kNNaqpKNLt375bU1FTdRcNBDWApV66cxMbG+j4QcYySQfZY0W46U+PDbwU/WtnsJgDZ50Zajry6NjH05u01lMjISJf948ePlwkTJmT6nH379unAQ/UHUf1AVq1aJdWrV5e9e/dKQEBAhtGy4eHhEh8fn7N9RAAAQO4pzcTFxbn88nu3bEiVKlV00HH58mX57LPP9CK4mzZtEl8iEAEAIA8JuT0Kxh0q66FmT1fq1asnO3fulFmzZsmzzz6rF71NSEhwyYqoUTMREREetceqE60BAAATh+9mJi0tTVJSUnRQUqBAAYmJiXEeO3TokJw8eVKXcjxBRgQAAIszbv/x9hqeGD16tDz22GO6A+qVK1f0CJmNGzfKt99+q4fr9uzZU4YMGSLFihXTGZb+/fvrIMSTjqoKgQgAAMjg/Pnz8vzzz8vZs2d14KEmN1NBSKtWrZwjam02m57ITGVJ2rRpI3PnzhVPZSkQ2bJli7z77rty9OhR3XmlTJky8tFHH0nFihX14ncAACB3zyOycOHCux5Xy7uoZV7U5g2P+4h8/vnnOuoJDg7Ws6upKEhRPWpfe+01rxoDAACs20ckO3gciEyZMkXmz58vCxYs0B1VHBo3bix79uzxdfsAAIAf87g0o3rFNm3aNMN+VT9Sw3gAAIBvGbenNPP2GlbkcavU+OAjR45k2P/9999LpUqVfNUuAABwmw5DvC3NeDnqxjKBSK9evWTgwIGyfft23fHlzJkzsnTpUhk2bJi88sor2dNKAADyMuO/HVazulk0DvG8NDNq1Cg9ockjjzwi165d02UaNT2sCkTUGGIAAIBsC0RUVDVmzBgZPny4LtEkJSXpBXDUYjgAAMA/JjTLKVme0EzNP68CEAAAkL1sPhh+a9Xhux4HIi1atLjrpCjr16/3tk0AACCP8DgQuf/++10ep6am6iWC9+/fr5cHBgAAuX9mVcsGImpu+cxMmDBB9xcBAAC+Zbv9x9trWJHPWtWtWzdZtGiRry4HAADyAJ+tvhsbG6sXwAEAAL5lUJr5r44dO7q8OHa7XS8RvGvXLhk7dizvPQAACESyLyOi1pS5k81mkypVqsikSZOkdevWnl4OAADkYR4FIjdv3pQePXpIrVq1pGjRotnXKgAA4HRryTsv5xGx6IRmHnVWzZcvn856sMouAAA5x/BynRlf9DGxzKiZmjVryrFjx7KnNQAAIAOvV971wcyslglEpkyZohe4W7Nmje6kmpiY6LIBAAD4vI+I6ow6dOhQefzxx/Xjdu3auaR51OgZ9Vj1IwEAAL5jsOidyMSJE+Xll1+WDRs28N4CACAH2Qyb3ry9Rq7OiKiMh9KsWbPsbA8AAMhDPBq+a9UetwAA+DODmVVvqVy58p8GIxcvXsyR/ykAAOS1XiLeMXJ/RkT1E0k/syoAAECOBCLPPfeclCxZMss3AwAAnrP5YB4Qq84j4nYgQv8QAADMYfjx8F2bp6NmAAAAcjwjkpaW5rObAgAA99kM70sr6hq5vo8IAADIeYZh05u317AiAhEAACzOoI8IAACA75ERAQDA4hi+CwAATGP48RTv1uy5AgAA8gRKMwAAWJxNDL15ew0rIhABAMDiDEozAAAAvkdGBAAAizOY0AwAAJjF5sd9RBg1AwAATENpBgAAizP8uLMqgQgAALlktRnvEIgAAIAshRCG9xkRiwYi9BEBAACmoTQDAIDF2fx41AyBCAAAFmf48Twi1mwVAADIE8iIAACQK8bMGF5fw4oIRAAAsDjD8H4eEItOI0JpBgAAZDRt2jR56KGHpEiRIlKyZEnp0KGDHDp0yOWc5ORk6du3rxQvXlwKFy4snTp1knPnzokn6CMCAEAuKc0YXv7xxKZNm3SQsW3bNlm3bp2kpqZK69at5erVq85zBg8eLKtXr5ZPP/1Un3/mzBnp2LGjR/ehNAMAgMUZJkzxvnbtWpfHS5Ys0ZmR3bt3S9OmTeXy5cuycOFCWbZsmbRs2VKfs3jxYqlWrZoOXho0aODWfciIAACAP6UCD6VYsWL6qwpIVJYkOjraeU7VqlWlXLlyEhsbK+4iIwIAQB6a0CwxMdFlf2BgoN7uJi0tTQYNGiSNGzeWmjVr6n3x8fESEBAgYWFhLueGh4frY+63CwAA5IrSjOHlpkRGRkpoaKhzU51S/4zqK7J//35Zvny5z382MiIAAFiccTsn4u01lLi4OAkJCXHu/7NsSL9+/WTNmjWyefNmKVu2rHN/RESEXL9+XRISElyyImrUjDrmLjIiAADkISEhIS7bHwUidrtdByGrVq2S9evXS8WKFV2O16tXTwoUKCAxMTHOfWp478mTJ6Vhw4Zut4eMCAAAFmeYMGpGlWPUiJgvv/xSzyXi6PehyjnBwcH6a8+ePWXIkCG6A6sKavr376+DEHdHzCgEIgAAWJxhwhTv8+bN01+bN2/usl8N0X3hhRf09zNmzBCbzaYnMktJSZE2bdrI3LlzPboPgQgAAMi0NPNngoKCZM6cOXrLKgIRAAAszmYYevP2GlZEIAIAgMUZfrz6LqNmAACAaciIAABgcYYJo2ZyCoEIAACWZ/N6QjOrFkGs2SoAAJAnkBEBAMDiDEozAADAH1bftRoyIgAAWJzhxxkR+ogAAADTkBEBAMDiDD+e0IxABAAAizMozQAAAPgeGREAACzO0H+869ZJaQYAAGSJzY9X32XUDAAAMA2lGQAALM5g1AwAADCLwagZAAAA36OPCPzem58skGYDn5XSnR6WSl2aSpdJA+TwqeNmNwvIstLFw2XRiOlyasV2ufjl/8nOeavlgaiaLueM/dsAObbse33862lL5N7S5XnF/aA0Y3j5x4ryVCASHx8v/fv3l0qVKklgYKBERkZK27ZtJSYmRh+vUKGCzJw503m+3W6XYcOGSUhIiGzcuNF5jiNFFhwcrB937txZ1q9fb9rPhbv7fv8u6f1kF4l5a5l8OfU9Sb2ZKh3G9Jarydd46ZDrhBUOkfVv/VNSb9yQDn/vJXV7Py6jFrwul5IuO88Z+kwv6dP+eRkwe7w0HfSMfq+vnrpIAgsEmNp2ZJ1x+3PH282K8kxn1V9//VUaN24sYWFhMn36dKlVq5akpqbKt99+K3379pWDBw+6nH/z5k3p1auXrFmzRjZs2CD16tVzHps0aZI+dv36dX3djz/+WKKjo2Xy5MkyZswYE3463M2qye+6PJ4/ZKrOjOw9/LM0rvUgLx5ylaHP9JZTF+Llf94a7dx34twpl3P6PtVd/vefc2XNtlu/ZL00fYScWB4r7Rq1kk83fZ3jbYb3bLf/eHsNK8ozgUifPn10NLhjxw4pVKiQc3+NGjXkxRdfdDk3JSVFunTpIrt27ZItW7ZIlSpVXI4XKVJEIiIi9PflypWTpk2bSqlSpWTcuHHy9NNPZzgf1nL5apL+WrRIqNlNATz2RIOW8t3uLbJ0zCxpUuthOfPbOXlvzTJZvHaFPl4hIlJKFSsp63+MdT4n8VqS7Dz4k9Svdj+BCCzHmuGRj128eFHWrl2rMx93BiEOKkvikJSUJE888YT8/PPPsnXrVreDioEDB+pSzpdffvmH56gAJzEx0WVDzkpLS5NR774uDarXleoVonj5ketULBUpvZ78qxw5fULajXlRFnz9T3nzlb9L1+in9PGIovfor+cTfnN5nnocXrSEKW2G9wxKM7nbkSNHdJBQtWrVPz1XlVdUxuPAgQNSooT7f2mLFSsmJUuW1KWaPzJt2jSZOHGi29eE7w2dO0UOnDgi3/7jQ15e5Epqdsw9h/fL+CVv6cc/HT0gNSpESa8nnpOl360yu3nIJoYfzyOSJzIiKghxV+vWreXq1avy2muvZek+d+sMNHr0aLl8+bJzi4uL8/geyLqhc6fK2h2bZM3ri6TMPbdKa0BuE3/xghw4edRl38GTRyWyROlbxy/dyoSUDLuVGXFQj89dupCDLQXckycCkaioKB0gpO+QmplHHnlEl1fmz5+vyy3u+v333+XChQtSsWLFPzxHjdRRI3Du3JD9VICogpA1sTGyetoiqRBRlpcduVbsz3ukclnXf2eiylSQk+dP6+9/jY+TsxfPS4v7GzqPFylYSB6qWke2H9ib4+2Fjxg+GDFj0VEzeSIQUWWTNm3ayJw5c3S2I72EhIQMWZHVq1fLggULZMCAAW7dY9asWWKz2aRDhw4+azd8Y8jcKbJiwxpZOOJ/pUhwITl38Te9/SclmZcYuc7bq5bIw1XryPBnX5ZKpcrJs82flBcff1beXb3Uec6cVR/IyC6v6I6tNSpUloXDpsvZ38/LVz+sM7XtyDrDj+cRyTOjZlQQoobvPvzww3r4be3ateXGjRuybt06mTdvnu4Tcic1HFcN3VXzjKgOju+8847z2JUrV/ScJGr47/Hjx/Xw3ffff1/3AbnvvvtM+OlwNwu//kR/fXxkD5f98wZPka6tCByRu+z+ZZ88O6mvTOoxVF7t2ld+jT8lw+e/Jss3rHae8+anC6RgULC8M2Cynnfkh3/vlnZ/7ykpqddNbTuQpwMRNYnZnj17ZOrUqTJ06FA5e/as7oyq5gdRgUhmWrZsKV9//bU8+eSTOr3vCEbUMF21BQQE6GG8DRo00JOitWjRIod/Krgj8V/7eaHgV77ZsVFvdzP5o9l6g38w/LizqmH3pCcnfEoN3w0NDZVTv52gvwj8VsjjrlOPA37lRprIxrN6AEJ29PtLvP05seHY/5PCRTJOP+GJpCtXpUWl1tnW1qzKE31EAACANeWZ0gwAALmV4celGQIRAAAszvDBonUsegcAALIWRIj/ZkToIwIAAExDaQYAAIszfJDRsGY+hEAEAADLM8QHfUQsGopQmgEAAKahNAMAgMUZftxZlUAEAACLM/w4EKE0AwAATENGBAAAizOY0AwAAJjFoDQDAADge5RmAACwOIPSDAAAMIvhx6UZMiIAAFic4ceBCMN3AQCAaciIAABgcQZ9RAAAgFkMSjMAACAv2bx5s7Rt21ZKly6tMzJffPGFy3G73S7jxo2TUqVKSXBwsERHR8vhw4c9vg99RAAAyCUZEcPLP564evWq1KlTR+bMmZPp8TfeeENmz54t8+fPl+3bt0uhQoWkTZs2kpyc7NF96CMCAIDVGYbOSnh7DU889thjesuMyobMnDlT/v73v0v79u31vg8//FDCw8N15uS5555z+z5kRAAAyEMSExNdtpSUFI+vcfz4cYmPj9flGIfQ0FCpX7++xMbGenQtAhEAACzP8NEmEhkZqYMGxzZt2jSPW6OCEEVlQO6kHjuOuYvSDAAAeWj4blxcnISEhDj3BwYGipnIiAAAkIeEhIS4bFkJRCIiIvTXc+fOuexXjx3H3EUgAgCAxRkmjJq5m4oVK+qAIyYmxrlP9TdRo2caNmzo0bUozQAAYHGGCROaJSUlyZEjR1w6qO7du1eKFSsm5cqVk0GDBsmUKVMkKipKByZjx47Vc4506NDBo/sQiAAAYHGGCVO879q1S1q0aOF8PGTIEP21e/fusmTJEhkxYoSea6R3796SkJAgTZo0kbVr10pQUJBH9yEQAQAAGTRv3lzPF3K3wGbSpEl68waBCAAAFmdkobSS2TWsiEAEAACLM1j0DgAAwPfIiAAAYHGGCZ1VcwqBCAAAFmdQmgEAAPA9MiIAAFicQWkGAACYxaA0AwAA4HuUZgAAyCVTmnmHUTMAACCLIYThl2EIGREAACzP8OPOqjazGwAAAPIu+ogAAGB5ht8WZwhEAACwOMNvwxBKMwAAwERkRAAAsDzDb3MiBCIAAFicwagZAAAA32P4LgAAMA2lGQAALM5g0TsAAADfIyMCAIDFGWREAAAAfI+MCAAAFmcwfBcAAMD3GL4LAABMQ2kGAIBc0l3VH6d4JyMCAABMQ0YEAADLM1j0DgAAmMPw2zCE0gwAADARpRkAACzO8ON5RAhEAACwPMNvizOMmgEAAKYhIwIAgMUZfpsPIRABACCXMMQfkREBAMDiDD/urEofEQAAYBoCEQAAYBpKMwAA5Iol7wyvr2FFZEQAAIBpyIgAAGB5ht8O4CUQAQDA4gy/DUMozQAAABOREQEAwOIMP55HhEAEAADLM/y2OMOoGQAAYBoyIgAAWJzht/kQAhEAAHIBw29DETIiAABYnOHHnVXpIwIAADI1Z84cqVChggQFBUn9+vVlx44d4msEIgAAIINPPvlEhgwZIuPHj5c9e/ZInTp1pE2bNnL+/HnxJQIRAAByyaJ3hpd/PPHWW29Jr169pEePHlK9enWZP3++FCxYUBYtWuTTn40+Iiay2+3665UrV8xsBpC9bqTxCsPv39+Of8+zS2LiFZ9dIzEx0WV/YGCg3u50/fp12b17t4wePdq5z2azSXR0tMTGxoovEYiYyBGAVKtY08xmAAB88O95aGioz1/HgIAAiYiIkKgKlX1yvcKFC0tkZKTLPlV6mTBhgsu+3377TW7evCnh4eEu+9XjgwcPii8RiJiodOnSEhcXJ0WKFLFsb2Z/on4LUH8B1WseEhJidnMAn+M9nvNUJkQFIerf8+wQFBQkx48f1xkKX7U3/edN+mxITiMQMZFKc5UtW9bMJuRJKgghEIE/4z2es7IjE5I+GFFbTrrnnnskX758cu7cOZf96rHK0PgSnVUBAECGklC9evUkJibGuS8tLU0/btiwofgSGREAAJCBGrrbvXt3efDBB+Xhhx+WmTNnytWrV/UoGl8iEEGeoeqgqlOW2fVQILvwHocvPfvss3LhwgUZN26cxMfHy/333y9r167N0IHVW4Y9u8ccAQAA/AH6iAAAANMQiAAAANMQiAAAANMQiAAAANMQiMAvqOmJVY9ud2efHDNmjFStWlVPEqQm51HrJ6xcudK5XkTz5s1l0KBBLs+bNWuWHpWwfPly5zlqhkK1qf1lypSRtm3b6usAvqJGK/Tv318qVaqk32dqdmD1PnPM76CWaFfDKh3Ue3jYsGF6UrONGzc6z3G8V4ODg/Xjzp07y/r16/kfBdMRiCBPSUhIkEaNGsmHH36oF3NSS1tv3rxZD1MbMWKEXL58OdPnqWG/r776qnz55Zfy3HPPOferlSnPnj0rR48elc8//1yvUKmO9+7dOwd/KvirX3/9VU8qpQKG6dOny759+/TwyRYtWkjfvn0znK/WBunZs6d+f2/YsEEHyw6TJk3S79VDhw7p42FhYToAnzp1ag7/VEA6avguYKZmzZrZ+/fvbx8+fLi9aNGi9vDwcPv48eNdzjlx4oS9Xbt29kKFCtmLFClif+aZZ+zx8fH62OLFi1Uaw2VT+zLzyiuv6GucPn06w7ErV67YU1NTnW0aOHCgPS0tzd6vXz97WFiYfevWrRnarc5Jb9GiRboN69at8+p1AR577DF7mTJl7ElJSRlejEuXLumv5cuXt8+YMcOenJxsf+qpp+yRkZH2gwcPupzrOCe9cePG2W02W4bzgZxERgSW8MEHH0ihQoVk+/bt8sYbb+jf3tatW+ecVrh9+/Zy8eJF2bRpk95/7NgxncVQ1NehQ4dKjRo19G98anMcu5O6jiqrdO3aNdMFqtSqlPnz/3eOvxs3bki3bt3ks88+0/dVmRR3qJkIixYtSokGXlHvd5X9UJkP9XcjPZXRcEhKSpInnnhCfv75Z9m6datUqVLFrXsMHDhQl3JUpg8wCzOrwhJq166tyx9KVFSUvPPOO7oG3qpVK/1VpaTVCpSO5atValkFHjt37pSHHnrIGUTcbTEmtaz1pUuXdN8QdyxYsEB//emnn9x+jmMxw8qVK+u0OpBVR44c0UGCO++9yZMn61W8Dxw4ICVKlHD7HsWKFZOSJUvyXoWpyIjAMoHInUqVKiXnz5/X36t/XFUA4ghCFNUXQ/1GqI65y9NJhJs0aaIDnLFjx+rsiLdLbQOevofc1bp1a70GyGuvvebxi8x7FWYjEIElFChQwOWx+hBXpRRfUr8pquDl4MGDbp1fq1YtnY1Rnf5UqcfdYER1GDx8+LBUrFjRyxYjL1OZQfX3wJ336yOPPKLLK/Pnz9flFnf9/vvvei0R3qswE4EILK9atWoSFxenNwdVC1cjYFRmxLFktQoA/qxkoka0LF26VM6cOZPhuKqzpw821JBgFYyokTVquGNqaqpb/V1UCahTp04e/JRAxrJJmzZtZM6cOTrbkZ56/6fPiqxevVqXFAcMGODWy6mGpKu/Fx06dODlh2kIRGB5aoihyk6oTqZquO2OHTvk+eefl2bNmunlqRU1L4LqQ7J3717dFyQlJSXTa6mhiqrEU79+fd3PRAU0KnuxaNEiqVu3rg5G0qtTp44ePvn9999nCEauXbum53k4deqUbNu2TUaOHCkvv/yyvPLKK3qIJeANFYSoAFstwa6Gh6v3qipHzp49Wxo2bJjp35U1a9bIwoULpV+/fi7Hrly5ot+rKqBXgbUaYj5lyhT9d+K+++7jfxTMk6NjdIBMZDYMtn379vbu3bu7NXxXUUMXO3XqpIfZ3m34rpKQkGAfNWqUPSoqyh4QEKCHC0dHR9tXrVqlh+v+UZv27dtnL1mypG5bSkqKPscxXFhdp1SpUvYnn3zSvnLlSv4/w2fOnDlj79u3rx6Cq95najiv+ruwYcOGPxyaq46pvyt9+vTR72l1zp3v1XLlytk7d+5sX79+Pf+nYDpD/cfEOAgAAORhlGYAAIBpCEQAAIBpCEQAAIBpCEQAAIBpCEQAAIBpCEQAAIBpCEQAAIBpCESAPO6FF15wmeK7efPmMmjQoBxvx8aNG/XaKumnLr+TOv7FF1+4fc0JEyboafq9oVZRVvdVs/YC8D0CEcCiwYH68FObWkdHTcE9adIkj1cBzoqVK1fqZeV9FTwAwN3kv+tRAKZ59NFHZfHixXrdnH/961/St29fvUrx6NGjM5x7/fp1HbD4arE1AMgpZEQAiwoMDJSIiAgpX768XkRPLWj21VdfuZRT1IJlpUuXlipVquj9akEztTBfWFiYDijat2+vSwsOagG1IUOG6OPFixeXESNGqPWmXO6bvjSjAiG1mJ9aLFC1SWVn1KJq6rqOhf2KFi2qMyOqXUpaWppMmzZNLy8fHBysFw787LPPXO6jgqvKlSvr4+o6d7bTXapd6hoFCxaUSpUqydixYzNdIfndd9/V7Vfnqdfn8uXLLsfff/99vcpzUFCQVK1aVebOnetxWwBkDYEIkEuoD2yV+XCIiYmRQ4cOybp16/SKq+oDWC0bX6RIEdmyZYts3bpVChcurDMrjue9+eabsmTJEr3asFpN+OLFi7Jq1aq73letdPzPf/5Tr/iqVn5VH+rquuqDXa0Iq6h2nD17Vi8rr6ggRK1uPH/+fPn3v/8tgwcPlm7dusmmTZucAVPHjh2lbdu2uu/FSy+9JKNGjfL4NVE/q/p51CrK6t4LFiyQGTNmuJxz5MgRWbFihaxevVrWrl0rP/74o/Tp08d5fOnSpTJu3Dgd1Kmf77XXXtMBzQcffOBxewBkgdmr7gHISK08rFb5VdTqqevWrbMHBgbahw0b5jyuVg1WqwA7fPTRR/YqVao4VxBW1PHg4GD7t99+qx+rFYLfeOMN5/HU1FR72bJlnfdKv/LwoUOH9Iqt6v6ZUau8quOXLl1yWQm5YMGC9h9++MHl3J49e9q7dOmivx89erS9evXqLsdHjhyZ4VrpqeNqleQ/Mn36dHu9evWcj8ePH2/Ply+f/dSpU85933zzjd1ms9nPnj2rH9977732ZcuWuVxn8uTJ9oYNG+rvjx8/ru/7448//uF9AWQdfUQAi1JZDpV5UJkOVer461//qkeBONSqVculX8hPP/2kf/tXWYI7JScny9GjR3U5QmUt6tev7zyWP39+efDBBzOUZxxUtiJfvnzSrFkzt9ut2nDt2jVp1aqVy36Vlalbt67+XmUe7myH0rBhQ/HUJ598ojM16udLSkrSnXlDQkJczilXrpyUKVPG5T7q9VRZHPVaqef27NlTevXq5TxHXSc0NNTj9gDwHIEIYFGq38S8efN0sKH6gaig4U6FChVyeaw+iOvVq6dLDemVKFEiy+UgT6l2KF9//bVLAKCoPia+EhsbK127dpWJEyfqkpQKHJYvX67LT562VZV00gdGKgADkP0IRACLUoGG6hjqrgceeEBnCEqWLJkhK+BQqlQp2b59uzRt2tT5m//u3bv1czOjsi4qe6D6dqjOsuk5MjKqE6xD9erVdcBx8uTJP8ykqI6hjo63Dtu2bRNP/PDDD7oj75gxY5z7Tpw4keE81Y4zZ87oYM5xH5vNpjv4hoeH6/3Hjh3TQQ2AnEdnVcBPqA/Se+65R4+UUZ1Vjx8/ruf5GDBggJw6dUqfM3DgQHn99df1pGAHDx7UnTbvNgdIhQoVpHv37vLiiy/q5ziuqTp/KioQUKNlVBnpwoULOsOgyh3Dhg3THVRVh09V+tizZ4+8/fbbzg6gL7/8shw+fFiGDx+uSyTLli3TnU49ERUVpYMMlQVR91Almsw63qqRMOpnUKUr9bqo10ONnFEjkhSVUVGda9Xzf/nlF9m3b58eNv3WW2951B4AWUMgAvgJNTR18+bNuk+EGpGisg6q74PqI+LIkAwdOlT+9re/6Q9m1VdCBQ1PPfXUXa+rykNPP/20DlrU0FbVl+Lq1av6mCq9qA9yNeJFZRf69eun96sJ0dTIE/UBr9qhRu6oUo0azquoNqoRNyq4UUN71egaNVrFE+3atdPBjrqnmj1VZUjUPdNTWSX1ejz++OPSunVrqV27tsvwXDViRw3fVcGHygCpLI4KihxtBZC9DNVjNZvvAQAAkCkyIgAAwDQEIgAAwDQEIgAAwDQEIgAAwDQEIgAAwDQEIgAAwDQEIgAAwDQEIgAAwDQEIgAAwDQEIgAAwDQEIgAAwDQEIgAAQMzy/wHwztiUiqU/SAAAAABJRU5ErkJggg==",
      "text/plain": [
       "<Figure size 640x480 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from sklearn.metrics import ConfusionMatrixDisplay\n",
    "\n",
    "ConfusionMatrixDisplay.from_estimator(model, X_test, y_test,\n",
    "                                      display_labels=[\"not CKD\", \"CKD\"], cmap=\"Greens\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d545f4e",
   "metadata": {},
   "source": [
    "## 6. 資料洩漏：故意做錯一次\n",
    "\n",
    "用**純隨機雜訊**當資料：200 個「病人」、5,000 個毫無意義的特徵、標籤也是隨機的。正確答案應該是準確率 0.5 左右（猜硬幣）。\n",
    "\n",
    "- **錯誤做法**：先用全部資料挑出與標籤最相關的 20 個特徵，再做交叉驗證。\n",
    "- **正確做法**：把挑特徵放進 Pipeline，讓每一折只用該折的訓練資料挑。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "516dd756",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:04:26.953819Z",
     "iopub.status.busy": "2026-09-29T20:04:26.953761Z",
     "iopub.status.idle": "2026-09-29T20:04:26.996231Z",
     "shell.execute_reply": "2026-09-29T20:04:26.995914Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Leaky  (select, then CV): 0.84\n",
      "Proper (select inside CV): 0.55\n"
     ]
    }
   ],
   "source": [
    "from sklearn.feature_selection import SelectKBest, f_classif\n",
    "from sklearn.pipeline import make_pipeline\n",
    "\n",
    "rng = np.random.default_rng(42)\n",
    "X_noise = rng.normal(size=(200, 5000))\n",
    "y_noise = rng.integers(0, 2, size=200)\n",
    "\n",
    "X_picked = SelectKBest(f_classif, k=20).fit_transform(X_noise, y_noise)  # sees ALL labels\n",
    "wrong = cross_val_score(LogisticRegression(max_iter=1000), X_picked, y_noise, cv=5).mean()\n",
    "\n",
    "pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression(max_iter=1000))\n",
    "right = cross_val_score(pipe, X_noise, y_noise, cv=5).mean()\n",
    "\n",
    "print(f\"Leaky  (select, then CV): {wrong:.2f}\")\n",
    "print(f\"Proper (select inside CV): {right:.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc934897",
   "metadata": {},
   "source": [
    "錯誤做法在一堆雜訊上也能得到漂亮的準確率，因為挑特徵時已經「偷看」了驗證資料的答案。\n",
    "\n",
    "## 7. 動手試試\n",
    "\n",
    "1. 把第 4 節醫院 B 的比例從 `frac=0.2` 改成 `0.5`，對數直方圖的兩座山會怎麼變？如果**不知道**哪些病人來自醫院 B，你會怎麼判斷？\n",
    "2. 在 5-2 節把數值欄的 `SimpleImputer(strategy=\"median\")` 改成 `strategy=\"mean\"`，或在 `SimpleImputer` 加上 `add_indicator=True`，測試準確率有沒有變化？想想 2-2 節的發現，加上遺漏指標欄是好事還是壞事？\n",
    "3. 在第 6 節把 `k=20` 改成 `k=200`、或把特徵數 5000 改成 500，錯誤做法的準確率怎麼變？為什麼？"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
