Skip to content

Commit b151613

Browse files
igerberclaude
andcommitted
Reject pscore_trim=0.0 to prevent infinite IPW weights; update docs
- Change pscore_trim validation from [0, 0.5) to (0, 0.5) since zero trimming allows exact 1.0 propensity scores through to the weight formula pscore/(1-pscore), producing inf/NaN - Update REGISTRY.md fallback note to document error-mode re-raise - Add pscore_trim to CallawaySantAnna and CallawaySantAnnaResults docstrings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 96c71de commit b151613

4 files changed

Lines changed: 14 additions & 7 deletions

File tree

diff_diff/staggered.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ class CallawaySantAnna(
164164
event study aggregation. Requires ``n_bootstrap > 0``.
165165
When True, results include ``cband_crit_value`` and per-event-time
166166
``cband_conf_int`` entries controlling family-wise error rate.
167+
pscore_trim : float, default=0.01
168+
Trimming bound for propensity scores. Scores are clipped to
169+
``[pscore_trim, 1 - pscore_trim]`` before weight computation
170+
in IPW and DR estimation. Must be in ``(0, 0.5)``.
167171
168172
Attributes
169173
----------
@@ -265,8 +269,8 @@ def __init__(
265269
raise ValueError(
266270
f"estimation_method must be 'dr', 'ipw', or 'reg', " f"got '{estimation_method}'"
267271
)
268-
if not (0 <= pscore_trim < 0.5):
269-
raise ValueError(f"pscore_trim must be in [0, 0.5), got {pscore_trim}")
272+
if not (0 < pscore_trim < 0.5):
273+
raise ValueError(f"pscore_trim must be in (0, 0.5), got {pscore_trim}")
270274

271275
# Handle bootstrap_weight_type deprecation
272276
if bootstrap_weight_type is not None:

diff_diff/staggered_results.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ class CallawaySantAnnaResults:
9393
Effects aggregated by relative time (event study).
9494
group_effects : dict, optional
9595
Effects aggregated by treatment cohort.
96+
pscore_trim : float
97+
Propensity score trimming bound used during estimation.
9698
"""
9799

98100
group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]]

docs/methodology/REGISTRY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,8 @@ The multiplier bootstrap uses random weights w_i with E[w]=0 and Var(w)=1:
402402
- Trimming: Propensity scores clipped to `[pscore_trim, 1-pscore_trim]` (default
403403
0.01) before weight computation. Warning emitted when scores are trimmed.
404404
- Fallback: If IRLS fails entirely (LinAlgError/ValueError), falls back to
405-
unconditional propensity score with warning
405+
unconditional propensity score with warning. Exception: when
406+
`rank_deficient_action="error"`, the error is re-raised instead of falling back.
406407
- Control group with `control_group="not_yet_treated"`:
407408
- Always excludes cohort g from controls when computing ATT(g,t)
408409
- This applies to both pre-treatment (t < g) and post-treatment (t >= g) periods

tests/test_staggered.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3240,10 +3240,10 @@ def test_pscore_trim_above_half_raises(self):
32403240
with pytest.raises(ValueError, match="pscore_trim must be in"):
32413241
CallawaySantAnna(pscore_trim=0.6)
32423242

3243-
def test_pscore_trim_zero_succeeds(self):
3244-
"""pscore_trim=0.0 is valid (no trimming)."""
3245-
cs = CallawaySantAnna(pscore_trim=0.0)
3246-
assert cs.pscore_trim == 0.0
3243+
def test_pscore_trim_zero_raises(self):
3244+
"""pscore_trim=0.0 raises ValueError (would cause division by zero in IPW weights)."""
3245+
with pytest.raises(ValueError, match="pscore_trim must be in"):
3246+
CallawaySantAnna(pscore_trim=0.0)
32473247

32483248
def test_pscore_trim_in_results(self):
32493249
"""results.pscore_trim matches the estimator's setting after fit()."""

0 commit comments

Comments
 (0)