{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c000",
   "metadata": {},
   "source": [
    "# Nearest Neighbors with scikit-learn"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c001",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:29.959797Z",
     "iopub.status.busy": "2026-10-23T03:50:29.959563Z",
     "iopub.status.idle": "2026-10-23T03:50:30.951087Z",
     "shell.execute_reply": "2026-10-23T03:50:30.950598Z"
    },
    "tags": [
     "hide-input",
     "thebe-init",
     "runnable"
    ]
   },
   "outputs": [],
   "source": [
    "path_data = '../../data/'\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "plt.style.use('fivethirtyeight')\n",
    "%matplotlib inline\n",
    "\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "from sklearn.pipeline import make_pipeline\n",
    "from sklearn.metrics import accuracy_score"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c002",
   "metadata": {},
   "source": [
    "```{contents}\n",
    ":local:\n",
    ":depth: 2\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c003",
   "metadata": {},
   "source": [
    "In the previous sections we built a $k$-nearest neighbor classifier by hand: compute distances, sort, take the $k$ closest rows, and hold a majority vote. That code shows exactly what the method does, but it is slow and easy to get wrong. In practice, analysts use a library. In Chapter 13 you used scikit-learn's `LinearRegression` and `KNeighborsRegressor`; scikit-learn's `KNeighborsClassifier` does the same work for classification, with the same `fit` and `predict` steps.\n",
    "\n",
    "This section follows one business case through the whole workflow. A subscription company (phone and internet service) wants to know **which customers are likely to cancel next quarter**, so that its retention team can call them with an offer before they leave. Customers who cancel are said to *churn*. The steps are the ones you already know from the hand-built classifier:\n",
    "\n",
    "1. Choose the features and the target.\n",
    "2. Split the data into a training set and a test set.\n",
    "3. Put the features on a common scale, using the training set only.\n",
    "4. Fit the classifier, predict, and measure accuracy on the test set.\n",
    "5. Choose $k$ by comparing accuracy on data the classifier has not seen."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c004",
   "metadata": {},
   "source": [
    "## The Customer Churn Data\n",
    "\n",
    "```{index} customer churn example\n",
    "```\n",
    "\n",
    "```{index} churn\n",
    "```\n",
    "\n",
    "The file `customer_churn.csv` holds 1,000 customers of the company. Each row is one customer at the end of a quarter, with three numeric attributes and a label:\n",
    "\n",
    "- `tenure_months`: how many months the customer has been with the company,\n",
    "- `monthly_charges`: the customer's monthly bill in dollars,\n",
    "- `support_calls`: the number of calls the customer made to customer support last quarter,\n",
    "- `churn`: 1 if the customer cancelled during the next quarter, 0 if the customer stayed.\n",
    "\n",
    "The data are synthetic (generated for this book with a fixed random seed) but follow patterns that churn studies commonly report: newer customers, customers with high bills, and customers who keep calling support are more likely to leave."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c005",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.952584Z",
     "iopub.status.busy": "2026-10-23T03:50:30.952448Z",
     "iopub.status.idle": "2026-10-23T03:50:30.958168Z",
     "shell.execute_reply": "2026-10-23T03:50:30.957833Z"
    },
    "tags": []
   },
   "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>customer_id</th>\n",
       "      <th>tenure_months</th>\n",
       "      <th>monthly_charges</th>\n",
       "      <th>support_calls</th>\n",
       "      <th>churn</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>C0001</td>\n",
       "      <td>11</td>\n",
       "      <td>62.23</td>\n",
       "      <td>1</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>C0002</td>\n",
       "      <td>60</td>\n",
       "      <td>76.71</td>\n",
       "      <td>1</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>C0003</td>\n",
       "      <td>48</td>\n",
       "      <td>58.86</td>\n",
       "      <td>2</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>C0004</td>\n",
       "      <td>26</td>\n",
       "      <td>87.30</td>\n",
       "      <td>5</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>C0005</td>\n",
       "      <td>7</td>\n",
       "      <td>84.22</td>\n",
       "      <td>2</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "  customer_id  tenure_months  monthly_charges  support_calls  churn\n",
       "0       C0001             11            62.23              1      1\n",
       "1       C0002             60            76.71              1      0\n",
       "2       C0003             48            58.86              2      1\n",
       "3       C0004             26            87.30              5      1\n",
       "4       C0005              7            84.22              2      1"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "churn = pd.read_csv(path_data + 'customer_churn.csv')\n",
    "churn.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c006",
   "metadata": {},
   "source": [
    "How common is churn? The `value_counts` method with `normalize=True` gives the proportion of customers in each class."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c007",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.959340Z",
     "iopub.status.busy": "2026-10-23T03:50:30.959279Z",
     "iopub.status.idle": "2026-10-23T03:50:30.961506Z",
     "shell.execute_reply": "2026-10-23T03:50:30.961192Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "churn\n",
       "0    0.714\n",
       "1    0.286\n",
       "Name: proportion, dtype: float64"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "churn['churn'].value_counts(normalize=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c008",
   "metadata": {},
   "source": [
    "About 29% of the customers churned and 71% stayed. The classes are *imbalanced*: one class is much more common than the other. Keep that 71% in mind. A \"classifier\" that predicts \"stays\" for every customer, without looking at any data, is already right about 71% of the time, so a real classifier has to beat that.\n",
    "\n",
    "Comparing the average attributes of the two groups shows why the features should help."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "c009",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.962351Z",
     "iopub.status.busy": "2026-10-23T03:50:30.962297Z",
     "iopub.status.idle": "2026-10-23T03:50:30.965197Z",
     "shell.execute_reply": "2026-10-23T03:50:30.964879Z"
    },
    "tags": []
   },
   "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>tenure_months</th>\n",
       "      <th>monthly_charges</th>\n",
       "      <th>support_calls</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>churn</th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "      <th></th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>41.957983</td>\n",
       "      <td>65.942269</td>\n",
       "      <td>1.198880</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>22.412587</td>\n",
       "      <td>76.277203</td>\n",
       "      <td>2.255245</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "       tenure_months  monthly_charges  support_calls\n",
       "churn                                               \n",
       "0          41.957983        65.942269       1.198880\n",
       "1          22.412587        76.277203       2.255245"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "churn.groupby('churn')[['tenure_months', 'monthly_charges', 'support_calls']].mean()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c010",
   "metadata": {},
   "source": [
    "Customers who churned had been with the company for about 22 months on average, compared with about 42 months for those who stayed. They also paid about \\$10 more per month and made about twice as many support calls."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c011",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.965996Z",
     "iopub.status.busy": "2026-10-23T03:50:30.965943Z",
     "iopub.status.idle": "2026-10-23T03:50:30.968177Z",
     "shell.execute_reply": "2026-10-23T03:50:30.967735Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Churn Rate by Support Calls\n",
    "#   1. Split the churn table into customers with 3 or more support calls and\n",
    "#      customers with fewer than 3.\n",
    "#   2. Compute the churn rate (the mean of the churn column) for each group,\n",
    "#      rounded to 3 decimals.\n",
    "# Print exactly:\n",
    "# Churn rate, 3+ calls: 0.566\n",
    "# Churn rate, under 3 calls: 0.218\n",
    "churn = pd.read_csv(path_data + 'customer_churn.csv')\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "c012",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.968985Z",
     "iopub.status.busy": "2026-10-23T03:50:30.968937Z",
     "iopub.status.idle": "2026-10-23T03:50:30.971537Z",
     "shell.execute_reply": "2026-10-23T03:50:30.971274Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Churn rate, 3+ calls: 0.566\n",
      "Churn rate, under 3 calls: 0.218\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "churn = pd.read_csv(path_data + 'customer_churn.csv')\n",
    "\n",
    "many_calls = churn[churn['support_calls'] >= 3]\n",
    "few_calls = churn[churn['support_calls'] < 3]\n",
    "\n",
    "print('Churn rate, 3+ calls:', round(many_calls['churn'].mean(), 3))\n",
    "print('Churn rate, under 3 calls:', round(few_calls['churn'].mean(), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c013",
   "metadata": {},
   "source": [
    "## Features, Target, and a Train-Test Split\n",
    "\n",
    "```{index} feature matrix; classification\n",
    "```\n",
    "\n",
    "```{index} target variable\n",
    "```\n",
    "\n",
    "```{index} train_test_split; classification\n",
    "```\n",
    "\n",
    "```{index} stratified split\n",
    "```\n",
    "\n",
    "As in Chapter 13, scikit-learn expects the inputs in two pieces:\n",
    "\n",
    "- `X`, a table of **features** (one column per attribute the classifier may use), and\n",
    "- `y`, the **target**: the column of labels we want to predict.\n",
    "\n",
    "The customer ID is only a name, not information about the customer, so it is not a feature."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "c014",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.972382Z",
     "iopub.status.busy": "2026-10-23T03:50:30.972333Z",
     "iopub.status.idle": "2026-10-23T03:50:30.974222Z",
     "shell.execute_reply": "2026-10-23T03:50:30.973909Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((1000, 3), (1000,))"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "features = ['tenure_months', 'monthly_charges', 'support_calls']\n",
    "\n",
    "X = churn[features]\n",
    "y = churn['churn']\n",
    "\n",
    "X.shape, y.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c015",
   "metadata": {},
   "source": [
    "Next we hold out a test set. Section 1403 did this by shuffling the rows and cutting the table in two; `train_test_split` does both steps at once. We keep 30% of the customers for testing and fix `random_state` so the split is the same every time the page runs.\n",
    "\n",
    "The argument `stratify=y` asks for a *stratified* split: the training and test sets keep the same share of churners as the full data. With imbalanced classes this matters, because an unlucky random split could leave the test set with too few churners to judge the classifier."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "c016",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.975007Z",
     "iopub.status.busy": "2026-10-23T03:50:30.974963Z",
     "iopub.status.idle": "2026-10-23T03:50:30.988109Z",
     "shell.execute_reply": "2026-10-23T03:50:30.987786Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "700 training customers and 300 test customers\n",
      "Churn rate in training set: 0.286\n",
      "Churn rate in test set: 0.287\n"
     ]
    }
   ],
   "source": [
    "X_train, X_test, y_train, y_test = train_test_split(\n",
    "    X, y, test_size=0.3, random_state=14, stratify=y)\n",
    "\n",
    "print(len(X_train), 'training customers and', len(X_test), 'test customers')\n",
    "print('Churn rate in training set:', round(y_train.mean(), 3))\n",
    "print('Churn rate in test set:', round(y_test.mean(), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c017",
   "metadata": {},
   "source": [
    "Both sets have a churn rate of about 0.29, the same as the full data. From here on, the test set stays locked away until we are ready to report how well the final classifier works."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "c018",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.988977Z",
     "iopub.status.busy": "2026-10-23T03:50:30.988926Z",
     "iopub.status.idle": "2026-10-23T03:50:30.990282Z",
     "shell.execute_reply": "2026-10-23T03:50:30.990024Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: A Different Split\n",
    "#   1. Split X and y again, this time keeping 20% for testing.\n",
    "#      Use random_state=1 and stratify=y.\n",
    "#   2. Print the number of training and test customers and the churn rate of the\n",
    "#      test set, rounded to 3 decimals.\n",
    "# Print exactly:\n",
    "# Training: 800\n",
    "# Test: 200\n",
    "# Test churn rate: 0.285\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "c019",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.991061Z",
     "iopub.status.busy": "2026-10-23T03:50:30.991009Z",
     "iopub.status.idle": "2026-10-23T03:50:30.993463Z",
     "shell.execute_reply": "2026-10-23T03:50:30.993141Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Training: 800\n",
      "Test: 200\n",
      "Test churn rate: 0.285\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(\n",
    "    X, y, test_size=0.2, random_state=1, stratify=y)\n",
    "\n",
    "print('Training:', len(X_tr))\n",
    "print('Test:', len(X_te))\n",
    "print('Test churn rate:', round(y_te.mean(), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c020",
   "metadata": {},
   "source": [
    "## Scaling Features with StandardScaler\n",
    "\n",
    "```{index} StandardScaler\n",
    "```\n",
    "\n",
    "```{index} scaling features; scikit-learn\n",
    "```\n",
    "\n",
    "```{index} standard units; StandardScaler\n",
    "```\n",
    "\n",
    "Distance adds up the squared differences in every feature. Our features have very different scales: tenure runs from 1 to 72 months, monthly charges from \\$20 to \\$120, and support calls only from 0 to 6. Without scaling, a difference of \\$10 on the bill counts a hundred times as much as a difference of one support call, even though support calls are the strongest sign of churn.\n",
    "\n",
    "In Sections 1402 and 1403 we converted each feature to standard units by subtracting its mean and dividing by its SD. scikit-learn's `StandardScaler` does the same thing. It works in two steps:\n",
    "\n",
    "- `fit` learns the mean and SD of each feature. **We fit on the training set only**, for the reason given in Section 1403: the test customers stand in for future customers, so they must not influence how the classifier is built.\n",
    "- `transform` applies those training means and SDs to any table: the training set, the test set, or a brand-new customer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "c021",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.994279Z",
     "iopub.status.busy": "2026-10-23T03:50:30.994225Z",
     "iopub.status.idle": "2026-10-23T03:50:30.997231Z",
     "shell.execute_reply": "2026-10-23T03:50:30.996880Z"
    },
    "tags": []
   },
   "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>mean</th>\n",
       "      <th>SD</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>tenure_months</th>\n",
       "      <td>36.765714</td>\n",
       "      <td>20.900772</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>monthly_charges</th>\n",
       "      <td>68.196971</td>\n",
       "      <td>19.111750</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>support_calls</th>\n",
       "      <td>1.464286</td>\n",
       "      <td>1.237571</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                      mean         SD\n",
       "tenure_months    36.765714  20.900772\n",
       "monthly_charges  68.196971  19.111750\n",
       "support_calls     1.464286   1.237571"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "scaler = StandardScaler()\n",
    "scaler.fit(X_train)\n",
    "\n",
    "pd.DataFrame({'mean': scaler.mean_, 'SD': scaler.scale_}, index=features)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "c022",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:30.998028Z",
     "iopub.status.busy": "2026-10-23T03:50:30.997984Z",
     "iopub.status.idle": "2026-10-23T03:50:31.000398Z",
     "shell.execute_reply": "2026-10-23T03:50:31.000008Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[ 0.25, -1.28, -1.18],\n",
       "       [-1.28,  1.09,  0.43],\n",
       "       [-0.95, -1.92, -1.18],\n",
       "       [ 1.54, -0.3 ,  0.43],\n",
       "       [ 0.2 , -0.26, -0.38]])"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "X_train_scaled = scaler.transform(X_train)\n",
    "X_test_scaled = scaler.transform(X_test)\n",
    "\n",
    "X_train_scaled[:5].round(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c023",
   "metadata": {},
   "source": [
    "The result is a NumPy array in standard units. Each column of the scaled *training* set now has mean 0 and SD 1. The scaled *test* set has means and SDs close to, but not exactly, 0 and 1, because it was scaled with the training statistics, which is what we want."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "c024",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.001203Z",
     "iopub.status.busy": "2026-10-23T03:50:31.001158Z",
     "iopub.status.idle": "2026-10-23T03:50:31.002911Z",
     "shell.execute_reply": "2026-10-23T03:50:31.002658Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Training means: [-0. -0.  0.]\n",
      "Training SDs:   [1. 1. 1.]\n",
      "Test means:     [-0.06  0.12  0.1 ]\n",
      "Test SDs:       [1.01 1.01 0.99]\n"
     ]
    }
   ],
   "source": [
    "print('Training means:', X_train_scaled.mean(axis=0).round(2))\n",
    "print('Training SDs:  ', X_train_scaled.std(axis=0).round(2))\n",
    "print('Test means:    ', X_test_scaled.mean(axis=0).round(2))\n",
    "print('Test SDs:      ', X_test_scaled.std(axis=0).round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c025",
   "metadata": {},
   "source": [
    "A new customer is scaled the same way. Here is a customer who joined 6 months ago, pays \\$95 a month, and called support 4 times last quarter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "c026",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.003723Z",
     "iopub.status.busy": "2026-10-23T03:50:31.003669Z",
     "iopub.status.idle": "2026-10-23T03:50:31.005814Z",
     "shell.execute_reply": "2026-10-23T03:50:31.005509Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([[-1.47,  1.4 ,  2.05]])"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "new_customer = pd.DataFrame({'tenure_months': [6],\n",
    "                             'monthly_charges': [95.0],\n",
    "                             'support_calls': [4]})\n",
    "\n",
    "scaler.transform(new_customer).round(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c027",
   "metadata": {},
   "source": [
    "In standard units, this customer's tenure is about 1.5 SDs below average, and both the bill and the number of support calls are well above average."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "c028",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.006623Z",
     "iopub.status.busy": "2026-10-23T03:50:31.006578Z",
     "iopub.status.idle": "2026-10-23T03:50:31.008027Z",
     "shell.execute_reply": "2026-10-23T03:50:31.007719Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Scale a New Customer\n",
    "#   1. Put a customer with tenure_months 48, monthly_charges 55.0, and\n",
    "#      support_calls 0 in a one-row DataFrame with the columns in features.\n",
    "#   2. Use the scaler fitted above (do not refit it) to convert the customer to\n",
    "#      standard units, and print the result rounded to 2 decimals.\n",
    "# Print exactly:\n",
    "# [[ 0.54 -0.69 -1.18]]\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "c029",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.008761Z",
     "iopub.status.busy": "2026-10-23T03:50:31.008717Z",
     "iopub.status.idle": "2026-10-23T03:50:31.010584Z",
     "shell.execute_reply": "2026-10-23T03:50:31.010335Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[ 0.54 -0.69 -1.18]]\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "loyal_customer = pd.DataFrame({'tenure_months': [48],\n",
    "                               'monthly_charges': [55.0],\n",
    "                               'support_calls': [0]})\n",
    "\n",
    "print(scaler.transform(loyal_customer).round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c030",
   "metadata": {},
   "source": [
    "## Fitting and Predicting with KNeighborsClassifier\n",
    "\n",
    "```{index} KNeighborsClassifier\n",
    "```\n",
    "\n",
    "```{index} fit; classifier\n",
    "```\n",
    "\n",
    "```{index} predict; classifier\n",
    "```\n",
    "\n",
    "```{index} accuracy_score\n",
    "```\n",
    "\n",
    "`KNeighborsClassifier` is scikit-learn's $k$-nearest neighbor classifier. The argument `n_neighbors` is $k$. Fitting a nearest neighbor classifier does very little work: `fit` simply stores the training rows and their labels. The real work happens in `predict`, which, for each new row, finds the $k$ nearest training rows and takes a majority vote, exactly like our `classify` function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "c031",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.011368Z",
     "iopub.status.busy": "2026-10-23T03:50:31.011325Z",
     "iopub.status.idle": "2026-10-23T03:50:31.017713Z",
     "shell.execute_reply": "2026-10-23T03:50:31.017330Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<style>.sk-global {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: #000;\n",
       "  --sklearn-color-text-muted: #666;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "}\n",
       "\n",
       ".sk-global.light {\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: black;\n",
       "  --sklearn-color-background: white;\n",
       "  --sklearn-color-border-box: black;\n",
       "  --sklearn-color-icon: #696969;\n",
       "}\n",
       "\n",
       ".sk-global.dark {\n",
       "  --sklearn-color-text-on-default-background: white;\n",
       "  --sklearn-color-background: #111;\n",
       "  --sklearn-color-border-box: white;\n",
       "  --sklearn-color-icon: #878787;\n",
       "}\n",
       "\n",
       ".sk-global {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       ".sk-global pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       ".sk-global input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip-path: inset(100%);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       ".sk-global div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       ".sk-global div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       ".sk-global div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       ".sk-global div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       ".sk-global div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       ".sk-global div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       ".sk-global div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       ".sk-global div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       ".sk-global div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       ".sk-global div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       ".sk-global label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "  align-items: center;\n",
       "  justify-content: center;\n",
       "  gap: 0.5em;\n",
       "}\n",
       "\n",
       ".sk-global label.sk-toggleable__label .caption {\n",
       "  font-size: 0.6rem;\n",
       "  font-weight: lighter;\n",
       "  color: var(--sklearn-color-text-muted);\n",
       "}\n",
       "\n",
       ".sk-global label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       ".sk-global label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       ".sk-global div.sk-toggleable__content {\n",
       "  display: none;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       ".sk-global input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  display: block;\n",
       "  width: 100%;\n",
       "  overflow: visible;\n",
       "}\n",
       "\n",
       ".sk-global input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       ".sk-global div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       ".sk-global div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-label label.sk-toggleable__label,\n",
       ".sk-global div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       ".sk-global div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       ".sk-global div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       ".sk-global div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       ".sk-global div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       ".sk-global div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       ".sk-global div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       ".sk-global div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 0.5em;\n",
       "  text-align: center;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-3) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  border: var(--sklearn-color-fitted-level-0) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-0);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  border: var(--sklearn-color-fitted-level-0) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-0);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       ".sk-global a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       ".sk-global a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       ".sk-global a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       ".sk-global a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-top-container.sk-global {\n",
       "  /* pydata-sphinx-theme hides overflow, so scrolling is disabled.\n",
       "   We need to set it to !important and add tabindex=\"0\" in the HTML\n",
       "   to allow keyboard-only users to navigate the display. */\n",
       "  overflow-x: scroll !important;\n",
       "  max-width: 100%;\n",
       "}\n",
       "\n",
       ".estimator-table {\n",
       "    font-family: monospace;\n",
       "}\n",
       "\n",
       ".estimator-table summary {\n",
       "    padding: .5rem;\n",
       "    cursor: pointer;\n",
       "}\n",
       "\n",
       ".estimator-table summary::marker {\n",
       "    font-size: 0.7rem;\n",
       "}\n",
       "\n",
       ".estimator-table details[open] {\n",
       "    padding-left: 0.1rem;\n",
       "    padding-right: 0.1rem;\n",
       "    padding-bottom: 0.3rem;\n",
       "}\n",
       "\n",
       ".estimator-table .parameters-table {\n",
       "    margin-left: auto !important;\n",
       "    margin-right: auto !important;\n",
       "    margin-top: 0;\n",
       "}\n",
       "\n",
       ".estimator-table .parameters-table tr:nth-child(odd) {\n",
       "    background-color: #fff;\n",
       "}\n",
       "\n",
       ".estimator-table .parameters-table tr:nth-child(even) {\n",
       "    background-color: #f6f6f6;\n",
       "}\n",
       "\n",
       ".estimator-table .parameters-table tr:hover td {\n",
       "    background-color: #e0e0e0;\n",
       "}\n",
       "\n",
       ".estimator-table table :is(td, th) {\n",
       "    border: 1px solid rgba(106, 105, 104, 0.232);\n",
       "}\n",
       "\n",
       "/*\n",
       "    `table td`is set in notebook with right text-align.\n",
       "    We need to overwrite it.\n",
       "*/\n",
       ".estimator-table table td.param {\n",
       "    text-align: left;\n",
       "    position: relative;\n",
       "    padding: 0;\n",
       "}\n",
       "\n",
       ".user-set td {\n",
       "    color:rgb(255, 94, 0);\n",
       "    text-align: left !important;\n",
       "}\n",
       "\n",
       ".user-set td.value {\n",
       "    color:rgb(255, 94, 0);\n",
       "    background-color: transparent;\n",
       "}\n",
       "\n",
       ".default td, .estimator-table th {\n",
       "    color: black;\n",
       "    text-align: left !important;\n",
       "}\n",
       "\n",
       ".user-set td i,\n",
       ".default td i {\n",
       "    color: black;\n",
       "}\n",
       "\n",
       "td.fitted-att-type {\n",
       "    white-space: preserve nowrap;\n",
       "}\n",
       "\n",
       "/*\n",
       "    Styles for parameter documentation links\n",
       "    We need styling for visited so jupyter doesn't overwrite it\n",
       "*/\n",
       "a.param-doc-link,\n",
       "a.param-doc-link:link,\n",
       "a.param-doc-link:visited {\n",
       "    text-decoration: underline dashed;\n",
       "    text-underline-offset: .3em;\n",
       "    color: inherit;\n",
       "    display: block;\n",
       "    padding: .5em;\n",
       "}\n",
       "\n",
       "@supports(anchor-name: --doc-link) {\n",
       "    a.param-doc-link,\n",
       "    a.param-doc-link:link,\n",
       "    a.param-doc-link:visited {\n",
       "    anchor-name: --doc-link;\n",
       "    }\n",
       "}\n",
       "\n",
       "/* \"hack\" to make the entire area of the cell containing the link clickable */\n",
       "a.param-doc-link::before {\n",
       "    position: absolute;\n",
       "    content: \"\";\n",
       "    inset: 0;\n",
       "}\n",
       "\n",
       ".param-doc-description {\n",
       "    display: none;\n",
       "    position: absolute;\n",
       "    z-index: 9999;\n",
       "    left: 0;\n",
       "    padding: .5ex;\n",
       "    margin-left: 1.5em;\n",
       "    color: var(--sklearn-color-text);\n",
       "    box-shadow: .3em .3em .4em #999;\n",
       "    width: max-content;\n",
       "    text-align: left;\n",
       "    max-height: 10em;\n",
       "    overflow-y: auto;\n",
       "\n",
       "    /* unfitted */\n",
       "    background: var(--sklearn-color-unfitted-level-0);\n",
       "    border: thin solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       "@supports(position-area: center right) {\n",
       "    .param-doc-description {\n",
       "    position-area: center right;\n",
       "    position: fixed;\n",
       "    margin-left: 0;\n",
       "    }\n",
       "}\n",
       "\n",
       "/* Fitted state for parameter tooltips */\n",
       ".fitted .param-doc-description {\n",
       "    /* fitted */\n",
       "    background: var(--sklearn-color-fitted-level-0);\n",
       "    border: thin solid var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".param-doc-link:hover .param-doc-description {\n",
       "    display: block;\n",
       "}\n",
       "\n",
       ".copy-paste-icon {\n",
       "    background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tIUZvbnQgQXdlc29tZSBGcmVlIDYuNy4yIGJ5IEBmb250YXdlc29tZSAtIGh0dHBzOi8vZm9udGF3ZXNvbWUuY29tIExpY2Vuc2UgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbS9saWNlbnNlL2ZyZWUgQ29weXJpZ2h0IDIwMjUgRm9udGljb25zLCBJbmMuLS0+PHBhdGggZD0iTTIwOCAwTDMzMi4xIDBjMTIuNyAwIDI0LjkgNS4xIDMzLjkgMTQuMWw2Ny45IDY3LjljOSA5IDE0LjEgMjEuMiAxNC4xIDMzLjlMNDQ4IDMzNmMwIDI2LjUtMjEuNSA0OC00OCA0OGwtMTkyIDBjLTI2LjUgMC00OC0yMS41LTQ4LTQ4bDAtMjg4YzAtMjYuNSAyMS41LTQ4IDQ4LTQ4ek00OCAxMjhsODAgMCAwIDY0LTY0IDAgMCAyNTYgMTkyIDAgMC0zMiA2NCAwIDAgNDhjMCAyNi41LTIxLjUgNDgtNDggNDhMNDggNTEyYy0yNi41IDAtNDgtMjEuNS00OC00OEwwIDE3NmMwLTI2LjUgMjEuNS00OCA0OC00OHoiLz48L3N2Zz4=);\n",
       "    background-repeat: no-repeat;\n",
       "    background-size: 14px 14px;\n",
       "    background-position: 0;\n",
       "    display: inline-block;\n",
       "    width: 14px;\n",
       "    height: 14px;\n",
       "    cursor: pointer;\n",
       "    border: none;\n",
       "    padding: 0;\n",
       "    background-color: transparent;\n",
       "}\n",
       "\n",
       ".copy-paste-icon:focus-visible {\n",
       "    outline: 1px solid currentColor;\n",
       "    outline-offset: 1px;\n",
       "}\n",
       "\n",
       ".features {\n",
       "  font-family: monospace;\n",
       "  cursor: pointer;\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: .20em;\n",
       "  margin-bottom: 0.5em;\n",
       "  font-size: inherit; /* Needed for jupyter */\n",
       "}\n",
       "\n",
       ".features.fitted {\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       ".features summary {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  margin-bottom: 0;\n",
       "  text-align: center;\n",
       "  align-items: center;\n",
       "  justify-content: center;\n",
       "  gap: 0.5em;\n",
       "  padding: .25em;\n",
       "}\n",
       "\n",
       ".features details[open] > summary {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "  border-radius: .20em 0 0 0;\n",
       "}\n",
       "\n",
       ".features.fitted details[open] > summary {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "  border-radius: .20em 0 0 0;\n",
       "}\n",
       "\n",
       ".features details > summary .arrow::before {\n",
       "  content: \"▸\";\n",
       "  color: grey;\n",
       "}\n",
       "\n",
       ".features details[open] > summary .arrow::before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       ".features details:hover > summary {\n",
       "  margin: 0;\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       ".features.fitted details:hover > summary {\n",
       "  margin: 0;\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       ".features .features-container {\n",
       "  max-height: 10em;\n",
       "  overflow: auto;\n",
       "  scrollbar-width: thin;\n",
       "  padding: .25em 0.1rem;\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "  border-radius: 0 0 .5em .5em;\n",
       "}\n",
       "\n",
       ".features.fitted .features-container {\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       ".features .image-container {\n",
       "  block-size: 1em;\n",
       "  inline-size: 1em;\n",
       "  padding: 0;\n",
       "  margin: 0%;\n",
       "  display: flex;\n",
       "  justify-content: center;\n",
       "  align-items: center;\n",
       "}\n",
       "\n",
       ".features .copy-paste-icon {\n",
       "  background-size: 1em 1em;\n",
       "  width: 1em;\n",
       "  height: 1em;\n",
       "  filter: grayscale(100%) opacity(60%);\n",
       "}\n",
       "\n",
       ".features .features-container table {\n",
       "  width: 100%;\n",
       "  margin: 0.01em;\n",
       "}\n",
       "\n",
       ".features .features-container table tr:nth-child(odd) {\n",
       "  background-color: #fff;\n",
       "}\n",
       "\n",
       ".features .features-container table tr:nth-child(even) {\n",
       "  background-color: #f6f6f6;\n",
       "}\n",
       "\n",
       ".features .features-container table tr:hover {\n",
       "  background-color: #e0e0e0;\n",
       "}\n",
       "\n",
       ".features .features-container table {\n",
       "  table-layout: inherit;\n",
       "}\n",
       "\n",
       ".features .features-container table td {\n",
       "  text-align: left;\n",
       "  padding: 0 0.5em;\n",
       "  border: 1px solid rgba(106, 105, 104, 0.232);\n",
       "  white-space: nowrap;\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       ".total_features {\n",
       "  display: flex;\n",
       "  justify-content: center;\n",
       "  margin-top: 0.5em;\n",
       "}\n",
       "</style><body><div id=\"sk-container-id-1\" tabindex=\"0\" class=\"sk-top-container sk-global\"><div class=\"sk-text-repr-fallback\"><pre>KNeighborsClassifier()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually sk-global\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow\"><div><div>KNeighborsClassifier</div></div><div><a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html\">?<span>Documentation for KNeighborsClassifier</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></div></label><div class=\"sk-toggleable__content fitted\" data-param-prefix=\"\">\n",
       "        <div class=\"estimator-table\">\n",
       "            <details>\n",
       "                <summary>Parameters</summary>\n",
       "                <table class=\"parameters-table\">\n",
       "                  <tbody>\n",
       "                    \n",
       "        <tr class=\"default\">\n",
       "            <td><button type=\"button\" class=\"copy-paste-icon\"\n",
       "                 aria-label=\"Copy n_neighbors to clipboard\"\n",
       "                 onclick=\"copyToClipboard('n_neighbors',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></button></td>\n",
       "            <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-n_neighbors;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=n_neighbors,-int%2C%20default%3D5\">\n",
       "            n_neighbors\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-n_neighbors;\">\n",
       "            n_neighbors: int, default=5<br><br>Number of neighbors to use by default for :meth:`kneighbors` queries.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "            <td class=\"value\">5</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"default\">\n",
       "            <td><button type=\"button\" class=\"copy-paste-icon\"\n",
       "                 aria-label=\"Copy weights to clipboard\"\n",
       "                 onclick=\"copyToClipboard('weights',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></button></td>\n",
       "            <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-weights;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=weights,-%7B%27uniform%27%2C%20%27distance%27%7D%2C%20callable%20or%20None%2C%20default%3D%27uniform%27\">\n",
       "            weights\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-weights;\">\n",
       "            weights: {&#x27;uniform&#x27;, &#x27;distance&#x27;}, callable or None, default=&#x27;uniform&#x27;<br><br>Weight function used in prediction.  Possible values:<br><br>- &#x27;uniform&#x27; : uniform weights.  All points in each neighborhood<br>  are weighted equally.<br>- &#x27;distance&#x27; : weight points by the inverse of their distance.<br>  in this case, closer neighbors of a query point will have a<br>  greater influence than neighbors which are further away.<br>- [callable] : a user-defined function which accepts an<br>  array of distances, and returns an array of the same shape<br>  containing the weights.<br><br>Refer to the example entitled<br>:ref:`sphx_glr_auto_examples_neighbors_plot_classification.py`<br>showing the impact of the `weights` parameter on the decision<br>boundary.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "            <td class=\"value\">&#x27;uniform&#x27;</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"default\">\n",
       "            <td><button type=\"button\" class=\"copy-paste-icon\"\n",
       "                 aria-label=\"Copy algorithm to clipboard\"\n",
       "                 onclick=\"copyToClipboard('algorithm',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></button></td>\n",
       "            <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-algorithm;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=algorithm,-%7B%27auto%27%2C%20%27ball_tree%27%2C%20%27kd_tree%27%2C%20%27brute%27%7D%2C%20default%3D%27auto%27\">\n",
       "            algorithm\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-algorithm;\">\n",
       "            algorithm: {&#x27;auto&#x27;, &#x27;ball_tree&#x27;, &#x27;kd_tree&#x27;, &#x27;brute&#x27;}, default=&#x27;auto&#x27;<br><br>Algorithm used to compute the nearest neighbors:<br><br>- &#x27;ball_tree&#x27; will use :class:`BallTree`<br>- &#x27;kd_tree&#x27; will use :class:`KDTree`<br>- &#x27;brute&#x27; will use a brute-force search.<br>- &#x27;auto&#x27; will attempt to decide the most appropriate algorithm<br>  based on the values passed to :meth:`fit` method.<br><br>Note: fitting on sparse input will override the setting of<br>this parameter, using brute force.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "            <td class=\"value\">&#x27;auto&#x27;</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"default\">\n",
       "            <td><button type=\"button\" class=\"copy-paste-icon\"\n",
       "                 aria-label=\"Copy leaf_size to clipboard\"\n",
       "                 onclick=\"copyToClipboard('leaf_size',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></button></td>\n",
       "            <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-leaf_size;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=leaf_size,-int%2C%20default%3D30\">\n",
       "            leaf_size\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-leaf_size;\">\n",
       "            leaf_size: int, default=30<br><br>Leaf size passed to BallTree or KDTree.  This can affect the<br>speed of the construction and query, as well as the memory<br>required to store the tree.  The optimal value depends on the<br>nature of the problem.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "            <td class=\"value\">30</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"default\">\n",
       "            <td><button type=\"button\" class=\"copy-paste-icon\"\n",
       "                 aria-label=\"Copy p to clipboard\"\n",
       "                 onclick=\"copyToClipboard('p',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></button></td>\n",
       "            <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-p;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=p,-float%2C%20default%3D2\">\n",
       "            p\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-p;\">\n",
       "            p: float, default=2<br><br>Power parameter for the Minkowski metric. When p = 1, this is equivalent<br>to using manhattan_distance (l1), and euclidean_distance (l2) for p = 2.<br>For arbitrary p, minkowski_distance (l_p) is used. This parameter is expected<br>to be positive.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "            <td class=\"value\">2</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"default\">\n",
       "            <td><button type=\"button\" class=\"copy-paste-icon\"\n",
       "                 aria-label=\"Copy metric to clipboard\"\n",
       "                 onclick=\"copyToClipboard('metric',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></button></td>\n",
       "            <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-metric;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=metric,-str%20or%20callable%2C%20default%3D%27minkowski%27\">\n",
       "            metric\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-metric;\">\n",
       "            metric: str or callable, default=&#x27;minkowski&#x27;<br><br>Metric to use for distance computation. Default is &quot;minkowski&quot;, which<br>results in the standard Euclidean distance when p = 2. See the<br>documentation of `scipy.spatial.distance<br>&lt;https://docs.scipy.org/doc/scipy/reference/spatial.distance.html&gt;`_ and<br>the metrics listed in<br>:class:`~sklearn.metrics.pairwise.distance_metrics` for valid metric<br>values.<br><br>If metric is &quot;precomputed&quot;, X is assumed to be a distance matrix and<br>must be square during fit. X may be a :term:`sparse graph`, in which<br>case only &quot;nonzero&quot; elements may be considered neighbors.<br><br>If metric is a callable function, it takes two arrays representing 1D<br>vectors as inputs and must return one value indicating the distance<br>between those vectors. This works for Scipy&#x27;s metrics, but is less<br>efficient than passing the metric name as a string.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "            <td class=\"value\">&#x27;minkowski&#x27;</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"default\">\n",
       "            <td><button type=\"button\" class=\"copy-paste-icon\"\n",
       "                 aria-label=\"Copy metric_params to clipboard\"\n",
       "                 onclick=\"copyToClipboard('metric_params',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></button></td>\n",
       "            <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-metric_params;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=metric_params,-dict%2C%20default%3DNone\">\n",
       "            metric_params\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-metric_params;\">\n",
       "            metric_params: dict, default=None<br><br>Additional keyword arguments for the metric function.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "            <td class=\"value\">None</td>\n",
       "        </tr>\n",
       "    \n",
       "\n",
       "        <tr class=\"default\">\n",
       "            <td><button type=\"button\" class=\"copy-paste-icon\"\n",
       "                 aria-label=\"Copy n_jobs to clipboard\"\n",
       "                 onclick=\"copyToClipboard('n_jobs',\n",
       "                          this.parentElement.nextElementSibling)\"\n",
       "            ></button></td>\n",
       "            <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-n_jobs;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=n_jobs,-int%2C%20default%3DNone\">\n",
       "            n_jobs\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-n_jobs;\">\n",
       "            n_jobs: int, default=None<br><br>The number of parallel jobs to run for neighbors search.<br>``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.<br>``-1`` means using all processors. See :term:`Glossary &lt;n_jobs&gt;`<br>for more details.<br>Doesn&#x27;t affect :meth:`fit` method.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "            <td class=\"value\">None</td>\n",
       "        </tr>\n",
       "    \n",
       "                  </tbody>\n",
       "                </table>\n",
       "            </details>\n",
       "        </div>\n",
       "    \n",
       "        <div class=\"estimator-table\">\n",
       "            <details>\n",
       "                <summary>Fitted attributes</summary>\n",
       "                <table class=\"parameters-table\">\n",
       "                    <tbody>\n",
       "                        <tr>\n",
       "                        <th>Name</th>\n",
       "                        <th>Type</th>\n",
       "                        <th>Value</th>\n",
       "                        </tr>\n",
       "                        \n",
       "       <tr class=\"default\">\n",
       "           <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-classes_;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=classes_,-array%20of%20shape%20%28n_classes%2C%29\">\n",
       "            classes_\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-classes_;\">\n",
       "            classes_: array of shape (n_classes,)<br><br>Class labels known to the classifier</span>\n",
       "        </a>\n",
       "    </td>\n",
       "           <td class=\"fitted-att-type\">ndarray[int64](2,)</td>\n",
       "           <td>[0,1]</td>\n",
       "\n",
       "\n",
       "       </tr>\n",
       "    \n",
       "\n",
       "       <tr class=\"default\">\n",
       "           <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-effective_metric_;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=effective_metric_,-str%20or%20callble\">\n",
       "            effective_metric_\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-effective_metric_;\">\n",
       "            effective_metric_: str or callble<br><br>The distance metric used. It will be same as the `metric` parameter<br>or a synonym of it, e.g. &#x27;euclidean&#x27; if the `metric` parameter set to<br>&#x27;minkowski&#x27; and `p` parameter set to 2.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "           <td class=\"fitted-att-type\">str</td>\n",
       "           <td>&#x27;eu...an&#x27;</td>\n",
       "\n",
       "\n",
       "       </tr>\n",
       "    \n",
       "\n",
       "       <tr class=\"default\">\n",
       "           <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-effective_metric_params_;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=effective_metric_params_,-dict\">\n",
       "            effective_metric_params_\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-effective_metric_params_;\">\n",
       "            effective_metric_params_: dict<br><br>Additional keyword arguments for the metric function. For most metrics<br>will be same with `metric_params` parameter, but may also contain the<br>`p` parameter value if the `effective_metric_` attribute is set to<br>&#x27;minkowski&#x27;.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "           <td class=\"fitted-att-type\">dict</td>\n",
       "           <td>{}</td>\n",
       "\n",
       "\n",
       "       </tr>\n",
       "    \n",
       "\n",
       "       <tr class=\"default\">\n",
       "           <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-n_features_in_;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=n_features_in_,-int\">\n",
       "            n_features_in_\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-n_features_in_;\">\n",
       "            n_features_in_: int<br><br>Number of features seen during :term:`fit`.<br><br>.. versionadded:: 0.24</span>\n",
       "        </a>\n",
       "    </td>\n",
       "           <td class=\"fitted-att-type\">int</td>\n",
       "           <td>3</td>\n",
       "\n",
       "\n",
       "       </tr>\n",
       "    \n",
       "\n",
       "       <tr class=\"default\">\n",
       "           <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-n_samples_fit_;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=n_samples_fit_,-int\">\n",
       "            n_samples_fit_\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-n_samples_fit_;\">\n",
       "            n_samples_fit_: int<br><br>Number of samples in the fitted data.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "           <td class=\"fitted-att-type\">int</td>\n",
       "           <td>700</td>\n",
       "\n",
       "\n",
       "       </tr>\n",
       "    \n",
       "\n",
       "       <tr class=\"default\">\n",
       "           <td class=\"param\">\n",
       "        <a class=\"param-doc-link\"\n",
       "            style=\"anchor-name: --doc-link-outputs_2d_;\"\n",
       "            rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.9/modules/generated/sklearn.neighbors.KNeighborsClassifier.html#:~:text=outputs_2d_,-bool\">\n",
       "            outputs_2d_\n",
       "            <span class=\"param-doc-description\"\n",
       "            style=\"position-anchor: --doc-link-outputs_2d_;\">\n",
       "            outputs_2d_: bool<br><br>False when `y`&#x27;s shape is (n_samples, ) or (n_samples, 1) during fit<br>otherwise True.</span>\n",
       "        </a>\n",
       "    </td>\n",
       "           <td class=\"fitted-att-type\">bool</td>\n",
       "           <td>False</td>\n",
       "\n",
       "\n",
       "       </tr>\n",
       "    \n",
       "                    </tbody>\n",
       "                </table>\n",
       "            </details>\n",
       "        </div>\n",
       "    </div></div></div></div></div><script>/*  Authors: The scikit-learn developers\n",
       " SPDX-License-Identifier: BSD-3-Clause\n",
       "*/\n",
       "\n",
       "function copyToClipboard(text, element) {\n",
       "    // Get the parameter prefix from the closest toggleable content\n",
       "    const toggleableContent = element.closest('.sk-toggleable__content');\n",
       "    const paramPrefix = toggleableContent ? toggleableContent.dataset.paramPrefix : '';\n",
       "    const fullParamName = paramPrefix ? `${paramPrefix}${text}` : text;\n",
       "\n",
       "    const originalStyle = element.style;\n",
       "    const computedStyle = window.getComputedStyle(element);\n",
       "    const originalWidth = computedStyle.width;\n",
       "    const originalHTML = element.innerHTML.replace('Copied!', '');\n",
       "\n",
       "    navigator.clipboard.writeText(fullParamName)\n",
       "        .then(() => {\n",
       "            element.style.width = originalWidth;\n",
       "            element.style.color = 'green';\n",
       "            element.innerHTML = \"Copied!\";\n",
       "\n",
       "            setTimeout(() => {\n",
       "                element.innerHTML = originalHTML;\n",
       "                element.style = originalStyle;\n",
       "            }, 2000);\n",
       "        })\n",
       "        .catch(err => {\n",
       "            console.error('Failed to copy:', err);\n",
       "            element.style.color = 'red';\n",
       "            element.innerHTML = \"Failed!\";\n",
       "            setTimeout(() => {\n",
       "                element.innerHTML = originalHTML;\n",
       "                element.style = originalStyle;\n",
       "            }, 2000);\n",
       "        });\n",
       "    return false;\n",
       "}\n",
       "\n",
       "document.querySelectorAll('.copy-paste-icon').forEach(function(element) {\n",
       "    const toggleableContent = element.closest('.sk-toggleable__content');\n",
       "    const paramPrefix = toggleableContent ? toggleableContent.dataset.paramPrefix : '';\n",
       "\n",
       "    const parent = element.parentElement;\n",
       "    if (!parent || !parent.nextElementSibling) {\n",
       "        console.warn('Expected copy-paste icon is missing from the DOM structure');\n",
       "        return;\n",
       "    }\n",
       "\n",
       "    const paramName = element.parentElement.nextElementSibling\n",
       "        .textContent.trim().split(' ')[0];\n",
       "    const fullParamName = paramPrefix ? `${paramPrefix}${paramName}` : paramName;\n",
       "\n",
       "    element.setAttribute('title', fullParamName);\n",
       "});\n",
       "\n",
       "/**\n",
       " * Copy the list of feature names formatted as a Python list.\n",
       " *\n",
       " * @param {HTMLElement} element - The copy button inside a `.features` block; its siblings\n",
       " *   contain a `details` element and a table containing feature named.\n",
       " * @returns {boolean} Always returns `false` so callers can prevent the default click behavior.\n",
       " */\n",
       "function copyFeatureNamesToClipboard(element) {\n",
       "    var detailsElem = element.closest('.features').querySelector('details');\n",
       "    var wasOpen = detailsElem.open;\n",
       "    detailsElem.open = true;\n",
       "    var content = element.closest('.features').querySelector('tbody')\n",
       "                  .innerText.trim();\n",
       "    if (!wasOpen) detailsElem.open = false;\n",
       "    const rows = content.split('\\n').map(row => `    \"${row}\"`);\n",
       "    const formattedText = `[\\n${rows.join(',\\n')},\\n]`;\n",
       "    const originalHTML = element.innerHTML.replace('✔', '');\n",
       "    const originalStyle = element.style;\n",
       "    const copyMark = document.createElement('span');\n",
       "    copyMark.innerHTML = '✔';\n",
       "    copyMark.style.color = 'blue';\n",
       "    copyMark.style.fontSize = '1em';\n",
       "\n",
       "    navigator.clipboard.writeText(formattedText)\n",
       "        .then(() => {\n",
       "            element.style.display = 'none';\n",
       "            element.parentElement.appendChild(copyMark);\n",
       "\n",
       "            setTimeout(() => {\n",
       "                copyMark.remove();\n",
       "                element.innerHTML = originalHTML;\n",
       "                element.style = originalStyle;\n",
       "            }, 1000);\n",
       "        })\n",
       "        .catch(err => {\n",
       "            console.error('Failed to copy:', err);\n",
       "            element.style.color = 'orange';\n",
       "            element.innerHTML = \"Failed!\";\n",
       "            setTimeout(() => {\n",
       "                element.innerHTML = originalHTML;\n",
       "                element.style = originalStyle;\n",
       "            }, 1000);\n",
       "        });\n",
       "    return false;\n",
       "}\n",
       "/**\n",
       " * Adapted from Skrub\n",
       " * https://github.com/skrub-data/skrub/blob/403466d1d5d4dc76a7ef569b3f8228db59a31dc3/skrub/_reporting/_data/templates/report.js#L789\n",
       " * @returns \"light\" or \"dark\"\n",
       " */\n",
       "function detectTheme(element) {\n",
       "    const body = document.querySelector('body');\n",
       "\n",
       "    // Check VSCode theme\n",
       "    const themeKindAttr = body.getAttribute('data-vscode-theme-kind');\n",
       "    const themeNameAttr = body.getAttribute('data-vscode-theme-name');\n",
       "\n",
       "    if (themeKindAttr && themeNameAttr) {\n",
       "        const themeKind = themeKindAttr.toLowerCase();\n",
       "        const themeName = themeNameAttr.toLowerCase();\n",
       "\n",
       "        if (themeKind.includes(\"dark\") || themeName.includes(\"dark\")) {\n",
       "            return \"dark\";\n",
       "        }\n",
       "        if (themeKind.includes(\"light\") || themeName.includes(\"light\")) {\n",
       "            return \"light\";\n",
       "        }\n",
       "    }\n",
       "\n",
       "    // Check Jupyter theme\n",
       "    if (body.getAttribute('data-jp-theme-light') === 'false') {\n",
       "        return 'dark';\n",
       "    } else if (body.getAttribute('data-jp-theme-light') === 'true') {\n",
       "        return 'light';\n",
       "    }\n",
       "\n",
       "    // Guess based on a parent element's color\n",
       "    const color = window.getComputedStyle(element.parentNode, null).getPropertyValue('color');\n",
       "    const match = color.match(/^rgb\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*\\)\\s*$/i);\n",
       "    if (match) {\n",
       "        const [r, g, b] = [\n",
       "            parseFloat(match[1]),\n",
       "            parseFloat(match[2]),\n",
       "            parseFloat(match[3])\n",
       "        ];\n",
       "\n",
       "        // https://en.wikipedia.org/wiki/HSL_and_HSV#Lightness\n",
       "        const luma = 0.299 * r + 0.587 * g + 0.114 * b;\n",
       "\n",
       "        if (luma > 180) {\n",
       "            // If the text is very bright we have a dark theme\n",
       "            return 'dark';\n",
       "        }\n",
       "        if (luma < 75) {\n",
       "            // If the text is very dark we have a light theme\n",
       "            return 'light';\n",
       "        }\n",
       "        // Otherwise fall back to the next heuristic.\n",
       "    }\n",
       "\n",
       "    // Fallback to system preference\n",
       "    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n",
       "}\n",
       "\n",
       "\n",
       "function forceTheme(elementId) {\n",
       "    const estimatorElement = document.querySelector(`#${elementId}`);\n",
       "    if (estimatorElement === null) {\n",
       "        console.error(`Element with id ${elementId} not found.`);\n",
       "    } else {\n",
       "        const theme = detectTheme(estimatorElement);\n",
       "        estimatorElement.classList.add(theme);\n",
       "    }\n",
       "}\n",
       "\n",
       "forceTheme('sk-container-id-1');</script></body>"
      ],
      "text/plain": [
       "KNeighborsClassifier()"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "knn = KNeighborsClassifier(n_neighbors=5)\n",
    "knn.fit(X_train_scaled, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c032",
   "metadata": {},
   "source": [
    "To classify the new customer from the previous subsection, scale the customer with the same scaler and call `predict`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "c033",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.018590Z",
     "iopub.status.busy": "2026-10-23T03:50:31.018539Z",
     "iopub.status.idle": "2026-10-23T03:50:31.021377Z",
     "shell.execute_reply": "2026-10-23T03:50:31.021069Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "array([1])"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "new_scaled = scaler.transform(new_customer)\n",
    "knn.predict(new_scaled)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c034",
   "metadata": {},
   "source": [
    "The prediction is 1: this customer is likely to churn. The `kneighbors` method shows the evidence behind the vote, the distances to the 5 nearest training customers and their positions in the training set. Looking up those customers confirms that it is the same distance-and-vote procedure we wrote by hand."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "c035",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.022196Z",
     "iopub.status.busy": "2026-10-23T03:50:31.022150Z",
     "iopub.status.idle": "2026-10-23T03:50:31.025250Z",
     "shell.execute_reply": "2026-10-23T03:50:31.024945Z"
    },
    "tags": []
   },
   "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>tenure_months</th>\n",
       "      <th>monthly_charges</th>\n",
       "      <th>support_calls</th>\n",
       "      <th>churn</th>\n",
       "      <th>distance</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>535</th>\n",
       "      <td>18</td>\n",
       "      <td>88.13</td>\n",
       "      <td>4</td>\n",
       "      <td>1</td>\n",
       "      <td>0.677</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>386</th>\n",
       "      <td>22</td>\n",
       "      <td>95.25</td>\n",
       "      <td>4</td>\n",
       "      <td>1</td>\n",
       "      <td>0.766</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>774</th>\n",
       "      <td>3</td>\n",
       "      <td>101.50</td>\n",
       "      <td>3</td>\n",
       "      <td>1</td>\n",
       "      <td>0.888</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>83</th>\n",
       "      <td>14</td>\n",
       "      <td>79.35</td>\n",
       "      <td>4</td>\n",
       "      <td>1</td>\n",
       "      <td>0.904</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>181</th>\n",
       "      <td>2</td>\n",
       "      <td>102.25</td>\n",
       "      <td>3</td>\n",
       "      <td>1</td>\n",
       "      <td>0.913</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "     tenure_months  monthly_charges  support_calls  churn  distance\n",
       "535             18            88.13              4      1     0.677\n",
       "386             22            95.25              4      1     0.766\n",
       "774              3           101.50              3      1     0.888\n",
       "83              14            79.35              4      1     0.904\n",
       "181              2           102.25              3      1     0.913"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "distances, positions = knn.kneighbors(new_scaled)\n",
    "\n",
    "neighbors = X_train.iloc[positions[0]].copy()\n",
    "neighbors['churn'] = y_train.iloc[positions[0]].values\n",
    "neighbors['distance'] = distances[0].round(3)\n",
    "neighbors"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c036",
   "metadata": {},
   "source": [
    "Now classify every test customer at once and compare the predictions with the actual labels. Accuracy is the proportion of matches; `accuracy_score` computes it, and so does the classifier's own `score` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "c037",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.026029Z",
     "iopub.status.busy": "2026-10-23T03:50:31.025979Z",
     "iopub.status.idle": "2026-10-23T03:50:31.028688Z",
     "shell.execute_reply": "2026-10-23T03:50:31.028436Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "First ten predictions: [0 0 0 0 1 1 1 0 1 0]\n",
      "First ten actual:      [0 1 0 0 1 0 1 1 1 0]\n",
      "Test accuracy: 0.83\n"
     ]
    }
   ],
   "source": [
    "y_pred = knn.predict(X_test_scaled)\n",
    "\n",
    "print('First ten predictions:', y_pred[:10])\n",
    "print('First ten actual:     ', y_test.values[:10])\n",
    "print('Test accuracy:', round(accuracy_score(y_test, y_pred), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c038",
   "metadata": {},
   "source": [
    "The 5-nearest neighbor classifier is right for 83% of the test customers, well above the 71% of the always-\"stays\" rule.\n",
    "\n",
    "How much did scaling help? Fit the same classifier on the raw features and compare."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "c039",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.029542Z",
     "iopub.status.busy": "2026-10-23T03:50:31.029499Z",
     "iopub.status.idle": "2026-10-23T03:50:31.033658Z",
     "shell.execute_reply": "2026-10-23T03:50:31.033404Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test accuracy without scaling: 0.787\n",
      "Test accuracy with scaling:    0.83\n"
     ]
    }
   ],
   "source": [
    "knn_raw = KNeighborsClassifier(n_neighbors=5)\n",
    "knn_raw.fit(X_train, y_train)\n",
    "\n",
    "print('Test accuracy without scaling:', round(knn_raw.score(X_test, y_test), 3))\n",
    "print('Test accuracy with scaling:   ', round(knn.score(X_test_scaled, y_test), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c040",
   "metadata": {},
   "source": [
    "Without scaling, accuracy drops to about 79%: the classifier mostly ignores support calls, the most useful feature, because its scale is so small. Scaling is part of the nearest neighbor method, not an optional extra."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "c041",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.034687Z",
     "iopub.status.busy": "2026-10-23T03:50:31.034640Z",
     "iopub.status.idle": "2026-10-23T03:50:31.035967Z",
     "shell.execute_reply": "2026-10-23T03:50:31.035709Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Try k = 7\n",
    "#   1. Fit a KNeighborsClassifier with n_neighbors=7 on X_train_scaled and y_train.\n",
    "#   2. Predict the test set (X_test_scaled) and print the test accuracy rounded to 3 decimals.\n",
    "#   3. Predict new_customer (use new_scaled) and print the predicted label.\n",
    "# Print exactly:\n",
    "# Test accuracy: 0.847\n",
    "# New customer prediction: 1\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "c042",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.036764Z",
     "iopub.status.busy": "2026-10-23T03:50:31.036725Z",
     "iopub.status.idle": "2026-10-23T03:50:31.040097Z",
     "shell.execute_reply": "2026-10-23T03:50:31.039772Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test accuracy: 0.847\n",
      "New customer prediction: 1\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "knn7 = KNeighborsClassifier(n_neighbors=7)\n",
    "knn7.fit(X_train_scaled, y_train)\n",
    "\n",
    "print('Test accuracy:', round(accuracy_score(y_test, knn7.predict(X_test_scaled)), 3))\n",
    "print('New customer prediction:', knn7.predict(new_scaled)[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c043",
   "metadata": {},
   "source": [
    "## Pipelines and Choosing k\n",
    "\n",
    "```{index} pipeline\n",
    "```\n",
    "\n",
    "```{index} make_pipeline\n",
    "```\n",
    "\n",
    "```{index} validation set\n",
    "```\n",
    "\n",
    "```{index} choosing k\n",
    "```\n",
    "\n",
    "```{index} hyperparameter\n",
    "```\n",
    "\n",
    "Keeping track of a separate scaler and classifier is easy to get wrong, for example by forgetting to scale a new customer. A **pipeline** joins the steps into one object. When you call `fit` on the pipeline, it fits the scaler on the training data, transforms them, and fits the classifier; when you call `predict`, it transforms the new rows with the *training* statistics and then predicts. Scaling with training statistics only is built in."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "c044",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.040899Z",
     "iopub.status.busy": "2026-10-23T03:50:31.040856Z",
     "iopub.status.idle": "2026-10-23T03:50:31.047011Z",
     "shell.execute_reply": "2026-10-23T03:50:31.046737Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test accuracy: 0.83\n",
      "New customer prediction: 1\n"
     ]
    }
   ],
   "source": [
    "model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=5))\n",
    "model.fit(X_train, y_train)\n",
    "\n",
    "print('Test accuracy:', round(model.score(X_test, y_test), 3))\n",
    "print('New customer prediction:', model.predict(new_customer)[0])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c045",
   "metadata": {},
   "source": [
    "The pipeline gives the same accuracy and prediction as the two separate steps, and it takes the raw features directly.\n",
    "\n",
    "**Choosing $k$.** The number of neighbors is a choice we make, not something `fit` learns from the data; such choices are called *hyperparameters*. A natural idea is to try several values of $k$ and keep the one with the best test accuracy. But then the test set has been used to build the classifier, and its accuracy is no longer an honest estimate for new customers, the same problem as testing on the training set, only milder.\n",
    "\n",
    "The standard fix is a **validation set**: split the training set once more, use one part to fit and the other part to compare values of $k$, and save the test set for a single final check."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "c046",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.047911Z",
     "iopub.status.busy": "2026-10-23T03:50:31.047859Z",
     "iopub.status.idle": "2026-10-23T03:50:31.050160Z",
     "shell.execute_reply": "2026-10-23T03:50:31.049842Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "525 customers for fitting and 175 for validation\n"
     ]
    }
   ],
   "source": [
    "X_fit, X_val, y_fit, y_val = train_test_split(\n",
    "    X_train, y_train, test_size=0.25, random_state=14, stratify=y_train)\n",
    "\n",
    "print(len(X_fit), 'customers for fitting and', len(X_val), 'for validation')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "c047",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.050974Z",
     "iopub.status.busy": "2026-10-23T03:50:31.050925Z",
     "iopub.status.idle": "2026-10-23T03:50:31.084256Z",
     "shell.execute_reply": "2026-10-23T03:50:31.083906Z"
    },
    "tags": []
   },
   "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>k</th>\n",
       "      <th>validation accuracy</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1</td>\n",
       "      <td>0.720</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>3</td>\n",
       "      <td>0.794</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>5</td>\n",
       "      <td>0.817</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>7</td>\n",
       "      <td>0.834</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>9</td>\n",
       "      <td>0.823</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>11</td>\n",
       "      <td>0.840</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>13</td>\n",
       "      <td>0.823</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>15</td>\n",
       "      <td>0.823</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>17</td>\n",
       "      <td>0.823</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>19</td>\n",
       "      <td>0.817</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>21</td>\n",
       "      <td>0.817</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>23</td>\n",
       "      <td>0.823</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>25</td>\n",
       "      <td>0.834</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>27</td>\n",
       "      <td>0.829</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>29</td>\n",
       "      <td>0.834</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>31</td>\n",
       "      <td>0.829</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "     k  validation accuracy\n",
       "0    1                0.720\n",
       "1    3                0.794\n",
       "2    5                0.817\n",
       "3    7                0.834\n",
       "4    9                0.823\n",
       "5   11                0.840\n",
       "6   13                0.823\n",
       "7   15                0.823\n",
       "8   17                0.823\n",
       "9   19                0.817\n",
       "10  21                0.817\n",
       "11  23                0.823\n",
       "12  25                0.834\n",
       "13  27                0.829\n",
       "14  29                0.834\n",
       "15  31                0.829"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "k_values = np.arange(1, 32, 2)     # odd values of k avoid ties in a two-class vote\n",
    "val_accuracy = []\n",
    "\n",
    "for k in k_values:\n",
    "    candidate = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k))\n",
    "    candidate.fit(X_fit, y_fit)\n",
    "    val_accuracy.append(candidate.score(X_val, y_val))\n",
    "\n",
    "k_results = pd.DataFrame({'k': k_values, 'validation accuracy': np.round(val_accuracy, 3)})\n",
    "k_results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "c048",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.085155Z",
     "iopub.status.busy": "2026-10-23T03:50:31.085111Z",
     "iopub.status.idle": "2026-10-23T03:50:31.127575Z",
     "shell.execute_reply": "2026-10-23T03:50:31.127214Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 600x350 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig, ax = plt.subplots(figsize=(6, 3.5))\n",
    "ax.plot(k_results['k'], k_results['validation accuracy'], marker='o', lw=2)\n",
    "ax.set_xlabel('k (number of neighbors)')\n",
    "ax.set_ylabel('Validation accuracy')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c049",
   "metadata": {},
   "source": [
    "With $k = 1$ the classifier follows individual training customers too closely (it *overfits*), and validation accuracy is lowest. Accuracy rises as $k$ grows and then levels off; very large $k$ would eventually blur the pattern, because distant customers get a vote. We pick the $k$ with the highest validation accuracy, refit on the whole training set, and check the test set once."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "c050",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.128562Z",
     "iopub.status.busy": "2026-10-23T03:50:31.128499Z",
     "iopub.status.idle": "2026-10-23T03:50:31.133092Z",
     "shell.execute_reply": "2026-10-23T03:50:31.132850Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Chosen k: 11\n",
      "Test accuracy: 0.853\n"
     ]
    }
   ],
   "source": [
    "best_k = int(k_results.loc[k_results['validation accuracy'].idxmax(), 'k'])\n",
    "\n",
    "final_model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=best_k))\n",
    "final_model.fit(X_train, y_train)\n",
    "\n",
    "print('Chosen k:', best_k)\n",
    "print('Test accuracy:', round(final_model.score(X_test, y_test), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c051",
   "metadata": {},
   "source": [
    "Validation accuracy is highest at $k = 11$ (0.84), but every $k$ from 7 to 31 is within about two percentage points of it. Differences that small are partly the luck of which customers landed in the validation set, so any $k$ in that range is a reasonable choice. With $k = 11$, the final classifier is right for about 85% of the test customers, compared with 71% for the always-\"stays\" baseline."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "c052",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.134174Z",
     "iopub.status.busy": "2026-10-23T03:50:31.134098Z",
     "iopub.status.idle": "2026-10-23T03:50:31.135505Z",
     "shell.execute_reply": "2026-10-23T03:50:31.135230Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Compare a Few Values of k\n",
    "#   1. For k in [3, 7, 15], fit a pipeline (StandardScaler, then\n",
    "#      KNeighborsClassifier) on X_fit, y_fit and compute its accuracy on X_val, y_val.\n",
    "#   2. Print each k with its validation accuracy rounded to 3 decimals, then\n",
    "#      print the k with the highest validation accuracy.\n",
    "# Print exactly:\n",
    "# k = 3 validation accuracy: 0.794\n",
    "# k = 7 validation accuracy: 0.834\n",
    "# k = 15 validation accuracy: 0.823\n",
    "# Best k: 7\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "c053",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:31.136264Z",
     "iopub.status.busy": "2026-10-23T03:50:31.136215Z",
     "iopub.status.idle": "2026-10-23T03:50:31.143864Z",
     "shell.execute_reply": "2026-10-23T03:50:31.143614Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "k = 3 validation accuracy: 0.794\n",
      "k = 7 validation accuracy: 0.834\n",
      "k = 15 validation accuracy: 0.823\n",
      "Best k: 7\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "best_score = 0\n",
    "for k in [3, 7, 15]:\n",
    "    candidate = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=k))\n",
    "    candidate.fit(X_fit, y_fit)\n",
    "    score = candidate.score(X_val, y_val)\n",
    "    print('k =', k, 'validation accuracy:', round(score, 3))\n",
    "    if score > best_score:\n",
    "        best_score = score\n",
    "        best = k\n",
    "\n",
    "print('Best k:', best)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c054",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "- scikit-learn's `KNeighborsClassifier` carries out the same distance-and-vote procedure as the hand-built classifier, with `fit` and `predict`.\n",
    "- Split the data with `train_test_split` before doing anything else; `stratify=y` keeps the class proportions the same in both parts.\n",
    "- Fit `StandardScaler` on the training set only and use it to transform the test set and new cases; a pipeline does this automatically.\n",
    "- Choose $k$ with a validation set carved out of the training set, then report accuracy on the test set once.\n",
    "- Accuracy should always be compared with the baseline of predicting the most common class. The next section shows what accuracy hides."
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "celltoolbar": "Tags",
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "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.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
