Skip to content

Commit 09a3ef4

Browse files
igerberclaude
andcommitted
Address PR #369 R1 P3: refresh placebo docstring + per-draw τ regression
- _placebo_variance_se docstring step 3 now describes the warm-start semantics (was still saying "uniform initialization, fresh start" after PR #369 landed the warm-start). Adds Parameters entries for init_omega / init_lambda / _placebo_indices. - test_placebo_se_matches_r now also asserts elementwise match between Python's placebo_effects and R_PLACEBO_TAUS from the fixture, so a permutation that diverged on a single draw but happened to leave sd() unchanged would still trip the regression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1224a19 commit 09a3ef4

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

diff_diff/synthetic_did.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,9 +1709,17 @@ def _placebo_variance_se(
17091709
17101710
1. Randomly sample N₀ control indices (permutation)
17111711
2. Designate last N₁ as pseudo-treated, first (N₀-N₁) as pseudo-controls
1712-
3. Re-estimate both omega and lambda on the permuted data (from
1713-
uniform initialization, fresh start), matching R's behavior where
1714-
``update.omega=TRUE, update.lambda=TRUE`` are passed via ``opts``
1712+
3. Re-estimate both omega and lambda on the permuted data with
1713+
``update.omega=TRUE, update.lambda=TRUE`` semantics. Per-draw FW
1714+
is warm-started from the fit-time weights — ω is initialized with
1715+
``sum_normalize(init_omega[pseudo_control_idx])`` and λ is
1716+
initialized with ``init_lambda`` — matching R's
1717+
``vcov.R::placebo_se`` ``weights.boot$omega = sum_normalize(
1718+
weights$omega[ind[1:N0_placebo]])`` warm-start. The global FW
1719+
optimum is init-independent (strict convexity), but the 100-iter
1720+
pre-sparsify pass converges to different sparsification patterns
1721+
under uniform vs warm init on a handful of draws — warm-start
1722+
closes the resulting sub-percent SE drift against R.
17151723
4. Compute SDID estimate with re-estimated weights
17161724
5. Repeat `replications` times
17171725
6. SE = sqrt((r-1)/r) * sd(estimates)
@@ -1736,6 +1744,21 @@ def _placebo_variance_se(
17361744
Convergence threshold for Frank-Wolfe (for re-estimation).
17371745
replications : int, default=200
17381746
Number of placebo replications.
1747+
init_omega : np.ndarray, optional
1748+
Fit-time unit weights used to warm-start per-draw ω FW.
1749+
Subset to pseudo-controls and renormalized inside the loop;
1750+
mirrors R's ``weights.boot$omega = sum_normalize(weights$omega[
1751+
ind[1:N0_placebo]])``. Cold-start (uniform init) when ``None``.
1752+
init_lambda : np.ndarray, optional
1753+
Fit-time time weights used to warm-start per-draw λ FW;
1754+
mirrors R passing ``weights.boot$lambda = weights$lambda``
1755+
through. Cold-start when ``None``.
1756+
_placebo_indices : np.ndarray, optional
1757+
Private R-parity test seam. When provided, each row of shape
1758+
``(replications, n_control)`` replaces the per-draw
1759+
``rng.permutation(n_control)`` so a Python fit can consume
1760+
R's exact permutation sequence and produce a bit-identical SE
1761+
(see ``test_placebo_se_matches_r``).
17391762
17401763
Returns
17411764
-------

tests/test_methodology_sdid.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1608,15 +1608,32 @@ def capture_then_call(*args, **kwargs):
16081608
kwargs = dict(captured["kwargs"])
16091609
kwargs["replications"] = replications
16101610
kwargs["_placebo_indices"] = r_perms
1611-
se_n, _ = sdid._placebo_variance_se(*captured["args"], **kwargs)
1611+
se_n, placebo_effects_n = sdid._placebo_variance_se(
1612+
*captured["args"], **kwargs
1613+
)
16121614
Y_scale = sdid.results_.zeta_omega / kwargs["zeta_omega"]
16131615
py_se = se_n * Y_scale
1616+
py_taus = np.asarray(placebo_effects_n) * Y_scale
16141617
# Match R within cross-library FW tolerance (Rust vs R BLAS
16151618
# reductions differ at sub-ULP; 1e-8 absorbs that without
16161619
# masking a real divergence).
16171620
assert abs(py_se - r_se) < 1e-8, (
16181621
f"Python placebo SE {py_se} != R {r_se} (delta {py_se - r_se})"
16191622
)
1623+
# Per-draw τ regression: equal-SE doesn't imply equal sample, and
1624+
# the placebo τ vector is user-visible through ``placebo_effects``
1625+
# and feeds the empirical placebo p-value (synthetic_did.py
1626+
# around L1164-L1170). Compare elementwise so a permutation that
1627+
# diverged at a single draw — but happened to leave sd() unchanged
1628+
# — still trips the regression.
1629+
r_taus = np.asarray(payload["R_PLACEBO_TAUS"], dtype=float)
1630+
assert r_taus.shape == py_taus.shape == (replications,), (
1631+
f"shape mismatch: r {r_taus.shape}, py {py_taus.shape}"
1632+
)
1633+
np.testing.assert_allclose(
1634+
py_taus, r_taus, atol=1e-8, rtol=1e-8,
1635+
err_msg="Per-draw placebo τ diverges from R despite SE match",
1636+
)
16201637

16211638

16221639
# =============================================================================

0 commit comments

Comments
 (0)