Partially pooled ASCM — Covid-19 lockdowns in Turkey (Bilgel 2022)#
- Estimator:
Partially Pooled SCM (PPSCM) — partially pooled synthetic control at
nu = 0.5, withinference_method="bootstrap".- Source:
Bilgel, F. (2022), “Effects of Covid-19 lockdowns on social distancing in Turkey,” Econometrics Journal 25(3):781–805, 10.1093/ectj/utac016.
- Replication type:
Path A — the paper’s published estimates on the author’s own data, from his deposited replication package — and cross-validation against a pinned
augsynth0.2.0 run of his own call.- Status:
Verified against both. All six ATTs reproduce to 1.5e-4 against live
augsynthand to within half the paper’s last printed digit against the table, and liveaugsynthitself reproduces the table.
What the paper estimates#
Turkey imposed weekend and holiday lockdowns across 31 provinces in April and May 2020. The question is how much of the observed collapse in movement the lockdowns caused, as against what people would have done anyway. The paper answers it by building a synthetic control for the treated provinces out of the untreated ones, on Google Community Mobility Reports, and reading the gap over the 17 post-lockdown days.
Six mobility series are estimated separately: retail and recreation, grocery and pharmacy, parks, transit stations, workplaces, and residential. The first five measure time spent away from home and fall under lockdown; residential measures time spent at home and rises, which is why its sign is opposite.
Why this estimator#
The paper’s column 1 is multisynth from the augsynth R package — the
partially pooled estimator of Ben-Michael, Feller and Rothstein — at
nu = 0.5. Pooling matters here because there are 31 treated provinces, not
one. At nu = 0 each treated province gets its own synthetic control, which
fits each one well and averages noisily; at large nu a single synthetic
control serves all of them, which is stable but fits none exactly. Half is the
author’s choice between the two.
Partially Pooled SCM (PPSCM) is a port of that estimator, so this replication needed no new code: the case reads the author’s frames and fits them.
Two reference bases#
This page carries two comparisons. Comparing mlsynth to the printed table
measures two things at once — whether mlsynth implements this specification, and
whether the printed table was produced by it — and one number cannot separate
them. So the case also runs the author’s own call through a commit-pinned
augsynth 0.2.0 and compares against that.
The separation resolves cleanly here. Live augsynth reproduces every printed
figure to within rounding, so the table came from this call at this version.
Unlike the Song et al. case there is no drift between the
published artifact and the pinned package, which makes the live comparison the
binding one.
Results#
Every published estimate, against live augsynth and against what PPSCM
returns:
Mobility series |
Paper |
Live |
mlsynth |
Paper SE |
live SE |
|---|---|---|---|---|---|
Retail and recreation |
−25.08 |
−25.0818493 |
−25.0818010 |
5.49 |
5.4937 |
Grocery and pharmacy |
−53.10 |
−53.1003895 |
−53.1004403 |
10.43 |
10.4301 |
Parks |
−33.45 |
−33.4503451 |
−33.4503250 |
7.69 |
7.6928 |
Transit stations |
−16.76 |
−16.7622384 |
−16.7622501 |
3.90 |
3.9005 |
Workplaces |
−27.61 |
−27.6115815 |
−27.6114318 |
6.37 |
6.3666 |
Residential |
12.02 |
12.0190048 |
12.0189919 |
2.04 |
2.0410 |
Against live augsynth the largest distance is 1.5e-4, and the post-period
trajectories — 17 event times per outcome, 102 points — agree to 4.1e-4. Both
sit above the 1e-6 this project reaches on ridge ASCM. The gap is the quadratic
program: PPSCM and augsynth reach the partially pooled optimum through
different solvers at different stopping tolerances, which moves the last few
digits and nothing a reported estimate would notice.
The paper prints two decimals, so a published figure pins the true one only to within 0.005. Every distance above is under that, on estimates ranging from −53.10 to +12.02. This is the tightest agreement the published precision can express — a closer claim would need the author’s unrounded output, which the package does not carry.
Standard errors#
The standard errors agree to between 0.8 and 3.2 percent, and they are held to a looser tolerance than the estimates for a reason that is not tolerance-shopping. Column 1’s errors come from a wild bootstrap. Reproducing them to the digit would mean reproducing R’s random number stream, not merely the estimator, so the question the case can answer is whether the inference lands in the same place. It does. A broken inference routine would miss by far more than three percent, which is what the 10 percent tolerance is set to catch.
Two structural checks#
Two rows guard the design itself, and both come from the paper.
n_treated_residential is 24 where every other series has 31. That is the
paper’s own footnote a: residential mobility is reported for fewer provinces
than the rest. Reading the panel wrongly would collapse the difference.
n_post_periods is 17, which is the post-lockdown window Table 3 states. It is
recomputed from the data — last period minus first treated period plus one — so
a misread adoption date would move it.
Reading the replication package#
Two features of the deposited package change what a reader has to do with it.
The .rdata files are cumulative workspace saves. The retail file holds one
frame, the grocery file two, the residential file all six. Loading one and taking
the first object returns a different outcome’s panel, and the fit will succeed
and be wrong. Each frame has to be selected by name. The vendored
basedata/bilgel_turkey_lockdown.parquet does that once and tags each panel
with its outcome.
Sixteen provinces lift lockdown before the window closes, so the raw treatment
is not absorbing. The author handles this upstream, and Table 3 records the
action as “post-lockdown concatenation”: the _t0c1 frames reach the estimator
with a single adoption day and no reversal. The estimator therefore never sees
the on-and-off treatment that Table 3’s attribute row marks ASCM as unable to
accommodate — the paper’s own interactive-fixed-effects and matrix-completion
columns exist to cover that case.
Running the reference#
The reference is committed under
benchmarks/reference/bilgel_turkey_lockdown/ as
gold_live_augsynth.csv and gold_live_trajectory.csv, so the case runs in
CI without R. The script that produced them, reference.R, is committed
alongside and reads the same vendored parquet the Python side reads, through
nanoparquet. One file, two sides: an earlier comparison in this project ran
33 donors on the R side and 37 on the Python side because each read its own copy
of the inputs, and reading one file makes that impossible.
To install the pinned reference and re-run it live:
bash benchmarks/R/install_augsynth.sh
MLSYNTH_BENCH_LIVE_R=1 python benchmarks/run_benchmarks.py \
--case bilgel_turkey_lockdown
That regenerates the gold into a temporary directory and checks it against the
committed copy. A regenerated run currently reproduces it at 0.0, and the case
raises if that moves by more than 1e-8. It raises instead of reporting a row
because a row that exists only when R does could not be pinned in EXPECTED,
and an unpinned row is one nobody checks.
Case#
benchmarks/cases/bilgel_turkey_lockdown.py. Every row is a distance from a
reference, so a regression moves it and cannot be absorbed by re-fitting.