{ "cells": [ { "cell_type": "markdown", "id": "welcome", "metadata": {}, "source": [ "# Regression Primer — Guided Practice\n", "\n", "In the data-exploration primer, you described associations in house-sale records.\n", "Here you will use some of those records to **learn a prediction rule** and assess\n", "how well it works for houses the model was not trained on.\n", "\n", "**Question:** how well can we predict a historical sale price from information\n", "about a house?\n", "\n", "By the end, you should be able to:\n", "\n", "- identify input features and a numerical prediction target;\n", "- fit and interpret a simple linear regression;\n", "- compare predictions with a simple baseline on held-out records;\n", "- investigate model complexity and recognise possible overfitting;\n", "- choose a small model using validation results, then assess it on a final test set.\n" ] }, { "cell_type": "markdown", "id": "how-to-work", "metadata": {}, "source": [ "## How to work through this notebook\n", "\n", "Complete Exercises 1–4 in order. Study each **worked example**, then write\n", "your own code in the **Your turn** cells. These usually contain only a comment.\n", "The instructions state the goal and the object names needed by later cells;\n", "you choose the intermediate steps and variable names.\n", "\n", "Complete each task before running its **checkpoint**. A checkpoint stops\n", "with an explanation while required work is missing. After finishing all core\n", "tasks, restart the kernel and run all cells to check reproducibility.\n", "\n", "Keep the supplied sample and partitions fixed. Fit using training data,\n", "choose using validation data, and use the test data for the final assessment.\n", "Predictions written before an experiment need not be correct. Extensions\n", "are optional. Optional syntax hints can be opened after trying a task." ] }, { "cell_type": "markdown", "id": "dataset-setup", "metadata": {}, "source": [ "## Setup — familiar data, a new question\n", "\n", "Keep **`kc_house_data.csv` beside this notebook**. In Google Colab, upload the\n", "CSV into the session's files. Use a Python environment with `pandas`, `numpy`,\n", "`matplotlib`, and `scikit-learn` installed. The package is imported as `sklearn`.\n", "\n", "We reuse the King County sale records from 2014–2015. Prices are historical\n", "amounts in **USD**, and `sqft_living` is living area in **square feet**.\n", "A price is a numerical target, so this is a **regression** problem.\n", "\n", "Some properties were sold more than once. The supplied preparation keeps the\n", "earliest recorded sale for each `id`, then draws a reproducible random sample\n", "of **1,000 different houses**. This prevents repeated sales of one house from\n", "appearing on both sides of a split. Keeping one sale is a modelling choice,\n", "not a claim that the other sales are errors. The small sample keeps experiments\n", "quick and makes the limits of the resulting scores important. **You may also try to use the full dataset later**" ] }, { "cell_type": "code", "execution_count": null, "id": "load-and-prepare", "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from sklearn.model_selection import train_test_split\n", "from sklearn.linear_model import LinearRegression\n", "\n", "possible_paths = [Path(\"kc_house_data.csv\"), Path(\"Primer Assignments\") / \"kc_house_data.csv\"]\n", "data_path = next((path for path in possible_paths if path.is_file()), None)\n", "if data_path is None:\n", " raise FileNotFoundError(\"Put kc_house_data.csv beside this notebook, then run this cell again.\")\n", "\n", "houses = pd.read_csv(data_path)\n", "unique_houses = houses.sort_values(\"date\").drop_duplicates(subset=\"id\", keep=\"first\")\n", "#Change 1000 with len(unique_houses) to use the full dataset of unique houses\n", "modelling_data = unique_houses.sample(n=1000, random_state=44).copy()\n", "\n", "print(\"Sale records in the CSV:\", len(houses))\n", "print(\"Different houses in the modelling sample:\", modelling_data[\"id\"].nunique())\n", "modelling_data[[\"id\", \"sqft_living\", \"price\"]].head()" ] }, { "cell_type": "markdown", "id": "split-explanation", "metadata": {}, "source": [ "### Three groups with different jobs\n", "\n", "| Group | Share | What we use it for |\n", "|---|---|---|\n", "| Training | 60% | Learn coefficients and any preprocessing settings. |\n", "| Validation | 20% | Compare models and choose features or polynomial degree. |\n", "| Test | 20% | Assess the chosen approach after those decisions are made. |\n", "\n", "The supplied split first sets aside 20% for testing, then takes a quarter of\n", "the remaining 80% for validation: 0.25 × 0.80 = 0.20 of the original sample.\n", "`random_state` fixes the random allocation so comparisons can use the same houses.\n", "\n", "**Keep the test set aside until the final assessment in Exercise 4.** Repeatedly\n", "using its scores to choose a model would make it part of model selection.\n", "This random split asks about other houses from a similar historical setting;\n", "it does not demonstrate that the model forecasts prices in future years." ] }, { "cell_type": "code", "execution_count": null, "id": "fixed-splits", "metadata": {}, "outputs": [], "source": [ "development_data, test_data = train_test_split(\n", " modelling_data, test_size=0.20, random_state=44\n", ")\n", "train_data, validation_data = train_test_split(\n", " development_data, test_size=0.25, random_state=44\n", ")\n", "\n", "print(\"Training records:\", len(train_data))\n", "print(\"Validation records:\", len(validation_data))\n", "print(\"Test records reserved for the final assessment:\", len(test_data))\n", "\n", "# Supplied checks: a house must not appear in more than one group.\n", "assert set(train_data[\"id\"]).isdisjoint(validation_data[\"id\"])\n", "assert set(train_data[\"id\"]).isdisjoint(test_data[\"id\"])\n", "assert set(validation_data[\"id\"]).isdisjoint(test_data[\"id\"])" ] }, { "cell_type": "markdown", "id": "ex1-intro", "metadata": {}, "source": [ "## Exercise 1 — Learn and interpret a straight-line model\n", "\n", "A **feature** is information supplied to a model; the **target** is the value\n", "we want it to predict. Start with one feature, living area, and target price.\n", "\n", "In scikit-learn, `X` is a table with one column per feature, while `y` contains\n", "one target value per row. Double brackets keep `X` as a two-dimensional table;\n", "single brackets select the one-dimensional target column." ] }, { "cell_type": "code", "execution_count": null, "id": "features-and-target", "metadata": {}, "outputs": [], "source": [ "X_train = train_data[[\"sqft_living\"]]\n", "y_train = train_data[\"price\"]\n", "X_validation = validation_data[[\"sqft_living\"]]\n", "y_validation = validation_data[\"price\"]\n", "\n", "print(\"Training feature-table shape:\", X_train.shape)\n", "print(\"Training target shape:\", y_train.shape)\n", "print(\"Training living-area range:\", X_train[\"sqft_living\"].min(), \"to\", X_train[\"sqft_living\"].max())" ] }, { "cell_type": "markdown", "id": "fit-explanation", "metadata": {}, "source": [ "**Worked example — fitting.** Simple linear regression learns a rule of the form\n", "\n", "**predicted price = intercept + slope × living area**.\n", "\n", "`.fit(X_train, y_train)` learns the intercept and slope by minimising the sum\n", "of squared training errors. `.predict(...)` applies the fitted rule to inputs.\n", "Fitting is the learning step; predicting does not learn new coefficients.\n", "\n", "Before running the example, predict the sign of the slope. What association\n", "from the previous primer supports your prediction?\n", "\n", "_Your prediction:_" ] }, { "cell_type": "code", "execution_count": null, "id": "fit-linear-example", "metadata": {}, "outputs": [], "source": [ "linear_model = LinearRegression()\n", "linear_model.fit(X_train, y_train)\n", "\n", "print(f\"Intercept: USD {linear_model.intercept_:,.2f}\")\n", "print(f\"Slope: USD {linear_model.coef_[0]:,.2f} per additional square foot\")" ] }, { "cell_type": "code", "execution_count": null, "id": "linear-plot-example", "metadata": {}, "outputs": [], "source": [ "# An increasing grid gives a smooth line, rather than joining unsorted houses.\n", "area_grid = pd.DataFrame({\n", " \"sqft_living\": np.linspace(X_train[\"sqft_living\"].min(), X_train[\"sqft_living\"].max(), 200)\n", "})\n", "\n", "fig, ax = plt.subplots(figsize=(8, 4))\n", "ax.scatter(X_train[\"sqft_living\"], y_train, s=15, alpha=0.3, label=\"Training sales\")\n", "ax.plot(area_grid[\"sqft_living\"], linear_model.predict(area_grid), color=\"darkorange\", label=\"Fitted linear model\")\n", "ax.set_xlabel(\"Living area (square feet)\")\n", "ax.set_ylabel(\"Recorded sale price (USD)\")\n", "ax.set_title(\"One-feature regression fitted to the training data\")\n", "ax.ticklabel_format(style=\"plain\", axis=\"y\")\n", "ax.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "predict-task", "metadata": {}, "source": [ "**Your turn — construct inputs and use the rule.** Write complete code to\n", "create a DataFrame named **new_houses** with a **sqft_living** column containing\n", "1,500 and 2,500 square feet, in that order. Obtain **predicted_prices** using\n", "the fitted linear_model. Display the areas and predicted prices together.\n", "\n", "Use the same feature-column name as during fitting. The worked plot shows\n", "an example of constructing a prediction input table." ] }, { "cell_type": "code", "execution_count": null, "id": "predict-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: construct the input table, predict, and display the results." ] }, { "cell_type": "code", "execution_count": null, "id": "prediction-check", "metadata": {}, "outputs": [], "source": [ "assert \"new_houses\" in globals() and \"predicted_prices\" in globals(), \"Create new_houses and predicted_prices first.\"\n", "assert list(new_houses.columns) == [\"sqft_living\"], \"Check the input column name.\"\n", "assert new_houses[\"sqft_living\"].tolist() == [1500, 2500], \"Use the two requested areas, in order.\"\n", "assert np.asarray(predicted_prices).shape == (2,) and np.isfinite(predicted_prices).all(), \"Produce two finite predictions.\"\n", "print(\"Checkpoint passed: two input rows and two predictions are ready.\")" ] }, { "cell_type": "markdown", "id": "linear-interpretation", "metadata": {}, "source": [ "**Interpret:** According to this model, how much does predicted price change\n", "for an additional **100 square feet**? Show how the units lead to your answer.\n", "\n", "_Your answer:_\n", "\n", "**Question the rule:** What price would the intercept assign to zero living\n", "area? Why is that not a useful valuation? Does the fitted slope establish that\n", "extending a particular house would cause its price to change by that amount?\n", "\n", "_Your answer:_" ] }, { "cell_type": "markdown", "id": "bathroom-practice-task", "metadata": {}, "source": [ "**Your turn — fit another one-feature model.** Predict price using\n", "**only bathrooms**. Select its training inputs and price target, create\n", "a new LinearRegression named **bathroom_model**, and fit it.\n", "Print its intercept and slope with meaningful units.\n", "\n", "Then construct a training scatterplot and add its fitted line using an\n", "increasing input grid, with labels, a title, and a legend. Use the\n", "living-area worked example as a reference and write this implementation\n", "yourself. Keep linear_model, X_train, and y_train unchanged." ] }, { "cell_type": "code", "execution_count": null, "id": "bathroom-model-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: prepare inputs, create and fit bathroom_model, report coefficients, and plot." ] }, { "cell_type": "code", "execution_count": null, "id": "bathroom-model-check", "metadata": {}, "outputs": [], "source": [ "assert \"bathroom_model\" in globals(), \"Create bathroom_model before continuing.\"\n", "assert hasattr(bathroom_model, \"coef_\"), \"Fit the new model on training records.\"\n", "assert list(bathroom_model.feature_names_in_) == [\"bathrooms\"], \"Use a DataFrame containing only bathrooms.\"\n", "print(\"Checkpoint passed: the fitted bathroom model has the intended input.\")" ] }, { "cell_type": "markdown", "id": "bathroom-units-answer", "metadata": {}, "source": [ "**Interpret the units:** What are the units of this model's slope?\n", "Would a numerically larger slope than the living-area slope establish\n", "that bathrooms are a more useful predictor? Explain.\n", "\n", "_Your answer:_" ] }, { "cell_type": "markdown", "id": "ex2-intro", "metadata": {}, "source": [ "## Exercise 2 — Assess predictions on held-out houses\n", "\n", "A line can fit its training data and still make poor predictions elsewhere.\n", "We will compare it with a **baseline** that predicts the mean training price\n", "for every house. A useful model should offer evidence of improvement over\n", "that simple rule on the same validation records. Because the groups contain\n", "different houses, validation RMSE can be lower than training RMSE; it is not\n", "guaranteed to be higher. Compare competing models on the same validation group.\n", "\n", "**RMSE**, the root mean squared error, is the square root of the mean squared\n", "prediction error. Here it is in **USD**; lower is better. Squaring gives large\n", "errors more influence. RMSE is not a guarantee that every prediction falls\n", "within that amount of the true price. MSE, before taking the square root,\n", "would have units of USD squared.\n", "\n", "We also report **R²**: 1 means perfect predictions; 0 matches a constant equal\n", "to the mean target in the group being evaluated; negative values are possible.\n", "**R² is not a percentage of houses predicted correctly.** Our baseline uses\n", "the training mean, so its validation R² need not be exactly zero.\n", "\n", "The supplied `rmse` function packages the square-root calculation; you may\n", "call it without rewriting its definition." ] }, { "cell_type": "code", "execution_count": null, "id": "baseline-example", "metadata": {}, "outputs": [], "source": [ "from sklearn.dummy import DummyRegressor\n", "from sklearn.metrics import mean_squared_error, r2_score\n", "\n", "def rmse(actual, predicted):\n", " return float(np.sqrt(mean_squared_error(actual, predicted)))\n", "\n", "baseline = DummyRegressor(strategy=\"mean\")\n", "baseline.fit(X_train, y_train)\n", "baseline_train_rmse = rmse(y_train, baseline.predict(X_train))\n", "baseline_validation_predictions = baseline.predict(X_validation)\n", "baseline_validation_rmse = rmse(y_validation, baseline_validation_predictions)\n", "\n", "print(f\"Baseline predicts the training mean: USD {y_train.mean():,.2f}\")\n", "print(f\"Training RMSE: USD {baseline_train_rmse:,.2f}\")\n", "print(f\"Validation RMSE: USD {baseline_validation_rmse:,.2f}\")\n", "print(f\"Validation R²: {r2_score(y_validation, baseline_validation_predictions):.3f}\")" ] }, { "cell_type": "markdown", "id": "evaluation-task", "metadata": {}, "source": [ "**Your turn — implement evaluation and assemble a table.** Using the already\n", "fitted linear_model, write code to obtain **linear_validation_predictions**,\n", "**linear_train_rmse**, **linear_validation_rmse**, and **linear_validation_r2**.\n", "\n", "Construct and display a DataFrame named **linear_results** comparing the\n", "baseline with the living-area model. Include training and validation RMSE\n", "for each, with readable column labels and units.\n", "\n", "Also calculate and print **bathroom_validation_rmse**, using your fitted\n", "bathroom_model and the corresponding validation input column.\n", "\n", "Use the supplied rmse function and r2_score. Pair each prediction with the\n", "actual price for the same rows in the same order. Do not refit on validation\n", "data or use a plotting grid as a set of actual target values.\n", "\n", "
\n", "Optional table-construction hint\n", "\n", "A list of dictionaries can become a table with pd.DataFrame(rows).\n", "Each dictionary supplies one row, with its keys naming the columns.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "evaluation-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: predict, calculate all scores, construct linear_results, and display your comparisons." ] }, { "cell_type": "code", "execution_count": null, "id": "evaluation-check", "metadata": {}, "outputs": [], "source": [ "required = [\"linear_validation_predictions\", \"linear_train_rmse\", \"linear_validation_rmse\", \"linear_validation_r2\", \"linear_results\", \"bathroom_validation_rmse\"]\n", "assert all(name in globals() for name in required), \"Complete the predictions, scores, and comparison table first.\"\n", "assert np.asarray(linear_validation_predictions).shape == y_validation.shape, \"Check which rows you predicted.\"\n", "assert np.allclose(linear_validation_predictions, linear_model.predict(X_validation)), \"Use the fitted living-area model on validation inputs.\"\n", "assert np.isclose(linear_train_rmse, rmse(y_train, linear_model.predict(X_train))), \"Check training RMSE and units.\"\n", "assert np.isclose(linear_validation_rmse, rmse(y_validation, linear_validation_predictions)), \"Check validation RMSE and units.\"\n", "assert np.isclose(linear_validation_r2, r2_score(y_validation, linear_validation_predictions)), \"Check validation R².\"\n", "assert np.isclose(bathroom_validation_rmse, rmse(y_validation, bathroom_model.predict(validation_data[[\"bathrooms\"]]))), \"Check the bathroom model's validation score.\"\n", "assert isinstance(linear_results, pd.DataFrame) and len(linear_results) == 2, \"Create a two-row comparison table.\"\n", "print(\"Checkpoint passed: the scores use the intended rows and target units.\")" ] }, { "cell_type": "markdown", "id": "residual-explanation", "metadata": {}, "source": [ "**Your turn — calculate and plot residuals.** A residual is **actual price\n", "minus predicted price**. A positive residual means the model underestimated\n", "that sale price.\n", "\n", "Compute the living-area model's validation residuals as **linear_residuals**.\n", "Construct a scatterplot with predicted prices on the horizontal axis and\n", "residuals on the vertical axis. Add a horizontal zero line, a title, and\n", "labels with units. A zero line can be added with ax.axhline(0, ...).\n", "Build the calculation and plot yourself using the familiar plotting pattern." ] }, { "cell_type": "code", "execution_count": null, "id": "residual-plot", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: calculate linear_residuals and construct the diagnostic plot." ] }, { "cell_type": "markdown", "id": "evaluation-answers", "metadata": {}, "source": [ "**Compare:** Does the linear model improve on the baseline's validation RMSE?\n", "Quote both values. Would a small training RMSE alone answer that question?\n", "\n", "_Your answer:_\n", "\n", "**Inspect:** Describe one feature of the validation residuals. Do you see\n", "systematic patterns, large misses, or a changing spread? State what a positive\n", "residual means and one limitation of using living area alone.\n", "\n", "_Your answer:_\n", "\n", "**Compare one-feature models:** Which validation RMSE is lower, the living-area\n", "model's or the bathroom model's? Quote both values and explain why this is a more\n", "useful comparison than their raw slope magnitudes.\n", "\n", "_Your answer:_" ] }, { "cell_type": "markdown", "id": "ex3-intro", "metadata": {}, "source": [ "## Exercise 3 — Investigate polynomial complexity\n", "\n", "A straight line may miss curvature. A degree-2 polynomial adds a squared\n", "input; higher degrees add more powers. The degree is a **hyperparameter**:\n", "a setting we choose, unlike coefficients learned by `.fit()`.\n", "\n", "**Worked example — a pipeline.** `make_pipeline` connects three steps:\n", "`StandardScaler` centres and scales the living-area input, `PolynomialFeatures`\n", "constructs its powers, and `LinearRegression` learns coefficients. Scaling\n", "before taking powers reduces problems caused by very large raw numbers such\n", "as square footage raised to the eighth power.\n", "\n", "The whole pipeline uses the same `.fit()` and `.predict()` interface as our\n", "first model. Fit it on **training data only**. It then reuses that scaling\n", "when predicting validation inputs; do not fit a second scaler on validation data." ] }, { "cell_type": "code", "execution_count": null, "id": "quadratic-example", "metadata": {}, "outputs": [], "source": [ "from sklearn.pipeline import make_pipeline\n", "from sklearn.preprocessing import StandardScaler, PolynomialFeatures\n", "\n", "quadratic_model = make_pipeline(\n", " StandardScaler(),\n", " PolynomialFeatures(degree=2, include_bias=False),\n", " LinearRegression(),\n", ")\n", "quadratic_model.fit(X_train, y_train)\n", "print(f\"Degree-2 validation RMSE: USD {rmse(y_validation, quadratic_model.predict(X_validation)):,.2f}\")\n", "\n", "fig, ax = plt.subplots(figsize=(8, 4))\n", "ax.scatter(X_train[\"sqft_living\"], y_train, s=15, alpha=0.25, label=\"Training sales\")\n", "ax.plot(area_grid[\"sqft_living\"], linear_model.predict(area_grid), label=\"Degree 1\")\n", "ax.plot(area_grid[\"sqft_living\"], quadratic_model.predict(area_grid), label=\"Degree 2\")\n", "ax.set_xlabel(\"Living area (square feet)\")\n", "ax.set_ylabel(\"Recorded sale price (USD)\")\n", "ax.set_title(\"Two models, fitted to the same training houses\")\n", "ax.ticklabel_format(style=\"plain\", axis=\"y\")\n", "ax.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "polynomial-task", "metadata": {}, "source": [ "**Your turn — write the full experiment.** Using the degree-2 worked example\n", "as a reference, write a loop comparing polynomial degrees **1 to 8**.\n", "\n", "For each degree, create a **fresh pipeline**, fit on the original X_train and\n", "y_train, predict both training and validation groups, and calculate RMSE.\n", "Use the same data groups each time.\n", "\n", "Produce **polynomial_models**, a dictionary mapping each degree to its fitted\n", "pipeline, and **polynomial_results**, a list of eight dictionaries containing\n", "degree, training_rmse, and validation_rmse.\n", "Write the model construction, fitting, scoring, and result storage yourself.\n", "Keep the original feature tables unchanged.\n", "\n", "
\n", "Optional loop and storage hints\n", "\n", "for degree in range(1, 9): repeats an indented block eight times.\n", "Start a dictionary with {} and a list with [].\n", "mapping[key] = value stores an item; rows.append(record) adds a record.\n", "The worked example shows the pipeline components and their order.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "id": "polynomial-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: initialise the result containers and implement the complete degree experiment." ] }, { "cell_type": "markdown", "id": "polynomial-table-plot-task", "metadata": {}, "source": [ "**Your turn — organise and plot the experiment.** Turn polynomial_results into\n", "a DataFrame named **polynomial_table**, sort by degree, and display it.\n", "Plot training and validation RMSE against degree on the same axes, including\n", "markers, a legend, a title, and labels with units.\n", "\n", "This compares model performance rather than drawing fitted price curves.\n", "Build both the table and the plot yourself." ] }, { "cell_type": "code", "execution_count": null, "id": "polynomial-comparison", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: construct polynomial_table and plot the two error curves." ] }, { "cell_type": "markdown", "id": "degree-choice", "metadata": {}, "source": [ "**Your turn — make a validation-based choice.** Read the table and plot, then\n", "choose `selected_degree`. The smallest validation RMSE is a useful starting\n", "point; choosing a simpler degree with a very similar error can also be justified.\n", "A tiny score difference on one small validation set is weak evidence that a more\n", "complex model is reliably better.\n", "\n", "**Overfitting** means fitting training-specific details that do not generalise\n", "well. Look for a falling training error without a corresponding validation\n", "improvement. Do not assume the plot must show a perfect U-shape." ] }, { "cell_type": "code", "execution_count": null, "id": "degree-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: assign selected_degree from validation evidence and explain it below." ] }, { "cell_type": "markdown", "id": "polynomial-answers", "metadata": {}, "source": [ "**Explain your choice:** Which degree did you select, and what training and\n", "validation scores support it? Does the highest degree give the best validation\n", "result in this experiment?\n", "\n", "_Your answer:_\n", "\n", "**Interpret the experiment:** Is there evidence of overfitting on this split?\n", "Explain from the table or plot. Why would choosing the smallest training error,\n", "or repeatedly changing the split until a model wins, be misleading?\n", "\n", "_Your answer:_" ] }, { "cell_type": "markdown", "id": "ex4-intro", "metadata": {}, "source": [ "## Exercise 4 — Add information, choose a model, and assess it\n", "\n", "**Your turn — implement a multiple-feature model.** Keep sqft_living and add\n", "**one or two** features from bathrooms, bedrooms, sqft_lot, and yr_built.\n", "These record bathroom count, bedroom count, lot area in square feet, and\n", "construction year. Choose them for a reason.\n", "\n", "Construct **feature_columns** and a new LinearRegression named **multi_model**.\n", "Select its training data, fit it, and obtain **multi_validation_predictions**.\n", "Calculate **multi_validation_rmse** and **multi_validation_r2**.\n", "Create and display a labelled coefficient table or Series.\n", "Organise the whole implementation yourself using the patterns practised earlier.\n", "\n", "Use the fixed groups and leave the test set aside. Do not use raw numeric ZIP\n", "codes or IDs as measurements. You are not required to beat a particular score.\n", "\n", "**Before fitting:** justify your extra features and explain whether their\n", "values would be available before knowing the sale price.\n", "\n", "_Your reasoning:_" ] }, { "cell_type": "code", "execution_count": null, "id": "multiple-features-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: select features, create and fit multi_model, evaluate it, and display labelled coefficients." ] }, { "cell_type": "code", "execution_count": null, "id": "multiple-features-check", "metadata": {}, "outputs": [], "source": [ "assert all(name in globals() for name in [\"feature_columns\", \"multi_model\", \"multi_validation_predictions\", \"multi_validation_rmse\", \"multi_validation_r2\"]), \"Complete the multiple-feature implementation first.\"\n", "allowed = {\"sqft_living\", \"bathrooms\", \"bedrooms\", \"sqft_lot\", \"yr_built\"}\n", "assert \"sqft_living\" in feature_columns and 2 <= len(feature_columns) <= 3, \"Keep living area and add one or two features.\"\n", "assert len(set(feature_columns)) == len(feature_columns) and set(feature_columns) <= allowed, \"Check the feature list.\"\n", "expected = multi_model.predict(validation_data[feature_columns])\n", "assert np.asarray(multi_validation_predictions).shape == y_validation.shape, \"Check the validation rows.\"\n", "assert np.allclose(multi_validation_predictions, expected), \"Use the same feature columns and their original order.\"\n", "assert np.isclose(multi_validation_rmse, rmse(y_validation, expected)), \"Check validation RMSE.\"\n", "assert np.isclose(multi_validation_r2, r2_score(y_validation, expected)), \"Check validation R².\"\n", "multi_results_ready = True\n", "print(\"Checkpoint passed: the multiple-feature scores use the intended validation records.\")" ] }, { "cell_type": "markdown", "id": "validation-table-task", "metadata": {}, "source": [ "**Your turn — build the decision table.** Construct and display a DataFrame\n", "named **validation_comparison** for the four main approaches: baseline,\n", "living-area linear, your selected polynomial, and your multiple-feature model.\n", "Include a model description and validation RMSE with units.\n", "\n", "Reuse existing results, including the selected-degree row of polynomial_table.\n", "Build the table yourself. Compare the same validation houses and exclude test\n", "scores. The bathroom-only model was a separate one-feature practice comparison;\n", "the final selection below uses these four main approaches." ] }, { "cell_type": "code", "execution_count": null, "id": "validation-comparison", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: assemble and display validation_comparison from the four existing validation results." ] }, { "cell_type": "markdown", "id": "final-choice-task", "metadata": {}, "source": [ "**Your turn — commit to an approach before testing.** Set `chosen_name` to\n", "`\"baseline\"`, `\"linear\"`, `\"polynomial\"`, or `\"multiple\"`, using the validation\n", "comparison and your preference for simplicity when performance is similar.\n", "\n", "**Your decision:** name the approach and features, quote the relevant\n", "validation RMSE, and explain your choice. Do this before the final test cell.\n", "\n", "_Your answer:_\n", "\n", "**A leakage check:** the previous primer calculated `price_per_sqft` from\n", "`price` and `sqft_living`. Why would using `price_per_sqft` to predict an\n", "unknown sale price be an unfair shortcut, even if it produced an excellent score?\n", "\n", "_Your answer:_" ] }, { "cell_type": "code", "execution_count": null, "id": "final-choice-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: assign chosen_name after writing your validation-based decision." ] }, { "cell_type": "markdown", "id": "final-test-explanation", "metadata": {}, "source": [ "**Supplied final refit and assessment — a new workflow step.** After you choose\n", "an approach, this code keeps its features and degree fixed, makes a fresh copy\n", "with clone, fits on training **plus validation**, and evaluates on reserved test\n", "houses. A mean baseline is refitted on the same combined records.\n", "This new refitting step is provided; you will create the final plot yourself.\n", "\n", "Do not change the model in response to test results and then report another\n", "score on the same set as an independent final assessment." ] }, { "cell_type": "code", "execution_count": null, "id": "final-test-assessment", "metadata": {}, "outputs": [], "source": [ "from sklearn.base import clone\n", "\n", "assert \"validation_comparison\" in globals() and \"chosen_name\" in globals(), \"Complete your comparison table and model choice before testing.\"\n", "choices = {\n", " \"baseline\": (baseline, [\"sqft_living\"]),\n", " \"linear\": (linear_model, [\"sqft_living\"]),\n", " \"polynomial\": (polynomial_models[selected_degree], [\"sqft_living\"]),\n", " \"multiple\": (multi_model, feature_columns),\n", "}\n", "assert chosen_name in choices, \"Use one of the four stated choice names.\"\n", "chosen_model, chosen_features = choices[chosen_name]\n", "final_model = clone(chosen_model)\n", "final_model.fit(development_data[chosen_features], development_data[\"price\"])\n", "final_predictions = final_model.predict(test_data[chosen_features])\n", "\n", "final_baseline = DummyRegressor(strategy=\"mean\")\n", "final_baseline.fit(development_data[[\"sqft_living\"]], development_data[\"price\"])\n", "test_baseline_predictions = final_baseline.predict(test_data[[\"sqft_living\"]])\n", "final_results = pd.DataFrame([\n", " {\"Model\": \"Refitted mean baseline\", \"Test RMSE (USD)\": rmse(test_data[\"price\"], test_baseline_predictions), \"Test R²\": r2_score(test_data[\"price\"], test_baseline_predictions)},\n", " {\"Model\": f\"Chosen: {chosen_name}\", \"Test RMSE (USD)\": rmse(test_data[\"price\"], final_predictions), \"Test R²\": r2_score(test_data[\"price\"], final_predictions)},\n", "])\n", "print(\"Final fit uses\", len(development_data), \"records; assessment uses\", len(test_data), \"test records.\")\n", "print(final_results.round(3).to_string(index=False))" ] }, { "cell_type": "markdown", "id": "test-plot-task", "metadata": {}, "source": [ "**Your turn — visualise the final predictions.** Construct an actual-versus-\n", "predicted scatterplot for the test houses: actual prices on the horizontal\n", "axis, **final_predictions** on the vertical axis. Add a dashed diagonal\n", "for perfect predictions, a title, and axis labels with units.\n", "\n", "Choose a range covering both sets of prices. The diagonal joins points\n", "with equal horizontal and vertical coordinates. Use this plot as evidence\n", "in your reflection; do not use it to choose a different model." ] }, { "cell_type": "code", "execution_count": null, "id": "test-plot-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Your code: plot actual and predicted test prices and add the perfect-prediction diagonal." ] }, { "cell_type": "markdown", "id": "final-reflection", "metadata": {}, "source": [ "## Final reflection and submission\n", "\n", "**Report:** give your chosen features and model, its validation RMSE before\n", "refitting, and its final test RMSE and R² after refitting. Does it improve on\n", "the final mean baseline? The validation and test groups differ, and the final\n", "fit uses more records, so the two error estimates need not match.\n", "\n", "_Your answer:_\n", "\n", "**Reflect:** did greater complexity or more features help in this experiment?\n", "State one limitation of the small historical sample or single random split,\n", "and one reason the model is not a current house-valuation service.\n", "\n", "_Your answer:_\n", "\n", "**Next step:** propose one small, testable extension for an advanced project.\n", "Say what data you would need and how you would compare approaches without\n", "repeatedly using the final test set to choose the winner.\n", "\n", "_Your answer:_\n", "\n", "### Submission checklist\n", "\n", "- Complete all core coding tasks, choices, and written answers; extensions are optional.\n", "- Keep the supplied sample and partitions fixed, and run the supplied checkpoints.\n", "- Explain prediction errors in the correct units; do not call R² an accuracy percentage.\n", "- Make your model choice before running the final test assessment.\n", "- Restart the kernel, run all cells, and save the notebook with your results.\n", "\n", "Correct comparisons and thoughtful explanations matter more than beating a\n", "particular score. Honest evidence that a model did not help is a valid finding." ] }, { "cell_type": "markdown", "id": "optional-reverse-intro", "metadata": {}, "source": [ "## Optional extensions — choose one if you have time\n", "\n", "### A. Reverse the prediction question\n", "\n", "The original direction predicts **price from living area**. Now fit a separate\n", "linear model predicting **living area from price**, using the same training\n", "and validation houses. Use new variable names so the core experiment is unchanged.\n", "\n", "**Your turn:** fit `reverse_model` on `train_data[[\"price\"]]` and target\n", "`train_data[\"sqft_living\"]`, then evaluate its validation RMSE. Adapt the plot\n", "from Exercise 1 with price on the horizontal axis and area on the vertical axis.\n", "Label the RMSE in **square feet**, not USD.\n", "\n", "Reversing the fitted prediction direction is not generally equivalent to\n", "algebraically inverting the first line: least squares minimises errors in\n", "the chosen target direction." ] }, { "cell_type": "code", "execution_count": null, "id": "reverse-your-turn", "metadata": { "tags": [ "student-task" ] }, "outputs": [], "source": [ "# Optional: construct and fit the reverse model, evaluate area predictions, and plot its fitted rule." ] }, { "cell_type": "markdown", "id": "optional-reverse-answer", "metadata": {}, "source": [ "**Your comparison:** what does the reversed model predict, and what is its\n", "validation RMSE with units? Why would comparing that number directly with a\n", "price-prediction RMSE not tell you which task is predicted more successfully?\n", "\n", "_Your answer:_" ] }, { "cell_type": "markdown", "id": "optional-extrapolation-intro", "metadata": {}, "source": [ "### B. Explore predictions beyond the training range\n", "\n", "**Worked experiment:** the inputs below lie outside the training living-area\n", "range. Compare the linear model with the degree-8 polynomial from Exercise 3.\n", "This is **extrapolation**. Predictions far from observed inputs can be unstable,\n", "especially for flexible models; no actual prices are supplied for these examples." ] }, { "cell_type": "code", "execution_count": null, "id": "extrapolation-example", "metadata": {}, "outputs": [], "source": [ "if 8 not in polynomial_models:\n", " print(\"INCOMPLETE: fit the polynomial models before trying this optional experiment.\")\n", "else:\n", " outside_houses = pd.DataFrame({\n", " \"sqft_living\": [0.5 * X_train[\"sqft_living\"].min(), 1.5 * X_train[\"sqft_living\"].max()]\n", " })\n", " outside_comparison = outside_houses.assign(\n", " linear_prediction_USD=linear_model.predict(outside_houses),\n", " degree_8_prediction_USD=polynomial_models[8].predict(outside_houses),\n", " )\n", " print(\"Observed training living-area range:\", X_train[\"sqft_living\"].min(), \"to\", X_train[\"sqft_living\"].max())\n", " print(outside_comparison.round(2).to_string(index=False))" ] }, { "cell_type": "markdown", "id": "optional-extrapolation-answer", "metadata": {}, "source": [ "Describe the predictions and their plausibility. If the two models agree,\n", "would that prove the predictions are reliable? Why can you not calculate an\n", "actual prediction error for these two hypothetical houses from this table?\n", "\n", "_Your answer:_" ] }, { "cell_type": "markdown", "id": "references", "metadata": {}, "source": [ "## Optional reference links\n", "\n", "These explain the supplied tools; you do not need to read them all to complete the primer.\n", "\n", "- [2026 course dataset](http://th-www.if.uj.edu.pl/~erichter/dydaktyka/Dydaktyka2026/DataScience-2026/kc_house_data.csv.zip)\n", "- [LinearRegression](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LinearRegression.html)\n", "- [Data leakage and consistent preprocessing](https://scikit-learn.org/stable/common_pitfalls.html)\n", "- [Model selection and held-out evaluation](https://scikit-learn.org/stable/modules/cross_validation.html)\n", "- [PolynomialFeatures](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.PolynomialFeatures.html)" ] } ], "metadata": { "assignment_version": 2, "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.11.14" } }, "nbformat": 4, "nbformat_minor": 5 }