{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  },
  "colab": {
   "name": "week-05-practice.ipynb",
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# Week 05: Validation, thresholds, precision, and recall\n\nMachine Learning in Engineering Science | NCKU + LAiMM | Fall 2026\n\n**Purpose:** a fully worked, self-paced in-class practice with an optional extension. Weeks 11 and 18 are optional examination-readiness materials. These notebooks do not create weekly graded assignments.\n\n**Before running:** choose a Python 3 kernel in Jupyter or a CPU runtime in Colab. Run every cell from top to bottom with Shift+Enter. Setup installs missing free packages. No API key or external dataset download is required.\n\n**Learning target:** complete the calculation, inspect intermediate values, and explain why the protocol supports a limited claim.\n\n**Time:** about 25-45 minutes including reading, prediction, execution, and explain-back.\n",
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 1. Set up and record the environment\n\nKeep the seed fixed for the first run. Record actual outputs and package versions rather than copying a claimed performance number.\n",
   "id": "cell-001"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "metadata": {},
   "source": "import sys, subprocess, importlib.util, platform, json, random, hashlib\nfrom pathlib import Path\nrequired = {\"numpy\": \"numpy\", \"matplotlib\": \"matplotlib\", \"sklearn\": \"scikit-learn\"}\nmissing = [package for module, package in required.items() if importlib.util.find_spec(module) is None]\nif missing:\n    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", *missing])\nimport numpy as np\nimport matplotlib.pyplot as plt\nSEED = 2026\nrandom.seed(SEED)\nnp.random.seed(SEED)\nrng = np.random.default_rng(SEED)\nARTIFACTS = Path(\"artifacts\")\nARTIFACTS.mkdir(exist_ok=True)\nprint(\"Python:\", platform.python_version(), \"NumPy:\", np.__version__, \"Seed:\", SEED)\n",
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Checkpoint.** The environment cell must finish before any analysis cell. Restart and run all if a variable is missing.\n",
   "id": "cell-003"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 2. Reserve validation and test separately\n\nClass 1 denotes the less frequent synthetic event. Select a decision threshold using validation only; preserve test for one final comparison.\n",
   "id": "cell-004"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "metadata": {},
   "source": "from sklearn.datasets import make_classification\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import precision_score,recall_score,f1_score,confusion_matrix\nX,y=make_classification(n_samples=800,n_features=8,n_informative=4,weights=[.85,.15],random_state=SEED)\nXtr,Xrest,ytr,yrest=train_test_split(X,y,test_size=.4,stratify=y,random_state=SEED)\nXva,Xte,yva,yte=train_test_split(Xrest,yrest,test_size=.5,stratify=yrest,random_state=SEED)\nmodel=LogisticRegression(max_iter=300).fit(Xtr,ytr)\npv=model.predict_proba(Xva)[:,1]\n",
   "id": "cell-005"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Checkpoint.** There are 480 training, 160 validation, and 160 test rows. No threshold has been selected using test outcomes.\n",
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 3. Select a threshold on validation\n\nFor illustration choose the validation threshold with highest F1 among a fixed grid. A real safety specification may instead demand a minimum recall and account for uncertainty.\n",
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "metadata": {},
   "source": "thresholds=np.linspace(.05,.95,19)\nscores=np.array([f1_score(yva,pv>=t,zero_division=0) for t in thresholds])\nthreshold=float(thresholds[scores.argmax()])\nplt.plot(thresholds,scores,\"o-\"); plt.xlabel(\"Decision threshold\"); plt.ylabel(\"Validation F1\"); plt.show()\nprint(\"Selected threshold:\",threshold)\n",
   "id": "cell-008"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Checkpoint.** The chosen number is specific to this dataset and protocol. It is not a universal optimal threshold.\n",
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 4. Explain every confusion-matrix cell\n\nRecall is TP/(TP+FN), also called sensitivity. Specificity is TN/(TN+FP), and precision is TP/(TP+FP). If the denominator is zero, the metric is undefined; software zero_division=0 is a reporting convention.\n",
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "metadata": {},
   "source": "pt=model.predict_proba(Xte)[:,1]; predicted=(pt>=threshold)\ntn,fp,fn,tp=confusion_matrix(yte,predicted,labels=[0,1]).ravel()\nsummary={\"threshold\":threshold,\"TP\":int(tp),\"FP\":int(fp),\"FN\":int(fn),\"TN\":int(tn),\n         \"precision\":float(precision_score(yte,predicted,zero_division=0)),\n         \"recall_sensitivity\":float(recall_score(yte,predicted,zero_division=0)),\n         \"specificity\":float(tn/(tn+fp)),\"F1\":float(f1_score(yte,predicted,zero_division=0))}\nprint(summary)\n",
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Checkpoint.** Identify a false negative and a false positive in words. State which cost your application should care about and why.\n",
   "id": "cell-012"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 6. Preserve your evidence\n\nExport a JSON record with a seed, package version, and measured result. Download the artifacts directory before leaving Colab; runtime storage is temporary.\n",
   "id": "cell-013"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "outputs": [],
   "metadata": {},
   "source": "record={\"week\":5,\"seed\":SEED,\"numpy\":np.__version__,\"python\":platform.python_version(),\"result\":summary}\n(ARTIFACTS/\"week-05-evidence.json\").write_text(json.dumps(record,indent=2))\nprint(json.dumps(record,indent=2))\n",
   "id": "cell-014"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**Checkpoint.** The evidence file must match the result you actually obtained. A saved screenshot alone does not permit rerunning the calculation.\n",
   "id": "cell-015"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 7. Try one change and explain it\n1. Predict the effect of one change before editing code. Change one parameter, input, or boundary condition only.\n2. Rerun all cells in a fresh kernel. Keep the original evidence record and compare the observed difference.\n3. Explain the result using the mechanism, not just the final number.\n4. Name one assumption that limits the claim, and a new test that could challenge it.\n\n**Troubleshooting:** if installation fails, restore network access; if a variable is undefined, run earlier cells; if your output differs, compare package versions, seed, and data construction. Do not repeatedly edit code until a preferred score appears.\n\n**Completion criterion:** one executed notebook, one evidence JSON, and a brief claim/evidence/limitation statement.\n\n### Reference documentation\n- [NumPy user guide](https://numpy.org/doc/stable/user/)\n- [scikit-learn user guide](https://scikit-learn.org/stable/user_guide.html)\n- [Course repository](https://github.com/CHjonnyYu/MLES-Fall-2026)\n",
   "id": "cell-016"
  }
 ]
}