A spillover contrast compares outcome changes for untreated branches that became indirectly exposed with changes for untreated branches that remained unexposed.
A spillover difference-in-differences contrast
Set a narrow target
For April through June, ask whether untreated branches sharing a queue with newly treated North changed their breach rate differently from untreated branches outside that queue network. This is a contrast about indirect exposure among branches without the screen. It is not the effect of giving North the screen. The exposure map determines groups using pre-rollout connections.
Use the same pre and post windows
For each branch, calculate June minus March breach rate, then subtract the mean change among still-unexposed branches from the mean change among indirectly exposed branches. In the fixture, exposed branches rise by 0.04 on average while unexposed branches rise by 0.005, giving a descriptive spillover contrast of 0.035. A positive value means a larger increase in service breaches for the exposed group under this outcome definition.
Defend the counterfactual
A causal reading requires that the groups would have had comparable untreated changes absent neighboring rollout, along with no anticipation and no spillovers into the supposedly unexposed group. The exposed branches may sit near a demand surge or receive staffing changes at the same time. Examine pre-rollout trends, routing records and stable denominators. Pre-trend auditing cannot prove an unobserved post-rollout trend but may reveal a clear mismatch.
Keep exposure separate from own treatment
A branch that adopts the screen during the post period is not part of either untreated group. Exclude it or use a richer design that models both paths. The code accepts group membership already determined for the study window and rejects overlap, but it does not reconstruct monthly exposure or adjust for changing membership. If West becomes indirectly exposed only halfway through April, a whole-quarter label hides timing. The panel clock handles transitions explicitly.
Interpret support honestly
Show branch counts and case denominators for each group, not only the rate contrast. If there is one exposed branch and one unexposed branch, the statistic is inspectable but uncertainty and generalization are weak. If all branches share one network, consider a design at the network level or a different estimand. The review project treats empty support as a stop condition.
Implementation
def untreated_spillover_change(branch_rates, indirectly_exposed,
unexposed, before_month, after_month):
exposed = set(indirectly_exposed)
comparison = set(unexposed)
if not exposed or not comparison or exposed & comparison:
raise ValueError("unsupported exposure groups")
if before_month >= after_month:
raise ValueError("invalid comparison window")
def average_change(branches):
return sum(branch_rates[(branch, after_month)] -
branch_rates[(branch, before_month)]
for branch in branches) / len(branches)
return average_change(exposed) - average_change(comparison)
rates = {("West", 3): .16, ("West", 6): .21,
("East", 3): .18, ("East", 6): .21,
("South", 3): .15, ("South", 6): .16,
("Harbor", 3): .14, ("Harbor", 6): .14}
spillover_change = untreated_spillover_change(
rates, {"West", "East"}, {"South", "Harbor"}, 3, 6)
assert abs(spillover_change - .035) < 1e-12Performance and operating cost
For E indirectly exposed and C unexposed branches, dictionary-backed calculation costs O(E + C) expected time and O(E + C) space for group sets. Rebuilding exposure from a network adds its own graph scan. The main constraint is credible comparison support.
Common Mistakes
- Do not include directly treated branches in either untreated group.
- Do not call the contrast causal without a defended no-spillover counterfactual trend.
- Do not ignore partial-period exposure or an empty comparison group.
