Skip to content

Commit 8b8dcb3

Browse files
igerberclaude
andcommitted
Complete replicate guard coverage and fix remaining review items
- Update REGISTRY CS support: reg/ipw/dr without covariates (not just reg) - Fix DoseResponseCurve df_survey: pass None (not 0 sentinel) for display - Fix fweight error string: "non-negative integers" matching REGISTRY - Add BaconDecomposition replicate rejection guard - Add rejection tests: MultiPeriodDiD, ImputationDiD, TwoStageDiD, BaconDecomposition (plus existing TWFE, StackedDiD coverage) - Update REGISTRY support matrix to include BaconDecomposition Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6c6494f commit 8b8dcb3

6 files changed

Lines changed: 66 additions & 7 deletions

File tree

diff_diff/bacon.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,13 @@ def fit(
462462
resolved_survey, survey_weights, survey_weight_type, survey_metadata = (
463463
_resolve_survey_for_fit(survey_design, data, "analytical")
464464
)
465+
# Reject replicate-weight designs — Bacon decomposition is a
466+
# diagnostic that does not compute replicate-based variance
467+
if resolved_survey is not None and resolved_survey.uses_replicate_variance:
468+
raise NotImplementedError(
469+
"BaconDecomposition does not support replicate-weight survey "
470+
"designs. Use a TSL-based survey design (strata/psu/fpc)."
471+
)
465472

466473
# Validate within-unit constancy for exact survey weights only.
467474
# The exact-weight path collapses to per-unit weights via groupby().first(),

diff_diff/continuous_did.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,7 @@ def fit(
684684
target="att",
685685
p_value=att_d_p,
686686
n_bootstrap=self.n_bootstrap,
687-
df_survey=_survey_df,
687+
df_survey=_survey_df if _survey_df != 0 else None,
688688
)
689689
dose_response_acrt = DoseResponseCurve(
690690
dose_grid=dvals,
@@ -695,7 +695,7 @@ def fit(
695695
target="acrt",
696696
p_value=acrt_d_p,
697697
n_bootstrap=self.n_bootstrap,
698-
df_survey=_survey_df,
698+
df_survey=_survey_df if _survey_df != 0 else None,
699699
)
700700

701701
# Strip bootstrap internals from gt_results

diff_diff/linalg.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ def _validate_weights(weights, weight_type, n):
411411
fractional = weights - np.round(weights)
412412
if np.any(np.abs(fractional) > 1e-10):
413413
raise ValueError(
414-
"Frequency weights (fweight) must be positive integers. "
414+
"Frequency weights (fweight) must be non-negative integers. "
415415
"Fractional values detected. Use pweight for non-integer weights."
416416
)
417417
return weights

docs/methodology/REGISTRY.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,14 +2046,16 @@ variance from the distribution of replicate estimates.
20462046
sum without changing the scale. Survey df uses `n_valid - 1` for
20472047
t-based inference.
20482048
- **Note:** Replicate-weight support matrix:
2049-
- **Supported**: CallawaySantAnna (reg, no bootstrap), ContinuousDiD
2049+
- **Supported**: CallawaySantAnna (reg/ipw/dr without covariates, no
2050+
bootstrap), ContinuousDiD
20502051
(no bootstrap), EfficientDiD (no bootstrap), TripleDifference (all
20512052
methods), LinearRegression (OLS path)
20522053
- **Rejected with NotImplementedError**: SunAbraham, TwoWayFixedEffects
20532054
(within-transformation must be recomputed per replicate),
20542055
DifferenceInDifferences, MultiPeriodDiD, StackedDiD (use
20552056
compute_survey_vcov directly), ImputationDiD, TwoStageDiD (custom
2056-
variance), SyntheticDiD, TROP (bootstrap-based variance)
2057+
variance), SyntheticDiD, TROP (bootstrap-based variance),
2058+
BaconDecomposition (diagnostic only)
20572059
- CS/ContinuousDiD/EfficientDiD reject replicate + `n_bootstrap > 0`
20582060
(replicate weights provide analytical variance)
20592061
- **Note:** When invalid replicates are dropped in `compute_replicate_vcov`

tests/test_survey.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,7 +2367,7 @@ def test_fractional_fweight_rejected_solve_ols(self):
23672367
y = np.random.randn(n)
23682368
w = np.array([1.5, 2.3, 1.0, 2.0, 1.7, 3.0, 1.0, 2.0, 1.0, 1.0])
23692369

2370-
with pytest.raises(ValueError, match="positive integers"):
2370+
with pytest.raises(ValueError, match="non-negative integers"):
23712371
solve_ols(X, y, weights=w, weight_type="fweight")
23722372

23732373
def test_fractional_fweight_rejected_compute_robust_vcov(self):
@@ -2378,7 +2378,7 @@ def test_fractional_fweight_rejected_compute_robust_vcov(self):
23782378
resid = np.random.randn(n)
23792379
w = np.array([1.5, 2.0, 1.0, 2.0, 1.0, 3.0, 1.0, 2.0, 1.0, 1.0])
23802380

2381-
with pytest.raises(ValueError, match="positive integers"):
2381+
with pytest.raises(ValueError, match="non-negative integers"):
23822382
compute_robust_vcov(X, resid, weights=w, weight_type="fweight")
23832383

23842384
def test_integer_fweight_accepted(self):

tests/test_survey_phase6.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,56 @@ def test_invalid_replicate_rscales_rejected(self):
14081408
replicate_method="JK1", replicate_rscales=[-1.0, 1.0],
14091409
)
14101410

1411+
def _replicate_sd_and_data(self):
1412+
data, rep_cols = TestEstimatorReplicateWeights._make_staggered_replicate_data()
1413+
sd = SurveyDesign(
1414+
weights="weight", replicate_weights=rep_cols,
1415+
replicate_method="JK1",
1416+
)
1417+
return data, sd
1418+
1419+
def test_multi_period_did_replicate_rejected(self):
1420+
"""MultiPeriodDiD rejects replicate-weight designs."""
1421+
from diff_diff.estimators import MultiPeriodDiD
1422+
data, sd = self._replicate_sd_and_data()
1423+
data["treated"] = (data["first_treat"] > 0).astype(int)
1424+
data["post"] = (data["time"] >= 3).astype(int)
1425+
with pytest.raises(NotImplementedError):
1426+
MultiPeriodDiD().fit(
1427+
data, outcome="outcome", treatment="treated",
1428+
time="post", survey_design=sd,
1429+
)
1430+
1431+
def test_imputation_did_replicate_rejected(self):
1432+
"""ImputationDiD rejects replicate-weight designs."""
1433+
from diff_diff.imputation import ImputationDiD
1434+
data, sd = self._replicate_sd_and_data()
1435+
with pytest.raises(NotImplementedError):
1436+
ImputationDiD().fit(
1437+
data, outcome="outcome", unit="unit", time="time",
1438+
first_treat="first_treat", survey_design=sd,
1439+
)
1440+
1441+
def test_two_stage_did_replicate_rejected(self):
1442+
"""TwoStageDiD rejects replicate-weight designs."""
1443+
from diff_diff.two_stage import TwoStageDiD
1444+
data, sd = self._replicate_sd_and_data()
1445+
with pytest.raises(NotImplementedError):
1446+
TwoStageDiD().fit(
1447+
data, outcome="outcome", unit="unit", time="time",
1448+
first_treat="first_treat", survey_design=sd,
1449+
)
1450+
1451+
def test_bacon_replicate_rejected(self):
1452+
"""BaconDecomposition rejects replicate-weight designs."""
1453+
from diff_diff.bacon import BaconDecomposition
1454+
data, sd = self._replicate_sd_and_data()
1455+
with pytest.raises(NotImplementedError):
1456+
BaconDecomposition().fit(
1457+
data, outcome="outcome", unit="unit", time="time",
1458+
first_treat="first_treat", survey_design=sd,
1459+
)
1460+
14111461

14121462
# =============================================================================
14131463
# Effective-sample and d.f. consistency tests

0 commit comments

Comments
 (0)