Skip to content

Commit 1b5c2f0

Browse files
igerberclaude
andcommitted
Remove allow_zero_se — let SE=0 produce NaN inference consistently
The allow_zero_se feature introduced P0 bugs: it couldn't distinguish census FPC (legitimate zero variance) from single-PSU (unidentified variance), and it created partial-NaN violations (finite CI with NaN t_stat). Reverting to the original behavior where SE=0 always produces all-NaN inference. Census FPC users can use analytical inference (n_bootstrap=0) which handles this case correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f23548f commit 1b5c2f0

7 files changed

Lines changed: 8 additions & 48 deletions

diff_diff/bootstrap_utils.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,6 @@ def compute_effect_bootstrap_stats(
221221
boot_dist: np.ndarray,
222222
alpha: float = 0.05,
223223
context: str = "bootstrap distribution",
224-
allow_zero_se: bool = False,
225224
) -> Tuple[float, Tuple[float, float], float]:
226225
"""
227226
Compute bootstrap statistics for a single effect.
@@ -239,11 +238,6 @@ def compute_effect_bootstrap_stats(
239238
Significance level.
240239
context : str, optional
241240
Description for warning messages.
242-
allow_zero_se : bool, default=False
243-
If True, treat SE=0.0 as legitimate zero sampling variance
244-
(e.g., census FPC where all bootstrap estimates are identical)
245-
instead of returning NaN. When SE=0, returns
246-
``(0.0, (original_effect, original_effect), 0.0)``.
247241
248242
Returns
249243
-------
@@ -284,9 +278,6 @@ def compute_effect_bootstrap_stats(
284278

285279
# Guard: if SE is not finite or zero, all inference fields must be NaN.
286280
if not np.isfinite(se) or se <= 0:
287-
# Census FPC: all bootstrap estimates identical → SE=0 is legitimate
288-
if allow_zero_se and se == 0.0:
289-
return 0.0, (original_effect, original_effect), np.nan
290281
warnings.warn(
291282
f"Bootstrap SE is non-finite or zero (n_valid={n_valid}) in {context}. "
292283
"Returning NaN for SE/CI/p-value.",
@@ -304,7 +295,6 @@ def compute_effect_bootstrap_stats_batch(
304295
original_effects: np.ndarray,
305296
bootstrap_matrix: np.ndarray,
306297
alpha: float = 0.05,
307-
allow_zero_se: bool = False,
308298
) -> tuple:
309299
"""
310300
Batch-compute bootstrap statistics for multiple effects at once.
@@ -317,10 +307,6 @@ def compute_effect_bootstrap_stats_batch(
317307
Bootstrap distributions, shape (n_bootstrap, n_effects).
318308
alpha : float, default=0.05
319309
Significance level.
320-
allow_zero_se : bool, default=False
321-
If True, treat SE=0.0 as legitimate zero sampling variance
322-
(e.g., census FPC where all bootstrap estimates are identical)
323-
instead of returning NaN.
324310
325311
Returns
326312
-------
@@ -399,15 +385,7 @@ def compute_effect_bootstrap_stats_batch(
399385

400386
# Guard: SE must be positive and finite
401387
se_valid = np.isfinite(batch_ses) & (batch_ses > 0)
402-
# Census FPC: SE=0 is legitimate zero sampling variance
403-
se_zero = np.isfinite(batch_ses) & (batch_ses == 0.0)
404-
if allow_zero_se and np.any(se_zero):
405-
zero_idx = idx[se_zero]
406-
ses[zero_idx] = 0.0
407-
ci_lowers[zero_idx] = original_effects[zero_idx]
408-
ci_uppers[zero_idx] = original_effects[zero_idx]
409-
p_values[zero_idx] = np.nan # p undefined when SE=0
410-
n_bad_se = int(np.sum(~se_valid & ~se_zero)) if allow_zero_se else int(np.sum(~se_valid))
388+
n_bad_se = int(np.sum(~se_valid))
411389
if n_bad_se > 0:
412390
warnings.warn(
413391
f"{n_bad_se} effect(s) had non-finite or zero bootstrap SE. "
@@ -429,7 +407,6 @@ def compute_effect_bootstrap_stats_batch(
429407
bootstrap_matrix[:, j],
430408
alpha=alpha,
431409
context=f"effect {j}",
432-
allow_zero_se=allow_zero_se,
433410
)
434411
ses[j] = se
435412
ci_lowers[j] = ci[0]

diff_diff/continuous_did.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,6 @@ def _bootstrap_gt_cell(gt, info):
14981498
boot_att_d[:, idx],
14991499
alpha=self.alpha,
15001500
context=f"ATT(d) at grid point {idx}",
1501-
allow_zero_se=_use_survey_bootstrap,
15021501
)
15031502
att_d_se[idx] = se
15041503
att_d_ci_lower[idx] = ci[0]
@@ -1510,7 +1509,6 @@ def _bootstrap_gt_cell(gt, info):
15101509
boot_acrt_d[:, idx],
15111510
alpha=self.alpha,
15121511
context=f"ACRT(d) at grid point {idx}",
1513-
allow_zero_se=_use_survey_bootstrap,
15141512
)
15151513
acrt_d_se[idx] = se
15161514
acrt_d_ci_lower[idx] = ci[0]
@@ -1532,7 +1530,6 @@ def _bootstrap_gt_cell(gt, info):
15321530
boot_att_glob,
15331531
alpha=self.alpha,
15341532
context="overall ATT_glob",
1535-
allow_zero_se=_use_survey_bootstrap,
15361533
)
15371534
result["overall_att_se"] = se
15381535
result["overall_att_ci"] = ci
@@ -1543,7 +1540,6 @@ def _bootstrap_gt_cell(gt, info):
15431540
boot_acrt_glob,
15441541
alpha=self.alpha,
15451542
context="overall ACRT_glob",
1546-
allow_zero_se=_use_survey_bootstrap,
15471543
)
15481544
result["overall_acrt_se"] = se
15491545
result["overall_acrt_ci"] = ci
@@ -1560,8 +1556,7 @@ def _bootstrap_gt_cell(gt, info):
15601556
boot_es[e],
15611557
alpha=self.alpha,
15621558
context=f"event study e={e}",
1563-
allow_zero_se=_use_survey_bootstrap,
1564-
)
1559+
)
15651560
es_se[e] = se_e
15661561
es_ci[e] = ci_e
15671562
es_p[e] = p_e

diff_diff/efficient_did_bootstrap.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ def _run_multiplier_bootstrap(
207207
bootstrap_atts[:, j],
208208
alpha=self.alpha,
209209
context=f"ATT(g={gt[0]}, t={gt[1]})",
210-
allow_zero_se=_use_survey_bootstrap,
211210
)
212211
gt_ses[gt] = se
213212
gt_cis[gt] = ci
@@ -221,7 +220,6 @@ def _run_multiplier_bootstrap(
221220
bootstrap_overall,
222221
alpha=self.alpha,
223222
context="overall ATT",
224-
allow_zero_se=_use_survey_bootstrap,
225223
)
226224

227225
es_ses = es_cis = es_pvs = None
@@ -233,8 +231,7 @@ def _run_multiplier_bootstrap(
233231
bootstrap_event_study[e],
234232
alpha=self.alpha,
235233
context=f"event study (e={e})",
236-
allow_zero_se=_use_survey_bootstrap,
237-
)
234+
)
238235
es_ses[e] = se
239236
es_cis[e] = ci
240237
es_pvs[e] = pv
@@ -248,8 +245,7 @@ def _run_multiplier_bootstrap(
248245
bootstrap_group[g],
249246
alpha=self.alpha,
250247
context=f"group effect (g={g})",
251-
allow_zero_se=_use_survey_bootstrap,
252-
)
248+
)
253249
g_ses[g] = se
254250
g_cis[g] = ci
255251
g_pvs[g] = pv

diff_diff/imputation_bootstrap.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,6 @@ def _run_bootstrap(
349349
boot_overall_shifted,
350350
alpha=self.alpha,
351351
context="ImputationDiD overall ATT",
352-
allow_zero_se=_use_survey_bootstrap,
353352
)
354353

355354
event_study_ses = None
@@ -367,7 +366,6 @@ def _run_bootstrap(
367366
shifted_h,
368367
alpha=self.alpha,
369368
context=f"ImputationDiD event study (h={h})",
370-
allow_zero_se=_use_survey_bootstrap,
371369
)
372370
event_study_ses[h] = se_h
373371
event_study_cis[h] = ci_h
@@ -388,7 +386,6 @@ def _run_bootstrap(
388386
shifted_g,
389387
alpha=self.alpha,
390388
context=f"ImputationDiD group effect (g={g})",
391-
allow_zero_se=_use_survey_bootstrap,
392389
)
393390
group_ses[g] = se_g
394391
group_cis[g] = ci_g

diff_diff/staggered_bootstrap.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ def _run_multiplier_bootstrap(
427427
# Batch compute bootstrap statistics for ATT(g,t)
428428
batch_ses, batch_ci_lo, batch_ci_hi, batch_pv = _compute_effect_bootstrap_stats_batch_func(
429429
original_atts, bootstrap_atts_gt, alpha=self.alpha,
430-
allow_zero_se=_use_survey_bootstrap,
430+
431431
)
432432
gt_ses = {}
433433
gt_cis = {}
@@ -445,7 +445,7 @@ def _run_multiplier_bootstrap(
445445
else:
446446
overall_se, overall_ci, overall_p_value = _compute_effect_bootstrap_stats_func(
447447
original_overall, bootstrap_overall, alpha=self.alpha, context="overall ATT",
448-
allow_zero_se=_use_survey_bootstrap,
448+
449449
)
450450

451451
# Batch compute bootstrap statistics for event study effects
@@ -458,7 +458,7 @@ def _run_multiplier_bootstrap(
458458
es_boot_matrix = np.column_stack([bootstrap_event_study[e] for e in rel_periods])
459459
es_ses, es_ci_lo, es_ci_hi, es_pv = _compute_effect_bootstrap_stats_batch_func(
460460
es_effects, es_boot_matrix, alpha=self.alpha,
461-
allow_zero_se=_use_survey_bootstrap,
461+
462462
)
463463
event_study_ses = {e: float(es_ses[i]) for i, e in enumerate(rel_periods)}
464464
event_study_cis = {
@@ -476,7 +476,7 @@ def _run_multiplier_bootstrap(
476476
grp_boot_matrix = np.column_stack([bootstrap_group[g] for g in group_list])
477477
grp_ses, grp_ci_lo, grp_ci_hi, grp_pv = _compute_effect_bootstrap_stats_batch_func(
478478
grp_effects, grp_boot_matrix, alpha=self.alpha,
479-
allow_zero_se=_use_survey_bootstrap,
479+
480480
)
481481
group_effect_ses = {g: float(grp_ses[i]) for i, g in enumerate(group_list)}
482482
group_effect_cis = {

diff_diff/sun_abraham.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,7 +1476,6 @@ def _run_rao_wu_bootstrap(
14761476
boot_dist,
14771477
alpha=self.alpha,
14781478
context=f"event study e={e}",
1479-
allow_zero_se=True,
14801479
)
14811480
event_study_ses[e] = se
14821481
event_study_cis[e] = ci
@@ -1488,7 +1487,6 @@ def _run_rao_wu_bootstrap(
14881487
bootstrap_overall,
14891488
alpha=self.alpha,
14901489
context="overall ATT",
1491-
allow_zero_se=True,
14921490
)
14931491

14941492
return SABootstrapResults(

diff_diff/two_stage_bootstrap.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,6 @@ def _run_bootstrap(
311311
boot_overall_shifted,
312312
alpha=self.alpha,
313313
context="TwoStageDiD overall ATT",
314-
allow_zero_se=_use_survey_bootstrap,
315314
)
316315

317316
# --- Event study bootstrap ---
@@ -412,7 +411,6 @@ def _run_bootstrap(
412411
shifted_h,
413412
alpha=self.alpha,
414413
context=f"TwoStageDiD event study (h={h})",
415-
allow_zero_se=_use_survey_bootstrap,
416414
)
417415
event_study_ses[h] = se_h
418416
event_study_cis[h] = ci_h
@@ -476,7 +474,6 @@ def _run_bootstrap(
476474
shifted_g,
477475
alpha=self.alpha,
478476
context=f"TwoStageDiD group effect (g={g})",
479-
allow_zero_se=_use_survey_bootstrap,
480477
)
481478
group_ses[g] = se_g
482479
group_cis[g] = ci_g

0 commit comments

Comments
 (0)