Skip to content

Commit a0063b8

Browse files
igerberclaude
andcommitted
HeterogeneousAdoptionDiD Phase 4.5 B: weighted mass-point 2SLS + event-study survey composition + sup-t bootstrap
Closes the two Phase 4.5 A NotImplementedError gates on design='mass_point' + weights/survey and aggregate='event_study' + weights/survey. Weighted 2SLS in _fit_mass_point_2sls follows the Wooldridge 2010 Ch. 12 pweight convention (w² in HC1 meat, w·u in CR1 cluster score, weighted bread Z'WX). HC1 and CR1 match estimatr::iv_robust bit-exactly at atol=1e-10 (new cross-language golden). Per-unit IF on β̂-scale scales so compute_survey_if_variance(psi, trivial) ≈ V_HC1 at atol=1e-10 (PR #359 convention applied uniformly). Event-study path threads weights + survey through the per-horizon loop, composing Binder-TSL variance per horizon and populating survey_metadata + variance_formula + effective_dose_mean (previously hardcoded None). New _sup_t_multiplier_bootstrap helper reuses generate_survey_multiplier_weights_batch / generate_bootstrap_weights_batch from diff_diff.bootstrap_utils — no custom Rademacher draws, no (1/n) prefactor. At H=1 reduces to Φ⁻¹(1-α/2) ≈ 1.96 (reduction-locked). New __init__ kwargs: n_bootstrap=999, seed=None. New fit() kwarg: cband=True. HeterogeneousAdoptionDiDEventStudyResults gains survey_metadata + variance_formula + effective_dose_mean + cband_* fields, surfaced through to_dict / to_dataframe / summary / __repr__. Unweighted event-study output (att/se) bit-exactly preserved; cband disabled on the unweighted path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e28080d commit a0063b8

9 files changed

Lines changed: 1642 additions & 175 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **`HeterogeneousAdoptionDiD` mass-point `survey=` / `weights=` + event-study `aggregate="event_study"` survey composition + multiplier-bootstrap sup-t simultaneous confidence band (Phase 4.5 B).** Closes the two Phase 4.5 A `NotImplementedError` gates: `design="mass_point" + weights/survey` and `aggregate="event_study" + weights/survey`. Weighted 2SLS sandwich in `_fit_mass_point_2sls` follows the Wooldridge 2010 Ch. 12 pweight convention (`w²` in the HC1 meat, `w·u` in the CR1 cluster score, weighted bread `Z'WX`); HC1 and CR1 ("stata" `se_type`) bit-parity with `estimatr::iv_robust(..., weights=, clusters=)` at `atol=1e-10` (new cross-language golden at `benchmarks/data/estimatr_iv_robust_golden.json`, generated by `benchmarks/R/generate_estimatr_iv_robust_golden.R`; `estimatr` added to `benchmarks/R/requirements.R`). `_fit_mass_point_2sls` gains `weights=` + `return_influence=` kwargs and now always returns a 3-tuple `(beta, se, psi)` — `psi` is the per-unit IF on the β̂-scale scaled so `compute_survey_if_variance(psi, trivial_resolved) ≈ V_HC1[1,1]` at `atol=1e-10` (PR #359 IF scale convention applied uniformly; no `sum(psi²)` claims). Event-study path threads `weights_unit_full` / `resolved_survey_unit_full` through the per-horizon loop, composing Binder-TSL variance per horizon via `compute_survey_if_variance` (continuous + mass-point) and populating `survey_metadata` / `variance_formula` / `effective_dose_mean` (previously hardcoded `None` at `had.py:3366`). New multiplier-bootstrap sup-t: `_sup_t_multiplier_bootstrap` reuses `diff_diff.bootstrap_utils.generate_survey_multiplier_weights_batch` (PSU-level draws with stratum centering, FPC scaling, lonely-PSU handling) / `generate_bootstrap_weights_batch` (unit-level on the `weights=` shortcut), composes `delta = weights @ IF` with NO `(1/n)` prefactor (matching `staggered_bootstrap.py:373` idiom), normalizes by per-horizon analytical SE, and takes the `(1-alpha)`-quantile of the sup-t distribution. At H=1 the quantile reduces to `Φ⁻¹(1 − alpha/2) ≈ 1.96` up to MC noise (regression-locked by `TestSupTReducesToNormalAtH1`). `HeterogeneousAdoptionDiD.__init__` gains `n_bootstrap: int = 999` and `seed: Optional[int] = None` (CS-parity singular seed); `fit()` gains `cband: bool = True` (only consulted on weighted event-study). `HeterogeneousAdoptionDiDEventStudyResults` extended with `variance_formula`, `effective_dose_mean`, `cband_low`, `cband_high`, `cband_crit_value`, `cband_method`, `cband_n_bootstrap` (all `None` on unweighted fits); surfaced in `to_dict`, `to_dataframe`, `summary`, `__repr__`. Unweighted event-study with `cband=False` preserves pre-Phase 4.5 B numerical output bit-exactly (stability invariant, locked by regression tests). Zero-weight subpopulation convention carries over from PR #359 (filter for design decisions; preserve full ResolvedSurveyDesign for variance). Non-pweight SurveyDesigns (`aweight`, `fweight`, replicate designs) raise `NotImplementedError` on both new paths (reciprocal-guard discipline). Pretest surfaces (`qug_test`, `stute_test`, `yatchew_hr_test`, joint variants, `did_had_pretest_workflow`) remain unweighted in this release — Phase 4.5 C / C0. See `docs/methodology/REGISTRY.md` §HeterogeneousAdoptionDiD "Weighted 2SLS (Phase 4.5 B)", "Event-study survey composition", and "Sup-t multiplier bootstrap" for derivations and invariants.
1112
- **`HeterogeneousAdoptionDiD.fit(survey=..., weights=...)` on continuous-dose paths (Phase 4.5 survey support).** The `continuous_at_zero` (paper Design 1') and `continuous_near_d_lower` (Design 1 continuous-near-d̲) designs accept survey weights through two interchangeable kwargs: `weights=<array>` (pweight shortcut, weighted-robust SE from the CCT-2014 lprobust port) and `survey=SurveyDesign(weights, strata, psu, fpc)` (design-based inference via Binder-TSL variance using the existing `compute_survey_if_variance` helper at `diff_diff/survey.py:1802`). Point estimates match across both entry paths; SE diverges by design (pweight-only vs PSU-aggregated). `HeterogeneousAdoptionDiDResults.survey_metadata` is a repo-standard `SurveyMetadata` dataclass (weight_type / effective_n / design_effect / sum_weights / weight_range / n_strata / n_psu / df_survey); HAD-specific extras (`variance_formula` label, `effective_dose_mean`) are separate top-level result fields. `to_dict()` surfaces the full `SurveyMetadata` object plus `variance_formula` + `effective_dose_mean`; `summary()` renders `variance_formula`, `effective_n`, `effective_dose_mean`, and (when the survey= path is used) `df_survey`; `__repr__` surfaces `variance_formula` + `effective_dose_mean` when present. The HAD `mass_point` design and `aggregate="event_study"` path raise `NotImplementedError` under survey/weights (deferred to Phase 4.5 B: weighted 2SLS + event-study survey composition); the HAD pretests stay unweighted in this release (Phase 4.5 C). Parity ceiling acknowledged — no public weighted-CCF bias-corrected local-linear reference exists in any language; methodology confidence comes from (1) uniform-weights bit-parity at `atol=1e-14` on the full lprobust output struct, (2) cross-language weighted-OLS parity (manual R reference) at `atol=1e-12`, and (3) Monte Carlo oracle consistency on known-τ DGPs. `_nprobust_port.lprobust` gains `weights=` and `return_influence=` (used internally by the Binder-TSL path); `bias_corrected_local_linear` removes the Phase 1c `NotImplementedError` on `weights=` and forwards. Auto-bandwidth selection remains unweighted in this release — pass `h`/`b` explicitly for weight-aware bandwidths. See `docs/methodology/REGISTRY.md` §HeterogeneousAdoptionDiD "Weighted extension (Phase 4.5 survey support)".
1213
- **`stute_joint_pretest`, `joint_pretrends_test`, `joint_homogeneity_test` + `StuteJointResult`** (HeterogeneousAdoptionDiD Phase 3 follow-up). Joint Cramér-von Mises pretests across K horizons with shared-η Mammen wild bootstrap (preserves vector-valued empirical-process unit-level dependence per Delgado-Manteiga 2001 / Hlávka-Hušková 2020). The core `stute_joint_pretest` is residuals-in; two thin data-in wrappers construct per-horizon residuals for the two nulls the paper spells out: mean-independence (step 2 pre-trends, `OLS(Y_t − Y_base ~ 1)` per pre-period) and linearity (step 3 joint, `OLS(Y_t − Y_base ~ 1 + D)` per post-period). Sum-of-CvMs aggregation (`S_joint = Σ_k S_k`); per-horizon scale-invariant exact-linear short-circuit. Closes the paper Section 4.2 step-2 gap that Phase 3 `did_had_pretest_workflow` previously flagged with an "Assumption 7 pre-trends test NOT run" caveat. See `docs/methodology/REGISTRY.md` §HeterogeneousAdoptionDiD "Joint Stute tests" for algorithm, invariants, and scope exclusion of Eq 18 linear-trend detrending (deferred to Phase 4 Pierce-Schott replication).
1314
- **`did_had_pretest_workflow(aggregate="event_study")`**: multi-period dispatch on balanced ≥3-period panels. Runs QUG at `F` + joint pre-trends Stute across earlier pre-periods + joint homogeneity-linearity Stute across post-periods. Step 2 closure requires ≥2 pre-periods; with only a single pre-period (the base `F-1`) `pretrends_joint=None` and the verdict flags the skip. Reuses the Phase 2b event-study panel validator (last-cohort auto-filter under staggered timing with `UserWarning`; `ValueError` when `first_treat_col=None` and the panel is staggered). The data-in wrappers `joint_pretrends_test` and `joint_homogeneity_test` also route through that same validator internally, so direct wrapper calls inherit the last-cohort filter and constant-post-dose invariant. `HADPretestReport` extended with `pretrends_joint`, `homogeneity_joint`, and `aggregate` fields; serialization methods (`summary`, `to_dict`, `to_dataframe`, `__repr__`) preserve the Phase 3 output bit-exactly on `aggregate="overall"` — no `aggregate` key, no header row, no schema drift — and only surface the new fields on `aggregate="event_study"`.

TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ Deferred items from PR reviews that were not addressed before merge.
9292
| Clustered-DGP parity: Phase 1c's DGP 4 uses manual `h=b=0.3` to sidestep an nprobust-internal singleton-cluster bug in `lpbwselect.mse.dpi`'s pilot fits. Once nprobust ships a fix (or we derive one independently), add a clustered-auto-bandwidth parity test. | `benchmarks/R/generate_nprobust_lprobust_golden.R` | Phase 1c | Low |
9393
| `HeterogeneousAdoptionDiD` joint cross-horizon covariance on event study: per-horizon SEs use INDEPENDENT sandwiches in Phase 2b (paper-faithful pointwise CIs per Pierce-Schott Figure 2). A follow-up could derive an IF-based stacking of per-horizon scores for joint cross-horizon inference (needed for joint hypothesis tests across event-time horizons). Block-bootstrap is a reasonable alternative. | `diff_diff/had.py::_fit_event_study` | Phase 2b | Low |
9494
| `HeterogeneousAdoptionDiD` event-study staggered-timing beyond last cohort: Phase 2b auto-filters staggered panels to the last cohort per paper Appendix B.2. Earlier-cohort treatment effects are not identified by HAD; redirecting to `ChaisemartinDHaultfoeuille` / `did_multiplegt_dyn` is the paper's prescription. A full staggered HAD would require a different identification path (out of paper scope). | `diff_diff/had.py::_validate_had_panel_event_study` | Phase 2b | Low |
95-
| `HeterogeneousAdoptionDiD` Phase 4.5 B: `survey=` / `weights=` on `design="mass_point"` (weighted 2SLS + weighted-sandwich variance; the Wooldridge 2010 Ch. 12 weighted-IV sandwich has a Stata `ivregress ... [pweight=...]` + R `AER::ivreg(weights=...)` parity anchor). Also ships `aggregate="event_study"` + survey/weights via per-horizon IPW + shared PSU multiplier bootstrap across horizons. This PR (Phase 4.5 A) raises `NotImplementedError` on both paths. | `diff_diff/had.py::_fit_mass_point_2sls`, `diff_diff/had.py::_fit_event_study` | Phase 4.5 B | Medium |
95+
| `HeterogeneousAdoptionDiD` joint cross-horizon analytical covariance on the weighted event-study path: Phase 4.5 B ships multiplier-bootstrap sup-t simultaneous CIs on the weighted event-study path but pointwise analytical variance is still independent across horizons. A follow-up could derive the full H × H analytical covariance from the per-horizon IF matrix (`Psi.T @ Psi` under survey weighting) for an analytical alternative to the bootstrap. Would also let the unweighted event-study path ship a sup-t band. | `diff_diff/had.py::_fit_event_study` | follow-up | Low |
96+
| `HeterogeneousAdoptionDiD` unweighted event-study sup-t band: Phase 4.5 B ships sup-t only on the WEIGHTED event-study path (to preserve pre-PR bit-exact output on unweighted). Extending sup-t to unweighted event-study (either via the multiplier bootstrap with unit-level iid multipliers or via analytical joint cross-horizon covariance) is a symmetric follow-up. | `diff_diff/had.py::_fit_event_study` | follow-up | Low |
9697
| `HeterogeneousAdoptionDiD` Phase 4.5 C0: QUG-under-survey decision gate. `qug_test` uses a ratio of extreme order statistics `D_{(1)} / (D_{(2)} - D_{(1)})` — extreme-value theory under inverse-probability weighting is a research area, not a standard toolkit. Lit-review Guillou-Hall (2001), Chen-Chen (2004); likely outcome is `NotImplementedError` on `qug_test(..., weights=...)` with a clear pointer to the Stute/Yatchew/joint pretests as the survey-supported alternatives. | `diff_diff/had_pretests.py::qug_test` | Phase 4.5 C0 | Low |
9798
| `HeterogeneousAdoptionDiD` Phase 4.5 C: pretests under survey (`stute_test`, `yatchew_hr_test`, `stute_joint_pretest`, `joint_pretrends_test`, `joint_homogeneity_test`, `did_had_pretest_workflow`). Rao-Wu rescaled bootstrap for the Stute-family (weighted η generation + PSU clustering in the bootstrap draw); weighted OLS residuals + weighted variance estimator for Yatchew. | `diff_diff/had_pretests.py` | Phase 4.5 C | Medium |
9899
| `HeterogeneousAdoptionDiD` Phase 4.5: weight-aware auto-bandwidth MSE-DPI selector. Phase 4.5 A ships weighted `lprobust` with an unweighted DPI selector; users who want a weight-aware bandwidth must pass `h`/`b` explicitly. Extending `lpbwselect_mse_dpi` to propagate weights through density, second-derivative, and variance stages is ~300 LoC of methodology and was out of scope. | `diff_diff/_nprobust_port.py::lpbwselect_mse_dpi` | Phase 4.5 | Low |
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Generate cross-language weighted-2SLS parity fixture for HAD Phase 4.5 B
2+
# (mass-point + weights).
3+
#
4+
# Purpose: validate ``_fit_mass_point_2sls(..., weights=...)`` against
5+
# ``estimatr::iv_robust(y ~ d | Z, weights=w, se_type=...)`` bit-exactly.
6+
# estimatr's HC1 sandwich and Stata-style CR1 under pweights match the
7+
# Wooldridge 2010 Ch. 12 / Angrist-Pischke 4.1.3 pweight convention
8+
# that the Python port implements (w² in the HC1 meat, w·u in the CR1
9+
# cluster score, weighted bread Z'WX). estimatr's classical SE uses a
10+
# different DOF / projection convention and is skipped in parity tests
11+
# (documented deviation; diverges by O(1/n) at non-uniform weights).
12+
#
13+
# Usage:
14+
# Rscript benchmarks/R/generate_estimatr_iv_robust_golden.R
15+
#
16+
# Output:
17+
# benchmarks/data/estimatr_iv_robust_golden.json
18+
#
19+
# Phase 4.5 B of HeterogeneousAdoptionDiD (de Chaisemartin et al. 2026).
20+
# Python test loader: tests/test_estimatr_iv_robust_parity.py.
21+
22+
library(jsonlite)
23+
library(estimatr)
24+
25+
stopifnot(packageVersion("estimatr") >= "1.0")
26+
27+
# -------------------------------------------------------------------------
28+
# DGP builders
29+
# -------------------------------------------------------------------------
30+
31+
dgp_mass_point <- function(n, seed, weight_pattern = "uniform",
32+
include_cluster = FALSE, d_lower = 0.3) {
33+
# Mass-point dose: a fraction at d_lower, rest uniform(d_lower, 1).
34+
set.seed(seed)
35+
n_mass <- round(0.15 * n)
36+
d_mass <- rep(d_lower, n_mass)
37+
d_cont <- runif(n - n_mass, d_lower, 1.0)
38+
d <- c(d_mass, d_cont)
39+
# Reshuffle to avoid ordered-by-dose artifacts.
40+
perm <- sample.int(n)
41+
d <- d[perm]
42+
43+
# True DGP: dy = 2 * d + 0.3 * d^2 + eps
44+
dy <- 2.0 * d + 0.3 * d^2 + rnorm(n, sd = 0.4)
45+
46+
# Weights
47+
w <- switch(weight_pattern,
48+
"uniform" = rep(1.0, n),
49+
"mild" = 1.0 + 0.3 * rnorm(n),
50+
"informative" = 1.0 + 2.0 * abs(d - 0.5) + runif(n, 0, 0.2),
51+
"heavy_tailed" = pmax(0.1, rlnorm(n, meanlog = 0, sdlog = 0.5))
52+
)
53+
# Clip to positive.
54+
w <- pmax(w, 0.01)
55+
56+
cluster <- if (include_cluster) sample.int(max(4, n %/% 20), n, replace = TRUE) else NULL
57+
58+
list(d = d, dy = dy, w = w, cluster = cluster, d_lower = d_lower)
59+
}
60+
61+
# -------------------------------------------------------------------------
62+
# Fit weighted 2SLS with estimatr at specified se_type.
63+
# -------------------------------------------------------------------------
64+
65+
fit_iv_robust <- function(dgp, se_type, use_cluster = FALSE) {
66+
d <- dgp$d
67+
dy <- dgp$dy
68+
w <- dgp$w
69+
Z <- as.integer(d > dgp$d_lower)
70+
df <- data.frame(d = d, dy = dy, Z = Z, w = w)
71+
if (use_cluster) df$cluster <- dgp$cluster
72+
73+
fit <- if (use_cluster) {
74+
iv_robust(dy ~ d | Z, data = df, weights = w, clusters = cluster,
75+
se_type = se_type)
76+
} else {
77+
iv_robust(dy ~ d | Z, data = df, weights = w, se_type = se_type)
78+
}
79+
80+
list(
81+
beta = as.numeric(coef(fit)["d"]),
82+
se = as.numeric(fit$std.error["d"]),
83+
# Intercept for manual sandwich verification.
84+
alpha = as.numeric(coef(fit)["(Intercept)"]),
85+
se_intercept = as.numeric(fit$std.error["(Intercept)"]),
86+
n = as.integer(nobs(fit)),
87+
se_type = se_type
88+
)
89+
}
90+
91+
# -------------------------------------------------------------------------
92+
# Build the DGP × se_type fixture grid.
93+
# -------------------------------------------------------------------------
94+
95+
# Each DGP × se_type combination becomes one fixture entry. DGPs vary
96+
# sample size, weight informativeness, and cluster structure so the
97+
# Python test exercises all three sandwich variants (HC1, classical, CR1).
98+
fixtures <- list()
99+
100+
dgps <- list(
101+
list(n = 200, seed = 42, weight = "uniform", cluster = FALSE, name = "uniform_n200"),
102+
list(n = 500, seed = 123, weight = "mild", cluster = FALSE, name = "mild_n500"),
103+
list(n = 500, seed = 7, weight = "informative", cluster = FALSE, name = "informative_n500"),
104+
list(n = 1000, seed = 321, weight = "heavy_tailed", cluster = FALSE, name = "heavy_n1000"),
105+
list(n = 600, seed = 99, weight = "informative", cluster = TRUE, name = "informative_cluster_n600")
106+
)
107+
108+
# For the non-clustered DGPs, emit HC1 + classical entries (Python
109+
# parity tests target HC1; classical deviates by O(1/n) and is recorded
110+
# as a reference only). For the clustered DGP, emit the Stata-style CR1
111+
# entry (matches `diff_diff/had.py::_fit_mass_point_2sls` CR1 convention
112+
# bit-exactly; see Gate #0 verification in the Phase 4.5 B plan).
113+
for (dgp_spec in dgps) {
114+
dgp <- dgp_mass_point(
115+
n = dgp_spec$n,
116+
seed = dgp_spec$seed,
117+
weight_pattern = dgp_spec$weight,
118+
include_cluster = dgp_spec$cluster
119+
)
120+
121+
if (dgp_spec$cluster) {
122+
entry <- list(
123+
name = dgp_spec$name,
124+
n = dgp_spec$n,
125+
d_lower = dgp$d_lower,
126+
weight_pattern = dgp_spec$weight,
127+
seed = dgp_spec$seed,
128+
d = dgp$d,
129+
dy = dgp$dy,
130+
w = dgp$w,
131+
cluster = dgp$cluster,
132+
cr1 = fit_iv_robust(dgp, se_type = "stata", use_cluster = TRUE)
133+
)
134+
} else {
135+
entry <- list(
136+
name = dgp_spec$name,
137+
n = dgp_spec$n,
138+
d_lower = dgp$d_lower,
139+
weight_pattern = dgp_spec$weight,
140+
seed = dgp_spec$seed,
141+
d = dgp$d,
142+
dy = dgp$dy,
143+
w = dgp$w,
144+
cluster = NULL,
145+
hc1 = fit_iv_robust(dgp, se_type = "HC1", use_cluster = FALSE),
146+
classical = fit_iv_robust(dgp, se_type = "classical", use_cluster = FALSE)
147+
)
148+
}
149+
fixtures[[dgp_spec$name]] <- entry
150+
}
151+
152+
# -------------------------------------------------------------------------
153+
# Serialize
154+
# -------------------------------------------------------------------------
155+
156+
out <- list(
157+
metadata = list(
158+
description = "estimatr::iv_robust weighted 2SLS parity fixture for HAD Phase 4.5 B",
159+
estimatr_version = as.character(packageVersion("estimatr")),
160+
r_version = as.character(getRversion()),
161+
n_dgps = length(fixtures),
162+
hc1_atol = 1e-10,
163+
cr1_atol = 1e-10,
164+
notes = paste(
165+
"HC1 (se_type='HC1') and CR1 (se_type='stata') under pweights match",
166+
"Python's _fit_mass_point_2sls bit-exactly at atol=1e-10. Classical",
167+
"(se_type='classical') uses estimatr's projection-form DOF convention",
168+
"(n-k + X_hat'WX_hat bread) which differs from Python's sandwich form",
169+
"(sum(w)-k + Z'W^2Z meat); included as a reference without parity",
170+
"assertion.",
171+
sep = " "
172+
)
173+
),
174+
fixtures = fixtures
175+
)
176+
177+
# Ensure output directory exists.
178+
out_dir <- "benchmarks/data"
179+
if (!dir.exists(out_dir)) dir.create(out_dir, recursive = TRUE)
180+
out_path <- file.path(out_dir, "estimatr_iv_robust_golden.json")
181+
write_json(out, path = out_path, digits = 17, auto_unbox = TRUE, null = "null")
182+
message(sprintf("Wrote %d DGP fixtures to %s", length(fixtures), out_path))

benchmarks/R/requirements.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ required_packages <- c(
1212
"fixest", # Fast TWFE and basic DiD
1313
"triplediff", # Ortiz-Villavicencio & Sant'Anna (2025) triple difference
1414
"survey", # Lumley (2004) complex survey analysis
15+
"estimatr", # Blair et al. (2019) weighted robust / IV SE (HAD mass-point parity)
1516

1617
# Utilities
1718
"jsonlite", # JSON output for Python interop

benchmarks/data/estimatr_iv_robust_golden.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)