Skip to content

Commit a3d2343

Browse files
igerberclaude
andcommitted
Bacon empty-comparisons guard, document zero-weight df, complete test coverage
- Bacon: raise ValueError when all 2x2 comparisons dropped after zero-weight filtering (prevents empty decomposition results) - Document survey df convention: total n (including zero-weight rows) matches R's survey::degf() after subset() — preserves design structure - Add SyntheticDiD and TROP replicate rejection tests - Add CS ipw/dr (no covariates) replicate positive-path tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8b8dcb3 commit a3d2343

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

diff_diff/bacon.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,13 @@ def fit(
585585
weights=survey_weights,
586586
)
587587

588+
if not comparisons:
589+
raise ValueError(
590+
"No valid 2x2 comparisons remain after filtering. "
591+
"All cells have zero effective weight or insufficient data. "
592+
"Check subpopulation/domain definition."
593+
)
594+
588595
# Normalize weights to sum to 1
589596
total_weight = sum(c.weight for c in comparisons)
590597
if total_weight > 0:

docs/methodology/REGISTRY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,6 +2093,11 @@ Domain estimation preserving full design structure.
20932093
- **Note:** Weight validation relaxed from "strictly positive" to
20942094
"non-negative" to support zero-weight observations. Negative weights
20952095
still rejected. All-zero weight vectors rejected at solver level.
2096+
- **Note:** Survey df and variance adjustment factors use total n
2097+
(including zero-weight rows), matching R's `survey::degf()` convention
2098+
after `subset()`. This preserves the design structure for correct
2099+
variance estimation. Zero-weight rows contribute zero-valued scores
2100+
to the sandwich meat but are counted in df = n_PSU - n_strata.
20962101
- **Note:** For replicate-weight designs, `subpopulation()` zeros out both
20972102
full-sample and replicate weight columns for excluded observations,
20982103
preserving all replicate metadata.

tests/test_survey_phase6.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,57 @@ def test_bacon_replicate_rejected(self):
14581458
first_treat="first_treat", survey_design=sd,
14591459
)
14601460

1461+
def test_synthetic_did_replicate_rejected(self):
1462+
"""SyntheticDiD rejects replicate-weight designs."""
1463+
from diff_diff.synthetic_did import SyntheticDiD
1464+
data, sd = self._replicate_sd_and_data()
1465+
data["treated"] = (data["first_treat"] > 0).astype(int)
1466+
with pytest.raises(NotImplementedError):
1467+
SyntheticDiD().fit(
1468+
data, outcome="outcome", unit="unit", time="time",
1469+
treatment="treated", survey_design=sd,
1470+
)
1471+
1472+
@pytest.mark.slow
1473+
def test_trop_replicate_rejected(self):
1474+
"""TROP rejects replicate-weight designs."""
1475+
from diff_diff.trop import TROP
1476+
data, sd = self._replicate_sd_and_data()
1477+
data["treated"] = (data["first_treat"] > 0).astype(int)
1478+
with pytest.raises(NotImplementedError):
1479+
TROP().fit(
1480+
data, outcome="outcome", unit="unit", time="time",
1481+
treatment="treated", survey_design=sd,
1482+
)
1483+
1484+
1485+
class TestCSReplicateMethodCoverage:
1486+
"""Test that CS replicate weights work for all supported estimation methods."""
1487+
1488+
@staticmethod
1489+
def _make_cs_replicate_data():
1490+
data, rep_cols = TestEstimatorReplicateWeights._make_staggered_replicate_data()
1491+
sd = SurveyDesign(
1492+
weights="weight", replicate_weights=rep_cols,
1493+
replicate_method="JK1",
1494+
)
1495+
return data, sd
1496+
1497+
@pytest.mark.parametrize("method", ["ipw", "dr"])
1498+
def test_cs_replicate_ipw_dr_no_covariates(self, method):
1499+
"""CS replicate weights work for ipw/dr without covariates."""
1500+
from diff_diff import CallawaySantAnna
1501+
data, sd = self._make_cs_replicate_data()
1502+
result = CallawaySantAnna(
1503+
estimation_method=method, n_bootstrap=0,
1504+
).fit(
1505+
data, "outcome", "unit", "time", "first_treat",
1506+
survey_design=sd,
1507+
)
1508+
assert np.isfinite(result.overall_att)
1509+
assert np.isfinite(result.overall_se)
1510+
assert result.survey_metadata is not None
1511+
14611512

14621513
# =============================================================================
14631514
# Effective-sample and d.f. consistency tests

0 commit comments

Comments
 (0)