{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3274d4ef",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "# Initialize Otter\n",
    "import otter\n",
    "grader = otter.Notebook(\"practice08.ipynb\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-001",
   "metadata": {},
   "source": [
    "**Student names and e-mails:**\n",
    "\n",
    "_YOUR NAME — your@calvin.edu_\n",
    "\n",
    "_YOUR NAME — your@calvin.edu_"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-002",
   "metadata": {},
   "source": [
    "# Practice 08 — Miles per Gallon: Linear Regression, Ridge and Lasso\n",
    "\n",
    "In 1973, an oil embargo quadrupled the price of oil, and American drivers waited in long lines at gas stations. Two years later, Congress set fuel-economy standards for new cars, starting with the 1978 models. Suddenly, how far a car could go on a gallon of gas mattered.\n",
    "\n",
    "Can a line predict a car's fuel economy from its weight, its engine and its year? And what goes wrong when it learns from only a handful of cars?\n",
    "\n",
    "| SLO | Description |\n",
    "|-----|-------------|\n",
    "| **10A** | Train and interpret linear regression models |\n",
    "| **10B** | Apply Ridge and Lasso regularization to reduce overfitting and explain the effect of the regularization parameter |\n",
    "| **10C** | Compute and interpret regression performance metrics — MAE, RMSE, and R² |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-003",
   "metadata": {},
   "source": [
    "---\n",
    "## The Dataset: 398 Cars, 1970–1982\n",
    "\n",
    "These are real cars, from the StatLib library at Carnegie Mellon University. The data was used at the 1983 American Statistical Association Exposition, and it now lives in the UCI Machine Learning Repository (\"Auto MPG\", CC BY 4.0). Its documentation says: *\"The data concerns city-cycle fuel consumption in miles per gallon.\"*\n",
    "\n",
    "| Column | What it holds |\n",
    "|---|---|\n",
    "| `name` | make and model, e.g. \"ford pinto\" |\n",
    "| `origin` | where the car was made: USA, Europe or Japan |\n",
    "| `model_year` | 1970 to 1982 |\n",
    "| `cylinders` | number of engine cylinders, 3 to 8 |\n",
    "| `displacement` | engine size, in cubic inches |\n",
    "| `horsepower` | engine power (6 cars have a blank) |\n",
    "| `weight` | in pounds (lb) |\n",
    "| `acceleration` | time to accelerate, in seconds: a bigger number is a slower car |\n",
    "| `mpg` | **the label**: miles per gallon |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-004",
   "metadata": {},
   "source": [
    "### Setup (given — just run it)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-005",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import plotly.express as px\n",
    "from sklearn.model_selection import train_test_split, cross_validate, GridSearchCV\n",
    "from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet\n",
    "from sklearn.pipeline import Pipeline, make_pipeline\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.metrics import mean_absolute_error, root_mean_squared_error, r2_score\n",
    "\n",
    "cars = pd.read_csv(\"https://cs.calvin.edu/courses/data/202/26fa/datasets/mpg.csv\")\n",
    "print(len(cars), \"cars | average:\", round(cars[\"mpg\"].mean(), 1), \"mpg\")\n",
    "cars.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-006",
   "metadata": {},
   "source": [
    "The features are the six number columns and `origin`. The split is given: 25% of the cars for testing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-007",
   "metadata": {},
   "outputs": [],
   "source": [
    "numeric = [\"cylinders\", \"displacement\", \"horsepower\", \"weight\", \"acceleration\", \"model_year\"]\n",
    "X = cars[numeric + [\"origin\"]]\n",
    "y = cars[\"mpg\"]\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=1)\n",
    "print(len(X_train), \"training cars |\", len(X_test), \"test cars\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-008",
   "metadata": {},
   "source": [
    "---\n",
    "## Part 1 — One Line (SLO 10A)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-009",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig = px.scatter(x=X_train[\"weight\"], y=y_train, opacity=0.5, labels={\"x\": \"weight (lb)\", \"y\": \"miles per gallon\"},\n",
    "                 title=\"298 Training Cars: Fuel Economy vs. Weight\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-010",
   "metadata": {},
   "source": [
    "> 🎯 **Predict First:** Draw a straight line through the cloud in your head. About how many miles per gallon does an extra 1,000 lb cost?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-012",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 10A.1 — One Line Through Weight *(2 pts)*\n",
    "\n",
    "1. Fit a `LinearRegression` on `X_train[[\"weight\"]]` and `y_train`. Store it in `line`.\n",
    "2. Store its slope in `slope` and its intercept in `intercept`. (`coef_` holds one slope per column, so take its first element.)\n",
    "3. Store the predicted mpg of a 3,000 lb car in `mpg_3000`. (*Hint:* `predict` needs a table, `pd.DataFrame({\"weight\": [3000]})`, and returns an array.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-014",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "line = ...\n",
    "slope = ...\n",
    "intercept = ...\n",
    "mpg_3000 = ...\n",
    "\n",
    "print(f\"mpg = {intercept:.2f} + ({slope:.5f}) × weight\")\n",
    "print(f\"1,000 lb more: {slope * 1000:+.1f} mpg | a 3,000 lb car: {mpg_3000:.1f} mpg\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74915328",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"10A.1\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-021",
   "metadata": {},
   "source": [
    "Each extra 1,000 lb goes with about **7.8 fewer** miles per gallon. The intercept, 46.6 mpg for a car that weighs nothing, only positions the line: no car weighs 0 lb."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-022",
   "metadata": {},
   "source": [
    "---\n",
    "## Part 2 — How Far Off? (SLO 10C)\n",
    "\n",
    "As in Monday's class, score the line on the **test** cars."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-024",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 10C.1 — MAE, RMSE and R² *(2 pts)*\n",
    "\n",
    "1. Store the line's predictions for the test cars, `X_test[[\"weight\"]]`, in `predicted`.\n",
    "2. Store the line's MAE, RMSE and R² on the test cars in `mae`, `rmse` and `r2`. The three functions were imported in the setup; each takes the actual values first, then the predictions.\n",
    "\n",
    "The lazy model is given."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-026",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "predicted = ...\n",
    "mae = ...\n",
    "rmse = ...\n",
    "r2 = ...\n",
    "\n",
    "lazy = np.full(len(y_test), y_train.mean())          # always guess the training average\n",
    "print(f\"line | MAE {mae:.2f} | RMSE {rmse:.2f} | R² {r2:.2f}\")\n",
    "print(f\"lazy | MAE {mean_absolute_error(y_test, lazy):.2f} | RMSE {root_mean_squared_error(y_test, lazy):.2f} | R² {r2_score(y_test, lazy):.2f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e041b9c0",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"10C.1\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-033",
   "metadata": {},
   "source": [
    "The line is typically about **2.9 mpg** off (MAE). RMSE, 3.8, counts the big misses more. R² = 0.74: the line removes about three quarters of the lazy model's squared error.\n",
    "\n",
    "> 📌 **One split:** these are 100 test cars."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-035",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 10C.2 — Who Does the Line Get Wrong? *(2 pts)*\n",
    "\n",
    "`era` (given) groups the test cars by model year.\n",
    "\n",
    "1. Store each test car's residual, actual − predicted, in `residuals`.\n",
    "2. Store the average residual for each era in `by_era`. (*Hint:* `groupby(era)`, then `mean()`.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-037",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "era = pd.cut(X_test[\"model_year\"], [1969, 1973, 1977, 1982], labels=[\"1970-73\", \"1974-77\", \"1978-82\"])\n",
    "\n",
    "residuals = ...\n",
    "by_era = ...\n",
    "\n",
    "print(by_era.round(2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e398ac9f",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"10C.2\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-044",
   "metadata": {},
   "source": [
    "A positive residual means a car gets **more** miles per gallon than its weight predicts. The 1970–73 cars average 2.3 mpg *below* the line, and the 1978–82 cars 2.3 mpg *above* it. After the oil crisis and the new standards, carmakers got more miles out of every pound. A line on weight alone can't know a car's year."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-045",
   "metadata": {},
   "source": [
    "---\n",
    "## Part 3 — More Columns (SLO 10A)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-047",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 10A.2 — Weight and Year Together *(2 pts)*\n",
    "\n",
    "Fit a `LinearRegression` on `X_train[[\"weight\", \"model_year\"]]` and `y_train`. Store it in `two`. Then store its weight slope in `weight_slope` and its model-year slope in `year_slope` (in the same order as the columns)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-049",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "two = ...\n",
    "weight_slope = ...\n",
    "year_slope = ...\n",
    "\n",
    "print(f\"1,000 lb more, same model year: {weight_slope * 1000:+.1f} mpg (weight alone: {slope * 1000:+.1f})\")\n",
    "print(f\"one model year newer, same weight: {year_slope:+.2f} mpg\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "46abafab",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"10A.2\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-056",
   "metadata": {},
   "source": [
    "Each slope now means \"**holding the other column fixed**.\" At the same weight, each newer model year goes with 0.8 more mpg. At the same model year, 1,000 lb cost 6.7 mpg instead of 7.8: alone, weight also took the blame for the older cars, which were heavier *and* less efficient.\n",
    "\n",
    "### All Columns, in a Pipeline (given — just run it)\n",
    "\n",
    "The six numbers (blank horsepower filled with the median, as in Week 9, then scaled), plus `origin`, one-hot with `drop=\"first\"` so that Europe becomes the baseline:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-057",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = Pipeline([\n",
    "    (\"prep\", ColumnTransformer([\n",
    "        (\"num\", make_pipeline(SimpleImputer(strategy=\"median\"), StandardScaler()), numeric),\n",
    "        (\"words\", OneHotEncoder(drop=\"first\"), [\"origin\"]),\n",
    "    ])),\n",
    "    (\"line\", LinearRegression()),\n",
    "])\n",
    "scores = cross_validate(model, X_train, y_train, cv=5, scoring=\"neg_root_mean_squared_error\", return_train_score=True)\n",
    "print(f\"all columns | RMSE on the folds it learned from: {-scores['train_score'].mean():.2f} | on the held-out fold: {-scores['test_score'].mean():.2f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-058",
   "metadata": {},
   "source": [
    "(scikit-learn reports *negative* RMSE, so the minus signs turn it back.) A small gap, 3.39 against 3.53: with 298 cars, 8 coefficients don't overfit."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-059",
   "metadata": {},
   "source": [
    "---\n",
    "## Part 4 — A Small Garage: Overfitting and Penalties (SLO 10B)\n",
    "\n",
    "Suppose a magazine had road-tested only **10** cars. To keep things simple, this part uses the six number columns only."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-060",
   "metadata": {},
   "outputs": [],
   "source": [
    "few = X_train.sample(10, random_state=1)[numeric]        # pretend these 10 are all the cars we have\n",
    "few_mpg = y_train[few.index]\n",
    "others = X_train.drop(few.index)[numeric]                # the other 288 training cars stand in for new cars\n",
    "others_mpg = y_train[others.index]\n",
    "display(cars.loc[few.index, [\"name\", \"model_year\", \"weight\", \"displacement\", \"cylinders\", \"horsepower\", \"mpg\"]])\n",
    "\n",
    "small = make_pipeline(SimpleImputer(strategy=\"median\"), StandardScaler(), LinearRegression()).fit(few, few_mpg)\n",
    "everything = make_pipeline(SimpleImputer(strategy=\"median\"), StandardScaler(), LinearRegression()).fit(X_train[numeric], y_train)\n",
    "print(f\"line from 10 cars | RMSE on its 10 cars: {root_mean_squared_error(few_mpg, small.predict(few)):.2f}\"\n",
    "      f\" | on the other 288: {root_mean_squared_error(others_mpg, small.predict(others)):.2f}\")\n",
    "print(\"other cars predicted below 0 mpg:\", (small.predict(others) < 0).sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-061",
   "metadata": {},
   "source": [
    "> 🎯 **Predict First:** Below are the line's slopes, in mpg per standard deviation of each column, learned from the 10 cars and from all 298. Which ones look **too big**? Which point the **wrong way**?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-062",
   "metadata": {},
   "outputs": [],
   "source": [
    "# small[-1] is the pipeline's last step, the LinearRegression\n",
    "display(pd.DataFrame({\"from all 298 cars\": everything[-1].coef_, \"from 10 cars\": small[-1].coef_}, index=numeric).round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-063",
   "metadata": {},
   "source": [
    "Three symptoms of overfitting in a line:\n",
    "\n",
    "* **Too big:** displacement −30.2 mpg per typical step, on cars that average 23.6 mpg\n",
    "* **The wrong way:** weight **+15.0** and cylinders +10.9. Heavier cars, with more cylinders, get *more* miles per gallon?\n",
    "* **A tug of war:** in these 10 cars, weight, displacement, cylinders and horsepower are near-copies (correlations from 0.91 to 0.99). The line pushes hard on some and pulls hard on others. The pushes cancel on these 10 cars, not on new ones: 19 of the other 288 cars are predicted to get *less than zero* miles per gallon"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-065",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 10B.1 — Ridge: Choose alpha *(2 pts)*\n",
    "\n",
    "Build a `GridSearchCV` called `ridge_search` on the pipeline `make_pipeline(SimpleImputer(strategy=\"median\"), StandardScaler(), Ridge())`, with the grid `{\"ridge__alpha\": alphas}`, `cv=5`, `scoring=\"neg_root_mean_squared_error\"` and `return_train_score=True`. Fit it on the 10 cars: `few` and `few_mpg`.\n",
    "\n",
    "(`ridge__alpha` is the `alpha` of the step named `ridge`: `make_pipeline` names each step after its class.) The plot is given."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-067",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "alphas = [0.01, 0.1, 1, 10, 100, 1000]\n",
    "ridge_search = ...\n",
    "...\n",
    "\n",
    "results = pd.DataFrame({\"alpha\": alphas,\n",
    "                        \"folds it learned from\": -ridge_search.cv_results_[\"mean_train_score\"],\n",
    "                        \"held-out fold\": -ridge_search.cv_results_[\"mean_test_score\"]})\n",
    "px.line(results, x=\"alpha\", y=[\"folds it learned from\", \"held-out fold\"], markers=True, log_x=True,\n",
    "        labels={\"value\": \"RMSE (mpg)\", \"variable\": \"scored on\"}, title=\"Ridge on 10 Cars: Choosing alpha by 5-Fold CV\").show()\n",
    "print(\"best alpha:\", ridge_search.best_params_[\"ridge__alpha\"], \"| held-out RMSE:\", round(-ridge_search.best_score_, 2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "678c3b04",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"10B.1\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-074",
   "metadata": {},
   "source": [
    "Read the plot from left to right: with almost no penalty, the line misses the folds it learned from by 1 to 2 mpg and the held-out fold by more than 6: **overfitting**. At alpha = 1,000, both errors are high: **underfitting**. The best alpha is in between. Here a **bigger** alpha means a **simpler** model.\n",
    "\n",
    "> 📌 **One sample:** each held-out fold is only 2 cars, so this curve is shaky.\n",
    "\n",
    "What did Ridge do to the slopes? (given)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-075",
   "metadata": {},
   "outputs": [],
   "source": [
    "display(pd.DataFrame({\"plain line, 10 cars\": small[-1].coef_,\n",
    "                      \"Ridge, 10 cars\": ridge_search.best_estimator_[-1].coef_}, index=numeric).round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-076",
   "metadata": {},
   "source": [
    "Ridge **shares the blame**: weight, displacement, cylinders and horsepower each get a small slope, about −1.2 to −1.4. No more tug of war, and every sign points the sensible way."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-078",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "source": [
    "### Task 10B.2 — Lasso: Which Columns Survive? *(2 pts)*\n",
    "\n",
    "Do the same with Lasso: build a `GridSearchCV` called `lasso_search` on `make_pipeline(SimpleImputer(strategy=\"median\"), StandardScaler(), Lasso(max_iter=100_000))`, with the grid `{\"lasso__alpha\": lasso_alphas}`, `cv=5` and `scoring=\"neg_root_mean_squared_error\"`, and fit it on the 10 cars. (`max_iter` lets Lasso's step-by-step search run long enough to settle.)\n",
    "\n",
    "The kept and dropped columns are given."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-080",
   "metadata": {
    "tags": [
     "otter_answer_cell"
    ]
   },
   "outputs": [],
   "source": [
    "lasso_alphas = [0.01, 0.1, 0.3, 1, 3, 10]\n",
    "lasso_search = ...\n",
    "...\n",
    "\n",
    "lasso_slopes = pd.Series(lasso_search.best_estimator_[-1].coef_, index=numeric)\n",
    "print(\"best alpha:\", lasso_search.best_params_[\"lasso__alpha\"], \"| held-out RMSE:\", round(-lasso_search.best_score_, 2))\n",
    "print(\"kept:   \", list(lasso_slopes[lasso_slopes != 0].index))\n",
    "print(\"dropped:\", list(lasso_slopes[lasso_slopes == 0].index))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2ddd32bc",
   "metadata": {
    "deletable": false,
    "editable": false
   },
   "outputs": [],
   "source": [
    "grader.check(\"10B.2\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-087",
   "metadata": {},
   "source": [
    "Lasso keeps **one** of the four near-copies, displacement, plus the model year, and drops the rest. Ridge shared the slope among the near-copies; Lasso picks one. (With another 10 cars, it could just as well have picked weight.)\n",
    "\n",
    "### Elastic Net: Both at Once (given — just run it)\n",
    "\n",
    "Elastic net charges both penalties, mixed, with two knobs: `alpha` sets **how strong** the penalty is, and `l1_ratio` sets **the balance**: 1 is pure Lasso, 0 is pure Ridge. With two knobs, `GridSearchCV` tries every pair."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-088",
   "metadata": {},
   "outputs": [],
   "source": [
    "net_search = GridSearchCV(make_pipeline(SimpleImputer(strategy=\"median\"), StandardScaler(), ElasticNet(max_iter=100_000)),\n",
    "                          {\"elasticnet__alpha\": [0.01, 0.1, 0.3, 1, 3], \"elasticnet__l1_ratio\": [0.1, 0.5, 0.9, 1]},\n",
    "                          cv=5, scoring=\"neg_root_mean_squared_error\")\n",
    "net_search.fit(few, few_mpg)\n",
    "print(net_search.best_params_, \"| held-out RMSE:\", round(-net_search.best_score_, 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-089",
   "metadata": {},
   "source": [
    "On these 10 cars, cross-validation picks a mostly-Ridge balance (`l1_ratio = 0.1`).\n",
    "\n",
    "### The Final Comparison (given — just run it)\n",
    "\n",
    "The other 288 training cars were never used to fit or tune these models, so they can stand in for new cars:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-090",
   "metadata": {},
   "outputs": [],
   "source": [
    "one_line = LinearRegression().fit(few[[\"weight\"]], few_mpg)\n",
    "lazy_10 = np.full(len(others_mpg), few_mpg.mean())\n",
    "display(pd.Series({\"lazy: the 10 cars' average\": root_mean_squared_error(others_mpg, lazy_10),\n",
    "                   \"one line (weight)\": root_mean_squared_error(others_mpg, one_line.predict(others[[\"weight\"]])),\n",
    "                   \"plain line, 6 columns\": root_mean_squared_error(others_mpg, small.predict(others)),\n",
    "                   \"Ridge\": root_mean_squared_error(others_mpg, ridge_search.predict(others)),\n",
    "                   \"Lasso\": root_mean_squared_error(others_mpg, lasso_search.predict(others)),\n",
    "                   \"elastic net\": root_mean_squared_error(others_mpg, net_search.predict(others))},\n",
    "                  name=\"learned from 10 cars: RMSE on the other 288 (mpg)\").round(2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-091",
   "metadata": {},
   "source": [
    "The plain line, with six columns for 10 cars, does worse than just guessing the 10 cars' average. The penalties bring it down to about 4 mpg, a little better than one line through weight.\n",
    "\n",
    "Now, **once**, open the test set:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "p08-092",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"test RMSE | plain line, all 298 training cars:\", round(root_mean_squared_error(y_test, everything.predict(X_test[numeric])), 2))\n",
    "print(\"test RMSE | Ridge, only 10 cars:              \", round(root_mean_squared_error(y_test, ridge_search.predict(X_test[numeric])), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-093",
   "metadata": {},
   "source": [
    "**More data is the best cure:** with 298 cars, the plain line needs no penalty. But with only 10, a penalty got surprisingly close.\n",
    "\n",
    "> 📌 **One split:** these are 100 test cars, and one random set of 10."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "p08-094",
   "metadata": {},
   "source": [
    "---\n",
    "## Submission\n",
    "\n",
    "Save your notebook, then upload the **`.ipynb` file** directly to our Moodle assignment page. Don't export, zip, or submit anything else — just the notebook."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "otter": {
   "OK_FORMAT": true,
   "assignment_name": "practice08",
   "tests": {
    "10A.1": {
     "name": "10A.1",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert isinstance(line, LinearRegression) and line.n_features_in_ == 1, \"fit the line on X_train[['weight']] only\"\n>>> assert slope < 0, 'heavier cars should get fewer miles per gallon'\n>>> assert 15 < mpg_3000 < 35, 'predict for a single 3,000 lb car and take element [0]'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "10A.2": {
     "name": "10A.2",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert isinstance(two, LinearRegression) and two.n_features_in_ == 2, \"fit on X_train[['weight', 'model_year']]\"\n>>> assert weight_slope < 0 < year_slope, 'weight first, then model_year'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "10B.1": {
     "name": "10B.1",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert isinstance(ridge_search, GridSearchCV) and ridge_search.cv == 5, 'use GridSearchCV with cv=5'\n>>> assert isinstance(ridge_search.estimator[-1], Ridge), \"the pipeline's last step should be Ridge()\"\n>>> assert hasattr(ridge_search, 'best_estimator_'), 'fit the search on few and few_mpg'\n>>> assert 'mean_train_score' in ridge_search.cv_results_, 'use return_train_score=True'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "10B.2": {
     "name": "10B.2",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert isinstance(lasso_search, GridSearchCV) and lasso_search.cv == 5, 'use GridSearchCV with cv=5'\n>>> assert isinstance(lasso_search.estimator[-1], Lasso), \"the pipeline's last step should be Lasso(max_iter=100_000)\"\n>>> assert hasattr(lasso_search, 'best_estimator_'), 'fit the search on few and few_mpg'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "10C.1": {
     "name": "10C.1",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert len(predicted) == len(y_test), 'predict the test cars'\n>>> assert 0 < mae <= rmse, 'RMSE is never smaller than MAE'\n>>> assert 0 < r2 < 1, 'R² should lie between 0 and 1 here'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    },
    "10C.2": {
     "name": "10C.2",
     "points": 2,
     "suites": [
      {
       "cases": [
        {
         "code": ">>> assert len(residuals) == len(y_test), 'one residual per test car'\n>>> assert len(by_era) == 3, 'one average per era'\n",
         "hidden": false,
         "locked": false
        }
       ],
       "scored": true,
       "setup": "",
       "teardown": "",
       "type": "doctest"
      }
     ]
    }
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
