{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.x"
  },
  "colab": {
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Lab 11 - Differential Privacy 🔒📉\n",
    "### CIS 350: AI for Cybersecurity - Smart Defense for the Digital Business\n",
    "\n",
    "**Worth:** 50 points  ·  **Estimated time:** ~60 minutes  ·  **Submit:** this `.ipynb` in Canvas\n",
    "**Required software:** Google Colab (or Jupyter) with `scikit-learn`, `numpy`, `matplotlib` (preinstalled in Colab).\n",
    "**Prerequisites:** Weeks 2-4 (features, train/test, accuracy) and Week 13 (privacy-preserving ML).\n",
    "\n",
    "## The idea\n",
    "**Differential privacy (DP)** protects individuals by adding carefully-tuned random **noise**. You still learn useful patterns, but no single person's data changes the result much. A knob called the **privacy budget epsilon** controls it: **small epsilon = more noise = more privacy** (but lower accuracy). In this lab you add **Laplace noise** to the training data at several epsilons and **measure the privacy vs. accuracy trade-off** - reproducing the Week 13 lecture curve.\n",
    "\n",
    "## Learning objectives\n",
    "By the end you can: (1) train a baseline model; (2) add DP noise scaled by sensitivity/epsilon; (3) sweep epsilon and measure accuracy; (4) plot and interpret the trade-off; (5) choose a reasonable epsilon.\n",
    "\n",
    "Run cells top to bottom with **Shift + Enter** and fill every **`TODO`** blank (`____`).\n",
    "\n",
    "> **Academic integrity:** individual or pair work. **AI tools are not permitted** on labs."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 1 · Train a baseline model (no privacy)\n",
    "First, how accurate is the model with NO noise? This is our 'no privacy' ceiling."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")   # keep output clean\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from sklearn.datasets import make_classification\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "# A synthetic \"customer risk\" dataset (same as the Week 13 lecture figure)\n",
    "X, y = make_classification(n_samples=3000, n_features=6, n_informative=5, n_redundant=0,\n",
    "                           class_sep=1.9, random_state=0)\n",
    "Xs = StandardScaler().fit_transform(X)                 # put features on a comparable scale\n",
    "Xtr, Xte, ytr, yte = train_test_split(Xs, y, test_size=0.3, random_state=0)\n",
    "\n",
    "CLIP = 2.0                 # clip each feature to [-2, 2] so one record can't swing it too far\n",
    "SENSITIVITY = 2 * CLIP     # the per-feature range after clipping (how much one person can change a value)\n",
    "\n",
    "base = LogisticRegression(max_iter=1000).fit(Xtr, ytr)\n",
    "# TODO 1: baseline test accuracy (true test labels, model predictions on Xte).\n",
    "base_acc = accuracy_score(yte, base.predict(____))\n",
    "print(\"baseline accuracy (no privacy):\", round(base_acc, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 2 · A differentially-private training function\n",
    "DP adds **Laplace noise** to the (clipped) training features. The noise `scale` is **sensitivity / epsilon** - smaller epsilon means a bigger scale, i.e. more noise and more privacy."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def train_with_dp(epsilon, trials=25):\n",
    "    \"\"\"Differential privacy by INPUT PERTURBATION: add Laplace noise to the training\n",
    "    features, then train. Averages several noisy runs so the curve is smooth.\"\"\"\n",
    "    Xtr_clip = np.clip(Xtr, -CLIP, CLIP)\n",
    "    # TODO 2: the Laplace noise scale for differential privacy is sensitivity / epsilon.\n",
    "    scale = SENSITIVITY / ____\n",
    "    accs = []\n",
    "    for t in range(trials):\n",
    "        rng = np.random.default_rng(97 * t + int(epsilon * 100))   # reproducible noise\n",
    "        # TODO 3: add Laplace noise (mean 0, the scale above) shaped like the training data.\n",
    "        noisy = Xtr_clip + rng.laplace(0.0, ____, Xtr_clip.shape)\n",
    "        model = LogisticRegression(max_iter=1000).fit(noisy, ytr)\n",
    "        accs.append(accuracy_score(yte, model.predict(Xte)))\n",
    "    return float(np.mean(accs))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 3 · Try two privacy budgets\n",
    "Compare a **strong-privacy** setting (small epsilon) with a **weak-privacy** setting (large epsilon)."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "print(\"epsilon=0.1 (strong privacy):\", round(train_with_dp(0.1), 3))\n",
    "print(\"epsilon=8.0 (weak privacy):  \", round(train_with_dp(8.0), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 4 · Sweep epsilon and measure accuracy\n",
    "Now measure accuracy across a range of privacy budgets."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "epsilons = [0.1, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0, 16.0]\n",
    "# TODO 4: build a list of accuracies by calling train_with_dp on each epsilon.\n",
    "accs = [train_with_dp(e) for e in ____]\n",
    "for e, a in zip(epsilons, accs):\n",
    "    print(\"epsilon =\", e, \" accuracy =\", round(a, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 5 · Plot the privacy vs. accuracy trade-off\n",
    "A log-scale x-axis makes the trade-off easy to see."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "plt.figure(figsize=(7, 4.5))\n",
    "plt.plot(epsilons, accs, \"-o\", label=\"DP model\")\n",
    "plt.axhline(base_acc, ls=\"--\", color=\"orange\", label=\"no privacy (baseline)\")\n",
    "plt.axhline(0.5, ls=\":\", color=\"red\", label=\"random guessing\")\n",
    "plt.xscale(\"log\"); plt.xticks(epsilons, [str(e) for e in epsilons])\n",
    "plt.xlabel(\"privacy budget epsilon (smaller = more privacy)\")\n",
    "plt.ylabel(\"test accuracy\"); plt.ylim(0.45, 1.0)\n",
    "plt.title(\"Privacy vs. accuracy trade-off\"); plt.legend(); plt.grid(alpha=0.3); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 6 · Choose a reasonable epsilon\n",
    "Find the **smallest** epsilon (most privacy) that still keeps accuracy within **0.05** of the baseline."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# TODO 5: smallest epsilon whose accuracy is >= base_acc - 0.05.\n",
    "good = [e for e, a in zip(epsilons, accs) if a >= base_acc - ____]\n",
    "best_eps = min(good)\n",
    "print(\"smallest epsilon within 0.05 of baseline:\", best_eps)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Part 7 · Reflection questions\n",
    "Double-click and replace each *your answer here*. This is where most of the credit is."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Your answers**\n",
    "\n",
    "1. **What was the baseline accuracy, and what accuracy did epsilon=0.1 give? Why so different?** *your answer here*\n",
    "2. **In your own words, what does epsilon control, and which direction is 'more private'?** *your answer here*\n",
    "3. **Describe the privacy vs. accuracy trade-off you see in the plot.** *your answer here*\n",
    "4. **Which epsilon did Part 6 choose, and why is it a reasonable business compromise?** *your answer here*\n",
    "5. **Name one other privacy-preserving technique from lecture (federated learning or encrypted computation) and when you'd use it instead of (or with) DP.** *your answer here*"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ✅ Before you submit\n",
    "- [ ] **Runtime -> Run all** with no errors.  - [ ] All `TODO` blanks filled.\n",
    "- [ ] All 5 reflection questions answered.  - [ ] **File -> Download -> Download .ipynb**, upload to Canvas.\n",
    "\n",
    "## Troubleshooting\n",
    "- `NameError` -> you skipped a cell; run from the top with **Run all**.\n",
    "- Accuracy is ~0.5 at every epsilon -> check TODO 2 (`scale = SENSITIVITY / epsilon`, not `* epsilon`).\n",
    "- The curve isn't smooth -> that's OK; noise is random. Increase `trials` to smooth it further.\n",
    "- Numbers slightly different -> tiny variation across scikit-learn versions is fine (±0.01).\n",
    "\n",
    "## Extension (optional, ungraded)\n",
    "Add noise to the LABELS instead of the features (randomly flip a fraction of `ytr`). How does the trade-off compare? Which feels more private, and why?"
   ]
  }
 ]
}