Accelerated Foundation in Applied Causal InferenceFor Economists · 15 modules · 45 lessons

13.1 When SUTVA Fails: Spillovers and Exposure Mappings

about 2 hoursAgriculture: fertiliser adoption spillovers; weather-insurance networks
Lesson 13.1 · Module 13
When SUTVA Fails: Spillovers and Exposure Mappings
Video coming soon. The written lesson and the Colab notebook are ready below.
Colab notebook (.ipynb)
By the end of this lesson you can
  • ✓Recognise interference between units in health, agriculture, education and labour markets
  • ✓Simulate a farmer network where treatment spills over to neighbours
  • ✓Define an exposure mapping and separate direct from spillover effects
  • ✓Show that ignoring spillovers biases the naive treatment–control comparison
Lesson plan (120 minutes)
Written lesson

Every method so far assumed SUTVA

The potential-outcomes notation Y(1), Y(0) quietly assumes that my outcome depends only on my treatment (Lesson 4.1). In many policy settings that is false:

  • Public health: my vaccination protects my neighbours.
  • Agriculture: farmers copy successful neighbours, and runoff crosses field boundaries.
  • Education: tutoring some pupils changes the classroom for everyone.
  • Labour markets: training some job seekers can push others down the hiring queue.

With interference, "the" treatment effect is no longer one number. There is a direct effect (of my own treatment) and a spillover effect (of others' treatment on me), and a naive comparison of treated and control units mixes them.

Python · Colab cell
!pip install -q networkx numpy pandas statsmodels matplotlib
import numpy as np, pandas as pd, networkx as nx
import statsmodels.formula.api as smf
import matplotlib.pyplot as plt
rng = np.random.default_rng(101)

A fertiliser subsidy in a farmer network

500 farmers are connected in a small-world network (dense local clusters with a few long links), a common model of village social ties. 30% of farmers are randomly given a subsidised high-yield fertiliser. Direct effect: +50 units of yield. Spillover: +30 for a farmer whose neighbours are all treated, proportionally less otherwise.

Python · Colab cell
N = 500
G = nx.watts_strogatz_graph(n=N, k=6, p=0.1, seed=1)
treatment = rng.binomial(1, 0.3, N)
A = nx.to_numpy_array(G)                                      # adjacency matrix
degree = A.sum(axis=1)
peer_exposure = (A @ treatment) / degree                      # share of neighbours treated

yields = rng.normal(100, 10, N) + 50 * treatment + 30 * peer_exposure + rng.normal(0, 5, N)
df = pd.DataFrame({"farmer": range(N), "treatment": treatment, "peer_exposure": peer_exposure,
                   "crop_yield": yields, "degree": degree})

pos = nx.spring_layout(G, seed=3)
plt.figure(figsize=(7, 6))
nx.draw_networkx(G.subgraph(range(80)), {i: pos[i] for i in range(80)}, with_labels=False, node_size=60,
                 node_color=["#ef4444" if t else "#94a3b8" for t in treatment[:80]])
plt.title("Part of the farmer network (red = subsidised)"); plt.axis("off"); plt.show()

Exposure mappings

An exposure mapping (Aronow and Samii) summarises how the whole treatment vector affects one unit. The choice is a modelling assumption, like a DAG. Common choices:

MappingDefinitionAssumes
Any treated neighbour1 if ≥ 1 neighbour treatedone treated contact is enough
Share of neighbours treated(A·D)ᵢ / degreeᵢeffect scales with the fraction
Number of treated neighbours(A·D)ᵢeffect scales with the count

We combine own treatment with the neighbour share into four exposure conditions:

Python · Colab cell
df["exposed"] = (df.peer_exposure > 0).astype(int)
df["condition"] = np.select(
    [(df.treatment == 1) & (df.exposed == 1), (df.treatment == 1) & (df.exposed == 0),
     (df.treatment == 0) & (df.exposed == 1)],
    ["treated & exposed", "treated only", "spillover only"], default="pure control")
print(df.groupby("condition").agg(n=("farmer", "size"), mean_yield=("crop_yield", "mean")).round(1))

Direct and spillover effects

Python · Colab cell
naive = smf.ols("crop_yield ~ treatment", data=df).fit(cov_type="HC1")
network = smf.ols("crop_yield ~ treatment + peer_exposure", data=df).fit(cov_type="HC1")
print(f"Naive treated-vs-control difference: {naive.params['treatment']:.1f}")
print(f"Direct effect (network-aware):       {network.params['treatment']:.1f}  (truth 50)")
print(f"Spillover, all neighbours treated:   {network.params['peer_exposure']:.1f}  (truth 30)")

total_policy = 50 * 0.3 + 30 * df.peer_exposure.mean()
print(f"Average gain per farmer from the programme (direct + spillover): {total_policy:.1f}")

Here random assignment keeps the direct effect roughly unbiased, but the naive comparison still misses the spillovers entirely, so it understates the programme's total value: control farmers gained too. In other designs (for example, when treated units cluster together, or when spillovers are negative, as with displacement in labour markets), the naive estimate can be biased in either direction.

The why

Policy decisions need the total effect of a roll-out at a given coverage, which includes spillovers. A pilot that treats a few scattered people and compares them with their neighbours estimates something quite different from what a national programme would achieve.

Exercise

Exercise

Make the spillover negative (−30: neighbours compete for the same scarce buyers) and repeat. Then increase coverage from 30% to 70%. How do the naive estimate and the total programme value change?

Show a solution
Python · Colab cell
for cover in (0.3, 0.7):
    d = rng.binomial(1, cover, N)
    expo = (A @ d) / degree
    y = rng.normal(100, 10, N) + 50 * d - 30 * expo + rng.normal(0, 5, N)
    sim = pd.DataFrame({"y": y, "d": d, "e": expo})
    naive_c = smf.ols("y ~ d", data=sim).fit().params["d"]
    print(f"coverage {cover:.0%}: naive {naive_c:.1f} | per-farmer programme value {50 * d.mean() - 30 * expo.mean():.1f}")
# The naive estimate stays near 50, but with negative spillovers the per-farmer value is far below
# what the direct effect implies (50 x coverage = 15 and 35): competition erodes most of the gain.

Checklist and recap

  • I can name situations where SUTVA fails.
  • I can define an exposure mapping and build exposure conditions from a network.
  • I can estimate direct and spillover effects and explain why total programme value needs both.