{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Week 4: Interactive Logistic Regression Demonstrations\n",
    "## MPS311/439 Machine Learning\n",
    "\n",
    "This notebook contains interactive visualizations to help you understand:\n",
    "- Why linear regression fails for classification\n",
    "- How the sigmoid function works\n",
    "- Decision boundaries and confidence\n",
    "- Precision-Recall tradeoffs\n",
    "- When logistic regression succeeds and fails"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install required packages (run this cell first in Google Colab)\n",
    "import sys\n",
    "if 'google.colab' in sys.modules:\n",
    "    !pip install ipywidgets -q\n",
    "    from google.colab import output\n",
    "    output.enable_custom_widget_manager()\n",
    "\n",
    "# Import all necessary libraries\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib.colors import ListedColormap\n",
    "import ipywidgets as widgets\n",
    "from IPython.display import display, clear_output\n",
    "from sklearn.linear_model import LogisticRegression, LinearRegression\n",
    "from sklearn.datasets import make_classification, load_breast_cancer\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score, accuracy_score\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')\n",
    "\n",
    "# Set random seed for reproducibility\n",
    "np.random.seed(42)\n",
    "\n",
    "# Set plot style\n",
    "plt.style.use('seaborn-v0_8-darkgrid')\n",
    "\n",
    "print(\"✅ All libraries loaded successfully!\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "## 🎯 Interactive Demo 1: Why Linear Regression Fails for Classification\n",
    "\n",
    "**Key Learning Goal:** See how linear regression produces invalid probabilities (< 0 or > 1) and is sensitive to outliers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def demo_regression_vs_classification(add_outlier=False, outlier_position=10):\n",
    "    \"\"\"\n",
    "    Compare linear regression and logistic regression for binary classification.\n",
    "    \"\"\"\n",
    "    # Generate simple 1D classification data\n",
    "    np.random.seed(42)\n",
    "    X_class0 = np.random.normal(2, 0.5, 30).reshape(-1, 1)\n",
    "    X_class1 = np.random.normal(5, 0.5, 30).reshape(-1, 1)\n",
    "    \n",
    "    X = np.vstack([X_class0, X_class1])\n",
    "    y = np.hstack([np.zeros(30), np.ones(30)])\n",
    "    \n",
    "    # Add outlier if requested\n",
    "    if add_outlier:\n",
    "        X = np.vstack([X, [[outlier_position]]])\n",
    "        y = np.append(y, 1)\n",
    "    \n",
    "    # Fit both models\n",
    "    lin_reg = LinearRegression().fit(X, y)\n",
    "    log_reg = LogisticRegression().fit(X, y)\n",
    "    \n",
    "    # Create prediction space\n",
    "    X_plot = np.linspace(-1, 11, 300).reshape(-1, 1)\n",
    "    y_lin_pred = lin_reg.predict(X_plot)\n",
    "    y_log_pred = log_reg.predict_proba(X_plot)[:, 1]\n",
    "    \n",
    "    # Create figure\n",
    "    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n",
    "    \n",
    "    # Plot 1: Linear Regression\n",
    "    ax1.scatter(X[y==0], y[y==0], c='blue', s=100, alpha=0.6, label='Class 0', edgecolors='k')\n",
    "    ax1.scatter(X[y==1], y[y==1], c='red', s=100, alpha=0.6, label='Class 1', edgecolors='k')\n",
    "    ax1.plot(X_plot, y_lin_pred, 'g-', linewidth=3, label='Linear Regression')\n",
    "    ax1.axhline(y=0, color='gray', linestyle='--', alpha=0.3)\n",
    "    ax1.axhline(y=1, color='gray', linestyle='--', alpha=0.3)\n",
    "    \n",
    "    # Highlight invalid predictions\n",
    "    invalid_below = y_lin_pred < 0\n",
    "    invalid_above = y_lin_pred > 1\n",
    "    ax1.fill_between(X_plot.ravel(), -0.3, 0, where=invalid_below.ravel(), \n",
    "                     alpha=0.3, color='orange', label='Invalid: P < 0')\n",
    "    ax1.fill_between(X_plot.ravel(), 1, 1.3, where=invalid_above.ravel(), \n",
    "                     alpha=0.3, color='orange', label='Invalid: P > 1')\n",
    "    \n",
    "    ax1.set_xlabel('Feature Value (x)', fontsize=12)\n",
    "    ax1.set_ylabel('Prediction', fontsize=12)\n",
    "    ax1.set_title('❌ Linear Regression: Invalid Probabilities', fontsize=14, fontweight='bold')\n",
    "    ax1.legend(loc='best')\n",
    "    ax1.set_ylim([-0.3, 1.3])\n",
    "    ax1.grid(True, alpha=0.3)\n",
    "    \n",
    "    # Plot 2: Logistic Regression\n",
    "    ax2.scatter(X[y==0], y[y==0], c='blue', s=100, alpha=0.6, label='Class 0', edgecolors='k')\n",
    "    ax2.scatter(X[y==1], y[y==1], c='red', s=100, alpha=0.6, label='Class 1', edgecolors='k')\n",
    "    ax2.plot(X_plot, y_log_pred, 'purple', linewidth=3, label='Logistic Regression')\n",
    "    ax2.axhline(y=0.5, color='black', linestyle='--', linewidth=2, alpha=0.5, label='Decision Threshold (0.5)')\n",
    "    ax2.axhline(y=0, color='gray', linestyle='--', alpha=0.3)\n",
    "    ax2.axhline(y=1, color='gray', linestyle='--', alpha=0.3)\n",
    "    \n",
    "    # Highlight valid probability range\n",
    "    ax2.fill_between(X_plot.ravel(), 0, 1, alpha=0.1, color='green', label='Valid: 0 ≤ P ≤ 1')\n",
    "    \n",
    "    ax2.set_xlabel('Feature Value (x)', fontsize=12)\n",
    "    ax2.set_ylabel('P(Class = 1 | x)', fontsize=12)\n",
    "    ax2.set_title('✅ Logistic Regression: Valid Probabilities', fontsize=14, fontweight='bold')\n",
    "    ax2.legend(loc='best')\n",
    "    ax2.set_ylim([-0.3, 1.3])\n",
    "    ax2.grid(True, alpha=0.3)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    # Print analysis\n",
    "    print(\"\\n📊 Analysis:\")\n",
    "    print(f\"  • Linear Regression: {np.sum(invalid_below) + np.sum(invalid_above)} invalid predictions\")\n",
    "    print(f\"  • Logistic Regression: Always outputs valid probabilities [0, 1]\")\n",
    "    if add_outlier:\n",
    "        print(f\"\\n⚠️  With outlier at x={outlier_position}, linear regression is heavily influenced!\")\n",
    "\n",
    "# Create interactive widget\n",
    "widgets.interact(\n",
    "    demo_regression_vs_classification,\n",
    "    add_outlier=widgets.Checkbox(value=False, description='Add Outlier'),\n",
    "    outlier_position=widgets.FloatSlider(min=6, max=12, step=0.5, value=10, \n",
    "                                         description='Outlier X:', \n",
    "                                         style={'description_width': 'initial'})\n",
    ");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🎓 Key Takeaways:**\n",
    "- Linear regression can predict probabilities < 0 or > 1 (impossible!)\n",
    "- Outliers dramatically affect linear regression\n",
    "- Logistic regression always outputs valid probabilities\n",
    "\n",
    "**💡 Try This:**\n",
    "1. Turn on the outlier - see how linear regression breaks!\n",
    "2. Move the outlier further right - watch the chaos!\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 🎯 Interactive Demo 2: Exploring the Sigmoid Function\n",
    "\n",
    "**Key Learning Goal:** Understand how the sigmoid function transforms any number into a valid probability."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def demo_sigmoid_function(slope=1.0, shift=0.0, show_derivative=False):\n",
    "    \"\"\"\n",
    "    Interactive sigmoid function visualization.\n",
    "    \"\"\"\n",
    "    # Define sigmoid function\n",
    "    def sigmoid(z):\n",
    "        return 1 / (1 + np.exp(-z))\n",
    "    \n",
    "    def sigmoid_derivative(z):\n",
    "        s = sigmoid(z)\n",
    "        return s * (1 - s)\n",
    "    \n",
    "    # Generate data\n",
    "    z = np.linspace(-10, 10, 400)\n",
    "    z_transformed = slope * (z - shift)\n",
    "    y = sigmoid(z_transformed)\n",
    "    y_deriv = sigmoid_derivative(z_transformed) * slope\n",
    "    \n",
    "    # Create figure\n",
    "    fig, ax = plt.subplots(figsize=(12, 6))\n",
    "    \n",
    "    # Plot sigmoid\n",
    "    ax.plot(z, y, 'b-', linewidth=3, label=f'σ(z) = 1 / (1 + e^(-{slope}*(z-{shift})))')\n",
    "    \n",
    "    # Plot derivative if requested\n",
    "    if show_derivative:\n",
    "        ax.plot(z, y_deriv, 'r--', linewidth=2, alpha=0.7, label=\"σ'(z) - Derivative\")\n",
    "    \n",
    "    # Add reference lines\n",
    "    ax.axhline(y=0.5, color='black', linestyle='--', linewidth=1.5, alpha=0.5, label='Decision Boundary (0.5)')\n",
    "    ax.axhline(y=0, color='gray', linestyle=':', alpha=0.3)\n",
    "    ax.axhline(y=1, color='gray', linestyle=':', alpha=0.3)\n",
    "    ax.axvline(x=shift, color='green', linestyle=':', alpha=0.5, label=f'Center at z={shift}')\n",
    "    \n",
    "    # Highlight probability regions\n",
    "    ax.fill_between(z, 0, 0.5, where=(y < 0.5), alpha=0.1, color='blue', label='Class 0 Region')\n",
    "    ax.fill_between(z, 0.5, 1, where=(y >= 0.5), alpha=0.1, color='red', label='Class 1 Region')\n",
    "    \n",
    "    # Add annotation for key points\n",
    "    ax.plot(shift, 0.5, 'go', markersize=12, label=f'Center: ({shift:.1f}, 0.5)')\n",
    "    \n",
    "    # Labels and formatting\n",
    "    ax.set_xlabel('z = w₀ + w₁x₁ + w₂x₂ + ... (Linear Combination)', fontsize=13, fontweight='bold')\n",
    "    ax.set_ylabel('σ(z) = P(y=1|x)', fontsize=13, fontweight='bold')\n",
    "    ax.set_title('The Sigmoid Function: Squashing Any Number to [0, 1]', fontsize=15, fontweight='bold')\n",
    "    ax.legend(loc='best', fontsize=10)\n",
    "    ax.grid(True, alpha=0.3)\n",
    "    ax.set_ylim([-0.1, 1.1])\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    # Print key properties\n",
    "    print(\"\\n📊 Sigmoid Properties:\")\n",
    "    print(f\"  • Input range: (-∞, +∞)\")\n",
    "    print(f\"  • Output range: (0, 1) - Always a valid probability!\")\n",
    "    print(f\"  • σ(0) = {sigmoid(0):.3f} (decision point)\")\n",
    "    print(f\"  • σ({shift}) = {sigmoid(0):.3f} (center with current shift)\")\n",
    "    print(f\"  • Current slope (steepness): {slope}x\")\n",
    "    if slope > 1:\n",
    "        print(f\"    → Steeper curve = More confident predictions\")\n",
    "    elif slope < 1:\n",
    "        print(f\"    → Gentler curve = More uncertain predictions\")\n",
    "    print(f\"\\n💡 In logistic regression: z = w₀ + w₁x₁ + w₂x₂ + ...\")\n",
    "    print(f\"   Then we apply sigmoid: P(y=1|x) = σ(z)\")\n",
    "\n",
    "# Create interactive widget\n",
    "widgets.interact(\n",
    "    demo_sigmoid_function,\n",
    "    slope=widgets.FloatSlider(min=0.2, max=3.0, step=0.1, value=1.0, \n",
    "                              description='Slope (Steepness):', \n",
    "                              style={'description_width': 'initial'}),\n",
    "    shift=widgets.FloatSlider(min=-5, max=5, step=0.5, value=0, \n",
    "                              description='Horizontal Shift:', \n",
    "                              style={'description_width': 'initial'}),\n",
    "    show_derivative=widgets.Checkbox(value=False, description='Show Derivative')\n",
    ");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🎓 Key Takeaways:**\n",
    "- Sigmoid takes ANY number and outputs a probability [0, 1]\n",
    "- The center point (σ = 0.5) is where we make our decision\n",
    "- Slope controls confidence: steeper = more confident\n",
    "- Horizontal shift moves the decision boundary\n",
    "\n",
    "**💡 Try This:**\n",
    "1. Increase slope to 3 - see how it becomes more like a step function!\n",
    "2. Shift right/left - see how the decision point moves\n",
    "3. Check \"Show Derivative\" - see where the function changes fastest\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 🎯 Interactive Demo 3: 2D Decision Boundaries\n",
    "\n",
    "**Key Learning Goal:** Visualize how logistic regression creates linear decision boundaries in 2D space."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def demo_decision_boundary(w0=0, w1=1, w2=1, show_probabilities=True):\n",
    "    \"\"\"\n",
    "    Interactive 2D decision boundary visualization.\n",
    "    \"\"\"\n",
    "    # Generate 2D classification data\n",
    "    np.random.seed(42)\n",
    "    X, y = make_classification(n_samples=200, n_features=2, n_redundant=0, \n",
    "                               n_informative=2, n_clusters_per_class=1,\n",
    "                               class_sep=1.5, random_state=42)\n",
    "    \n",
    "    # Create mesh for plotting\n",
    "    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1\n",
    "    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1\n",
    "    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),\n",
    "                         np.linspace(y_min, y_max, 200))\n",
    "    \n",
    "    # Compute probabilities using specified weights\n",
    "    def sigmoid(z):\n",
    "        return 1 / (1 + np.exp(-z))\n",
    "    \n",
    "    z = w0 + w1 * xx + w2 * yy\n",
    "    probs = sigmoid(z)\n",
    "    \n",
    "    # Create figure\n",
    "    fig, ax = plt.subplots(figsize=(10, 8))\n",
    "    \n",
    "    # Plot probability contours if requested\n",
    "    if show_probabilities:\n",
    "        contour_filled = ax.contourf(xx, yy, probs, levels=20, cmap='RdBu_r', alpha=0.6)\n",
    "        cbar = plt.colorbar(contour_filled, ax=ax)\n",
    "        cbar.set_label('P(Class = 1 | x)', fontsize=11, fontweight='bold')\n",
    "    \n",
    "    # Plot decision boundary (P = 0.5)\n",
    "    ax.contour(xx, yy, probs, levels=[0.5], colors='black', linewidths=3)\n",
    "    \n",
    "    # Plot confidence contours\n",
    "    ax.contour(xx, yy, probs, levels=[0.1, 0.3, 0.7, 0.9], colors='gray', \n",
    "               linewidths=1, linestyles='dashed', alpha=0.5)\n",
    "    \n",
    "    # Plot data points\n",
    "    scatter0 = ax.scatter(X[y==0, 0], X[y==0, 1], c='blue', s=80, \n",
    "                          edgecolors='k', linewidths=1.5, alpha=0.8, label='Class 0')\n",
    "    scatter1 = ax.scatter(X[y==1, 0], X[y==1, 1], c='red', s=80, \n",
    "                          edgecolors='k', linewidths=1.5, alpha=0.8, label='Class 1')\n",
    "    \n",
    "    # Add decision boundary equation\n",
    "    equation = f'Decision Boundary: {w0:.1f} + {w1:.1f}·x₁ + {w2:.1f}·x₂ = 0'\n",
    "    ax.text(0.5, 0.02, equation, transform=ax.transAxes, \n",
    "            fontsize=12, fontweight='bold',\n",
    "            bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7),\n",
    "            ha='center')\n",
    "    \n",
    "    ax.set_xlabel('Feature 1 (x₁)', fontsize=12, fontweight='bold')\n",
    "    ax.set_ylabel('Feature 2 (x₂)', fontsize=12, fontweight='bold')\n",
    "    ax.set_title('Logistic Regression: Linear Decision Boundary', fontsize=14, fontweight='bold')\n",
    "    ax.legend(loc='upper right', fontsize=11)\n",
    "    ax.grid(True, alpha=0.3)\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    # Calculate and display statistics\n",
    "    predictions = (probs.ravel() > 0.5).astype(int)\n",
    "    print(\"\\n📊 Model Analysis:\")\n",
    "    print(f\"  • Decision boundary: {w0:.1f} + {w1:.1f}·x₁ + {w2:.1f}·x₂ = 0\")\n",
    "    print(f\"  • Boundary is {'steep' if abs(w2/w1) > 2 else 'gentle' if abs(w2/w1) < 0.5 else 'moderate'}\")\n",
    "    print(f\"\\n💡 Interpretation:\")\n",
    "    if w1 > 0:\n",
    "        print(f\"  • Feature 1 ↑ → Probability of Class 1 ↑\")\n",
    "    else:\n",
    "        print(f\"  • Feature 1 ↑ → Probability of Class 1 ↓\")\n",
    "    if w2 > 0:\n",
    "        print(f\"  • Feature 2 ↑ → Probability of Class 1 ↑\")\n",
    "    else:\n",
    "        print(f\"  • Feature 2 ↑ → Probability of Class 1 ↓\")\n",
    "\n",
    "# Create interactive widget\n",
    "widgets.interact(\n",
    "    demo_decision_boundary,\n",
    "    w0=widgets.FloatSlider(min=-3, max=3, step=0.2, value=0, \n",
    "                           description='w₀ (Intercept):', \n",
    "                           style={'description_width': 'initial'}),\n",
    "    w1=widgets.FloatSlider(min=-3, max=3, step=0.2, value=1, \n",
    "                           description='w₁ (Weight for x₁):', \n",
    "                           style={'description_width': 'initial'}),\n",
    "    w2=widgets.FloatSlider(min=-3, max=3, step=0.2, value=1, \n",
    "                           description='w₂ (Weight for x₂):', \n",
    "                           style={'description_width': 'initial'}),\n",
    "    show_probabilities=widgets.Checkbox(value=True, description='Show Probability Gradient')\n",
    ");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🎓 Key Takeaways:**\n",
    "- Decision boundary is a straight line (linear!)\n",
    "- Points far from boundary = high confidence\n",
    "- Points near boundary = uncertain predictions\n",
    "- Weights control the angle and position of the boundary\n",
    "\n",
    "**💡 Try This:**\n",
    "1. Set w₁=2, w₂=0 - vertical boundary (only x₁ matters)\n",
    "2. Set w₁=0, w₂=2 - horizontal boundary (only x₂ matters)\n",
    "3. Adjust w₀ to shift the boundary without changing angle\n",
    "4. Try w₁=-2 to flip which side predicts which class!\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 🎯 Interactive Demo 4: Threshold Adjustment & Precision-Recall Tradeoff\n",
    "\n",
    "**Key Learning Goal:** Understand the fundamental tradeoff between precision and recall by adjusting the decision threshold."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Load real breast cancer data\n",
    "data = load_breast_cancer()\n",
    "X, y = data.data, data.target\n",
    "\n",
    "# Use only 2 features for visualization\n",
    "X_2d = X[:, [0, 1]]  # mean radius and mean texture\n",
    "X_train, X_test, y_train, y_test = train_test_split(X_2d, y, test_size=0.3, random_state=42)\n",
    "\n",
    "# Standardize features\n",
    "scaler = StandardScaler()\n",
    "X_train_scaled = scaler.fit_transform(X_train)\n",
    "X_test_scaled = scaler.transform(X_test)\n",
    "\n",
    "# Train logistic regression\n",
    "model = LogisticRegression(random_state=42)\n",
    "model.fit(X_train_scaled, y_train)\n",
    "\n",
    "# Get probability predictions\n",
    "y_proba = model.predict_proba(X_test_scaled)[:, 1]\n",
    "\n",
    "print(\"✅ Breast Cancer Model Trained!\")\n",
    "print(f\"   Dataset: {len(X_train)} training samples, {len(X_test)} test samples\")\n",
    "print(f\"   Features: Mean Radius & Mean Texture\")\n",
    "print(f\"   Classes: Malignant (1) vs Benign (0)\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def demo_threshold_tradeoff(threshold=0.5):\n",
    "    \"\"\"\n",
    "    Interactive demonstration of precision-recall tradeoff.\n",
    "    \"\"\"\n",
    "    # Apply threshold\n",
    "    y_pred = (y_proba >= threshold).astype(int)\n",
    "    \n",
    "    # Calculate metrics\n",
    "    cm = confusion_matrix(y_test, y_pred)\n",
    "    tn, fp, fn, tp = cm.ravel()\n",
    "    \n",
    "    accuracy = accuracy_score(y_test, y_pred)\n",
    "    precision = precision_score(y_test, y_pred, zero_division=0)\n",
    "    recall = recall_score(y_test, y_pred, zero_division=0)\n",
    "    f1 = f1_score(y_test, y_pred, zero_division=0)\n",
    "    \n",
    "    # Create figure with 3 subplots\n",
    "    fig = plt.figure(figsize=(16, 5))\n",
    "    \n",
    "    # Subplot 1: Confusion Matrix\n",
    "    ax1 = plt.subplot(1, 3, 1)\n",
    "    im = ax1.imshow(cm, cmap='Blues', alpha=0.8)\n",
    "    \n",
    "    # Add text annotations\n",
    "    for i in range(2):\n",
    "        for j in range(2):\n",
    "            text = ax1.text(j, i, cm[i, j], ha=\"center\", va=\"center\", \n",
    "                           fontsize=24, fontweight='bold',\n",
    "                           color=\"white\" if cm[i, j] > cm.max()/2 else \"black\")\n",
    "    \n",
    "    ax1.set_xticks([0, 1])\n",
    "    ax1.set_yticks([0, 1])\n",
    "    ax1.set_xticklabels(['Predicted\\nBenign (0)', 'Predicted\\nMalignant (1)'], fontsize=10)\n",
    "    ax1.set_yticklabels(['Actual\\nBenign (0)', 'Actual\\nMalignant (1)'], fontsize=10)\n",
    "    ax1.set_title(f'Confusion Matrix\\n(Threshold = {threshold:.2f})', fontsize=13, fontweight='bold')\n",
    "    \n",
    "    # Add labels for each cell\n",
    "    ax1.text(0, -0.5, f'TN={tn}', ha='center', fontsize=9, color='green', fontweight='bold')\n",
    "    ax1.text(1, -0.5, f'FP={fp}', ha='center', fontsize=9, color='red', fontweight='bold')\n",
    "    ax1.text(0, 1.5, f'FN={fn}', ha='center', fontsize=9, color='red', fontweight='bold')\n",
    "    ax1.text(1, 1.5, f'TP={tp}', ha='center', fontsize=9, color='green', fontweight='bold')\n",
    "    \n",
    "    plt.colorbar(im, ax=ax1)\n",
    "    \n",
    "    # Subplot 2: Metrics Bar Chart\n",
    "    ax2 = plt.subplot(1, 3, 2)\n",
    "    metrics = ['Accuracy', 'Precision', 'Recall', 'F1-Score']\n",
    "    values = [accuracy, precision, recall, f1]\n",
    "    colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']\n",
    "    \n",
    "    bars = ax2.barh(metrics, values, color=colors, alpha=0.7, edgecolor='black', linewidth=2)\n",
    "    \n",
    "    # Add value labels on bars\n",
    "    for i, (bar, value) in enumerate(zip(bars, values)):\n",
    "        ax2.text(value + 0.02, i, f'{value:.3f}', va='center', fontweight='bold', fontsize=11)\n",
    "    \n",
    "    ax2.set_xlim([0, 1.1])\n",
    "    ax2.set_xlabel('Score', fontsize=12, fontweight='bold')\n",
    "    ax2.set_title('Performance Metrics', fontsize=13, fontweight='bold')\n",
    "    ax2.grid(axis='x', alpha=0.3)\n",
    "    ax2.axvline(x=0.5, color='gray', linestyle='--', alpha=0.5)\n",
    "    \n",
    "    # Subplot 3: Threshold Analysis\n",
    "    ax3 = plt.subplot(1, 3, 3)\n",
    "    \n",
    "    # Calculate metrics across thresholds\n",
    "    thresholds = np.linspace(0.05, 0.95, 50)\n",
    "    precisions = []\n",
    "    recalls = []\n",
    "    f1_scores = []\n",
    "    \n",
    "    for t in thresholds:\n",
    "        y_pred_t = (y_proba >= t).astype(int)\n",
    "        precisions.append(precision_score(y_test, y_pred_t, zero_division=0))\n",
    "        recalls.append(recall_score(y_test, y_pred_t, zero_division=0))\n",
    "        f1_scores.append(f1_score(y_test, y_pred_t, zero_division=0))\n",
    "    \n",
    "    ax3.plot(thresholds, precisions, 'o-', label='Precision', linewidth=2, markersize=4, color='#ff7f0e')\n",
    "    ax3.plot(thresholds, recalls, 's-', label='Recall', linewidth=2, markersize=4, color='#2ca02c')\n",
    "    ax3.plot(thresholds, f1_scores, '^-', label='F1-Score', linewidth=2, markersize=4, color='#d62728')\n",
    "    \n",
    "    # Highlight current threshold\n",
    "    ax3.axvline(x=threshold, color='black', linestyle='--', linewidth=2, label=f'Current ({threshold:.2f})')\n",
    "    ax3.plot(threshold, precision, 'o', color='#ff7f0e', markersize=12, markeredgecolor='black', markeredgewidth=2)\n",
    "    ax3.plot(threshold, recall, 's', color='#2ca02c', markersize=12, markeredgecolor='black', markeredgewidth=2)\n",
    "    ax3.plot(threshold, f1, '^', color='#d62728', markersize=12, markeredgecolor='black', markeredgewidth=2)\n",
    "    \n",
    "    ax3.set_xlabel('Decision Threshold', fontsize=12, fontweight='bold')\n",
    "    ax3.set_ylabel('Score', fontsize=12, fontweight='bold')\n",
    "    ax3.set_title('Precision-Recall Tradeoff', fontsize=13, fontweight='bold')\n",
    "    ax3.legend(loc='best', fontsize=10)\n",
    "    ax3.grid(True, alpha=0.3)\n",
    "    ax3.set_ylim([0, 1.05])\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    # Print detailed interpretation\n",
    "    print(\"\\n\" + \"=\"*70)\n",
    "    print(f\"📊 ANALYSIS FOR THRESHOLD = {threshold:.2f}\")\n",
    "    print(\"=\"*70)\n",
    "    \n",
    "    print(f\"\\n🔢 Confusion Matrix Breakdown:\")\n",
    "    print(f\"  • True Negatives (TN):  {tn:3d} - Correctly identified benign cases\")\n",
    "    print(f\"  • False Positives (FP): {fp:3d} - Benign cases wrongly flagged as malignant\")\n",
    "    print(f\"  • False Negatives (FN): {fn:3d} - Malignant cases missed (DANGEROUS!)\")\n",
    "    print(f\"  • True Positives (TP):  {tp:3d} - Correctly identified malignant cases\")\n",
    "    \n",
    "    print(f\"\\n📈 Performance Metrics:\")\n",
    "    print(f\"  • Accuracy:  {accuracy:.3f} - Overall correctness\")\n",
    "    print(f\"  • Precision: {precision:.3f} - Of predicted malignant, how many are actually malignant?\")\n",
    "    print(f\"  • Recall:    {recall:.3f} - Of actual malignant, how many did we catch?\")\n",
    "    print(f\"  • F1-Score:  {f1:.3f} - Harmonic mean of precision and recall\")\n",
    "    \n",
    "    print(f\"\\n💡 Clinical Interpretation:\")\n",
    "    if threshold < 0.3:\n",
    "        print(\"  🚨 AGGRESSIVE screening (Low threshold):\")\n",
    "        print(\"     ✓ Catches almost all cancer cases (high recall)\")\n",
    "        print(\"     ✗ Many false alarms (low precision)\")\n",
    "        print(\"     → Good for initial screening where missing cancer is worse than false alarms\")\n",
    "    elif threshold > 0.7:\n",
    "        print(\"  🎯 CONSERVATIVE approach (High threshold):\")\n",
    "        print(\"     ✓ Few false alarms (high precision)\")\n",
    "        print(\"     ✗ Might miss some cancer cases (lower recall)\")\n",
    "        print(\"     → Use when confirmatory tests are expensive or risky\")\n",
    "    else:\n",
    "        print(\"  ⚖️ BALANCED approach (Default threshold):\")\n",
    "        print(\"     Reasonable trade-off between catching cancers and avoiding false alarms\")\n",
    "    \n",
    "    print(f\"\\n🎓 Key Insight:\")\n",
    "    print(\"  There is NO FREE LUNCH! You cannot maximize both precision and recall.\")\n",
    "    print(\"  Choose threshold based on the cost of different types of errors in your application.\")\n",
    "    print(\"=\"*70)\n",
    "\n",
    "# Create interactive widget\n",
    "widgets.interact(\n",
    "    demo_threshold_tradeoff,\n",
    "    threshold=widgets.FloatSlider(min=0.1, max=0.9, step=0.05, value=0.5,\n",
    "                                  description='Decision Threshold:',\n",
    "                                  style={'description_width': 'initial'},\n",
    "                                  readout_format='.2f')\n",
    ");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🎓 Key Takeaways:**\n",
    "- Default threshold (0.5) isn't always optimal!\n",
    "- **Precision vs Recall Tradeoff:**\n",
    "  - Lower threshold → Higher recall (catch more cancers) but lower precision (more false alarms)\n",
    "  - Higher threshold → Higher precision (fewer false alarms) but lower recall (miss some cancers)\n",
    "- **Choice depends on application:**\n",
    "  - Medical screening: Prefer high recall (don't miss diseases)\n",
    "  - Spam filtering: Prefer high precision (real emails must not go to spam)\n",
    "\n",
    "**💡 Try This:**\n",
    "1. Set threshold to 0.2 - see how recall increases but precision drops\n",
    "2. Set threshold to 0.8 - see how precision increases but recall drops\n",
    "3. Find the threshold that maximizes F1-score (balance)\n",
    "4. Imagine you're a doctor: which threshold would you choose? Why?\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 🎯 Interactive Demo 5: When Logistic Regression Fails\n",
    "\n",
    "**Key Learning Goal:** Understand that linear boundaries cannot solve all classification problems."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def demo_linear_vs_nonlinear(dataset_type='Linear', noise_level=0.1):\n",
    "    \"\"\"\n",
    "    Compare logistic regression performance on linearly and non-linearly separable data.\n",
    "    \"\"\"\n",
    "    np.random.seed(42)\n",
    "    \n",
    "    if dataset_type == 'Linear':\n",
    "        # Generate linearly separable data\n",
    "        X, y = make_classification(n_samples=300, n_features=2, n_redundant=0,\n",
    "                                   n_informative=2, n_clusters_per_class=1,\n",
    "                                   class_sep=2.0, flip_y=noise_level*2, random_state=42)\n",
    "        title_suffix = \"Linearly Separable\"\n",
    "    elif dataset_type == 'Circular':\n",
    "        # Generate circular pattern (non-linear)\n",
    "        n_samples = 300\n",
    "        # Inner circle (class 0)\n",
    "        r_inner = np.random.uniform(0, 1.5, n_samples//2)\n",
    "        theta_inner = np.random.uniform(0, 2*np.pi, n_samples//2)\n",
    "        X_inner = np.column_stack([r_inner * np.cos(theta_inner), \n",
    "                                   r_inner * np.sin(theta_inner)])\n",
    "        # Outer circle (class 1)\n",
    "        r_outer = np.random.uniform(2.5, 4, n_samples//2)\n",
    "        theta_outer = np.random.uniform(0, 2*np.pi, n_samples//2)\n",
    "        X_outer = np.column_stack([r_outer * np.cos(theta_outer), \n",
    "                                   r_outer * np.sin(theta_outer)])\n",
    "        X = np.vstack([X_inner, X_outer])\n",
    "        y = np.hstack([np.zeros(n_samples//2), np.ones(n_samples//2)])\n",
    "        # Add noise\n",
    "        X += np.random.normal(0, noise_level*2, X.shape)\n",
    "        title_suffix = \"Circular (Non-Linear)\"\n",
    "    elif dataset_type == 'XOR':\n",
    "        # Generate XOR pattern (non-linear)\n",
    "        n_samples = 300\n",
    "        # Four clusters\n",
    "        X1 = np.random.randn(n_samples//4, 2) * 0.6 + np.array([1.5, 1.5])\n",
    "        X2 = np.random.randn(n_samples//4, 2) * 0.6 + np.array([-1.5, -1.5])\n",
    "        X3 = np.random.randn(n_samples//4, 2) * 0.6 + np.array([1.5, -1.5])\n",
    "        X4 = np.random.randn(n_samples//4, 2) * 0.6 + np.array([-1.5, 1.5])\n",
    "        X = np.vstack([X1, X2, X3, X4])\n",
    "        y = np.hstack([np.ones(n_samples//2), np.zeros(n_samples//2)])\n",
    "        # Add noise\n",
    "        X += np.random.normal(0, noise_level, X.shape)\n",
    "        title_suffix = \"XOR Pattern (Non-Linear)\"\n",
    "    else:  # Moons\n",
    "        from sklearn.datasets import make_moons\n",
    "        X, y = make_moons(n_samples=300, noise=noise_level, random_state=42)\n",
    "        title_suffix = \"Two Moons (Non-Linear)\"\n",
    "    \n",
    "    # Train logistic regression\n",
    "    model = LogisticRegression(random_state=42)\n",
    "    model.fit(X, y)\n",
    "    \n",
    "    # Calculate accuracy\n",
    "    y_pred = model.predict(X)\n",
    "    accuracy = accuracy_score(y, y_pred)\n",
    "    \n",
    "    # Create mesh for decision boundary\n",
    "    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1\n",
    "    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1\n",
    "    xx, yy = np.meshgrid(np.linspace(x_min, x_max, 200),\n",
    "                         np.linspace(y_min, y_max, 200))\n",
    "    \n",
    "    Z = model.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]\n",
    "    Z = Z.reshape(xx.shape)\n",
    "    \n",
    "    # Create figure\n",
    "    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))\n",
    "    \n",
    "    # Plot 1: Data only\n",
    "    ax1.scatter(X[y==0, 0], X[y==0, 1], c='blue', s=60, alpha=0.8, \n",
    "                edgecolors='k', linewidths=1, label='Class 0')\n",
    "    ax1.scatter(X[y==1, 0], X[y==1, 1], c='red', s=60, alpha=0.8, \n",
    "                edgecolors='k', linewidths=1, label='Class 1')\n",
    "    ax1.set_xlabel('Feature 1', fontsize=12, fontweight='bold')\n",
    "    ax1.set_ylabel('Feature 2', fontsize=12, fontweight='bold')\n",
    "    ax1.set_title(f'Dataset: {title_suffix}', fontsize=13, fontweight='bold')\n",
    "    ax1.legend(loc='best')\n",
    "    ax1.grid(True, alpha=0.3)\n",
    "    \n",
    "    # Plot 2: Decision boundary\n",
    "    contour = ax2.contourf(xx, yy, Z, levels=20, cmap='RdBu_r', alpha=0.6)\n",
    "    ax2.contour(xx, yy, Z, levels=[0.5], colors='black', linewidths=3)\n",
    "    \n",
    "    # Plot misclassified points\n",
    "    correct = y_pred == y\n",
    "    ax2.scatter(X[correct & (y==0), 0], X[correct & (y==0), 1], \n",
    "                c='blue', s=60, alpha=0.8, edgecolors='k', linewidths=1, label='Class 0 (Correct)')\n",
    "    ax2.scatter(X[correct & (y==1), 0], X[correct & (y==1), 1], \n",
    "                c='red', s=60, alpha=0.8, edgecolors='k', linewidths=1, label='Class 1 (Correct)')\n",
    "    ax2.scatter(X[~correct, 0], X[~correct, 1], \n",
    "                c='yellow', s=100, alpha=1, edgecolors='red', linewidths=3, \n",
    "                marker='X', label='Misclassified', zorder=5)\n",
    "    \n",
    "    ax2.set_xlabel('Feature 1', fontsize=12, fontweight='bold')\n",
    "    ax2.set_ylabel('Feature 2', fontsize=12, fontweight='bold')\n",
    "    ax2.set_title(f'Logistic Regression Boundary | Accuracy: {accuracy:.1%}', \n",
    "                  fontsize=13, fontweight='bold')\n",
    "    ax2.legend(loc='best')\n",
    "    ax2.grid(True, alpha=0.3)\n",
    "    \n",
    "    plt.colorbar(contour, ax=ax2, label='P(Class = 1)')\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    # Print analysis\n",
    "    print(\"\\n\" + \"=\"*70)\n",
    "    print(f\"📊 PERFORMANCE ANALYSIS: {title_suffix}\")\n",
    "    print(\"=\"*70)\n",
    "    print(f\"\\n  • Accuracy: {accuracy:.1%}\")\n",
    "    print(f\"  • Misclassified points: {np.sum(~correct)} out of {len(y)}\")\n",
    "    \n",
    "    if dataset_type == 'Linear':\n",
    "        print(\"\\n  ✅ SUCCESS! Linear boundary works well for linearly separable data.\")\n",
    "        print(\"     The straight line effectively separates the two classes.\")\n",
    "    else:\n",
    "        print(\"\\n  ❌ FAILURE! Linear boundary cannot capture the non-linear pattern.\")\n",
    "        print(\"     A straight line is fundamentally insufficient for this problem.\")\n",
    "        print(\"\\n  💡 Solutions:\")\n",
    "        print(\"     1. Feature Engineering: Add polynomial features (x₁², x₂², x₁·x₂)\")\n",
    "        print(\"     2. Use non-linear models: QDA, Decision Trees, Neural Networks\")\n",
    "        print(\"     3. Kernel methods: SVM with RBF kernel\")\n",
    "    \n",
    "    print(\"=\"*70)\n",
    "\n",
    "# Create interactive widget\n",
    "widgets.interact(\n",
    "    demo_linear_vs_nonlinear,\n",
    "    dataset_type=widgets.Dropdown(\n",
    "        options=['Linear', 'Circular', 'XOR', 'Moons'],\n",
    "        value='Linear',\n",
    "        description='Dataset Type:',\n",
    "        style={'description_width': 'initial'}\n",
    "    ),\n",
    "    noise_level=widgets.FloatSlider(\n",
    "        min=0.0, max=0.5, step=0.05, value=0.1,\n",
    "        description='Noise Level:',\n",
    "        style={'description_width': 'initial'}\n",
    "    )\n",
    ");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🎓 Key Takeaways:**\n",
    "- Logistic regression draws a **straight line** decision boundary\n",
    "- Works great for linearly separable data\n",
    "- **Fails on non-linear patterns** (circles, XOR, moons)\n",
    "- Need different approaches for complex patterns:\n",
    "  - Feature engineering (polynomial features)\n",
    "  - Non-linear models (QDA, trees, neural networks)\n",
    "\n",
    "**💡 Try This:**\n",
    "1. Start with 'Linear' - see the success (high accuracy)\n",
    "2. Switch to 'Circular' - watch it struggle (low accuracy)\n",
    "3. Try 'XOR' - even worse! A straight line can't separate opposite corners\n",
    "4. Try 'Moons' - see the challenge of curved boundaries\n",
    "5. Increase noise - see how performance degrades\n",
    "\n",
    "**🔮 Preview Week 5:** Next week we'll learn about QDA, which can handle curved boundaries!\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 🎯 Bonus Demo: Loss Function Visualization\n",
    "\n",
    "**Key Learning Goal:** Understand why we use cross-entropy loss instead of squared error."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def demo_loss_functions(show_both=True):\n",
    "    \"\"\"\n",
    "    Visualize cross-entropy vs squared error loss.\n",
    "    \"\"\"\n",
    "    # Generate predictions from 0 to 1\n",
    "    predictions = np.linspace(0.01, 0.99, 100)\n",
    "    \n",
    "    # Calculate losses for true label = 1\n",
    "    cross_entropy_y1 = -np.log(predictions)\n",
    "    squared_error_y1 = (1 - predictions) ** 2\n",
    "    \n",
    "    # Calculate losses for true label = 0\n",
    "    cross_entropy_y0 = -np.log(1 - predictions)\n",
    "    squared_error_y0 = (0 - predictions) ** 2\n",
    "    \n",
    "    # Create figure\n",
    "    fig, axes = plt.subplots(1, 2, figsize=(15, 5))\n",
    "    \n",
    "    # Plot 1: When true label = 1\n",
    "    ax1 = axes[0]\n",
    "    ax1.plot(predictions, cross_entropy_y1, 'b-', linewidth=3, label='Cross-Entropy Loss')\n",
    "    if show_both:\n",
    "        ax1.plot(predictions, squared_error_y1, 'r--', linewidth=3, label='Squared Error Loss')\n",
    "    \n",
    "    # Highlight key regions\n",
    "    ax1.axvline(x=0.5, color='gray', linestyle=':', alpha=0.5)\n",
    "    ax1.fill_between(predictions, 0, 10, where=(predictions < 0.5), \n",
    "                     alpha=0.1, color='red', label='Wrong Prediction Region')\n",
    "    ax1.fill_between(predictions, 0, 10, where=(predictions >= 0.5), \n",
    "                     alpha=0.1, color='green', label='Correct Prediction Region')\n",
    "    \n",
    "    # Add annotations\n",
    "    ax1.annotate('Confident &\\nWrong\\n→ HUGE Penalty!', \n",
    "                xy=(0.1, cross_entropy_y1[9]), xytext=(0.15, 6),\n",
    "                arrowprops=dict(arrowstyle='->', lw=2, color='red'),\n",
    "                fontsize=11, fontweight='bold', color='red',\n",
    "                bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7))\n",
    "    \n",
    "    ax1.annotate('Confident &\\nCorrect\\n→ Small Penalty', \n",
    "                xy=(0.95, cross_entropy_y1[-5]), xytext=(0.7, 2),\n",
    "                arrowprops=dict(arrowstyle='->', lw=2, color='green'),\n",
    "                fontsize=11, fontweight='bold', color='green',\n",
    "                bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))\n",
    "    \n",
    "    ax1.set_xlabel('Predicted Probability P(y=1)', fontsize=12, fontweight='bold')\n",
    "    ax1.set_ylabel('Loss', fontsize=12, fontweight='bold')\n",
    "    ax1.set_title('Loss When True Label = 1', fontsize=13, fontweight='bold')\n",
    "    ax1.legend(loc='upper right', fontsize=10)\n",
    "    ax1.grid(True, alpha=0.3)\n",
    "    ax1.set_ylim([0, 8])\n",
    "    \n",
    "    # Plot 2: When true label = 0\n",
    "    ax2 = axes[1]\n",
    "    ax2.plot(predictions, cross_entropy_y0, 'b-', linewidth=3, label='Cross-Entropy Loss')\n",
    "    if show_both:\n",
    "        ax2.plot(predictions, squared_error_y0, 'r--', linewidth=3, label='Squared Error Loss')\n",
    "    \n",
    "    # Highlight key regions\n",
    "    ax2.axvline(x=0.5, color='gray', linestyle=':', alpha=0.5)\n",
    "    ax2.fill_between(predictions, 0, 10, where=(predictions > 0.5), \n",
    "                     alpha=0.1, color='red', label='Wrong Prediction Region')\n",
    "    ax2.fill_between(predictions, 0, 10, where=(predictions <= 0.5), \n",
    "                     alpha=0.1, color='green', label='Correct Prediction Region')\n",
    "    \n",
    "    # Add annotations\n",
    "    ax2.annotate('Confident &\\nWrong\\n→ HUGE Penalty!', \n",
    "                xy=(0.9, cross_entropy_y0[-10]), xytext=(0.65, 6),\n",
    "                arrowprops=dict(arrowstyle='->', lw=2, color='red'),\n",
    "                fontsize=11, fontweight='bold', color='red',\n",
    "                bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7))\n",
    "    \n",
    "    ax2.annotate('Confident &\\nCorrect\\n→ Small Penalty', \n",
    "                xy=(0.05, cross_entropy_y0[4]), xytext=(0.25, 2),\n",
    "                arrowprops=dict(arrowstyle='->', lw=2, color='green'),\n",
    "                fontsize=11, fontweight='bold', color='green',\n",
    "                bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.7))\n",
    "    \n",
    "    ax2.set_xlabel('Predicted Probability P(y=1)', fontsize=12, fontweight='bold')\n",
    "    ax2.set_ylabel('Loss', fontsize=12, fontweight='bold')\n",
    "    ax2.set_title('Loss When True Label = 0', fontsize=13, fontweight='bold')\n",
    "    ax2.legend(loc='upper left', fontsize=10)\n",
    "    ax2.grid(True, alpha=0.3)\n",
    "    ax2.set_ylim([0, 8])\n",
    "    \n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "    \n",
    "    # Print explanation\n",
    "    print(\"\\n\" + \"=\"*70)\n",
    "    print(\"📚 WHY CROSS-ENTROPY LOSS?\")\n",
    "    print(\"=\"*70)\n",
    "    print(\"\\n✅ Cross-Entropy Advantages:\")\n",
    "    print(\"  1. Heavily penalizes confident wrong predictions\")\n",
    "    print(\"     → Model learns to be cautious when uncertain\")\n",
    "    print(\"  2. Convex optimization landscape with sigmoid\")\n",
    "    print(\"     → Gradient descent finds global optimum\")\n",
    "    print(\"  3. Encourages well-calibrated probabilities\")\n",
    "    print(\"     → Predicted probabilities match actual frequencies\")\n",
    "    \n",
    "    print(\"\\n❌ Squared Error Problems:\")\n",
    "    print(\"  1. Milder penalty for confident wrong predictions\")\n",
    "    print(\"     → Model not punished enough for mistakes\")\n",
    "    print(\"  2. Non-convex with sigmoid (multiple local minima)\")\n",
    "    print(\"     → Gradient descent might get stuck\")\n",
    "    print(\"  3. Designed for regression, not classification\")\n",
    "    \n",
    "    print(\"\\n🎯 Key Insight:\")\n",
    "    print(\"  Cross-entropy grows exponentially as confidence in wrong answer increases.\")\n",
    "    print(\"  This strong penalty forces the model to learn better decision boundaries!\")\n",
    "    print(\"=\"*70)\n",
    "\n",
    "# Create interactive widget\n",
    "widgets.interact(\n",
    "    demo_loss_functions,\n",
    "    show_both=widgets.Checkbox(value=True, description='Compare with Squared Error')\n",
    ");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**🎓 Key Takeaways:**\n",
    "- **Cross-entropy loss** heavily penalizes confident wrong predictions\n",
    "- Creates a convex optimization problem (good for training!)\n",
    "- Encourages well-calibrated probabilities\n",
    "- Much better than squared error for classification\n",
    "\n",
    "**💡 Try This:**\n",
    "1. Uncheck \"Compare with Squared Error\" to focus on cross-entropy alone\n",
    "2. Notice how the penalty explodes when confident and wrong!\n",
    "\n",
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 🎉 Summary: Key Concepts Covered\n",
    "\n",
    "### What We Learned Today:\n",
    "\n",
    "1. **Why Linear Regression Fails** ❌\n",
    "   - Produces invalid probabilities (< 0 or > 1)\n",
    "   - Sensitive to outliers\n",
    "   - Doesn't respect probability constraints\n",
    "\n",
    "2. **The Sigmoid Function** 📈\n",
    "   - Transforms any number to valid probability [0, 1]\n",
    "   - S-shaped curve\n",
    "   - σ(z) = 1 / (1 + e^(-z))\n",
    "\n",
    "3. **Logistic Regression Model** 🎯\n",
    "   - Linear combination: z = w₀ + w₁x₁ + w₂x₂ + ...\n",
    "   - Apply sigmoid: P(y=1|x) = σ(z)\n",
    "   - Decision: Predict 1 if P > 0.5, else 0\n",
    "\n",
    "4. **Decision Boundaries** 📐\n",
    "   - Always a straight line (linear!)\n",
    "   - Points far from boundary = confident\n",
    "   - Points near boundary = uncertain\n",
    "\n",
    "5. **Precision vs Recall Tradeoff** ⚖️\n",
    "   - **Precision**: Of predicted positives, how many are correct?\n",
    "   - **Recall**: Of actual positives, how many did we catch?\n",
    "   - Can't maximize both - must choose based on application!\n",
    "   - Threshold adjustment controls the tradeoff\n",
    "\n",
    "6. **Limitations** ⚠️\n",
    "   - Only works for linearly separable data\n",
    "   - Fails on non-linear patterns (circles, XOR, etc.)\n",
    "   - Solution: Feature engineering or non-linear models\n",
    "\n",
    "7. **Cross-Entropy Loss** 📊\n",
    "   - Better than squared error for classification\n",
    "   - Heavily penalizes confident wrong predictions\n",
    "   - Creates convex optimization landscape\n",
    "\n",
    "### 🚀 Next Week Preview:\n",
    "**Discriminant Analysis (LDA/QDA)**\n",
    "- Handle non-linear decision boundaries\n",
    "- Understand probabilistic classification\n",
    "- When to use LDA vs QDA vs Logistic Regression\n",
    "\n",
    "---\n",
    "\n",
    "## 📚 Practice Exercises\n",
    "\n",
    "1. **Go back to Demo 4**: Try to find the threshold that maximizes F1-score for the cancer dataset\n",
    "\n",
    "2. **Experiment with Demo 3**: \n",
    "   - Can you position the boundary to separate the two classes perfectly?\n",
    "   - What combination of w₀, w₁, w₂ works best?\n",
    "\n",
    "3. **Think About Your Own Problem**:\n",
    "   - If you were building a fraud detection system, would you prefer high precision or high recall? Why?\n",
    "   - What about a disease screening test?\n",
    "   - What about a recommendation system?\n",
    "\n",
    "4. **Challenge**: Load your own dataset and apply logistic regression!\n",
    "   - Use `sklearn.datasets.load_*` to load a built-in dataset\n",
    "   - Or upload your own CSV file\n",
    "   - Try different thresholds and see how metrics change\n",
    "\n",
    "---\n",
    "\n",
    "### 💬 Questions?\n",
    "Feel free to experiment with all the interactive demos above! Try different parameter values and see what happens. The best way to learn is by doing! 🎓"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.8.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
