{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "26826e61",
   "metadata": {},
   "source": [
    "# A1 · L3 — The line\n",
    "\n",
    "One channel, one line. What least squares actually minimises, and what the two\n",
    "numbers it returns mean to a marketer: **base sales** and **incremental sales per\n",
    "$1,000 of TV**.\n",
    "\n",
    "Every figure and number this notebook produces is what the lesson prints."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "85667a5a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:06.914936Z",
     "iopub.status.busy": "2026-08-19T09:12:06.914671Z",
     "iopub.status.idle": "2026-08-19T09:12:07.172569Z",
     "shell.execute_reply": "2026-08-19T09:12:07.171783Z"
    }
   },
   "outputs": [],
   "source": [
    "# hide — plumbing so this file runs both in the repo and in Colab.\n",
    "try:\n",
    "    from _figkit import save_fig, record\n",
    "except ImportError:  # Colab — no repo, no problem\n",
    "    def save_fig(name, plot, **kw):\n",
    "        import matplotlib.pyplot as plt\n",
    "        fig, ax = plt.subplots(figsize=kw.get('figsize', (7, 4.2))); plot(ax); plt.show()\n",
    "    def record(key, value):\n",
    "        print(f'{key} = {value}'); return value\n",
    "\n",
    "from pathlib import Path\n",
    "REL = 'public/datasets/a1-regression/advertising-media-mix.csv'\n",
    "here = Path.cwd()\n",
    "LOCAL = next((p / REL for p in [here, *here.parents] if (p / REL).exists()), None)\n",
    "CSV = LOCAL or 'https://raw.githubusercontent.com/Dr-Shashank-S-Sharma/expedify-ai-courses/main/datasets/a1-regression/advertising-media-mix.csv'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "1339065b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.175966Z",
     "iopub.status.busy": "2026-08-19T09:12:07.175585Z",
     "iopub.status.idle": "2026-08-19T09:12:07.386758Z",
     "shell.execute_reply": "2026-08-19T09:12:07.386542Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>tv_spend</th>\n",
       "      <th>sales</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>230.1</td>\n",
       "      <td>22.1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>44.5</td>\n",
       "      <td>10.4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>17.2</td>\n",
       "      <td>9.3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>151.5</td>\n",
       "      <td>18.5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>180.8</td>\n",
       "      <td>12.9</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   tv_spend  sales\n",
       "0     230.1   22.1\n",
       "1      44.5   10.4\n",
       "2      17.2    9.3\n",
       "3     151.5   18.5\n",
       "4     180.8   12.9"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#| caption: Load the data\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "df = pd.read_csv(CSV)\n",
    "x = df['tv_spend']     # TV budget, $ thousands\n",
    "y = df['sales']        # sales, thousands of units\n",
    "\n",
    "df[['tv_spend', 'sales']].head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4acd5415",
   "metadata": {},
   "source": [
    "## 1. Any line has an error. Measure it.\n",
    "\n",
    "Before finding the best line, be able to score *a* line. Guess two, score both."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "4f9d7d0a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.387940Z",
     "iopub.status.busy": "2026-08-19T09:12:07.387856Z",
     "iopub.status.idle": "2026-08-19T09:12:07.390482Z",
     "shell.execute_reply": "2026-08-19T09:12:07.390280Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "intercept 14.02  slope 0.0000  ->  SSE 5,417\n",
      "intercept  4.00  slope 0.0900  ->  SSE 6,810\n",
      "intercept  7.00  slope 0.0475  ->  SSE 2,103\n"
     ]
    }
   ],
   "source": [
    "#| caption: Score a line by its total squared error\n",
    "def sse(intercept, slope):\n",
    "    \"\"\"Sum of squared errors: how wrong this line is, over every market.\"\"\"\n",
    "    predicted = intercept + slope * x\n",
    "    return float(((y - predicted) ** 2).sum())\n",
    "\n",
    "GUESSES = [(y.mean(), 0.0), (4.0, 0.09), (7.0, 0.0475)]\n",
    "for intercept, slope in GUESSES:\n",
    "    print(f'intercept {intercept:5.2f}  slope {slope:.4f}  ->  SSE {sse(intercept, slope):,.0f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "73ee80ed",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.391385Z",
     "iopub.status.busy": "2026-08-19T09:12:07.391299Z",
     "iopub.status.idle": "2026-08-19T09:12:07.481438Z",
     "shell.execute_reply": "2026-08-19T09:12:07.481174Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sse_flat = 5,417\n",
      "sse_steep = 6,810\n",
      "figure three-lines -> the-line.three-lines.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "# hide — figure only\n",
    "record('sse_flat', f'{sse(y.mean(), 0.0):,.0f}')\n",
    "record('sse_steep', f'{sse(4.0, 0.09):,.0f}')\n",
    "\n",
    "def plot(ax):\n",
    "    ax.scatter(x, y, s=18, alpha=0.45)\n",
    "    grid = np.linspace(x.min(), x.max(), 50)\n",
    "    styles = [(':', 'flat: ignore TV'), ('--', 'too steep'), ('-', 'least squares')]\n",
    "    for (intercept, slope), (ls, label) in zip(GUESSES, styles):\n",
    "        ax.plot(grid, intercept + slope * grid, ls, linewidth=1.9,\n",
    "                label=f'{label} — SSE {sse(intercept, slope):,.0f}')\n",
    "    ax.set_xlabel('TV budget ($ thousands)')\n",
    "    ax.set_ylabel('Sales (thousands of units)')\n",
    "    ax.legend(frameon=False, fontsize=9)\n",
    "\n",
    "save_fig('three-lines', plot, figsize=(7, 4.6))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a346c580",
   "metadata": {},
   "source": [
    "## 2. Every slope has an error. Plot the whole curve.\n",
    "\n",
    "Sweep the slope, hold the best intercept for each, and the error traces a bowl with\n",
    "exactly one bottom. That bowl is why there is a single right answer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "d4a36cc2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.484011Z",
     "iopub.status.busy": "2026-08-19T09:12:07.483849Z",
     "iopub.status.idle": "2026-08-19T09:12:07.504716Z",
     "shell.execute_reply": "2026-08-19T09:12:07.504030Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "lowest error at slope ~ 0.0477\n"
     ]
    }
   ],
   "source": [
    "#| caption: The error curve, and its one bottom\n",
    "slopes = np.linspace(0.0, 0.10, 200)\n",
    "errors = [sse(y.mean() - s * x.mean(), s) for s in slopes]   # best intercept for each slope\n",
    "best = slopes[int(np.argmin(errors))]\n",
    "print(f'lowest error at slope ~ {best:.4f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2cf7ae2a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.506883Z",
     "iopub.status.busy": "2026-08-19T09:12:07.506657Z",
     "iopub.status.idle": "2026-08-19T09:12:07.579903Z",
     "shell.execute_reply": "2026-08-19T09:12:07.579655Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure error-curve -> the-line.error-curve.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "# hide — figure only\n",
    "def plot(ax):\n",
    "    ax.plot(slopes, errors)\n",
    "    ax.axvline(best, linestyle='--', linewidth=1.2, color='#8a8a94')\n",
    "    ax.annotate('the answer', xy=(best, min(errors)), xytext=(10, 26),\n",
    "                textcoords='offset points', fontsize=9, color='#8a8a94')\n",
    "    ax.set_xlabel('Slope — extra sales per $1,000 of TV')\n",
    "    ax.set_ylabel('Total squared error')\n",
    "\n",
    "save_fig('error-curve', plot, figsize=(7, 3.8))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9a846e8d",
   "metadata": {},
   "source": [
    "## 3. Fit it properly\n",
    "\n",
    "`polyfit` finds the bottom of that bowl exactly, without searching."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "613e045b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.581070Z",
     "iopub.status.busy": "2026-08-19T09:12:07.581001Z",
     "iopub.status.idle": "2026-08-19T09:12:07.583609Z",
     "shell.execute_reply": "2026-08-19T09:12:07.583348Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "base sales   (intercept) = 7.03 thousand units\n",
      "incremental  (slope)     = 0.0475 thousand units per $1,000 of TV\n",
      "R2                       = 0.612\n"
     ]
    }
   ],
   "source": [
    "#| caption: Fit the line\n",
    "slope, intercept = np.polyfit(x, y, 1)\n",
    "predicted = intercept + slope * x\n",
    "r2 = 1 - ((y - predicted) ** 2).sum() / ((y - y.mean()) ** 2).sum()\n",
    "\n",
    "print(f'base sales   (intercept) = {intercept:.2f} thousand units')\n",
    "print(f'incremental  (slope)     = {slope:.4f} thousand units per $1,000 of TV')\n",
    "print(f'R2                       = {r2:.3f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "dfdad9bc",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.584679Z",
     "iopub.status.busy": "2026-08-19T09:12:07.584593Z",
     "iopub.status.idle": "2026-08-19T09:12:07.660891Z",
     "shell.execute_reply": "2026-08-19T09:12:07.660290Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "slope = 0.0475\n",
      "intercept = 7.03\n",
      "r2 = 0.612\n",
      "sse_best = 2,103\n",
      "units_per_100k = 4.8\n",
      "units_per_1k = 47.5\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure fitted-line -> the-line.fitted-line.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "# hide — figure + metrics\n",
    "record('slope', round(float(slope), 4))\n",
    "record('intercept', round(float(intercept), 2))\n",
    "record('r2', round(float(r2), 3))\n",
    "record('sse_best', f'{sse(intercept, slope):,.0f}')\n",
    "record('units_per_100k', round(float(slope) * 100, 1))\n",
    "record('units_per_1k', round(float(slope) * 1000, 1))\n",
    "\n",
    "def plot(ax):\n",
    "    ax.scatter(x, y, s=20, alpha=0.55, label='markets')\n",
    "    grid = np.linspace(0, x.max(), 50)\n",
    "    ax.plot(grid, intercept + slope * grid, linewidth=2, label='fitted line')\n",
    "    ax.axhline(intercept, linestyle=':', linewidth=1.2, color='#8a8a94')\n",
    "    ax.annotate('base sales', xy=(2, intercept), xytext=(0, 8),\n",
    "                textcoords='offset points', fontsize=9, color='#8a8a94')\n",
    "    ax.set_xlabel('TV budget ($ thousands)')\n",
    "    ax.set_ylabel('Sales (thousands of units)')\n",
    "    ax.legend(frameon=False, fontsize=9)\n",
    "\n",
    "save_fig('fitted-line', plot, figsize=(7, 4.4))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de504760",
   "metadata": {},
   "source": [
    "## 4. The only chart a marketer needs\n",
    "\n",
    "Split predicted sales into what you'd have sold anyway and what the budget bought."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "78b70296",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.662435Z",
     "iopub.status.busy": "2026-08-19T09:12:07.662339Z",
     "iopub.status.idle": "2026-08-19T09:12:07.664060Z",
     "shell.execute_reply": "2026-08-19T09:12:07.663867Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "$  0k TV ->  7.03k units (7.03 base + 0.00 incremental)\n",
      "$ 50k TV ->  9.41k units (7.03 base + 2.38 incremental)\n",
      "$100k TV -> 11.79k units (7.03 base + 4.75 incremental)\n",
      "$200k TV -> 16.54k units (7.03 base + 9.51 incremental)\n"
     ]
    }
   ],
   "source": [
    "#| caption: Base vs incremental at four budgets\n",
    "budgets = [0, 50, 100, 200]\n",
    "for budget in budgets:\n",
    "    incremental = slope * budget\n",
    "    print(f'${budget:3d}k TV -> {intercept + incremental:5.2f}k units '\n",
    "          f'({intercept:.2f} base + {incremental:.2f} incremental)')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "bf7d1131",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:07.664941Z",
     "iopub.status.busy": "2026-08-19T09:12:07.664871Z",
     "iopub.status.idle": "2026-08-19T09:12:07.729371Z",
     "shell.execute_reply": "2026-08-19T09:12:07.729128Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "pred_at_100 = 11.79\n",
      "incremental_at_100 = 4.75\n",
      "base_share_at_100 = 60%\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure base-vs-incremental -> the-line.base-vs-incremental.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "# hide — figure + metrics\n",
    "record('pred_at_100', round(float(intercept + slope * 100), 2))\n",
    "record('incremental_at_100', round(float(slope * 100), 2))\n",
    "record('base_share_at_100', f'{100 * intercept / (intercept + slope * 100):.0f}%')\n",
    "\n",
    "def plot(ax):\n",
    "    labels = [f'${b}k' for b in budgets]\n",
    "    base = [intercept] * len(budgets)\n",
    "    inc = [slope * b for b in budgets]\n",
    "    ax.bar(labels, base, label='base sales')\n",
    "    ax.bar(labels, inc, bottom=base, label='incremental — what TV bought')\n",
    "    ax.set_xlabel('TV budget')\n",
    "    ax.set_ylabel('Predicted sales (thousands of units)')\n",
    "    ax.legend(frameon=False, fontsize=9)\n",
    "\n",
    "save_fig('base-vs-incremental', plot, figsize=(7, 4.0))"
   ]
  }
 ],
 "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.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
