{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "354b7bee",
   "metadata": {},
   "source": [
    "# A1 · L5 — More than one thing matters\n",
    "\n",
    "Radio and newspaper join the model. R² jumps — and one of the three channels turns\n",
    "out to have been doing nothing at all.\n",
    "\n",
    "Every figure and number this notebook produces is what the lesson prints."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "f485b651",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:01.348722Z",
     "iopub.status.busy": "2026-08-19T09:12:01.348547Z",
     "iopub.status.idle": "2026-08-19T09:12:01.625846Z",
     "shell.execute_reply": "2026-08-19T09:12:01.625017Z"
    }
   },
   "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": "89232f98",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:01.629768Z",
     "iopub.status.busy": "2026-08-19T09:12:01.629434Z",
     "iopub.status.idle": "2026-08-19T09:12:01.845124Z",
     "shell.execute_reply": "2026-08-19T09:12:01.844890Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tv_spend         alone: coefficient +0.0475   R2 0.612\n",
      "radio_spend      alone: coefficient +0.2025   R2 0.332\n",
      "newspaper_spend  alone: coefficient +0.0547   R2 0.052\n"
     ]
    }
   ],
   "source": [
    "#| caption: One channel at a time, then all three\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "df = pd.read_csv(CSV)\n",
    "y = df['sales']\n",
    "\n",
    "def fit(columns):\n",
    "    \"\"\"Least squares on any set of columns. Returns coefficients and R².\"\"\"\n",
    "    X = np.column_stack([np.ones(len(df))] + [df[c] for c in columns])\n",
    "    coef, *_ = np.linalg.lstsq(X, y, rcond=None)\n",
    "    residual = y - X @ coef\n",
    "    r2 = 1 - (residual ** 2).sum() / ((y - y.mean()) ** 2).sum()\n",
    "    return dict(zip(['intercept'] + columns, coef)), float(r2)\n",
    "\n",
    "for channel in ['tv_spend', 'radio_spend', 'newspaper_spend']:\n",
    "    coef, r2 = fit([channel])\n",
    "    print(f'{channel:16s} alone: coefficient {coef[channel]:+.4f}   R2 {r2:.3f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "13d980d8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:01.846326Z",
     "iopub.status.busy": "2026-08-19T09:12:01.846243Z",
     "iopub.status.idle": "2026-08-19T09:12:01.848751Z",
     "shell.execute_reply": "2026-08-19T09:12:01.848538Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tv_spend         alone +0.0475   with the others +0.0458\n",
      "radio_spend      alone +0.2025   with the others +0.1885\n",
      "newspaper_spend  alone +0.0547   with the others -0.0010\n",
      "\n",
      "R2 with all three: 0.897\n"
     ]
    }
   ],
   "source": [
    "#| caption: All three together — and what happens to newspaper\n",
    "CHANNELS = ['tv_spend', 'radio_spend', 'newspaper_spend']\n",
    "full, r2_full = fit(CHANNELS)\n",
    "\n",
    "for channel in CHANNELS:\n",
    "    alone = fit([channel])[0][channel]\n",
    "    print(f'{channel:16s} alone {alone:+.4f}   with the others {full[channel]:+.4f}')\n",
    "print(f'\\nR2 with all three: {r2_full:.3f}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "c9c9f260",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:01.849728Z",
     "iopub.status.busy": "2026-08-19T09:12:01.849666Z",
     "iopub.status.idle": "2026-08-19T09:12:01.916889Z",
     "shell.execute_reply": "2026-08-19T09:12:01.916659Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "alone_tv = 0.0475\n",
      "joint_tv = 0.0458\n",
      "alone_radio = 0.2025\n",
      "joint_radio = 0.1885\n",
      "alone_newspaper = 0.0547\n",
      "joint_newspaper = -0.001\n",
      "intercept_full = 2.94\n",
      "r2_full = 0.897\n",
      "figure coefficients -> more-than-one-thing-matters.coefficients.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "# hide — metrics + figure\n",
    "for channel in CHANNELS:\n",
    "    short = channel.replace('_spend', '')\n",
    "    record(f'alone_{short}', round(float(fit([channel])[0][channel]), 4))\n",
    "    record(f'joint_{short}', round(float(full[channel]), 4))\n",
    "record('intercept_full', round(float(full['intercept']), 2))\n",
    "record('r2_full', round(r2_full, 3))\n",
    "\n",
    "def plot(ax):\n",
    "    labels = ['TV', 'Radio', 'Newspaper']\n",
    "    alone = [fit([c])[0][c] for c in CHANNELS]\n",
    "    joint = [full[c] for c in CHANNELS]\n",
    "    pos = np.arange(len(labels))\n",
    "    ax.bar(pos - 0.19, alone, width=0.38, label='fitted alone')\n",
    "    ax.bar(pos + 0.19, joint, width=0.38, label='fitted with the others')\n",
    "    ax.axhline(0, linewidth=1, color='#8a8a94')\n",
    "    ax.set_xticks(pos, labels)\n",
    "    ax.set_ylabel('Extra sales per $1,000 (thousands of units)')\n",
    "    ax.legend(frameon=False, fontsize=9)\n",
    "\n",
    "save_fig('coefficients', plot, figsize=(7, 4.2))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9df26d1",
   "metadata": {},
   "source": [
    "## 1. What each channel adds\n",
    "\n",
    "Build the model up one channel at a time and watch R²."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "260b5d68",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:01.918057Z",
     "iopub.status.busy": "2026-08-19T09:12:01.917987Z",
     "iopub.status.idle": "2026-08-19T09:12:01.920613Z",
     "shell.execute_reply": "2026-08-19T09:12:01.920418Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1 channel(s): R2 0.612   (+0.612)\n",
      "2 channel(s): R2 0.897   (+0.285)\n",
      "3 channel(s): R2 0.897   (+0.000)\n"
     ]
    }
   ],
   "source": [
    "#| caption: R² as each channel joins\n",
    "steps = [['tv_spend'],\n",
    "         ['tv_spend', 'radio_spend'],\n",
    "         ['tv_spend', 'radio_spend', 'newspaper_spend']]\n",
    "\n",
    "previous = 0.0\n",
    "for columns in steps:\n",
    "    _, r2 = fit(columns)\n",
    "    print(f'{len(columns)} channel(s): R2 {r2:.3f}   (+{r2 - previous:.3f})')\n",
    "    previous = r2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "d3615455",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:01.921598Z",
     "iopub.status.busy": "2026-08-19T09:12:01.921524Z",
     "iopub.status.idle": "2026-08-19T09:12:01.965996Z",
     "shell.execute_reply": "2026-08-19T09:12:01.965740Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "r2_tv = 0.612\n",
      "r2_tv_radio = 0.897\n",
      "gain_radio = 0.285\n",
      "gain_newspaper = 0.0\n",
      "figure r2-ladder -> more-than-one-thing-matters.r2-ladder.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "# hide — metrics + figure\n",
    "r2s = [fit(columns)[1] for columns in steps]\n",
    "record('r2_tv', round(r2s[0], 3))\n",
    "record('r2_tv_radio', round(r2s[1], 3))\n",
    "record('gain_radio', round(r2s[1] - r2s[0], 3))\n",
    "record('gain_newspaper', round(r2s[2] - r2s[1], 4))\n",
    "\n",
    "def plot(ax):\n",
    "    labels = ['TV', '+ Radio', '+ Newspaper']\n",
    "    ax.bar(labels, r2s)\n",
    "    for i, value in enumerate(r2s):\n",
    "        ax.annotate(f'{value:.3f}', xy=(i, value), xytext=(0, 4),\n",
    "                    textcoords='offset points', ha='center', fontsize=9)\n",
    "    ax.set_ylim(0, 1)\n",
    "    ax.set_ylabel('R² — share of variation explained')\n",
    "\n",
    "save_fig('r2-ladder', plot, figsize=(7, 3.6))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc8c1f66",
   "metadata": {},
   "source": [
    "## 2. Why newspaper looked like it worked\n",
    "\n",
    "Plot the two budgets against each other."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "ced016f2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:01.967116Z",
     "iopub.status.busy": "2026-08-19T09:12:01.967020Z",
     "iopub.status.idle": "2026-08-19T09:12:01.969182Z",
     "shell.execute_reply": "2026-08-19T09:12:01.968981Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "newspaper vs radio: +0.35\n",
      "newspaper vs TV:    +0.06\n"
     ]
    }
   ],
   "source": [
    "#| caption: Newspaper and radio budgets move together\n",
    "print(f\"newspaper vs radio: {df['newspaper_spend'].corr(df['radio_spend']):+.2f}\")\n",
    "print(f\"newspaper vs TV:    {df['newspaper_spend'].corr(df['tv_spend']):+.2f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "107fe397",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:01.970177Z",
     "iopub.status.busy": "2026-08-19T09:12:01.970093Z",
     "iopub.status.idle": "2026-08-19T09:12:02.022440Z",
     "shell.execute_reply": "2026-08-19T09:12:02.022176Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "corr_news_radio = 0.35\n",
      "corr_news_tv = 0.06\n",
      "figure newspaper-vs-radio -> more-than-one-thing-matters.newspaper-vs-radio.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "# hide — metrics + figure\n",
    "record('corr_news_radio', round(float(df['newspaper_spend'].corr(df['radio_spend'])), 2))\n",
    "record('corr_news_tv', round(float(df['newspaper_spend'].corr(df['tv_spend'])), 2))\n",
    "\n",
    "def plot(ax):\n",
    "    ax.scatter(df['radio_spend'], df['newspaper_spend'], s=22, alpha=0.7)\n",
    "    b, a = np.polyfit(df['radio_spend'], df['newspaper_spend'], 1)\n",
    "    grid = np.linspace(df['radio_spend'].min(), df['radio_spend'].max(), 30)\n",
    "    ax.plot(grid, a + b * grid, linewidth=1.8)\n",
    "    ax.set_xlabel('Radio budget ($ thousands)')\n",
    "    ax.set_ylabel('Newspaper budget ($ thousands)')\n",
    "\n",
    "save_fig('newspaper-vs-radio', plot, figsize=(7, 4.0))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "51cd20c4",
   "metadata": {},
   "source": [
    "## 3. What the money is buying\n",
    "\n",
    "Split predicted sales in the average market into base and each channel's contribution\n",
    "— the decomposition an MMM report is built on."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "9e06d449",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:02.023594Z",
     "iopub.status.busy": "2026-08-19T09:12:02.023526Z",
     "iopub.status.idle": "2026-08-19T09:12:02.026111Z",
     "shell.execute_reply": "2026-08-19T09:12:02.025926Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "base                   2.94k units\n",
      "tv_spend               6.73k units   ($147k spent)\n",
      "radio_spend            4.39k units   ($23k spent)\n",
      "newspaper_spend       -0.03k units   ($31k spent)\n",
      "predicted total       14.02k units\n"
     ]
    }
   ],
   "source": [
    "#| caption: Contribution in the average market\n",
    "means = df[CHANNELS].mean()\n",
    "contribution = {c: full[c] * means[c] for c in CHANNELS}\n",
    "total = full['intercept'] + sum(contribution.values())\n",
    "\n",
    "print(f\"base                 {full['intercept']:6.2f}k units\")\n",
    "for channel, amount in contribution.items():\n",
    "    print(f'{channel:20s} {amount:6.2f}k units   (${means[channel]:.0f}k spent)')\n",
    "print(f'predicted total      {total:6.2f}k units')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "9d3c6f44",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-19T09:12:02.027092Z",
     "iopub.status.busy": "2026-08-19T09:12:02.027027Z",
     "iopub.status.idle": "2026-08-19T09:12:02.083272Z",
     "shell.execute_reply": "2026-08-19T09:12:02.083022Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "contrib_tv = 6.73\n",
      "contrib_radio = 4.39\n",
      "contrib_newspaper = -0.03\n",
      "predicted_avg_market = 14.02\n",
      "newspaper_budget = 30.6\n",
      "radio_budget = 23.3\n",
      "radio_max_seen = 49.6\n",
      "radio_if_moved = 53.8\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure contribution -> more-than-one-thing-matters.contribution.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "# hide — metrics + figure\n",
    "record('contrib_tv', round(float(contribution['tv_spend']), 2))\n",
    "record('contrib_radio', round(float(contribution['radio_spend']), 2))\n",
    "record('contrib_newspaper', round(float(contribution['newspaper_spend']), 2))\n",
    "record('predicted_avg_market', round(float(total), 2))\n",
    "record('newspaper_budget', round(float(means['newspaper_spend']), 1))\n",
    "record('radio_budget', round(float(means['radio_spend']), 1))\n",
    "record('radio_max_seen', round(float(df['radio_spend'].max()), 1))\n",
    "record('radio_if_moved', round(float(means['radio_spend'] + means['newspaper_spend']), 1))\n",
    "\n",
    "def plot(ax):\n",
    "    parts = [('base', full['intercept'])] + [\n",
    "        ('TV', contribution['tv_spend']),\n",
    "        ('Radio', contribution['radio_spend']),\n",
    "        ('Newspaper', contribution['newspaper_spend']),\n",
    "    ]\n",
    "    left = 0.0\n",
    "    for label, value in parts:\n",
    "        ax.barh(['average market'], [value], left=left, label=f'{label} ({value:.2f}k)')\n",
    "        left += value\n",
    "    ax.set_xlabel('Predicted sales (thousands of units)')\n",
    "    ax.legend(frameon=False, fontsize=9, ncol=2)\n",
    "\n",
    "save_fig('contribution', plot, figsize=(7, 3.2))"
   ]
  }
 ],
 "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
}
