Skip to content

Commit 969ae82

Browse files
igerberclaude
andcommitted
Add bootstrap_weights tests for event-study/group aggregation paths, update docstrings for PR #165 round 5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d7ec694 commit 969ae82

3 files changed

Lines changed: 82 additions & 2 deletions

File tree

diff_diff/imputation_bootstrap.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ def _precompute_bootstrap_psi(
124124
125125
For each aggregation target (overall, per-horizon, per-group), computes
126126
psi_i = sum_t v_it * epsilon_tilde_it for each cluster. The multiplier
127-
bootstrap then perturbs these psi sums with Rademacher weights.
127+
bootstrap then perturbs these psi sums with multiplier weights
128+
(rademacher/mammen/webb; configurable via ``bootstrap_weights``).
128129
129130
Computational cost scales with the number of aggregation targets, since
130131
each target requires its own v_untreated computation (weight-dependent).
@@ -218,7 +219,8 @@ def _run_bootstrap(
218219
"""
219220
Run multiplier bootstrap on pre-computed influence function sums.
220221
221-
Uses T_b = sum_i w_b_i * psi_i where w_b_i are Rademacher weights
222+
Uses T_b = sum_i w_b_i * psi_i where w_b_i are multiplier weights
223+
(rademacher/mammen/webb; configurable via ``bootstrap_weights``)
222224
and psi_i are cluster-level influence function sums from Theorem 3.
223225
SE = std(T_b, ddof=1).
224226
"""

tests/test_imputation.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,46 @@ def test_bootstrap_weights_webb(self, ci_params):
11191119
assert br.overall_att_se > 0
11201120
assert np.isfinite(br.overall_att_p_value)
11211121

1122+
def test_bootstrap_weights_event_study(self, ci_params):
1123+
"""Bootstrap with non-default weights should work for event study aggregation."""
1124+
data = generate_test_data()
1125+
n_boot = ci_params.bootstrap(50)
1126+
est = ImputationDiD(
1127+
n_bootstrap=n_boot, bootstrap_weights="mammen", seed=42
1128+
)
1129+
results = est.fit(
1130+
data, outcome="outcome", unit="unit", time="time",
1131+
first_treat="first_treat", aggregate="event_study",
1132+
)
1133+
1134+
br = results.bootstrap_results
1135+
assert br is not None
1136+
assert br.weight_type == "mammen"
1137+
assert br.event_study_ses is not None
1138+
assert len(br.event_study_ses) > 0
1139+
for h, se in br.event_study_ses.items():
1140+
assert se > 0, f"Non-positive SE at horizon {h}"
1141+
1142+
def test_bootstrap_weights_group(self, ci_params):
1143+
"""Bootstrap with non-default weights should work for group aggregation."""
1144+
data = generate_test_data()
1145+
n_boot = ci_params.bootstrap(50)
1146+
est = ImputationDiD(
1147+
n_bootstrap=n_boot, bootstrap_weights="mammen", seed=42
1148+
)
1149+
results = est.fit(
1150+
data, outcome="outcome", unit="unit", time="time",
1151+
first_treat="first_treat", aggregate="group",
1152+
)
1153+
1154+
br = results.bootstrap_results
1155+
assert br is not None
1156+
assert br.weight_type == "mammen"
1157+
assert br.group_ses is not None
1158+
assert len(br.group_ses) > 0
1159+
for g, se in br.group_ses.items():
1160+
assert se > 0, f"Non-positive SE for group {g}"
1161+
11221162
def test_bootstrap_with_covariates(self, ci_params):
11231163
"""Bootstrap should work with covariates."""
11241164
data = generate_test_data()

tests/test_two_stage.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,44 @@ def test_bootstrap_weights_webb(self, ci_params):
10921092
assert br.overall_att_se > 0
10931093
assert np.isfinite(br.overall_att_p_value)
10941094

1095+
def test_bootstrap_weights_event_study(self, ci_params):
1096+
"""Bootstrap with non-default weights should work for event study aggregation."""
1097+
data = generate_test_data()
1098+
n_boot = ci_params.bootstrap(50)
1099+
results = TwoStageDiD(
1100+
n_bootstrap=n_boot, bootstrap_weights="mammen", seed=42
1101+
).fit(
1102+
data, outcome="outcome", unit="unit", time="time",
1103+
first_treat="first_treat", aggregate="event_study",
1104+
)
1105+
1106+
br = results.bootstrap_results
1107+
assert br is not None
1108+
assert br.weight_type == "mammen"
1109+
assert br.event_study_ses is not None
1110+
assert len(br.event_study_ses) > 0
1111+
for h, se in br.event_study_ses.items():
1112+
assert se > 0, f"Non-positive SE at horizon {h}"
1113+
1114+
def test_bootstrap_weights_group(self, ci_params):
1115+
"""Bootstrap with non-default weights should work for group aggregation."""
1116+
data = generate_test_data()
1117+
n_boot = ci_params.bootstrap(50)
1118+
results = TwoStageDiD(
1119+
n_bootstrap=n_boot, bootstrap_weights="mammen", seed=42
1120+
).fit(
1121+
data, outcome="outcome", unit="unit", time="time",
1122+
first_treat="first_treat", aggregate="group",
1123+
)
1124+
1125+
br = results.bootstrap_results
1126+
assert br is not None
1127+
assert br.weight_type == "mammen"
1128+
assert br.group_ses is not None
1129+
assert len(br.group_ses) > 0
1130+
for g, se in br.group_ses.items():
1131+
assert se > 0, f"Non-positive SE for group {g}"
1132+
10951133

10961134
# =============================================================================
10971135
# TestTwoStageDiDConvenience

0 commit comments

Comments
 (0)