{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "colab": {
   "provenance": [],
   "toc_visible": true
  },
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "cells": [
  {
   "id": "m13-l1-0",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Lesson 13.1 · When SUTVA Fails: Spillovers and Exposure Mappings\n",
    "\n",
    "*Accelerated Foundation in Applied Causal Inference*\n",
    "\n",
    "**By the end of this lesson you can:**\n",
    "\n",
    "- Recognise interference between units in health, agriculture, education and labour markets\n",
    "- Simulate a farmer network where treatment spills over to neighbours\n",
    "- Define an exposure mapping and separate direct from spillover effects\n",
    "- Show that ignoring spillovers biases the naive treatment–control comparison\n",
    "\n",
    "> Read the full explanations on the course website, then run each cell here in order (**Runtime → Run all** works on a fresh runtime)."
   ]
  },
  {
   "id": "m13-l1-1",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Every method so far assumed SUTVA"
   ]
  },
  {
   "id": "m13-l1-2",
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "!pip install -q networkx numpy pandas statsmodels matplotlib\n",
    "import numpy as np, pandas as pd, networkx as nx\n",
    "import statsmodels.formula.api as smf\n",
    "import matplotlib.pyplot as plt\n",
    "rng = np.random.default_rng(101)"
   ]
  },
  {
   "id": "m13-l1-3",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## A fertiliser subsidy in a farmer network"
   ]
  },
  {
   "id": "m13-l1-4",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Question it answers:** What does the fertiliser subsidy do to yields, for recipients and for their neighbours?"
   ]
  },
  {
   "id": "m13-l1-5",
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "N = 500\n",
    "G = nx.watts_strogatz_graph(n=N, k=6, p=0.1, seed=1)\n",
    "treatment = rng.binomial(1, 0.3, N)\n",
    "A = nx.to_numpy_array(G)                                      # adjacency matrix\n",
    "degree = A.sum(axis=1)\n",
    "peer_exposure = (A @ treatment) / degree                      # share of neighbours treated\n",
    "\n",
    "yields = rng.normal(100, 10, N) + 50 * treatment + 30 * peer_exposure + rng.normal(0, 5, N)\n",
    "df = pd.DataFrame({\"farmer\": range(N), \"treatment\": treatment, \"peer_exposure\": peer_exposure,\n",
    "                   \"crop_yield\": yields, \"degree\": degree})\n",
    "\n",
    "pos = nx.spring_layout(G, seed=3)\n",
    "plt.figure(figsize=(7, 6))\n",
    "nx.draw_networkx(G.subgraph(range(80)), {i: pos[i] for i in range(80)}, with_labels=False, node_size=60,\n",
    "                 node_color=[\"#ef4444\" if t else \"#94a3b8\" for t in treatment[:80]])\n",
    "plt.title(\"Part of the farmer network (red = subsidised)\"); plt.axis(\"off\"); plt.show()"
   ]
  },
  {
   "id": "m13-l1-6",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exposure mappings"
   ]
  },
  {
   "id": "m13-l1-7",
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "df[\"exposed\"] = (df.peer_exposure > 0).astype(int)\n",
    "df[\"condition\"] = np.select(\n",
    "    [(df.treatment == 1) & (df.exposed == 1), (df.treatment == 1) & (df.exposed == 0),\n",
    "     (df.treatment == 0) & (df.exposed == 1)],\n",
    "    [\"treated & exposed\", \"treated only\", \"spillover only\"], default=\"pure control\")\n",
    "print(df.groupby(\"condition\").agg(n=(\"farmer\", \"size\"), mean_yield=(\"crop_yield\", \"mean\")).round(1))"
   ]
  },
  {
   "id": "m13-l1-8",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Direct and spillover effects"
   ]
  },
  {
   "id": "m13-l1-9",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> **Question it answers:** How much of the yield gain is the farmer's own subsidy, and how much comes from subsidised neighbours?"
   ]
  },
  {
   "id": "m13-l1-10",
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "naive = smf.ols(\"crop_yield ~ treatment\", data=df).fit(cov_type=\"HC1\")\n",
    "network = smf.ols(\"crop_yield ~ treatment + peer_exposure\", data=df).fit(cov_type=\"HC1\")\n",
    "print(f\"Naive treated-vs-control difference: {naive.params['treatment']:.1f}\")\n",
    "print(f\"Direct effect (network-aware):       {network.params['treatment']:.1f}  (truth 50)\")\n",
    "print(f\"Spillover, all neighbours treated:   {network.params['peer_exposure']:.1f}  (truth 30)\")\n",
    "\n",
    "total_policy = 50 * 0.3 + 30 * df.peer_exposure.mean()\n",
    "print(f\"Average gain per farmer from the programme (direct + spillover): {total_policy:.1f}\")"
   ]
  },
  {
   "id": "m13-l1-11",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercise"
   ]
  },
  {
   "id": "m13-l1-12",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### ✍️ Exercise\n",
    "\n",
    "Make the spillover **negative** (−30: neighbours compete for the same scarce buyers) and repeat. Then\n",
    "increase coverage from 30% to 70%. How do the naive estimate and the total programme value change?"
   ]
  },
  {
   "id": "m13-l1-13",
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# Your answer here"
   ]
  },
  {
   "id": "m13-l1-14",
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Checklist and recap"
   ]
  }
 ]
}