Skip to content

Commit dd8f51b

Browse files
igerberclaude
andcommitted
Guard all replicate-df paths for NaN inference when rank <= 1
Fix three remaining gaps where undefined replicate df still produced finite inference: 1. safe_inference/safe_inference_batch: early-return all-NaN when df<=0 2. LinearRegression.get_inference: skip generic "df<=0 → normal" fallback for replicate designs so df=0 sentinel flows through to safe_inference 3. EfficientDiD: re-apply replicate guard after unit-level design rebuild overwrites self._survey_df 4. CallawaySantAnna: add guard at first df_survey read (covers general survey+covariate g,t path) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ab5377f commit dd8f51b

4 files changed

Lines changed: 21 additions & 1 deletion

File tree

diff_diff/efficient_did.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,10 @@ def fit(
527527
)
528528
# Use unit-level df (not panel-level) for t-distribution
529529
self._survey_df = self._unit_resolved_survey.df_survey
530+
# Re-apply replicate guard: undefined df → NaN inference
531+
if (self._survey_df is None
532+
and self._unit_resolved_survey.uses_replicate_variance):
533+
self._survey_df = 0
530534
else:
531535
self._unit_resolved_survey = None
532536

diff_diff/linalg.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2043,7 +2043,11 @@ def get_inference(
20432043
effective_df = self.df_
20442044

20452045
# Warn if df is non-positive and fall back to normal distribution
2046-
if effective_df is not None and effective_df <= 0:
2046+
# (skip for replicate designs — df=0 is intentional for NaN inference)
2047+
_is_replicate = (hasattr(self, 'survey_design') and self.survey_design is not None
2048+
and hasattr(self.survey_design, 'uses_replicate_variance')
2049+
and self.survey_design.uses_replicate_variance)
2050+
if effective_df is not None and effective_df <= 0 and not _is_replicate:
20472051
import warnings
20482052

20492053
warnings.warn(

diff_diff/staggered.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,11 @@ def fit(
14101410
# Survey df for safe_inference calls — use the unit-level resolved
14111411
# survey df computed in _precompute_structures for consistency.
14121412
df_survey = precomputed.get("df_survey")
1413+
# Guard: replicate design with undefined df (rank <= 1) → NaN inference
1414+
if (df_survey is None and resolved_survey is not None
1415+
and hasattr(resolved_survey, 'uses_replicate_variance')
1416+
and resolved_survey.uses_replicate_variance):
1417+
df_survey = 0
14131418

14141419
# Compute ATT(g,t) for each group-time combination
14151420
min_period = min(time_periods)

diff_diff/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ def safe_inference(effect, se, alpha=0.05, df=None):
177177
"""
178178
if not (np.isfinite(se) and se > 0):
179179
return np.nan, np.nan, (np.nan, np.nan)
180+
if df is not None and df <= 0:
181+
# Undefined degrees of freedom (e.g., rank-deficient replicate design)
182+
return np.nan, np.nan, (np.nan, np.nan)
180183
t_stat = effect / se
181184
p_value = compute_p_value(t_stat, df=df)
182185
conf_int = compute_confidence_interval(effect, se, alpha, df=df)
@@ -213,6 +216,10 @@ def safe_inference_batch(effects, ses, alpha=0.05, df=None):
213216
ci_lowers = np.full(n, np.nan)
214217
ci_uppers = np.full(n, np.nan)
215218

219+
# Undefined df (e.g., rank-deficient replicate design) → all NaN
220+
if df is not None and df <= 0:
221+
return t_stats, p_values, ci_lowers, ci_uppers
222+
216223
valid = np.isfinite(ses) & (ses > 0)
217224
if not np.any(valid):
218225
return t_stats, p_values, ci_lowers, ci_uppers

0 commit comments

Comments
 (0)