Skip to content

Commit 3b1fe6b

Browse files
authored
Merge pull request #309 from igerber/sdid-validation
Add SyntheticDiD validation diagnostics
2 parents d7f625d + f0c9e5c commit 3b1fe6b

7 files changed

Lines changed: 1376 additions & 17 deletions

File tree

BRIEFING.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# SDID Practitioner Validation Tooling - Briefing
2+
3+
## Problem
4+
5+
A data scientist runs `SyntheticDiD`, gets an ATT and a p-value, and then
6+
faces the question: *should I trust this estimate?* The library gives them the
7+
point estimate and inference, but the validation workflow - the steps between
8+
"I got a number" and "I'm confident enough to present this" - is largely
9+
left to the practitioner to assemble from scratch.
10+
11+
The standard validation workflow for synthetic control methods is well
12+
understood in the econometrics literature (Arkhangelsky et al. 2021,
13+
Abadie et al. 2010, Abadie 2021). The pieces include pre-treatment fit
14+
assessment, weight diagnostics, placebo/falsification tests, sensitivity
15+
analysis, and cross-estimator comparison. Our library provides some of the
16+
raw ingredients (pre-treatment RMSE, weight dicts, placebo effects array)
17+
but doesn't connect them into an accessible diagnostic workflow.
18+
19+
The gap is most visible in `practitioner.py`, where `_handle_synthetic`
20+
recommends in-time placebos and leave-one-out analysis but provides only
21+
comment-only pseudo-code. A practitioner following that guidance hits a wall.
22+
23+
## Current state
24+
25+
What we have today:
26+
27+
- `results.pre_treatment_fit` (RMSE) with a warning when it exceeds the
28+
treated pre-period SD
29+
- `results.get_unit_weights_df()` and `results.get_time_weights_df()`
30+
- Three variance methods: placebo (default), bootstrap, and jackknife (just
31+
landed in v3.1.1)
32+
- `results.placebo_effects` - stores per-iteration estimates for all three
33+
variance methods, but for jackknife these are positional LOO estimates
34+
with no unit labels
35+
- `results.summary()` shows top-5 unit weights and count of non-trivial weights
36+
- `practitioner.py` guidance that names the right steps but can't point to
37+
runnable code for most of them
38+
39+
What the practitioner must currently build themselves:
40+
41+
- Mapping jackknife LOO estimates back to unit identities to answer "which
42+
unit, when dropped, changes my estimate the most?"
43+
- In-time placebo tests (re-estimate with a fake treatment date)
44+
- Any weight concentration metric beyond eyeballing the sorted list
45+
- Any sense of whether their RMSE is "bad enough to worry about" beyond
46+
the binary warning
47+
- Regularization sensitivity (does the ATT change if I perturb zeta?)
48+
- Pre-treatment trajectory data for plotting (the Y matrices are internal
49+
to `fit()` and not returned)
50+
51+
## Context from prior discussion
52+
53+
The jackknife work created an interesting opportunity. The delete-one-re-estimate
54+
loop already runs for SE computation. The per-unit ATT estimates are stored in
55+
`results.placebo_effects`. The missing piece is a presentation layer that maps
56+
those estimates to unit identities and surfaces the diagnostic interpretation
57+
(which units are influential, how stable is the estimate to unit composition).
58+
59+
More broadly, the validation gaps fall into two categories:
60+
61+
1. **Low-marginal-cost additions** - things where the computation already
62+
exists and we just need to expose or label it (LOO diagnostic from
63+
jackknife, weight concentration metrics, trajectory data extraction)
64+
65+
2. **New functionality** - things that require new estimation loops or
66+
helpers (in-time placebo, regularization sensitivity sweep)
67+
68+
The practitioner guidance in `practitioner.py` should evolve alongside any
69+
new tooling so that the recommended steps point to real, runnable code paths.
70+
71+
## What "done" looks like
72+
73+
A practitioner using SyntheticDiD should be able to follow a credible
74+
validation workflow using library-provided tools and guidance, without
75+
needing to reverse-engineer internals or write substantial boilerplate.
76+
The validation steps recognized in the literature should either be directly
77+
supported or have clear, concrete guidance for how to perform them with
78+
the library's API.
79+
80+
This is not about adding visualization or plotting (that's a separate
81+
concern). It's about making the computational and diagnostic building
82+
blocks accessible and well-documented through the results API and
83+
practitioner guidance.

diff_diff/practitioner.py

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -505,35 +505,74 @@ def _handle_synthetic(results: Any):
505505
steps = [
506506
_step(
507507
baker_step=6,
508-
label="Check pre-treatment fit quality",
508+
label="Check pre-treatment fit and weight concentration",
509509
why=(
510510
"Synthetic DiD relies on pre-treatment fit to construct "
511-
"weights. Poor fit suggests the synthetic control may not "
512-
"approximate the counterfactual well."
511+
"weights. Poor fit or highly concentrated unit weights "
512+
"suggest the synthetic control may not approximate the "
513+
"counterfactual well."
513514
),
514515
code=(
515-
"# Check pre-treatment fit and unit weight concentration:\n"
516516
"print(f'Pre-treatment fit (RMSE): {results.pre_treatment_fit:.4f}')\n"
517-
"# Highly concentrated weights suggest fragile estimates"
517+
"concentration = results.get_weight_concentration()\n"
518+
"print(f\"Effective N: {concentration['effective_n']:.1f}\")\n"
519+
"print(f\"Top-5 weight share: {concentration['top_k_share']:.2%}\")"
518520
),
519521
step_name="sensitivity",
520522
),
521523
_step(
522524
baker_step=6,
523-
label="In-time or in-space placebo",
525+
label="In-time placebo",
524526
why=(
525-
"Test robustness by re-estimating on a placebo treatment "
526-
"period (in-time) or excluding treated units one at a time "
527-
"(leave-one-out). These are the natural falsification "
528-
"checks for synthetic control methods."
527+
"Re-estimate on shifted fake treatment dates in the "
528+
"pre-period. A credible design yields near-zero placebo "
529+
"ATTs — departures signal that something is being picked "
530+
"up pre-treatment, weakening the causal interpretation."
529531
),
530532
code=(
531-
"# In-time placebo: re-estimate with a fake treatment date\n"
532-
"# Leave-one-out: drop each treated unit and re-estimate"
533+
"placebo_df = results.in_time_placebo()\n"
534+
"print(placebo_df)"
533535
),
534536
priority="medium",
535537
step_name="sensitivity",
536538
),
539+
_step(
540+
baker_step=6,
541+
label="Leave-one-out influence (jackknife)",
542+
why=(
543+
"If the estimate is driven by a single unit, robustness "
544+
"is weak. Fit with variance_method='jackknife' and inspect "
545+
"which units move the ATT the most."
546+
),
547+
code=(
548+
"# Requires variance_method='jackknife' AND enough support for LOO\n"
549+
"# (n_treated >= 2 and >= 2 effective-weight controls).\n"
550+
"if getattr(results, '_loo_unit_ids', None) is not None:\n"
551+
" loo_df = results.get_loo_effects_df()\n"
552+
" print(loo_df.head(10))\n"
553+
"else:\n"
554+
" print('LOO not available - re-fit with '\n"
555+
" 'variance_method=\"jackknife\" and ensure >=2 treated units '\n"
556+
" 'with positive effective support.')"
557+
),
558+
priority="medium",
559+
step_name="sensitivity",
560+
),
561+
_step(
562+
baker_step=6,
563+
label="Regularization sensitivity (zeta_omega)",
564+
why=(
565+
"The unit-weight regularization is auto-selected from "
566+
"data. Show whether the ATT moves materially across a "
567+
"grid of values to gauge robustness to this choice."
568+
),
569+
code=(
570+
"sens_df = results.sensitivity_to_zeta_omega()\n"
571+
"print(sens_df)"
572+
),
573+
priority="low",
574+
step_name="sensitivity",
575+
),
537576
_step(
538577
baker_step=8,
539578
label="Compare with staggered estimators (CS, SA)",

0 commit comments

Comments
 (0)