{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "2843f62f",
   "metadata": {},
   "source": [
    "# Introduction to AI · From writing rules to learning them\n",
    "\n",
    "The same digits, the same sixty-four pixels, the same kind of if-then rules.\n",
    "The only change: nobody writes them.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "83d94168",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T13:40:20.535169Z",
     "iopub.status.busy": "2026-08-17T13:40:20.534968Z",
     "iopub.status.idle": "2026-08-17T13:40:21.880839Z",
     "shell.execute_reply": "2026-08-17T13:40:21.880607Z"
    }
   },
   "outputs": [],
   "source": [
    "# hide — plumbing so this file runs both in the repo and in Colab.\n",
    "try:\n",
    "    from _figkit import save_fig, record\n",
    "except ImportError:  # Colab — no repo, no problem\n",
    "    def save_fig(name, plot, **kw):\n",
    "        import matplotlib.pyplot as plt\n",
    "        fig, ax = plt.subplots(figsize=kw.get('figsize', (7, 4.2))); plot(ax); plt.show()\n",
    "    def record(key, value):\n",
    "        print(f'{key} = {value}'); return value\n",
    "import time\n",
    "import numpy as np\n",
    "from sklearn.datasets import load_digits\n",
    "from sklearn.tree import DecisionTreeClassifier, export_text\n",
    "from sklearn.model_selection import train_test_split\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "40a2d3ad",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T13:40:21.882113Z",
     "iopub.status.busy": "2026-08-17T13:40:21.881997Z",
     "iopub.status.idle": "2026-08-17T13:40:21.895290Z",
     "shell.execute_reply": "2026-08-17T13:40:21.895032Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "n_train = 1,257\n",
      "n_test = 540\n",
      "n_inputs = 64\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "64"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#| caption: The same 1,797 digits as last lesson\n",
    "D = load_digits()\n",
    "X, Y = D.data, D.target        # 64 raw pixels — no features invented by anyone\n",
    "Xtr, Xte, Ytr, Yte = train_test_split(X, Y, test_size=0.3, random_state=7, stratify=Y)\n",
    "record('n_train', f'{len(Ytr):,}')\n",
    "record('n_test', f'{len(Yte):,}')\n",
    "record('n_inputs', X.shape[1])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4783aa78",
   "metadata": {},
   "source": [
    "## 1. Let it write its own rules\n",
    "\n",
    "A decision tree does exactly what the operations manager did: it writes\n",
    "if-then rules. The difference is that it chooses them by looking at examples\n",
    "rather than by thinking about digits.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a30219d4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T13:40:21.896420Z",
     "iopub.status.busy": "2026-08-17T13:40:21.896326Z",
     "iopub.status.idle": "2026-08-17T13:40:21.912820Z",
     "shell.execute_reply": "2026-08-17T13:40:21.912592Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "learned_acc = 82.8%\n",
      "n_rules_written = 139\n",
      "fit_seconds = 0.01\n",
      "rules_per_second = 13,967\n",
      "hand_rules_acc = 24.2%\n",
      "guessing = 10%\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'10%'"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#| caption: Fit, and time how long the writing took\n",
    "clf = DecisionTreeClassifier(random_state=7)\n",
    "t0 = time.time(); clf.fit(Xtr, Ytr); secs = time.time() - t0\n",
    "\n",
    "acc = clf.score(Xte, Yte)\n",
    "n_rules = clf.get_n_leaves()\n",
    "\n",
    "record('learned_acc', f'{acc:.1%}')\n",
    "record('n_rules_written', f'{n_rules:,}')\n",
    "record('fit_seconds', f'{secs:.2f}')\n",
    "record('rules_per_second', f'{n_rules/max(secs,1e-6):,.0f}')\n",
    "# The previous lesson's result, READ rather than retyped. A number a figure\n",
    "# draws is a number the page asserts, and this one was measured by a different\n",
    "# notebook — so it is loaded from that run's metrics, and the fallback is only\n",
    "# for Colab, where the repo is not there to read.\n",
    "import json\n",
    "from pathlib import Path\n",
    "HAND = 0.242\n",
    "for base in [Path.cwd(), *Path.cwd().parents]:\n",
    "    f = base / 'content/figures/introduction-to-ai/what-you-cannot-write-down.metrics.json'\n",
    "    if f.exists():\n",
    "        HAND = float(json.loads(f.read_text())['acc_5cond'].rstrip('%')) / 100\n",
    "        break\n",
    "record('hand_rules_acc', f'{HAND:.1%}')\n",
    "record('guessing', '10%')\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "4d23acc9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T13:40:21.914312Z",
     "iopub.status.busy": "2026-08-17T13:40:21.914238Z",
     "iopub.status.idle": "2026-08-17T13:40:21.917594Z",
     "shell.execute_reply": "2026-08-17T13:40:21.917373Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "|--- pixel36 <= 0.50\n",
      "|   |--- pixel28 <= 2.50\n",
      "|   |   |--- pixel21 <= 1.00\n",
      "|   |   |   |--- pixel5 <= 10.50\n",
      "|   |   |   |   |--- pixel18 <= 6.00\n",
      "|   |   |   |   |   |--- class: 4\n",
      "|   |   |   |   |--- pixel18 >  6.00\n",
      "|   |   |   |   |   |--- truncated branch of depth 3\n",
      "|   |   |   |--- pixel5 >  10.50\n",
      "|   |   |   |   |--- class: 5\n",
      "|   |   |--- pixel21 >  1.00\n",
      "|   |   |   |--- pixel62 <= 7.50\n",
      "|   |   |   |   |--- class: 0\n",
      "|   |   |   |--- pixel62 >  7.50\n",
      "rule_sample_depth = 4\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "4"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#| caption: Read one of the rules it wrote\n",
    "txt = export_text(clf, feature_names=[f'pixel{i}' for i in range(64)], max_depth=4)\n",
    "print('\\n'.join(txt.splitlines()[:14]))\n",
    "record('rule_sample_depth', 4)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "fa14aa98",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T13:40:21.918656Z",
     "iopub.status.busy": "2026-08-17T13:40:21.918572Z",
     "iopub.status.idle": "2026-08-17T13:40:21.969510Z",
     "shell.execute_reply": "2026-08-17T13:40:21.969293Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure written-vs-learned -> from-writing-rules-to-learning-them.written-vs-learned.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "#| caption: Hand-written against learned, same task\n",
    "def plot(ax):\n",
    "    labels = ['guessing', 'the best rules\\na person wrote', 'rules the machine\\nwrote itself']\n",
    "    vals = [10.0, 100*HAND, 100*acc]\n",
    "    bars = ax.bar(labels, vals)\n",
    "    bars[0].set_alpha(.35); bars[1].set_alpha(.6)\n",
    "    for b_, v in zip(bars, vals):\n",
    "        ax.text(b_.get_x()+b_.get_width()/2, v+1.6, f'{v:.1f}%', ha='center', fontsize=11)\n",
    "    ax.set_ylabel('digits identified correctly (%)')\n",
    "    ax.set_ylim(0, 108)\n",
    "save_fig('written-vs-learned', plot, figsize=(7, 3.8))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9e007285",
   "metadata": {},
   "source": [
    "## 2. The thing that actually changed\n",
    "\n",
    "Hand-written knowledge scales with people. Learned knowledge scales with\n",
    "examples — so show it more of them.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "a4937a7b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-17T13:40:21.970636Z",
     "iopub.status.busy": "2026-08-17T13:40:21.970554Z",
     "iopub.status.idle": "2026-08-17T13:40:22.130714Z",
     "shell.execute_reply": "2026-08-17T13:40:22.130470Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "acc_20 = 34%\n",
      "acc_100 = 58%\n",
      "acc_all = 83%\n",
      "beats_hand_at = 20\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure more-examples -> from-writing-rules-to-learning-them.more-examples.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "#| caption: What another thousand examples is worth\n",
    "sizes = [20, 50, 100, 200, 400, 800, len(Ytr)]\n",
    "curve = []\n",
    "for n in sizes:\n",
    "    m = DecisionTreeClassifier(random_state=7).fit(Xtr[:n], Ytr[:n])\n",
    "    curve.append(m.score(Xte, Yte))\n",
    "\n",
    "record('acc_20', f'{curve[0]:.0%}')\n",
    "record('acc_100', f'{curve[2]:.0%}')\n",
    "record('acc_all', f'{curve[-1]:.0%}')\n",
    "record('beats_hand_at', next(n for n, a in zip(sizes, curve) if a > HAND))\n",
    "\n",
    "def plot(ax):\n",
    "    ax.plot(sizes, [100*c for c in curve], marker='o', lw=2.2)\n",
    "    ax.axhline(100*HAND, ls='--', lw=1.2)\n",
    "    ax.text(sizes[-1], 27, 'the best rules a person wrote', ha='right', fontsize=9)\n",
    "    ax.set_xscale('log')\n",
    "    ax.set_xlabel('examples shown to it (log scale)')\n",
    "    ax.set_ylabel('digits identified correctly (%)')\n",
    "    ax.set_ylim(0, 100)\n",
    "save_fig('more-examples', plot, figsize=(7, 3.6))\n"
   ]
  }
 ],
 "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.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
