{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fe038cc8",
   "metadata": {},
   "source": [
    "# Day 2 — The Prediction Machine 🐧\n",
    "\n",
    "> **Mission briefing:** A field station just radioed in with a spreadsheet of penguin measurements — and no species labels. Their expert is out sick. The Lab's job: build a machine that identifies a penguin's species from its measurements alone, so the station can label the whole backlog automatically. This is your first real model.\n",
    "\n",
    "**Previously in the Lab:** as the Data Detective you cleaned the penguin data and spotted three species forming clear clusters in a scatter plot.\n",
    "\n",
    "**Today you will:**\n",
    "- Learn **supervised learning**: features, labels, and the sacred train/test split\n",
    "- Build **k-Nearest Neighbors** from scratch with NumPy, then in 3 lines of scikit-learn\n",
    "- *See* a model think, with **decision boundaries** and a readable **decision tree**\n",
    "- Diagnose mistakes with a **confusion matrix**, and uncover the **feature-scaling trap**\n",
    "\n",
    "**Today you unlock:** 🔓 **Prediction Machine** — slide four measurements, get a live species prediction in your AI Studio.\n",
    "\n",
    "<details data-resource-precision=\"2\"><summary>A precise reading</summary><p>Choose k and other settings using validation data or cross-validation within the training set, then evaluate once on an untouched test set. Fit scaling on training data only. A larger k makes a smoother boundary; too large can underfit. Comparing many settings on the same test set makes that score optimistic.</p></details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9f90962b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:19.461917Z",
     "iopub.status.busy": "2026-08-17T12:54:19.461781Z",
     "iopub.status.idle": "2026-08-17T12:54:19.706927Z",
     "shell.execute_reply": "2026-08-17T12:54:19.706077Z"
    }
   },
   "outputs": [],
   "source": [
    "# ╔═══════════════════════════════════════════════════════════════════╗\n",
    "# ║ SETUP — run me first! (identical in every Lab notebook)           ║\n",
    "# ╚═══════════════════════════════════════════════════════════════════╝\n",
    "import os, sys, pathlib\n",
    "\n",
    "IN_COLAB = \"google.colab\" in sys.modules\n",
    "SMOKE = os.environ.get(\"SMOKE_TEST\", \"0\") == \"1\"   # tiny fast run for automated tests\n",
    "\n",
    "REPO_URL = \"https://github.com/A-Halimi/ai-studio-internship.git\"  # instructor: set once\n",
    "\n",
    "if IN_COLAB:\n",
    "    if not pathlib.Path(\"/content/ai-studio-internship\").exists():\n",
    "        !git clone -q {REPO_URL} /content/ai-studio-internship\n",
    "    %cd /content/ai-studio-internship/notebooks/day02\n",
    "\n",
    "HERE = pathlib.Path.cwd()\n",
    "REPO = HERE.parents[1]                       # .../notebooks/dayNN -> repo root\n",
    "DATA_DIR = pathlib.Path(os.environ.get(\"COURSE_DATA_DIR\", REPO / \"data\"))\n",
    "SAMPLES_DIR = REPO / \"data_samples\"          # small datasets shipped with the repo\n",
    "MODELS_DIR = REPO / \"ai_studio\" / \"models\"   # where your Studio modules unlock\n",
    "for p in (REPO / \"data\", MODELS_DIR):\n",
    "    p.mkdir(parents=True, exist_ok=True)\n",
    "\n",
    "import random\n",
    "import numpy as np\n",
    "SEED = 42\n",
    "random.seed(SEED); np.random.seed(SEED)\n",
    "print(f\"✅ Setup done | Colab: {IN_COLAB} | Smoke test: {SMOKE}\")\n",
    "print(f\"   data: {DATA_DIR}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d1772127",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:19.708630Z",
     "iopub.status.busy": "2026-08-17T12:54:19.708434Z",
     "iopub.status.idle": "2026-08-17T12:54:23.959554Z",
     "shell.execute_reply": "2026-08-17T12:54:23.958299Z"
    }
   },
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "\n",
    "print(\"Toolkit ready 🧰\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d68e66d5",
   "metadata": {},
   "source": [
    "## Supervised learning: teaching by example\n",
    "\n",
    "Yesterday you *saw* the penguin species form clusters. Today you'll teach a machine to draw the boundaries itself — using the most common flavor of machine learning: **supervised learning**.\n",
    "\n",
    "The recipe has two ingredients:\n",
    "\n",
    "- **Features (`X`)** — the measurements we know for each penguin (bill length, flipper length, ...).\n",
    "- **Label (`y`)** — the answer we want to predict (the species).\n",
    "\n",
    "We show the machine hundreds of `(X, y)` pairs — \"*these* measurements → *that* species\" — and it learns the mapping. Then we hand it new measurements with the label hidden and ask it to guess.\n",
    "\n",
    "**The golden rule: never test on data you trained on.** We split the penguins into a **training set** (the machine studies these) and a **test set** (locked in a drawer). It's the difference between studying with past exam papers and then sitting a *brand-new* exam. If you only ever graded yourself on the exact questions you'd memorized, you'd have no idea whether you actually learned anything. Hiding the test set is the only honest way to measure real learning."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "67909aee",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:23.961590Z",
     "iopub.status.busy": "2026-08-17T12:54:23.961289Z",
     "iopub.status.idle": "2026-08-17T12:54:23.987231Z",
     "shell.execute_reply": "2026-08-17T12:54:23.986299Z"
    }
   },
   "outputs": [],
   "source": [
    "penguins = pd.read_csv(SAMPLES_DIR / \"penguins.csv\").dropna().reset_index(drop=True)\n",
    "\n",
    "FEATURES = [\"bill_length_mm\", \"bill_depth_mm\", \"flipper_length_mm\", \"body_mass_g\"]\n",
    "X = penguins[FEATURES]     # features: a DataFrame with 4 columns\n",
    "y = penguins[\"species\"]    # label: the species of each penguin\n",
    "\n",
    "print(f\"{len(penguins)} clean penguins\")\n",
    "print(f\"Features: {FEATURES}\")\n",
    "print(f\"Species : {sorted(y.unique())}\")\n",
    "penguins.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "29397d0d",
   "metadata": {},
   "source": [
    "### 🧪 Exercise 1 — Split the data honestly\n",
    "\n",
    "Use `train_test_split` to lock away **25%** of the penguins as a test set.\n",
    "\n",
    "- Pass `test_size=0.25`, `random_state=SEED` (so the split is reproducible), and `stratify=y`.\n",
    "- **`stratify=y`** keeps each species at the same proportion in both halves — important when one species is rarer than the others.\n",
    "- **Expected:** 249 training penguins and 84 test penguins."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "025485db",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:23.988920Z",
     "iopub.status.busy": "2026-08-17T12:54:23.988780Z",
     "iopub.status.idle": "2026-08-17T12:54:24.370620Z",
     "shell.execute_reply": "2026-08-17T12:54:24.369507Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "# ==================== YOUR CODE HERE ====================\n",
    "### HINT: train_test_split(X, y, test_size=0.25, random_state=SEED, stratify=y)\n",
    "...\n",
    "print(f\"Training penguins: {len(X_train)}\")\n",
    "print(f\"Testing penguins : {len(X_test)}\")\n",
    "assert len(X_test) == 84, \"With 333 penguins and test_size=0.25 you should hold out 84.\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1ead8d6b",
   "metadata": {},
   "source": [
    "## Machine #1: k-Nearest Neighbors\n",
    "\n",
    "The simplest classifier there is — and it works just how you'd guess a stranger's taste in music: **look at who they're standing next to.**\n",
    "\n",
    "To label a new penguin, **k-Nearest Neighbors (k-NN)**:\n",
    "1. measures the distance from the new penguin to *every* training penguin,\n",
    "2. finds the **k** closest ones (its \"nearest neighbors\"),\n",
    "3. takes a **majority vote** of their species.\n",
    "\n",
    "There's no \"training\" beyond memorizing the examples. Let's build it from scratch with NumPy so nothing is hidden — using just **two** features at first, so we can literally *see* it work: bill length and flipper length."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ccd8e681",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:24.372392Z",
     "iopub.status.busy": "2026-08-17T12:54:24.372116Z",
     "iopub.status.idle": "2026-08-17T12:54:24.379303Z",
     "shell.execute_reply": "2026-08-17T12:54:24.378412Z"
    }
   },
   "outputs": [],
   "source": [
    "TWO = [\"bill_length_mm\", \"flipper_length_mm\"]\n",
    "X_train2 = X_train[TWO].to_numpy()\n",
    "X_test2  = X_test[TWO].to_numpy()\n",
    "\n",
    "# k-NN votes by tallying labels, so turn species names into integer codes 0/1/2\n",
    "classes = np.array(sorted(y.unique()))               # ['Adelie', 'Chinstrap', 'Gentoo']\n",
    "name_to_code = {name: i for i, name in enumerate(classes)}\n",
    "y_train_codes = np.array([name_to_code[s] for s in y_train])\n",
    "y_test_codes  = np.array([name_to_code[s] for s in y_test])\n",
    "\n",
    "print(\"Two-feature training matrix:\", X_train2.shape)\n",
    "print(\"Classes:\", classes.tolist())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dff7d9bb",
   "metadata": {},
   "source": [
    "### 🧪 Exercise 2 — Build k-NN from scratch\n",
    "\n",
    "Fill in the body of `knn_predict`. Given one new penguin `x_new`, it should return the majority species-code among its `k` nearest neighbors.\n",
    "\n",
    "- **Distance:** `np.sqrt(((X_train - x_new) ** 2).sum(axis=1))` computes the distance to *all* training points at once.\n",
    "- **Nearest:** `np.argsort(distances)` returns the indices from closest to farthest — take the first `k`.\n",
    "- **Vote:** `np.bincount(votes).argmax()` returns the most common code."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "590ae728",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:24.380716Z",
     "iopub.status.busy": "2026-08-17T12:54:24.380586Z",
     "iopub.status.idle": "2026-08-17T12:54:24.384761Z",
     "shell.execute_reply": "2026-08-17T12:54:24.383833Z"
    }
   },
   "outputs": [],
   "source": [
    "def knn_predict(x_new, X_train, y_train, k=5):\n",
    "    \"\"\"Predict the label of x_new by majority vote of its k nearest neighbors.\"\"\"\n",
    "    # ==================== YOUR CODE HERE ====================\n",
    "    ### HINT: np.sqrt(((X_train - x_new)**2).sum(axis=1)) gives all distances at once\n",
    "    ### HINT: np.argsort gives the indices that would sort the array\n",
    "    ...\n",
    "\n",
    "\n",
    "# sanity check: predict the very first test penguin\n",
    "pred_code = knn_predict(X_test2[0], X_train2, y_train_codes, k=5)\n",
    "print(\"Predicted:\", classes[pred_code], \"  |  Actual:\", classes[y_test_codes[0]])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4359d228",
   "metadata": {},
   "source": [
    "### 🧪 Exercise 3 — Grade it on the test set\n",
    "\n",
    "Now run your `knn_predict` on **every** test penguin and measure the accuracy.\n",
    "\n",
    "- Build an array `preds` with one prediction per row of `X_test2`.\n",
    "- **Expected:** accuracy around **0.95** — and the self-check insists on beating 0.80."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "acbaa39b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:24.386093Z",
     "iopub.status.busy": "2026-08-17T12:54:24.385966Z",
     "iopub.status.idle": "2026-08-17T12:54:24.392691Z",
     "shell.execute_reply": "2026-08-17T12:54:24.391794Z"
    }
   },
   "outputs": [],
   "source": [
    "# ==================== YOUR CODE HERE ====================\n",
    "### HINT: call knn_predict on every row of X_test2 and collect the results into an array\n",
    "...\n",
    "accuracy = (preds == y_test_codes).mean()\n",
    "print(f\"Scratch k-NN accuracy on {len(X_test2)} unseen penguins: {accuracy:.3f}\")\n",
    "assert accuracy > 0.8, \"Your k-NN should beat 0.80 — check the distance and voting logic.\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c15986cf",
   "metadata": {},
   "source": [
    "## Watch it think: decision boundaries\n",
    "\n",
    "Here's how to *see* what the classifier learned. Take every point on the plane, ask the model which species it would predict there, and color the background to match. The result is a map of **decision boundaries** — the borders between \"I'd call this an Adelie\" and \"I'd call this a Gentoo.\"\n",
    "\n",
    "We give you the plotting helper; you supply the model. Let's compare two extremes: **k = 1** (trust only the single closest neighbor) versus **k = 15** (poll fifteen of them)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "78ea6d71",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:24.394216Z",
     "iopub.status.busy": "2026-08-17T12:54:24.394087Z",
     "iopub.status.idle": "2026-08-17T12:54:24.399828Z",
     "shell.execute_reply": "2026-08-17T12:54:24.398792Z"
    }
   },
   "outputs": [],
   "source": [
    "from matplotlib.colors import ListedColormap\n",
    "\n",
    "BG = ListedColormap([\"#cfe8cf\", \"#fdf0c0\", \"#f6cccc\"])   # faint background fills\n",
    "FG = ListedColormap([\"#1b7837\", \"#b8860b\", \"#b2182b\"])   # bold dot colors\n",
    "\n",
    "\n",
    "def plot_decision_boundary(predict_fn, X2, y_codes, ax, title):\n",
    "    \"\"\"Color the whole plane by the model's prediction, then scatter the data on top.\n",
    "\n",
    "    predict_fn takes an (N, 2) array of points and returns (N,) class codes.\n",
    "    \"\"\"\n",
    "    res = 60 if SMOKE else 200\n",
    "    x_min, x_max = X2[:, 0].min() - 1, X2[:, 0].max() + 1\n",
    "    y_min, y_max = X2[:, 1].min() - 5, X2[:, 1].max() + 5\n",
    "    xx, yy = np.meshgrid(np.linspace(x_min, x_max, res),\n",
    "                         np.linspace(y_min, y_max, res))\n",
    "    grid = np.c_[xx.ravel(), yy.ravel()]\n",
    "    Z = predict_fn(grid).reshape(xx.shape)\n",
    "    ax.contourf(xx, yy, Z, levels=[-0.5, 0.5, 1.5, 2.5], alpha=0.35, cmap=BG)\n",
    "    ax.scatter(X2[:, 0], X2[:, 1], c=y_codes, cmap=FG, edgecolor=\"k\", s=20)\n",
    "    ax.set_xlabel(\"Bill length (mm)\")\n",
    "    ax.set_ylabel(\"Flipper length (mm)\")\n",
    "    ax.set_title(title)\n",
    "\n",
    "\n",
    "def knn_grid(grid, k):\n",
    "    \"\"\"Wrap the scratch k-NN so it can label a whole grid of points.\"\"\"\n",
    "    return np.array([knn_predict(pt, X_train2, y_train_codes, k=k) for pt in grid])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "447a27eb",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:24.401348Z",
     "iopub.status.busy": "2026-08-17T12:54:24.401200Z",
     "iopub.status.idle": "2026-08-17T12:54:25.238060Z",
     "shell.execute_reply": "2026-08-17T12:54:25.236655Z"
    }
   },
   "outputs": [],
   "source": [
    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 5))\n",
    "plot_decision_boundary(lambda g: knn_grid(g, 1),  X_train2, y_train_codes, ax1,\n",
    "                       \"k = 1  (memorizes every point)\")\n",
    "plot_decision_boundary(lambda g: knn_grid(g, 15), X_train2, y_train_codes, ax2,\n",
    "                       \"k = 15  (smooth and general)\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31d82eb0",
   "metadata": {},
   "source": [
    "### 🧠 Memorizing vs. generalizing\n",
    "\n",
    "- **k = 1** carves a jagged little island around *every* training point — including the odd penguin sitting in the \"wrong\" place. It has essentially **memorized** the training data, noise and all: perfect on what it's seen, brittle on anything new. That's **overfitting**.\n",
    "- **k = 15** is smooth. By polling many neighbors it shrugs off individual weird penguins and captures the *general* trend. It might miss a true outlier, but it will **generalize** better to new penguins.\n",
    "\n",
    "This tension — memorizing the past vs. generalizing to the future — is one of the deepest ideas in all of machine learning. You'll meet it every single day."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e385d1be",
   "metadata": {},
   "source": [
    "### 🧪 Exercise 4 — The same thing in 3 lines\n",
    "\n",
    "You just built k-NN by hand. In practice nobody does — **scikit-learn** ships a fast, battle-tested version. See how little code it takes.\n",
    "\n",
    "- Create `KNeighborsClassifier(n_neighbors=5)`, `.fit(...)` it on the **same two features**, and `.score(...)` it on the test set.\n",
    "- **Expected:** almost exactly the accuracy your from-scratch version got. It's the same algorithm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "95579f00",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:25.239902Z",
     "iopub.status.busy": "2026-08-17T12:54:25.239745Z",
     "iopub.status.idle": "2026-08-17T12:54:25.340894Z",
     "shell.execute_reply": "2026-08-17T12:54:25.339567Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "# ==================== YOUR CODE HERE ====================\n",
    "### HINT: KNeighborsClassifier(n_neighbors=5), then .fit(X_train2, y_train_codes), then .score(...)\n",
    "...\n",
    "print(f\"sklearn k-NN accuracy : {sklearn_acc:.3f}\")\n",
    "print(f\"your scratch accuracy : {accuracy:.3f}\")\n",
    "assert abs(sklearn_acc - accuracy) < 0.05, \"Scratch and sklearn k-NN should give nearly the same score.\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a18a7226",
   "metadata": {},
   "source": [
    "## Machine #2: the Decision Tree\n",
    "\n",
    "k-NN never really *explains* itself. A **decision tree** does — it's a flowchart of yes/no questions the machine writes for itself: *\"Is the bill longer than 43 mm? If yes, go left; if no, go right...\"* Follow the branches to a species.\n",
    "\n",
    "### 🧪 Exercise 5 — Grow a tree\n",
    "\n",
    "- Create a `DecisionTreeClassifier(max_depth=3, random_state=SEED)`, fit it on the two features, and score it.\n",
    "- `max_depth=3` keeps the tree shallow enough to actually read — we'll draw it next."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7278be98",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:25.342562Z",
     "iopub.status.busy": "2026-08-17T12:54:25.342408Z",
     "iopub.status.idle": "2026-08-17T12:54:25.363275Z",
     "shell.execute_reply": "2026-08-17T12:54:25.362329Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.tree import DecisionTreeClassifier, plot_tree\n",
    "# ==================== YOUR CODE HERE ====================\n",
    "...\n",
    "print(f\"Decision tree accuracy: {tree_acc:.3f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "377e0852",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:25.365344Z",
     "iopub.status.busy": "2026-08-17T12:54:25.365196Z",
     "iopub.status.idle": "2026-08-17T12:54:25.409019Z",
     "shell.execute_reply": "2026-08-17T12:54:25.407847Z"
    }
   },
   "outputs": [],
   "source": [
    "plt.figure(figsize=(11, 6))\n",
    "plot_tree(tree, feature_names=[\"bill_length\", \"flipper_length\"],\n",
    "          class_names=list(classes), filled=True, rounded=True, fontsize=9)\n",
    "plt.title(\"The machine's if/else brain (max_depth = 3)\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca0baf53",
   "metadata": {},
   "source": [
    "### 📖 Read the tree out loud\n",
    "\n",
    "Start at the top box and follow one path down, reading it as a sentence: *\"If the bill length is below this threshold, and the flipper is below that one, then I predict **Adelie**.\"* Every box splits on one measurement; every leaf at the bottom is a final answer, colored by the species that dominates it.\n",
    "\n",
    "This is why decision trees are prized when a prediction must be **explained** — a doctor or a bank can trace the exact chain of reasoning. The \"who's standing next to me\" logic of k-NN can't offer that."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65d1b665",
   "metadata": {},
   "source": [
    "## Where does it go wrong? The confusion matrix\n",
    "\n",
    "Accuracy is a single number, and a single number hides *where* mistakes happen. A **confusion matrix** lays out, for every true species, what the model actually guessed — so a mix-up between two look-alike species stands out from a wild miss.\n",
    "\n",
    "### 🧪 Exercise 6 — Build the confusion matrix\n",
    "\n",
    "- Call `confusion_matrix(actual, predicted)` on the test set, using the sklearn k-NN's predictions.\n",
    "- We'll draw it as a heatmap. **The diagonal is correct; everything off the diagonal is a mix-up.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb3f3eb5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:25.410970Z",
     "iopub.status.busy": "2026-08-17T12:54:25.410829Z",
     "iopub.status.idle": "2026-08-17T12:54:25.470124Z",
     "shell.execute_reply": "2026-08-17T12:54:25.468687Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.metrics import confusion_matrix\n",
    "y_pred_codes = knn_sklearn.predict(X_test2)   # the sklearn k-NN's guesses on the test set\n",
    "# ==================== YOUR CODE HERE ====================\n",
    "### HINT: confusion_matrix(actual, predicted) — rows are the true species, columns the guesses\n",
    "...\n",
    "plt.figure(figsize=(6, 5))\n",
    "sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n",
    "            xticklabels=classes, yticklabels=classes)\n",
    "plt.xlabel(\"Predicted species\")\n",
    "plt.ylabel(\"Actual species\")\n",
    "plt.title(\"Confusion matrix — where the mistakes hide\")\n",
    "plt.show()\n",
    "print(cm)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1ee8af15",
   "metadata": {},
   "source": [
    "### 🔎 Reading the mistakes\n",
    "\n",
    "The diagonal is bright — most penguins are classified correctly. The few off-diagonal cells are the interesting part: with only two features, the model's slip-ups cluster around **Chinstrap**.\n",
    "\n",
    "That's not random. On these two measurements **Chinstrap sits in the middle** — its bill length is close to Gentoo's, and its flipper length is close to Adelie's — so a borderline penguin can drift into Chinstrap territory. Our hypothesis: hand the model **more measurements** (bill depth and body mass too) and those extra dimensions should pull the species further apart. Let's test that hunch."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "50bd43b3",
   "metadata": {},
   "source": [
    "## The model tournament — and a trap\n",
    "\n",
    "Time to use **all four** features and find the best `k`. We'll hold a tournament: train a k-NN for every `k` from 1 to 15, and see which scores highest on the test set.\n",
    "\n",
    "### 🧪 Exercise 7 — Run the tournament (raw features)\n",
    "\n",
    "- Complete the call to `tournament(...)` using the raw four-feature arrays `X_train4` and `X_test4`.\n",
    "- **Watch closely:** two features already scored about 0.98. Four features carry *more* information, so surely this does better... right?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5227da44",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:25.471825Z",
     "iopub.status.busy": "2026-08-17T12:54:25.471681Z",
     "iopub.status.idle": "2026-08-17T12:54:25.511631Z",
     "shell.execute_reply": "2026-08-17T12:54:25.510485Z"
    }
   },
   "outputs": [],
   "source": [
    "X_train4 = X_train.to_numpy()\n",
    "X_test4  = X_test.to_numpy()\n",
    "k_values = list(range(1, 16))\n",
    "\n",
    "\n",
    "def tournament(train_X, test_X):\n",
    "    \"\"\"Fit a k-NN for every k in k_values; return the list of test accuracies.\"\"\"\n",
    "    scores = []\n",
    "    for k in k_values:\n",
    "        m = KNeighborsClassifier(n_neighbors=k).fit(train_X, y_train_codes)\n",
    "        scores.append(m.score(test_X, y_test_codes))\n",
    "    return scores\n",
    "\n",
    "\n",
    "# ==================== YOUR CODE HERE ====================\n",
    "### HINT: run the tournament on the RAW four-feature arrays, X_train4 and X_test4\n",
    "...\n",
    "print(f\"Best accuracy with 4 raw features: {max(raw_scores):.3f}\")\n",
    "print(\"(Remember: two features already scored about 0.98...)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e764d72",
   "metadata": {},
   "source": [
    "### ⚠️ Wait — more information made it *worse*?\n",
    "\n",
    "That's the trap. k-NN decides who's \"nearest\" using **distance**, and distance sums up every feature's differences. Look at the raw scales:\n",
    "\n",
    "- bill length differs between penguins by a few **millimeters** (values ≈ 40)\n",
    "- body mass differs by hundreds or thousands of **grams** (values ≈ 4000)\n",
    "\n",
    "So `body_mass_g` **drowns out** everything else — \"nearest\" collapses to \"weighs about the same.\" And Adelie and Chinstrap weigh almost exactly the same, so the model can no longer tell them apart. It's like measuring the distance between two cities with one ruler in millimeters and another in kilometers, then adding the two numbers.\n",
    "\n",
    "**The fix: put every feature on the same scale.** `StandardScaler` rescales each column to mean 0 with a comparable spread, so a millimeter of bill and a gram of mass each get a *fair* vote."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98912f35",
   "metadata": {},
   "source": [
    "### 🧪 Exercise 8 — Scale, then rerun\n",
    "\n",
    "- We've already fit a `StandardScaler` on the training features and transformed both sets for you.\n",
    "- Run the **same** `tournament(...)` on the scaled arrays `X_train4s` and `X_test4s`, storing the result in `scaled_scores`.\n",
    "- **Expected:** accuracy jumps above **0.95** — now clearly beating the two-feature model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cf461ff7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:25.513334Z",
     "iopub.status.busy": "2026-08-17T12:54:25.513192Z",
     "iopub.status.idle": "2026-08-17T12:54:25.564664Z",
     "shell.execute_reply": "2026-08-17T12:54:25.563576Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "scaler = StandardScaler().fit(X_train4)\n",
    "X_train4s = scaler.transform(X_train4)\n",
    "X_test4s  = scaler.transform(X_test4)\n",
    "\n",
    "# ==================== YOUR CODE HERE ====================\n",
    "### HINT: call tournament(...) again, but on the scaled arrays X_train4s and X_test4s\n",
    "...\n",
    "best_k = k_values[int(np.argmax(scaled_scores))]\n",
    "print(f\"Best accuracy with 4 SCALED features: {max(scaled_scores):.3f}  at k = {best_k}\")\n",
    "assert max(scaled_scores) > 0.9, \"After scaling, accuracy should climb well above 0.90.\"\n",
    "\n",
    "plt.figure(figsize=(8, 5))\n",
    "plt.plot(k_values, raw_scores, marker=\"o\", label=\"raw features\")\n",
    "plt.plot(k_values, scaled_scores, marker=\"s\", label=\"scaled features\")\n",
    "plt.axvline(best_k, color=\"red\", linestyle=\"--\", label=f\"best k = {best_k}\")\n",
    "plt.xlabel(\"k (number of neighbors)\")\n",
    "plt.ylabel(\"Test accuracy\")\n",
    "plt.title(\"Putting features on the same scale changes everything\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ca3c08fe",
   "metadata": {},
   "source": [
    "## Ship the champion\n",
    "\n",
    "The tournament chose the best `k` **honestly**, on penguins the model never trained on. Now two professional moves give us the final, deployable model:\n",
    "\n",
    "1. **Bundle** the scaler and the classifier into a single `Pipeline`, so raw measurements can go straight in and get scaled automatically — exactly what the Studio app needs.\n",
    "2. **Retrain on every penguin we have** — now that `k` is locked in, more data makes a stronger model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5275cf87",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:25.566202Z",
     "iopub.status.busy": "2026-08-17T12:54:25.566053Z",
     "iopub.status.idle": "2026-08-17T12:54:25.576828Z",
     "shell.execute_reply": "2026-08-17T12:54:25.575802Z"
    }
   },
   "outputs": [],
   "source": [
    "from sklearn.pipeline import make_pipeline\n",
    "\n",
    "# One object that scales AND classifies. Feed it raw measurements (like the Studio's\n",
    "# sliders) and it handles the scaling internally before predicting.\n",
    "final_model = make_pipeline(\n",
    "    StandardScaler(),\n",
    "    KNeighborsClassifier(n_neighbors=best_k),\n",
    ")\n",
    "final_model.fit(X, y)   # X is a DataFrame (keeps column names); train on ALL penguins\n",
    "\n",
    "honest_estimate = max(scaled_scores)   # accuracy measured earlier on held-out test penguins\n",
    "print(f\"Final model: StandardScaler + {best_k}-NN, trained on all {len(X)} penguins.\")\n",
    "print(f\"Honest accuracy estimate (from the held-out test set): {honest_estimate:.3f}\")\n",
    "print(f\"Species it can predict: {list(final_model.classes_)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8feac1a4",
   "metadata": {},
   "source": [
    "## 🔓 Unlock your Studio module\n",
    "\n",
    "You trained a real classifier — now **ship it**. The cell below saves your model into `ai_studio/models/` in the exact shape the **🐧 Prediction Machine** tab expects:\n",
    "\n",
    "- `model` — your fitted scaler + classifier pipeline\n",
    "- `feature_names` — the four columns, in order, so the app feeds inputs correctly\n",
    "- `target_names` — the species names **in the model's own class order**, so the probability bars line up with the right label\n",
    "\n",
    "(No blanks to fill here — just run it and watch the padlock pop open.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6d9a3e23",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T12:54:25.578221Z",
     "iopub.status.busy": "2026-08-17T12:54:25.578088Z",
     "iopub.status.idle": "2026-08-17T12:54:25.599400Z",
     "shell.execute_reply": "2026-08-17T12:54:25.598025Z"
    }
   },
   "outputs": [],
   "source": [
    "import joblib\n",
    "\n",
    "bundle = {\n",
    "    \"model\": final_model,\n",
    "    \"feature_names\": FEATURES,\n",
    "    \"target_names\": list(final_model.classes_),   # SAME order as predict_proba's columns\n",
    "}\n",
    "out_path = MODELS_DIR / \"prediction_machine.joblib\"\n",
    "joblib.dump(bundle, out_path)\n",
    "\n",
    "REQUIRED_FILES = [\"prediction_machine.joblib\"]\n",
    "missing = [f for f in REQUIRED_FILES if not (MODELS_DIR / f).exists()]\n",
    "assert not missing, f\"Something didn't save: {missing}\"\n",
    "\n",
    "print(\"🔓 UNLOCKED: Prediction Machine!\")\n",
    "print(f\"   saved: {out_path}\")\n",
    "print(\"   Run your Studio and drag the sliders — your model predicts live:\")\n",
    "print(\"   python ai_studio/app.py   (from the repo root)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4306eecb",
   "metadata": {},
   "source": [
    "## 🏁 Checkpoint\n",
    "\n",
    "You built a real classifier — twice from scratch, then again with the pros' tools:\n",
    "\n",
    "- ✅ Split data into train/test and learned *why* hiding the test set is the only honest measure\n",
    "- ✅ Coded **k-Nearest Neighbors** yourself with NumPy\n",
    "- ✅ Saw **decision boundaries** and the memorize-vs-generalize tradeoff (k = 1 vs k = 15)\n",
    "- ✅ Grew a readable **decision tree** and traced its reasoning\n",
    "- ✅ Read a **confusion matrix** to find *which* species get mixed up\n",
    "- ✅ Sprang the **feature-scaling trap** — why more data can hurt, and how to fix it\n",
    "- ✅ **Unlocked 🐧 Prediction Machine** — your first Studio tab is live!\n",
    "\n",
    "Run `python ai_studio/app.py`, open the Prediction Machine tab, and drag the sliders. That's *your* model answering.\n",
    "\n",
    "> **Tomorrow (Day 3):** every penguin today came with its species already written down. But what can a machine learn when **nobody** labels the data — when you just hand it a pile of measurements and ask, *\"what groups are hiding in here?\"* That's **unsupervised learning**, and it unlocks your 🎨 Pattern Finder."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2ee2546e",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "<div align=\"center\" style=\"background:#14161E;border-radius:12px;padding:18px 12px 14px;margin-top:6px\">\n",
    "  <a href=\"https://ahalimi.com\" target=\"_blank\">\n",
    "    <img src=\"data:image/webp;base64,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\" alt=\"Dr. Abdelghafour HALIMI\" width=\"260\"/>\n",
    "  </a>\n",
    "</div>\n",
    "\n",
    "<div align=\"center\" style=\"text-align:center;margin-top:8px\"><sub>The Lab · AI Studio — Intro to AI · © 2026 <b>Dr. Abdelghafour HALIMI</b> · <a href=\"https://ahalimi.com\">ahalimi.com</a> · All rights reserved.</sub></div>"
   ]
  }
 ],
 "metadata": {
  "authors": [
   {
    "name": "Dr. Abdelghafour HALIMI"
   }
  ],
  "copyright": "© 2026 Dr. Abdelghafour HALIMI · ahalimi.com · All rights reserved",
  "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.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
