Skip to content

Commit eef8af4

Browse files
igerberclaude
andcommitted
Address PR #376 R1 (1 P1 + 1 P2)
R1 P1: deprecated `survey=SurveyDesign(...)` alias didn't trigger the SurveyDesign type guard on stute_test, yatchew_hr_test, stute_joint_pretest because the guard ran BEFORE the alias rebinding. Move the guard AFTER the soft-deprecation block so it covers both `survey_design=SurveyDesign(...)` (canonical) and `survey=SurveyDesign(...)` (deprecated alias) identically. Adds 3 regression tests in TestArrayInTypeGuard covering the alias path on all 3 array-in surfaces. R1 P2: REGISTRY.md had two contradictory notes on HAD survey support — the pre-Phase-4.5-C bullet said "pretests still do NOT accept survey/weights" while the Phase 4.5 C bullet listed all 8 surfaces as supporting them. Rewrote the older bullet to reflect the current Phase 4.5 B + C state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b0cf96b commit eef8af4

3 files changed

Lines changed: 92 additions & 37 deletions

File tree

diff_diff/had_pretests.py

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,19 +1558,9 @@ def stute_test(
15581558
if n_set > 1:
15591559
raise ValueError(HAD_DUAL_KNOB_MUTEX_MSG_ARRAY_IN)
15601560

1561-
# Type guard: array-in helpers reject SurveyDesign (cannot resolve column
1562-
# names without `data`).
1563-
if survey_design is not None and isinstance(survey_design, SurveyDesign):
1564-
raise TypeError(
1565-
"stute_test: `survey_design=` accepts a pre-resolved "
1566-
"ResolvedSurveyDesign only (array-in helpers have no `data` to "
1567-
"resolve column names against). For pweight-only, use "
1568-
"`survey_design=make_pweight_design(arr)`. For full PSU/strata/"
1569-
"FPC, pre-resolve via `SurveyDesign(...).resolve(data)` and pass "
1570-
"the result."
1571-
)
1572-
1573-
# Soft deprecation: route legacy survey=/weights= aliases to survey_design=.
1561+
# Soft deprecation: route legacy survey=/weights= aliases to survey_design=
1562+
# FIRST so the type guard below covers `survey=SurveyDesign(...)` too
1563+
# (PR #376 R1 P1: alias must behave identically to the canonical kwarg).
15741564
# The bit-exact normalization-order invariant requires passing UNNORMALIZED
15751565
# weights to make_pweight_design; the unified path's mean=1 step (~line
15761566
# 1669) fires downstream EXACTLY ONCE.
@@ -1585,6 +1575,20 @@ def stute_test(
15851575
)
15861576
survey_design = make_pweight_design(np.asarray(weights, dtype=np.float64))
15871577

1578+
# Type guard: array-in helpers reject SurveyDesign (cannot resolve column
1579+
# names without `data`). Runs AFTER alias rebinding so it covers both
1580+
# `survey_design=SurveyDesign(...)` and the deprecated
1581+
# `survey=SurveyDesign(...)` form identically.
1582+
if survey_design is not None and isinstance(survey_design, SurveyDesign):
1583+
raise TypeError(
1584+
"stute_test: `survey_design=` accepts a pre-resolved "
1585+
"ResolvedSurveyDesign only (array-in helpers have no `data` to "
1586+
"resolve column names against). For pweight-only, use "
1587+
"`survey_design=make_pweight_design(arr)`. For full PSU/strata/"
1588+
"FPC, pre-resolve via `SurveyDesign(...).resolve(data)` and pass "
1589+
"the result."
1590+
)
1591+
15881592
# Internal alias rebind: downstream code uses `survey` and `weights` as
15891593
# internal variable names (Phase 4.5 C convention). After the deprecation
15901594
# block, fold the canonical survey_design back into the legacy variable
@@ -2031,17 +2035,9 @@ def yatchew_hr_test(
20312035
if n_set > 1:
20322036
raise ValueError(HAD_DUAL_KNOB_MUTEX_MSG_ARRAY_IN)
20332037

2034-
# Type guard: array-in helpers reject SurveyDesign.
2035-
if survey_design is not None and isinstance(survey_design, SurveyDesign):
2036-
raise TypeError(
2037-
"yatchew_hr_test: `survey_design=` accepts a pre-resolved "
2038-
"ResolvedSurveyDesign only (array-in helpers have no `data` to "
2039-
"resolve column names against). For pweight-only, use "
2040-
"`survey_design=make_pweight_design(arr)`. For full PSU/strata/"
2041-
"FPC, pre-resolve via `SurveyDesign(...).resolve(data)`."
2042-
)
2043-
2044-
# Soft deprecation: route legacy survey=/weights= aliases to survey_design=.
2038+
# Soft deprecation: route legacy survey=/weights= aliases to survey_design=
2039+
# FIRST so the type guard below covers `survey=SurveyDesign(...)` too
2040+
# (PR #376 R1 P1: alias must behave identically to the canonical kwarg).
20452041
if survey is not None:
20462042
warnings.warn(HAD_DEPRECATION_MSG_SURVEY_KWARG, DeprecationWarning, stacklevel=2)
20472043
survey_design = survey
@@ -2053,6 +2049,18 @@ def yatchew_hr_test(
20532049
)
20542050
survey_design = make_pweight_design(np.asarray(weights, dtype=np.float64))
20552051

2052+
# Type guard: array-in helpers reject SurveyDesign. Runs AFTER alias
2053+
# rebinding so it covers both `survey_design=SurveyDesign(...)` and the
2054+
# deprecated `survey=SurveyDesign(...)` form identically.
2055+
if survey_design is not None and isinstance(survey_design, SurveyDesign):
2056+
raise TypeError(
2057+
"yatchew_hr_test: `survey_design=` accepts a pre-resolved "
2058+
"ResolvedSurveyDesign only (array-in helpers have no `data` to "
2059+
"resolve column names against). For pweight-only, use "
2060+
"`survey_design=make_pweight_design(arr)`. For full PSU/strata/"
2061+
"FPC, pre-resolve via `SurveyDesign(...).resolve(data)`."
2062+
)
2063+
20562064
# Internal alias rebind for back-compat with downstream code.
20572065
survey = survey_design
20582066
weights = None
@@ -2709,17 +2717,9 @@ def stute_joint_pretest(
27092717
if n_set > 1:
27102718
raise ValueError(HAD_DUAL_KNOB_MUTEX_MSG_ARRAY_IN)
27112719

2712-
# Type guard: array-in helpers reject SurveyDesign.
2713-
if survey_design is not None and isinstance(survey_design, SurveyDesign):
2714-
raise TypeError(
2715-
"stute_joint_pretest: `survey_design=` accepts a pre-resolved "
2716-
"ResolvedSurveyDesign only (array-in helpers have no `data` to "
2717-
"resolve column names against). For pweight-only, use "
2718-
"`survey_design=make_pweight_design(arr)`. For full PSU/strata/"
2719-
"FPC, pre-resolve via `SurveyDesign(...).resolve(data)`."
2720-
)
2721-
2722-
# Soft deprecation: route legacy survey=/weights= aliases to survey_design=.
2720+
# Soft deprecation: route legacy survey=/weights= aliases to survey_design=
2721+
# FIRST so the type guard below covers `survey=SurveyDesign(...)` too
2722+
# (PR #376 R1 P1: alias must behave identically to the canonical kwarg).
27232723
if survey is not None:
27242724
warnings.warn(HAD_DEPRECATION_MSG_SURVEY_KWARG, DeprecationWarning, stacklevel=2)
27252725
survey_design = survey
@@ -2731,6 +2731,18 @@ def stute_joint_pretest(
27312731
)
27322732
survey_design = make_pweight_design(np.asarray(weights, dtype=np.float64))
27332733

2734+
# Type guard: array-in helpers reject SurveyDesign. Runs AFTER alias
2735+
# rebinding so it covers both `survey_design=SurveyDesign(...)` and the
2736+
# deprecated `survey=SurveyDesign(...)` form identically.
2737+
if survey_design is not None and isinstance(survey_design, SurveyDesign):
2738+
raise TypeError(
2739+
"stute_joint_pretest: `survey_design=` accepts a pre-resolved "
2740+
"ResolvedSurveyDesign only (array-in helpers have no `data` to "
2741+
"resolve column names against). For pweight-only, use "
2742+
"`survey_design=make_pweight_design(arr)`. For full PSU/strata/"
2743+
"FPC, pre-resolve via `SurveyDesign(...).resolve(data)`."
2744+
)
2745+
27342746
# Internal alias rebind for back-compat with downstream code.
27352747
survey = survey_design
27362748
weights = None

docs/methodology/REGISTRY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2347,7 +2347,7 @@ Under `survey=SurveyDesign(weights, strata, psu, fpc)`, the variance composes vi
23472347
- **Note:** Monte Carlo oracle consistency — `tests/test_had_mc.py` validates that the weighted estimator recovers the oracle τ under informative sampling, with coverage near nominal and visible bias reduction vs unweighted. Slow-gated; 4 tests.
23482348
- **Note:** Auto-bandwidth selection (Phase 1b MSE-DPI via `lpbwselect_mse_dpi`) remains UNWEIGHTED in this phase; users who want a weight-aware bandwidth should pass `h`/`b` explicitly. The auto path with uniform weights reduces to the existing unweighted bandwidth selector, so the uniform-weights bit-parity chain is preserved.
23492349
- **Note:** Replicate-weight SurveyDesigns (BRR / Fay / JK1 / JKn / SDR) on the HAD continuous path raise `NotImplementedError` in this PR; Rao-Wu-style rescaled bootstrap is deferred to Phase 4.5 C (survey-under-pretests).
2350-
- **Note:** `HeterogeneousAdoptionDiD.fit()` dispatch matrix after Phase 4.5 B — survey / weights are supported on ALL design × aggregate combinations (continuous × {overall, event-study}, mass-point × {overall, event-study}). Pretests (`qug_test`, `stute_test`, `yatchew_hr_test`, joint Stute variants, `did_had_pretest_workflow`) still do NOT accept `survey=` / `weights=` — deferred to Phase 4.5 C / C0 per reciprocal-guard discipline.
2350+
- **Note:** `HeterogeneousAdoptionDiD.fit()` dispatch matrix after Phase 4.5 B + 4.5 C — survey/weights are supported on ALL design × aggregate combinations (continuous × {overall, event-study}, mass-point × {overall, event-study}). The HAD pretests (`qug_test`, `stute_test`, `yatchew_hr_test`, joint Stute variants, `did_had_pretest_workflow`) ship survey support in Phase 4.5 C (PR #370) — `qug_test` permanently rejects (Phase 4.5 C0 deferral; see "QUG Null Test" §); the linearity family supports pweight + PSU + FPC via PSU-level Mammen multipliers (Stute) + closed-form weighted variance components (Yatchew); replicate-weight and stratified designs raise `NotImplementedError` (parallel follow-ups). The canonical kwarg on all 8 HAD surfaces is `survey_design=` (see "Note (HAD survey-design API consolidation)" below); `survey=` / `weights=` remain accepted as deprecated aliases for one minor cycle.
23512351
- **Note (HAD survey-design API consolidation):** All 8 HAD surfaces — `HeterogeneousAdoptionDiD.fit`, `did_had_pretest_workflow`, `qug_test`, `stute_test`, `yatchew_hr_test`, `stute_joint_pretest`, `joint_pretrends_test`, `joint_homogeneity_test` — accept the canonical kwarg `survey_design=` (matching `ContinuousDiD`, `EfficientDiD`, `ChaisemartinDHaultfoeuille`). The pre-existing dual `survey=` and `weights=` kwargs become deprecated aliases (`DeprecationWarning`); both will be removed in the next minor release. Internal back-end behavior is UNCHANGED (the legacy paths for `weights=np.ndarray` and `survey=SurveyDesign(...)` still execute the same code; only the entry signature wraps them). Mutex semantics extend from 2-way (`survey + weights`) to 3-way (`survey_design + survey + weights`) — at most one may be non-None per call. Two distinct mutex error messages per surface group: data-in surfaces (HAD.fit + workflow + joint data-in wrappers) point users to `survey_design=SurveyDesign(weights='col_name', ...)`; array-in surfaces (`stute_test`/`yatchew_hr_test`/`stute_joint_pretest`/`qug_test`) point to `survey_design=make_pweight_design(arr)` (for pweight-only) or `survey_design=<pre-resolved ResolvedSurveyDesign>` (for full PSU/strata/FPC). Array-in helpers reject `survey_design=SurveyDesign(...)` with `TypeError` since they have no `data` to resolve column names against. The `make_pweight_design(weights: np.ndarray) -> ResolvedSurveyDesign` factory is exported from the `diff_diff` top level (formerly `survey._make_trivial_resolved`, kept as a permanent private alias for back-compat).
23522352

23532353
*Weighted 2SLS (Phase 4.5 B):* `_fit_mass_point_2sls(..., weights=, return_influence=)` extends the Wald-IV / 2SLS sandwich with pweight semantics:

tests/test_had_dual_knob_deprecation.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,18 +134,40 @@ def test_make_pweight_design_eq_underscore_alias(self):
134134

135135

136136
class TestArrayInTypeGuard:
137-
"""Array-in helpers reject SurveyDesign (cannot resolve column names)."""
137+
"""Array-in helpers reject SurveyDesign (cannot resolve column names).
138+
139+
Both the canonical `survey_design=SurveyDesign(...)` form AND the
140+
deprecated `survey=SurveyDesign(...)` alias trigger the same TypeError
141+
(PR #376 R1 P1: alias must behave identically to the canonical kwarg).
142+
"""
138143

139144
def test_stute_test_rejects_SurveyDesign(self, array_in_data):
140145
d, dy = array_in_data
141146
with pytest.raises(TypeError, match="make_pweight_design"):
142147
stute_test(d, dy, survey_design=SurveyDesign(weights="w"), n_bootstrap=199, seed=0)
143148

149+
def test_stute_test_rejects_SurveyDesign_via_legacy_alias(self, array_in_data):
150+
"""PR #376 R1 P1: `survey=SurveyDesign(...)` (deprecated alias) must
151+
trigger the same TypeError as `survey_design=SurveyDesign(...)`."""
152+
d, dy = array_in_data
153+
with pytest.raises(TypeError, match="make_pweight_design"):
154+
with warnings.catch_warnings():
155+
warnings.simplefilter("ignore", DeprecationWarning)
156+
stute_test(d, dy, survey=SurveyDesign(weights="w"), n_bootstrap=199, seed=0)
157+
144158
def test_yatchew_hr_test_rejects_SurveyDesign(self, array_in_data):
145159
d, dy = array_in_data
146160
with pytest.raises(TypeError, match="make_pweight_design"):
147161
yatchew_hr_test(d, dy, survey_design=SurveyDesign(weights="w"))
148162

163+
def test_yatchew_hr_test_rejects_SurveyDesign_via_legacy_alias(self, array_in_data):
164+
"""PR #376 R1 P1: alias parity with canonical kwarg."""
165+
d, dy = array_in_data
166+
with pytest.raises(TypeError, match="make_pweight_design"):
167+
with warnings.catch_warnings():
168+
warnings.simplefilter("ignore", DeprecationWarning)
169+
yatchew_hr_test(d, dy, survey=SurveyDesign(weights="w"))
170+
149171
def test_stute_joint_pretest_rejects_SurveyDesign(self):
150172
rng = np.random.default_rng(3)
151173
G = 30
@@ -164,6 +186,27 @@ def test_stute_joint_pretest_rejects_SurveyDesign(self):
164186
seed=0,
165187
)
166188

189+
def test_stute_joint_pretest_rejects_SurveyDesign_via_legacy_alias(self):
190+
"""PR #376 R1 P1: alias parity with canonical kwarg."""
191+
rng = np.random.default_rng(3)
192+
G = 30
193+
d = rng.uniform(0, 1, size=G)
194+
residuals = {0: rng.normal(0, 0.1, G)}
195+
fitted = {0: np.zeros(G)}
196+
X = np.column_stack([np.ones(G), d])
197+
with pytest.raises(TypeError, match="make_pweight_design"):
198+
with warnings.catch_warnings():
199+
warnings.simplefilter("ignore", DeprecationWarning)
200+
stute_joint_pretest(
201+
residuals_by_horizon=residuals,
202+
fitted_by_horizon=fitted,
203+
doses=d,
204+
design_matrix=X,
205+
survey=SurveyDesign(weights="w"),
206+
n_bootstrap=199,
207+
seed=0,
208+
)
209+
167210

168211
class TestScaleInvariance:
169212
"""Bit-exact normalization-order invariant (Stability invariant #7).

0 commit comments

Comments
 (0)