Skip to content

Commit 2449842

Browse files
igerberclaude
andcommitted
callaway-santanna: align docs with narrowed df_inference contract
Two P3 doc-only follow-ups: 1. llms-full.txt CS entry kept vcov_type before n_bootstrap in the pseudo-signature even after the constructor was reordered to append vcov_type at the end. Moved vcov_type to the end of the pseudo- signature so the generated guide matches the actual __init__ order (matches SA / WooldridgeDiD convention). 2. staggered_results.py class attribute docstring + inline field comment for df_inference still described it as populated on "Legitimate survey_design= fits" alongside survey_metadata. Updated to reflect the narrowed contract: df_inference is populated on the bare-cluster- synthesize path ONLY (where survey_metadata is None). When user provides survey_design=, df_inference stays None and the canonical df carrier is survey_metadata.df_survey. HonestDiD reads survey_metadata first, df_inference fallback. 3. test_explicit_survey_design_does_populate_survey_metadata docstring still said "+ df_inference also populated"; updated to match the body assertion (which already correctly asserts df_inference is None). No source/test logic changes. All 80 targeted tests + test_honest_did pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 7c6146e commit 2449842

3 files changed

Lines changed: 33 additions & 21 deletions

File tree

diff_diff/guides/llms-full.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,14 @@ CallawaySantAnna(
189189
estimation_method: str = "dr", # "dr", "ipw", or "reg"
190190
alpha: float = 0.05,
191191
cluster: str | None = None, # Cluster col; activates CR1 on the IF via synthesized SurveyDesign(psu=col). None → per-unit IF.
192-
vcov_type: str = "hc1", # {"hc1"} only — IF-based variance per Callaway & Sant'Anna (2021). Analytical-sandwich {classical, hc2, hc2_bm} and conley REJECTED at __init__ (see REGISTRY.md IF-vs-sandwich subsection).
193192
n_bootstrap: int = 0, # 0 = analytical SEs, 999+ recommended
194193
bootstrap_weights: str | None = None, # "rademacher", "mammen", or "webb"
195194
seed: int | None = None,
196195
rank_deficient_action: str = "warn",
197196
base_period: str = "varying", # "varying" or "universal"
198197
cband: bool = True, # Simultaneous confidence bands
199198
pscore_trim: float = 0.01, # Propensity score trimming bound
199+
vcov_type: str = "hc1", # {"hc1"} only — IF-based variance per Callaway & Sant'Anna (2021). Analytical-sandwich {classical, hc2, hc2_bm} and conley REJECTED at __init__ (see REGISTRY.md IF-vs-sandwich subsection).
200200
)
201201
```
202202

diff_diff/staggered_results.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,19 @@ class CallawaySantAnnaResults:
111111
``None`` when no clustering is active.
112112
df_inference : int, optional
113113
Cluster-level degrees of freedom for downstream inference (e.g.,
114-
``HonestDiD`` t-critical-value selection). Carried separately from
115-
``survey_metadata`` so consumers can distinguish "cluster df is
116-
available" from "user provided a survey design": bare
117-
``CallawaySantAnna(cluster=X).fit(...)`` populates ``df_inference``
118-
but leaves ``survey_metadata`` as ``None`` (preserving the
119-
survey/non-survey contract for ``DiagnosticReport`` / ``summary()``).
120-
Legitimate ``survey_design=`` fits populate both, and the two
121-
surfaces agree (``df_inference == survey_metadata.df_survey``).
114+
``HonestDiD`` t-critical-value selection) on the bare-``cluster=``
115+
synthesize path ONLY (the case where ``survey_metadata`` is
116+
intentionally ``None`` to preserve the survey/non-survey contract
117+
for ``DiagnosticReport`` / ``summary()``). When the user provides
118+
an explicit ``survey_design=`` (inject or conflict branches),
119+
``df_inference`` stays ``None`` and the canonical df carrier is
120+
``survey_metadata.df_survey`` — which holds the actual CS-internal
121+
df, including any post-resolve tightening (e.g., the
122+
``overall_effective_df`` recompute for replicate aggregations).
123+
``HonestDiD`` reads ``survey_metadata.df_survey`` first and falls
124+
back to ``df_inference`` only when ``survey_metadata`` is absent.
125+
Narrow contract prevents HonestDiD from silently overriding a
126+
tightened survey df with the original ``resolved_survey.df_survey``.
122127
"""
123128

124129
group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]]
@@ -170,13 +175,16 @@ class CallawaySantAnnaResults:
170175
cluster_name: Optional[str] = None
171176
n_clusters: Optional[int] = None
172177
# df_inference: cluster-level degrees of freedom for downstream
173-
# inference (e.g., HonestDiD's t-critical-value selection). Carried
174-
# SEPARATELY from survey_metadata so consumers can distinguish
175-
# "cluster df is available" from "user provided a survey design"
176-
# — bare CS(cluster="state").fit(...) populates df_inference but
177-
# leaves survey_metadata as None (DiagnosticReport / summary() must
178-
# not treat a bare-cluster fit as survey-backed). Legitimate survey
179-
# designs populate both (df_inference == survey_metadata.df_survey).
178+
# inference, populated on the bare-cluster-synthesize path ONLY.
179+
# When the user provides an explicit survey_design= (inject or
180+
# conflict branches), df_inference stays None and the canonical df
181+
# carrier is survey_metadata.df_survey (which holds the actual
182+
# CS-internal df, including any post-resolve tightening via the
183+
# overall_effective_df recompute at staggered.py:~1995-1999).
184+
# HonestDiD reads survey_metadata.df_survey first and falls back to
185+
# df_inference only when survey_metadata is absent. Narrow contract
186+
# prevents HonestDiD from silently overriding a tightened survey df
187+
# with the original resolved_survey.df_survey.
180188
df_inference: Optional[int] = None
181189

182190
# --- Inference-field aliases (balance/external-adapter compatibility) ---

tests/test_staggered.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4704,11 +4704,15 @@ def test_bare_cluster_does_not_set_survey_metadata(self):
47044704
)
47054705

47064706
def test_explicit_survey_design_does_populate_survey_metadata(self):
4707-
"""Counterpart to the test above: when user provides a real
4708-
SurveyDesign, survey_metadata IS populated (regardless of bare
4709-
cluster= status). Verifies the 'inject' branch path:
4710-
SurveyDesign(weights=...) + cluster=X → survey_metadata populated
4711-
+ df_inference also populated."""
4707+
"""Counterpart to test_bare_cluster_does_not_set_survey_metadata:
4708+
when user provides a real SurveyDesign, survey_metadata IS
4709+
populated (regardless of bare cluster= status). Verifies the
4710+
'inject' branch path: SurveyDesign(weights=...) + cluster=X →
4711+
survey_metadata populated; df_inference stays None per the
4712+
narrowed contract (canonical df carrier when survey_metadata is
4713+
present is survey_metadata.df_survey, which holds CS-internal
4714+
post-resolve-tightened df). HonestDiD reads survey_metadata
4715+
first, df_inference only as fallback."""
47124716
from diff_diff import SurveyDesign
47134717

47144718
data = _generate_clustered_staggered_data(seed=75)

0 commit comments

Comments
 (0)