{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Homework\n",
    "\n",
    "- Use this assignment to practice the chapter's concepts and coding skills in runnable Python code.\n",
    "- Sign in from the account menu before you start working on the assignment.\n",
    "- Write and run your code directly in the coding cells.\n",
    "- Multiple submissions are allowed. The highest score is kept.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "::::{div} assignment-question-card\n",
    "\n",
    "**1. Reading a Coefficient**\n",
    "\n",
    "In a regression of house price (in dollars) on square feet, rooms, and age, the coefficient on square feet is 120. Holding rooms and age fixed, each additional square foot is associated with a \\$120 higher predicted price.\n",
    "\n",
    "<div class=\"tf-options\" role=\"radiogroup\" aria-label=\"Question 1 answer\">\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q1\" value=\"true\"> True</label>\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q1\" value=\"false\"> False</label>\n",
    "</div>\n",
    "\n",
    "::::\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "### Solution\n",
    "# True\n",
    "# A coefficient in multiple regression is the change in the prediction for a one-unit increase in that feature, with the other features held fixed.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "::::{div} assignment-question-card\n",
    "\n",
    "**2. Correlated Predictors**\n",
    "\n",
    "When two predictors are strongly correlated with each other, such as square feet and rooms, their individual coefficients can be unstable and hard to interpret.\n",
    "\n",
    "<div class=\"tf-options\" role=\"radiogroup\" aria-label=\"Question 2 answer\">\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q2\" value=\"true\"> True</label>\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q2\" value=\"false\"> False</label>\n",
    "</div>\n",
    "\n",
    "::::\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "### Solution\n",
    "# True\n",
    "# The model has trouble separating the effects of predictors that move together (multicollinearity).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "::::{div} assignment-question-card\n",
    "\n",
    "**3. Training Error**\n",
    "\n",
    "A model's RMSE on its training data is the best estimate of how accurately it will predict the prices of new houses.\n",
    "\n",
    "<div class=\"tf-options\" role=\"radiogroup\" aria-label=\"Question 3 answer\">\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q3\" value=\"true\"> True</label>\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q3\" value=\"false\"> False</label>\n",
    "</div>\n",
    "\n",
    "::::\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "### Solution\n",
    "# False\n",
    "# Training error is optimistic because the model was fitted to those same houses. Test-set error is the honest estimate.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "::::{div} assignment-question-card\n",
    "\n",
    "**4. More Predictors**\n",
    "\n",
    "Adding more predictors to a regression can never make its predictions on new data worse.\n",
    "\n",
    "<div class=\"tf-options\" role=\"radiogroup\" aria-label=\"Question 4 answer\">\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q4\" value=\"true\"> True</label>\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q4\" value=\"false\"> False</label>\n",
    "</div>\n",
    "\n",
    "::::\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "### Solution\n",
    "# False\n",
    "# Extra predictors always lower training error but can fit noise (overfitting), which makes test-set predictions worse.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "::::{div} assignment-question-card\n",
    "\n",
    "**5. R-squared**\n",
    "\n",
    "A regression has an R-squared of 0.8 on the test set. Its predictions account for 80% of the variation in test-set prices.\n",
    "\n",
    "<div class=\"tf-options\" role=\"radiogroup\" aria-label=\"Question 5 answer\">\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q5\" value=\"true\"> True</label>\n",
    "  <label><input type=\"radio\" name=\"ch13-hw-q5\" value=\"false\"> False</label>\n",
    "</div>\n",
    "\n",
    "::::\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [],
   "source": [
    "### Solution\n",
    "# True\n",
    "# R-squared is 1 - SSE/SST: the share of the variation around the mean that the predictions explain.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### 6. Pricing Three Listings\n",
    "#   1. Fit a LinearRegression model of price on sqft, rooms, and age using homes.\n",
    "#   2. Predict the prices of the three listings, each rounded to 1 decimal.\n",
    "# Print exactly:\n",
    "# Listing 1: 211.5\n",
    "# Listing 2: 352.4\n",
    "# Listing 3: 296.4\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.linear_model import LinearRegression\n",
    "\n",
    "homes = pd.DataFrame({\n",
    "    \"sqft\": [2800, 2700, 2600, 2700, 1300, 2600, 2800, 1600, 1500, 1300, 2600, 2900, 2200, 2300, 2600, 1200],\n",
    "    \"rooms\": [9, 9, 8, 9, 5, 8, 9, 6, 6, 3, 9, 9, 7, 9, 9, 4],\n",
    "    \"age\": [44, 26, 42, 7, 45, 41, 27, 25, 17, 13, 15, 39, 20, 49, 20, 27],\n",
    "    \"price\": [400, 417, 350, 450, 151, 363, 390, 247, 259, 198, 403, 387, 326, 325, 409, 168],   # $1,000s\n",
    "})\n",
    "listings = pd.DataFrame({\n",
    "    \"sqft\": [1500, 2200, 1900],\n",
    "    \"rooms\": [5, 7, 6],\n",
    "    \"age\": [30, 10, 15],\n",
    "})\n",
    "\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "tags": [
     "hide-input",
     "homework-answer"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Listing 1: 211.5\n",
      "Listing 2: 352.4\n",
      "Listing 3: 296.4\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.linear_model import LinearRegression\n",
    "\n",
    "homes = pd.DataFrame({\n",
    "    \"sqft\": [2800, 2700, 2600, 2700, 1300, 2600, 2800, 1600, 1500, 1300, 2600, 2900, 2200, 2300, 2600, 1200],\n",
    "    \"rooms\": [9, 9, 8, 9, 5, 8, 9, 6, 6, 3, 9, 9, 7, 9, 9, 4],\n",
    "    \"age\": [44, 26, 42, 7, 45, 41, 27, 25, 17, 13, 15, 39, 20, 49, 20, 27],\n",
    "    \"price\": [400, 417, 350, 450, 151, 363, 390, 247, 259, 198, 403, 387, 326, 325, 409, 168],   # $1,000s\n",
    "})\n",
    "listings = pd.DataFrame({\n",
    "    \"sqft\": [1500, 2200, 1900],\n",
    "    \"rooms\": [5, 7, 6],\n",
    "    \"age\": [30, 10, 15],\n",
    "})\n",
    "\n",
    "model = LinearRegression().fit(homes[['sqft', 'rooms', 'age']], homes['price'])\n",
    "for number, value in enumerate(model.predict(listings), 1):\n",
    "    print(\"Listing \" + str(number) + \":\", round(value, 1))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### 7. Test-Set Errors\n",
    "# A model's predictions for six test houses are below.\n",
    "#   1. Compute the residuals (actual minus predicted) as a list.\n",
    "#   2. Compute the test RMSE with np.sqrt(mean_squared_error(...)), rounded to 2 decimals.\n",
    "# Print exactly:\n",
    "# Residuals: [-5, 10, -5, 15, -7, -5]\n",
    "# Test RMSE: 8.65\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.metrics import mean_squared_error, r2_score\n",
    "\n",
    "test = pd.DataFrame({\n",
    "    \"actual\": [245, 310, 280, 355, 298, 330],\n",
    "    \"predicted\": [250, 300, 285, 340, 305, 335],\n",
    "})\n",
    "\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "tags": [
     "hide-input",
     "homework-answer"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Residuals: [-5, 10, -5, 15, -7, -5]\n",
      "Test RMSE: 8.65\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.metrics import mean_squared_error, r2_score\n",
    "\n",
    "test = pd.DataFrame({\n",
    "    \"actual\": [245, 310, 280, 355, 298, 330],\n",
    "    \"predicted\": [250, 300, 285, 340, 305, 335],\n",
    "})\n",
    "\n",
    "residuals = test['actual'] - test['predicted']\n",
    "print(\"Residuals:\", residuals.tolist())\n",
    "print(\"Test RMSE:\", round(np.sqrt(mean_squared_error(test['actual'], test['predicted'])), 2))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### 8. Nearest-Neighbor Regression\n",
    "# Predict a house's price from the 3 most similar houses. Both features are\n",
    "# already in standard units.\n",
    "#   1. Compute the distance from new_house to each house (or use\n",
    "#      KNeighborsRegressor(n_neighbors=3) from sklearn.neighbors).\n",
    "#   2. Predict the price as the average price of the 3 nearest houses,\n",
    "#      rounded to 1 decimal.\n",
    "# Print exactly:\n",
    "# Predicted price: 298.3\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "houses = pd.DataFrame({\n",
    "    \"size_su\": [-1.2, -0.5, 0.0, 0.4, 0.9, 1.5, -0.8, 0.2],\n",
    "    \"age_su\": [0.8, -0.3, 0.5, -1.0, 0.2, -0.6, 1.2, -0.2],\n",
    "    \"price\": [210, 265, 280, 330, 350, 410, 230, 300],\n",
    "})\n",
    "new_house = np.array([0.3, -0.4])\n",
    "\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "tags": [
     "hide-input",
     "homework-answer"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicted price: 298.3\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "houses = pd.DataFrame({\n",
    "    \"size_su\": [-1.2, -0.5, 0.0, 0.4, 0.9, 1.5, -0.8, 0.2],\n",
    "    \"age_su\": [0.8, -0.3, 0.5, -1.0, 0.2, -0.6, 1.2, -0.2],\n",
    "    \"price\": [210, 265, 280, 330, 350, 410, 230, 300],\n",
    "})\n",
    "new_house = np.array([0.3, -0.4])\n",
    "\n",
    "features = houses[['size_su', 'age_su']]\n",
    "houses['distance'] = np.sqrt(((features - new_house) ** 2).sum(axis=1))\n",
    "nearest = houses.sort_values('distance').head(3)\n",
    "print(\"Predicted price:\", round(nearest['price'].mean(), 1))\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### 9. Checking Two Predictors\n",
    "#   1. Compute the correlation between square feet and number of bathrooms,\n",
    "#      rounded to 2 decimals.\n",
    "#   2. Flag the pair if the absolute correlation is above 0.8.\n",
    "# Print exactly:\n",
    "# Correlation: 0.83\n",
    "# Multicollinearity flag: True\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "houses = pd.DataFrame({\n",
    "    \"sqft\": [1200, 1500, 1700, 1900, 2200, 2600, 3000],\n",
    "    \"bathrooms\": [1, 2, 2, 2, 3, 2, 4],\n",
    "})\n",
    "\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "tags": [
     "hide-input",
     "homework-answer"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Correlation: 0.83\n",
      "Multicollinearity flag: True\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "houses = pd.DataFrame({\n",
    "    \"sqft\": [1200, 1500, 1700, 1900, 2200, 2600, 3000],\n",
    "    \"bathrooms\": [1, 2, 2, 2, 3, 2, 4],\n",
    "})\n",
    "\n",
    "r = round(houses['sqft'].corr(houses['bathrooms']), 2)\n",
    "print(\"Correlation:\", r)\n",
    "print(\"Multicollinearity flag:\", abs(r) > 0.8)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### 10. Choosing a Model\n",
    "# Three models of increasing complexity were fitted. Their RMSEs (in $1,000s) are below.\n",
    "#   1. Print the model with the lowest TEST RMSE.\n",
    "#   2. For the most complex model C, compute the gap: test RMSE minus training RMSE.\n",
    "# Print exactly:\n",
    "# Best model: B\n",
    "# Gap for C: 12.5\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "models = pd.DataFrame({\n",
    "    \"train_rmse\": [24.0, 18.5, 9.0],\n",
    "    \"test_rmse\": [25.5, 20.0, 21.5],\n",
    "}, index=[\"A\", \"B\", \"C\"])\n",
    "\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "tags": [
     "hide-input",
     "homework-answer"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Best model: B\n",
      "Gap for C: 12.5\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "models = pd.DataFrame({\n",
    "    \"train_rmse\": [24.0, 18.5, 9.0],\n",
    "    \"test_rmse\": [25.5, 20.0, 21.5],\n",
    "}, index=[\"A\", \"B\", \"C\"])\n",
    "\n",
    "print(\"Best model:\", models['test_rmse'].idxmin())\n",
    "print(\"Gap for C:\", models.loc['C', 'test_rmse'] - models.loc['C', 'train_rmse'])\n",
    "# Model C fits the training data best but predicts new houses worse: overfitting.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<style>\n",
    ".ch13-hw-grader { max-width: 900px; margin-top: 1rem; }\n",
    ".ch13-hw-feedback-list { display: grid; gap: 0.45rem; margin: 0 0 1rem; }\n",
    ".ch13-hw-feedback-list[hidden] { display: none; }\n",
    ".ch13-hw-feedback-row { border: 1px solid #d8dee4; border-radius: 6px; padding: 0.55rem 0.65rem; }\n",
    ".ch13-hw-feedback-row strong { margin-right: 0.35rem; }\n",
    ".ch13-hw-feedback-row.correct { border-color: #aceebb; background: #dafbe1; color: #116329; }\n",
    ".ch13-hw-feedback-row.incorrect { border-color: #ffcecb; background: #ffebe9; color: #cf222e; }\n",
    ".ch13-hw-actions { display: flex; gap: 0.75rem; align-items: center; margin: 1rem 0 0; flex-wrap: wrap; }\n",
    ".ch13-hw-actions button { border: 1px solid #176de8; border-radius: 6px; background: #176de8; color: white; padding: 0.55rem 0.9rem; cursor: pointer; font-weight: 600; }\n",
    ".ch13-hw-actions button.secondary { background: white; color: #176de8; }\n",
    ".ch13-hw-actions.is-locked { opacity: 0.55; }\n",
    ".ch13-hw-actions button:disabled { cursor: not-allowed; opacity: 0.65; }\n",
    ".ch13-hw-result { font-weight: 700; }\n",
    "</style>\n",
    "\n",
    "<div class='ch13-hw-grader' id='ch13-hw-grader'>\n",
    "  <div class='ch13-hw-feedback-list' aria-live='polite' hidden>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q1'><strong>Question 1</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q2'><strong>Question 2</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q3'><strong>Question 3</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q4'><strong>Question 4</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q5'><strong>Question 5</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q6'><strong>Question 6</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q7'><strong>Question 7</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q8'><strong>Question 8</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q9'><strong>Question 9</strong><span></span></div>\n",
    "    <div class='ch13-hw-feedback-row' data-question='q10'><strong>Question 10</strong><span></span></div>\n",
    "  </div>\n",
    "  <div class='ch13-hw-actions'>\n",
    "    <button type='button' id='ch13-hw-submit' disabled>Submit</button>\n",
    "    <button type='button' class='secondary' id='ch13-hw-reset'>Clear Results</button>\n",
    "    <span class='ch13-hw-result' id='ch13-hw-result' aria-live='polite'></span>\n",
    "  </div>\n",
    "</div>\n",
    "\n",
    "<script>\n",
    "(function () {\n",
    "  var homeworkId = 'ch13-homework';\n",
    "\n",
    "  function getParams() {\n",
    "    var params = new URLSearchParams(window.location.search);\n",
    "    return {\n",
    "      canvas_course_id: params.get('canvas_course_id') || params.get('course_id') || '',\n",
    "      canvas_assignment_id: params.get('canvas_assignment_id') || params.get('assignment_id') || '',\n",
    "      canvas_user_id: params.get('canvas_user_id') || params.get('user_id') || ''\n",
    "    };\n",
    "  }\n",
    "\n",
    "  function loadSessionIdentity(identity, actions, submit, result) {\n",
    "    submit.disabled = true;\n",
    "    actions.classList.add('is-locked');\n",
    "    result.textContent = 'Sign in from the account menu to submit this assignment.';\n",
    "\n",
    "    return fetch('/api/v1/session.php', { credentials: 'same-origin' })\n",
    "      .then(function (response) { return response.ok ? response.json() : null; })\n",
    "      .then(function (payload) {\n",
    "        if (!payload || !payload.authenticated || !payload.identity) return identity;\n",
    "        identity.canvas_user_id = payload.identity.canvas_user_id || identity.canvas_user_id || '';\n",
    "        identity.student_identifier = payload.identity.student_identifier || '';\n",
    "        submit.disabled = false;\n",
    "        actions.classList.remove('is-locked');\n",
    "        result.textContent = payload.identity.display_name ? 'Signed in as ' + payload.identity.display_name + '.' : 'Signed in.';\n",
    "        return identity;\n",
    "      })\n",
    "      .catch(function () { return identity; });\n",
    "  }\n",
    "\n",
    "  function codeFromCell(cell) {\n",
    "    var cm = cell.querySelector('.CodeMirror');\n",
    "    if (cm && cm.CodeMirror && typeof cm.CodeMirror.getValue === 'function') return cm.CodeMirror.getValue();\n",
    "    var cmContent = cell.querySelector('.cm-content');\n",
    "    if (cmContent && cmContent.cmView && cmContent.cmView.view) return cmContent.cmView.view.state.doc.toString();\n",
    "    var textarea = cell.querySelector('textarea');\n",
    "    if (textarea) return textarea.value;\n",
    "    var pre = cell.querySelector('.cell_input pre');\n",
    "    return pre ? pre.textContent : '';\n",
    "  }\n",
    "\n",
    "  function collectCodeCells() {\n",
    "    var cells = Array.prototype.slice.call(document.querySelectorAll('div.cell.tag_thebe-interactive')).slice(0, 5);\n",
    "    var code = {};\n",
    "    cells.forEach(function (cell, index) {\n",
    "      code['q' + (index + 6)] = codeFromCell(cell);\n",
    "    });\n",
    "    return code;\n",
    "  }\n",
    "\n",
    "  function collectAnswers() {\n",
    "    var answers = {};\n",
    "    ['q1', 'q2', 'q3', 'q4', 'q5'].forEach(function (question) {\n",
    "      var checked = document.querySelector('input[name=\"ch13-hw-' + question + '\"]:checked');\n",
    "      answers[question] = checked ? checked.value : '';\n",
    "    });\n",
    "    return answers;\n",
    "  }\n",
    "\n",
    "  function applyFeedback(grader, feedback) {\n",
    "    var list = grader.querySelector('.ch13-hw-feedback-list');\n",
    "    list.hidden = false;\n",
    "    ['q1', 'q2', 'q3', 'q4', 'q5', 'q6', 'q7', 'q8', 'q9', 'q10'].forEach(function (question) {\n",
    "      var row = grader.querySelector('[data-question=' + question + ']');\n",
    "      var message = row.querySelector('span');\n",
    "      var item = feedback[question];\n",
    "      row.className = 'ch13-hw-feedback-row';\n",
    "      if (!item) return;\n",
    "      message.textContent = item.message + ' (' + item.score + ' / ' + item.max_score + ')';\n",
    "      row.classList.add(item.correct ? 'correct' : 'incorrect');\n",
    "    });\n",
    "  }\n",
    "\n",
    "  function clearFeedback(grader) {\n",
    "    var list = grader.querySelector('.ch13-hw-feedback-list');\n",
    "    list.hidden = true;\n",
    "    grader.querySelectorAll('.ch13-hw-feedback-row').forEach(function (row) {\n",
    "      row.className = 'ch13-hw-feedback-row';\n",
    "      var message = row.querySelector('span');\n",
    "      if (message) message.textContent = '';\n",
    "    });\n",
    "  }\n",
    "\n",
    "  function saveAttempt(payload) {\n",
    "    return fetch('/api/v1/homework-attempts.php', {\n",
    "      method: 'POST',\n",
    "      headers: { 'Content-Type': 'application/json' },\n",
    "      credentials: 'same-origin',\n",
    "      body: JSON.stringify(payload)\n",
    "    }).then(function (response) {\n",
    "      return response.text().then(function (text) {\n",
    "        var payload = text ? JSON.parse(text) : null;\n",
    "        if (!payload || !response.ok || !payload.ok) {\n",
    "          throw new Error((payload && (payload.message || payload.error)) || 'Submission API is unavailable.');\n",
    "        }\n",
    "        return payload;\n",
    "      });\n",
    "    });\n",
    "  }\n",
    "\n",
    "  function initHomeworkGrader() {\n",
    "    var grader = document.getElementById('ch13-hw-grader');\n",
    "    if (!grader || grader.dataset.ready === 'true') return;\n",
    "    grader.dataset.ready = 'true';\n",
    "\n",
    "    var identity = getParams();\n",
    "    var actions = grader.querySelector('.ch13-hw-actions');\n",
    "    var result = document.getElementById('ch13-hw-result');\n",
    "    var submit = document.getElementById('ch13-hw-submit');\n",
    "    var reset = document.getElementById('ch13-hw-reset');\n",
    "\n",
    "    loadSessionIdentity(identity, actions, submit, result);\n",
    "\n",
    "    submit.addEventListener('click', function () {\n",
    "      clearFeedback(grader);\n",
    "      submit.disabled = true;\n",
    "      result.textContent = 'Submitting homework.';\n",
    "\n",
    "      saveAttempt({\n",
    "        homework_id: homeworkId,\n",
    "        answers: collectAnswers(),\n",
    "        code: collectCodeCells(),\n",
    "        canvas_course_id: identity.canvas_course_id,\n",
    "        canvas_assignment_id: identity.canvas_assignment_id,\n",
    "        canvas_user_id: identity.canvas_user_id\n",
    "      })\n",
    "        .then(function (payload) {\n",
    "          var best = (payload.best_score === null || typeof payload.best_score === 'undefined') ? payload.score : payload.best_score;\n",
    "          var attempt = (payload.attempt_count === null || typeof payload.attempt_count === 'undefined') ? '' : ' Attempt ' + payload.attempt_count + '.';\n",
    "          applyFeedback(grader, payload.feedback || {});\n",
    "          result.textContent = 'Submission saved.' + attempt + ' Score: ' + payload.score + ' / ' + payload.max_score + '. Best score: ' + best + ' / ' + payload.max_score + '. Canvas sync: ' + payload.canvas_sync_status + '.';\n",
    "        })\n",
    "        .catch(function (error) {\n",
    "          result.textContent = 'Could not save this attempt: ' + error.message;\n",
    "        })\n",
    "        .finally(function () {\n",
    "          submit.disabled = false;\n",
    "        });\n",
    "    });\n",
    "\n",
    "    reset.addEventListener('click', function () {\n",
    "      clearFeedback(grader);\n",
    "      result.textContent = '';\n",
    "    });\n",
    "  }\n",
    "\n",
    "  if (document.readyState === 'loading') {\n",
    "    document.addEventListener('DOMContentLoaded', initHomeworkGrader);\n",
    "  } else {\n",
    "    initHomeworkGrader();\n",
    "  }\n",
    "})();\n",
    "</script>\n"
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "cell_metadata_filter": "-all",
   "main_language": "python",
   "notebook_metadata_filter": "-all"
  },
  "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.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
