From a0063b879ba170e46c96afb4368fbda35d3c681b Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 16:33:08 -0400 Subject: [PATCH 1/9] HeterogeneousAdoptionDiD Phase 4.5 B: weighted mass-point 2SLS + event-study survey composition + sup-t bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CHANGELOG.md | 1 + TODO.md | 3 +- .../R/generate_estimatr_iv_robust_golden.R | 182 ++++ benchmarks/R/requirements.R | 1 + .../data/estimatr_iv_robust_golden.json | 1 + diff_diff/had.py | 969 +++++++++++++++--- docs/methodology/REGISTRY.md | 23 +- tests/test_estimatr_iv_robust_parity.py | 206 ++++ tests/test_had.py | 431 +++++++- 9 files changed, 1642 insertions(+), 175 deletions(-) create mode 100644 benchmarks/R/generate_estimatr_iv_robust_golden.R create mode 100644 benchmarks/data/estimatr_iv_robust_golden.json create mode 100644 tests/test_estimatr_iv_robust_parity.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f2357b8..4d6bfb9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **`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. - **`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=` (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)". - **`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). - **`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"`. diff --git a/TODO.md b/TODO.md index 5c34735b..90bb7268 100644 --- a/TODO.md +++ b/TODO.md @@ -92,7 +92,8 @@ Deferred items from PR reviews that were not addressed before merge. | 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 | | `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 | | `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 | -| `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 | +| `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 | +| `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 | | `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 | | `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 | | `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 | diff --git a/benchmarks/R/generate_estimatr_iv_robust_golden.R b/benchmarks/R/generate_estimatr_iv_robust_golden.R new file mode 100644 index 00000000..c5f9f906 --- /dev/null +++ b/benchmarks/R/generate_estimatr_iv_robust_golden.R @@ -0,0 +1,182 @@ +# Generate cross-language weighted-2SLS parity fixture for HAD Phase 4.5 B +# (mass-point + weights). +# +# Purpose: validate ``_fit_mass_point_2sls(..., weights=...)`` against +# ``estimatr::iv_robust(y ~ d | Z, weights=w, se_type=...)`` bit-exactly. +# estimatr's HC1 sandwich and Stata-style CR1 under pweights match the +# Wooldridge 2010 Ch. 12 / Angrist-Pischke 4.1.3 pweight convention +# that the Python port implements (w² in the HC1 meat, w·u in the CR1 +# cluster score, weighted bread Z'WX). estimatr's classical SE uses a +# different DOF / projection convention and is skipped in parity tests +# (documented deviation; diverges by O(1/n) at non-uniform weights). +# +# Usage: +# Rscript benchmarks/R/generate_estimatr_iv_robust_golden.R +# +# Output: +# benchmarks/data/estimatr_iv_robust_golden.json +# +# Phase 4.5 B of HeterogeneousAdoptionDiD (de Chaisemartin et al. 2026). +# Python test loader: tests/test_estimatr_iv_robust_parity.py. + +library(jsonlite) +library(estimatr) + +stopifnot(packageVersion("estimatr") >= "1.0") + +# ------------------------------------------------------------------------- +# DGP builders +# ------------------------------------------------------------------------- + +dgp_mass_point <- function(n, seed, weight_pattern = "uniform", + include_cluster = FALSE, d_lower = 0.3) { + # Mass-point dose: a fraction at d_lower, rest uniform(d_lower, 1). + set.seed(seed) + n_mass <- round(0.15 * n) + d_mass <- rep(d_lower, n_mass) + d_cont <- runif(n - n_mass, d_lower, 1.0) + d <- c(d_mass, d_cont) + # Reshuffle to avoid ordered-by-dose artifacts. + perm <- sample.int(n) + d <- d[perm] + + # True DGP: dy = 2 * d + 0.3 * d^2 + eps + dy <- 2.0 * d + 0.3 * d^2 + rnorm(n, sd = 0.4) + + # Weights + w <- switch(weight_pattern, + "uniform" = rep(1.0, n), + "mild" = 1.0 + 0.3 * rnorm(n), + "informative" = 1.0 + 2.0 * abs(d - 0.5) + runif(n, 0, 0.2), + "heavy_tailed" = pmax(0.1, rlnorm(n, meanlog = 0, sdlog = 0.5)) + ) + # Clip to positive. + w <- pmax(w, 0.01) + + cluster <- if (include_cluster) sample.int(max(4, n %/% 20), n, replace = TRUE) else NULL + + list(d = d, dy = dy, w = w, cluster = cluster, d_lower = d_lower) +} + +# ------------------------------------------------------------------------- +# Fit weighted 2SLS with estimatr at specified se_type. +# ------------------------------------------------------------------------- + +fit_iv_robust <- function(dgp, se_type, use_cluster = FALSE) { + d <- dgp$d + dy <- dgp$dy + w <- dgp$w + Z <- as.integer(d > dgp$d_lower) + df <- data.frame(d = d, dy = dy, Z = Z, w = w) + if (use_cluster) df$cluster <- dgp$cluster + + fit <- if (use_cluster) { + iv_robust(dy ~ d | Z, data = df, weights = w, clusters = cluster, + se_type = se_type) + } else { + iv_robust(dy ~ d | Z, data = df, weights = w, se_type = se_type) + } + + list( + beta = as.numeric(coef(fit)["d"]), + se = as.numeric(fit$std.error["d"]), + # Intercept for manual sandwich verification. + alpha = as.numeric(coef(fit)["(Intercept)"]), + se_intercept = as.numeric(fit$std.error["(Intercept)"]), + n = as.integer(nobs(fit)), + se_type = se_type + ) +} + +# ------------------------------------------------------------------------- +# Build the DGP × se_type fixture grid. +# ------------------------------------------------------------------------- + +# Each DGP × se_type combination becomes one fixture entry. DGPs vary +# sample size, weight informativeness, and cluster structure so the +# Python test exercises all three sandwich variants (HC1, classical, CR1). +fixtures <- list() + +dgps <- list( + list(n = 200, seed = 42, weight = "uniform", cluster = FALSE, name = "uniform_n200"), + list(n = 500, seed = 123, weight = "mild", cluster = FALSE, name = "mild_n500"), + list(n = 500, seed = 7, weight = "informative", cluster = FALSE, name = "informative_n500"), + list(n = 1000, seed = 321, weight = "heavy_tailed", cluster = FALSE, name = "heavy_n1000"), + list(n = 600, seed = 99, weight = "informative", cluster = TRUE, name = "informative_cluster_n600") +) + +# For the non-clustered DGPs, emit HC1 + classical entries (Python +# parity tests target HC1; classical deviates by O(1/n) and is recorded +# as a reference only). For the clustered DGP, emit the Stata-style CR1 +# entry (matches `diff_diff/had.py::_fit_mass_point_2sls` CR1 convention +# bit-exactly; see Gate #0 verification in the Phase 4.5 B plan). +for (dgp_spec in dgps) { + dgp <- dgp_mass_point( + n = dgp_spec$n, + seed = dgp_spec$seed, + weight_pattern = dgp_spec$weight, + include_cluster = dgp_spec$cluster + ) + + if (dgp_spec$cluster) { + entry <- list( + name = dgp_spec$name, + n = dgp_spec$n, + d_lower = dgp$d_lower, + weight_pattern = dgp_spec$weight, + seed = dgp_spec$seed, + d = dgp$d, + dy = dgp$dy, + w = dgp$w, + cluster = dgp$cluster, + cr1 = fit_iv_robust(dgp, se_type = "stata", use_cluster = TRUE) + ) + } else { + entry <- list( + name = dgp_spec$name, + n = dgp_spec$n, + d_lower = dgp$d_lower, + weight_pattern = dgp_spec$weight, + seed = dgp_spec$seed, + d = dgp$d, + dy = dgp$dy, + w = dgp$w, + cluster = NULL, + hc1 = fit_iv_robust(dgp, se_type = "HC1", use_cluster = FALSE), + classical = fit_iv_robust(dgp, se_type = "classical", use_cluster = FALSE) + ) + } + fixtures[[dgp_spec$name]] <- entry +} + +# ------------------------------------------------------------------------- +# Serialize +# ------------------------------------------------------------------------- + +out <- list( + metadata = list( + description = "estimatr::iv_robust weighted 2SLS parity fixture for HAD Phase 4.5 B", + estimatr_version = as.character(packageVersion("estimatr")), + r_version = as.character(getRversion()), + n_dgps = length(fixtures), + hc1_atol = 1e-10, + cr1_atol = 1e-10, + notes = paste( + "HC1 (se_type='HC1') and CR1 (se_type='stata') under pweights match", + "Python's _fit_mass_point_2sls bit-exactly at atol=1e-10. Classical", + "(se_type='classical') uses estimatr's projection-form DOF convention", + "(n-k + X_hat'WX_hat bread) which differs from Python's sandwich form", + "(sum(w)-k + Z'W^2Z meat); included as a reference without parity", + "assertion.", + sep = " " + ) + ), + fixtures = fixtures +) + +# Ensure output directory exists. +out_dir <- "benchmarks/data" +if (!dir.exists(out_dir)) dir.create(out_dir, recursive = TRUE) +out_path <- file.path(out_dir, "estimatr_iv_robust_golden.json") +write_json(out, path = out_path, digits = 17, auto_unbox = TRUE, null = "null") +message(sprintf("Wrote %d DGP fixtures to %s", length(fixtures), out_path)) diff --git a/benchmarks/R/requirements.R b/benchmarks/R/requirements.R index 1dff6666..1de743cf 100644 --- a/benchmarks/R/requirements.R +++ b/benchmarks/R/requirements.R @@ -12,6 +12,7 @@ required_packages <- c( "fixest", # Fast TWFE and basic DiD "triplediff", # Ortiz-Villavicencio & Sant'Anna (2025) triple difference "survey", # Lumley (2004) complex survey analysis + "estimatr", # Blair et al. (2019) weighted robust / IV SE (HAD mass-point parity) # Utilities "jsonlite", # JSON output for Python interop diff --git a/benchmarks/data/estimatr_iv_robust_golden.json b/benchmarks/data/estimatr_iv_robust_golden.json new file mode 100644 index 00000000..3d9bca35 --- /dev/null +++ b/benchmarks/data/estimatr_iv_robust_golden.json @@ -0,0 +1 @@ +{"metadata":{"description":"estimatr::iv_robust weighted 2SLS parity fixture for HAD Phase 4.5 B","estimatr_version":"1.0.6","r_version":"4.5.2","n_dgps":5,"hc1_atol":1e-10,"cr1_atol":1e-10,"notes":"HC1 (se_type='HC1') and CR1 (se_type='stata') under pweights match Python's _fit_mass_point_2sls bit-exactly at atol=1e-10. Classical (se_type='classical') uses estimatr's projection-form DOF convention (n-k + X_hat'WX_hat bread) which differs from Python's sandwich form (sum(w)-k + Z'W^2Z meat); included as a reference without parity assertion."},"fixtures":{"uniform_n200":{"name":"uniform_n200","n":200,"d_lower":0.29999999999999999,"weight_pattern":"uniform","seed":42,"d":[0.5513198141008615,0.43027150973211969,0.94036423044744877,0.72243185932748011,0.69654189685825252,0.29999999999999999,0.39426661806646734,0.89478280299808821,0.6746656273957341,0.84928494298364965,0.29999999999999999,0.88131333824712776,0.73652942003682254,0.29999999999999999,0.66054433088283981,0.71277481249999253,0.62041924337390808,0.88304125617723905,0.65994824904482807,0.58744458807632327,0.87971093966625624,0.29999999999999999,0.98797203854192039,0.30551931711379438,0.81631693243980408,0.29999999999999999,0.61632054608780884,0.95312388914171597,0.72123636479955167,0.95595278930850325,0.80376010464970016,0.81327213400509202,0.29999999999999999,0.36298636144492774,0.8156118202488869,0.83515543106477708,0.38132226928137242,0.29999999999999999,0.74922186322510242,0.74798513862770044,0.81346957169007506,0.57648113165050741,0.30016722760628906,0.48990063029341396,0.53315038839355111,0.52193734624888743,0.95427057300694285,0.53339904788881543,0.29999999999999999,0.29999999999999999,0.46838131775148212,0.60222587422467766,0.67505299567710608,0.86773859888780858,0.29999999999999999,0.81386601070407771,0.62484868592582643,0.57167579797096546,0.63249795709270984,0.87206131108105178,0.69455278909299523,0.29999999999999999,0.37960302636492999,0.29999999999999999,0.97382560963742426,0.46346646787133067,0.29999999999999999,0.67616368671879168,0.43150833081454038,0.93282197110820553,0.35618512660730628,0.98475849987007669,0.52686016673687841,0.72824505041353405,0.3599284454481676,0.29999999999999999,0.29999999999999999,0.56569146837573492,0.32620172300375999,0.43818723959848283,0.50553668895736337,0.57226152005605402,0.54272377374581993,0.99222421024460339,0.70712280175648623,0.89533792547881597,0.97967662725131954,0.8316809872863814,0.57314242697320872,0.95437606764025984,0.48276157467626035,0.73318674513138826,0.4198850312735885,0.94344275039620695,0.7262562167830765,0.39833536588121204,0.77501500754151487,0.29999999999999999,0.29999999999999999,0.96266776279080657,0.33009215721394863,0.30159107625950127,0.35770629066973925,0.88025949138682336,0.93401669163722545,0.62360497578047214,0.70587462934199718,0.63557899494189762,0.75194231488276264,0.32725554378703237,0.39499965794384478,0.5136528586270287,0.30276383715681732,0.80714831999503067,0.88576109160203487,0.29999999999999999,0.84307635384611779,0.29999999999999999,0.40435043671168386,0.80337857615668318,0.66336716439109289,0.29999999999999999,0.80354908637236799,0.44599896986037491,0.80356501389760515,0.7420551092363894,0.961304227868095,0.576108701271005,0.7729250921634957,0.43789614168927071,0.29999999999999999,0.60504010948352516,0.29999999999999999,0.66008539581671355,0.70143312925938517,0.41911373687908049,0.61287873967085027,0.82415677031967782,0.54970539954956621,0.77961881058290594,0.29999999999999999,0.47880017703864719,0.88520298199728131,0.68534585905726997,0.82078225242439651,0.97379920550156385,0.61691210079006853,0.29999999999999999,0.69223292237147682,0.29999999999999999,0.77409378113225102,0.45201038876548405,0.56284297523088755,0.30109938795212654,0.42568901749327775,0.81789869545027605,0.84555771301966154,0.66008905426133424,0.97030361765064299,0.3203600732376799,0.84516664897091687,0.94795132402796289,0.30513390281703323,0.46359237902797756,0.43263175478205085,0.98147793964017183,0.93462098545860495,0.50029767435044048,0.79354534882586447,0.3970971174072474,0.78524337427224955,0.6750329029979184,0.29999999999999999,0.45159711767919358,0.41053364572580903,0.45146979053970426,0.82606579388957468,0.7383717411663383,0.9580101659288629,0.72849316310603163,0.5789397879969328,0.95971898438874625,0.40391262469347566,0.73341146802995349,0.84307752992026508,0.5489661676809191,0.29999999999999999,0.93254416759591541,0.30096659050323066,0.95617009475827208,0.29999999999999999,0.76719856027048072,0.29999999999999999,0.44536128097679462,0.77611492469441146,0.75989460328128189,0.29999999999999999,0.87498274673707777,0.92142843385227025,0.38224115315824747],"dy":[1.1000795785054169,0.6526817207616783,2.6461085682978913,1.4927305700218096,1.9178157762902512,0.14636702795642487,0.64872064741920465,1.922015927297239,1.3294972041759079,2.4544381650776068,0.617894119480635,2.0933309769873247,1.2588528329020094,0.33531308939617016,1.8512118691060566,2.0813566708385292,1.8558599735364503,1.4477562505776689,2.2705402829694141,1.6851656512905115,1.9809022948877557,0.90844311151930424,1.8802166101052988,0.20057875354294957,1.8521660455110265,0.14760165737688802,1.4226039962585093,2.6978636825410875,1.1849778085859846,1.890682997547793,1.8199548768276947,1.4179292892645006,0.47368641604548872,1.1146026171369232,2.2186084384847313,2.03309490635149,0.065644275250727691,0.60540130526953373,2.0927530322858665,1.9890928225024131,1.7491323769581277,0.17268948842024856,0.65175122009167841,1.2813027278488245,1.1698970096704,1.1885652865909242,2.3543569931134938,0.9935325646441544,1.1509912903006199,0.81515735994957716,0.50550884491870818,1.8658847320792717,1.9685985302499323,2.2909978660483028,-0.038051760874237783,1.5987228489797742,1.621033662858433,1.2588843643687961,1.5242169353678778,2.9561073207955904,1.2064745214771802,-0.21828004597165473,0.91191369898964991,0.35196126350263529,2.4105685358553051,0.66641939627876057,1.5118221921340544,1.440004184177756,0.72794229108576503,2.0601863945564274,1.0954559599900637,2.2993779847411902,0.48674812837744497,1.6137440497320283,1.0628183037139776,0.64259636514902507,0.92102885664577627,1.1687959371698504,0.66117078108219351,1.1269246827228601,1.4849210357896128,0.74420981510035067,1.1604172659300671,2.2514162130078663,1.2606841390497752,1.617421106822184,1.9949903611090674,2.1055930419620559,1.0783034639519786,1.8680471150237374,1.1008072985421302,1.1329566371899444,1.3110106048881325,1.9600726012874778,1.6863983859632392,0.86467458350592907,1.7301282261028252,1.3507528169771463,0.29686881715602209,2.6615424729278043,0.70550183912544184,0.29638698358012394,0.72629325884828466,2.2916846892316101,1.9595420435936934,1.0550420072290105,1.6223325990877628,1.7877849255987206,1.6441264693700299,0.13182932343197185,0.31413637307027076,0.79909936497559753,0.42218420649082733,1.8011723371146515,2.2750932250213909,0.45315318457103737,1.4538341158914361,0.86984239795139995,0.96793344361988309,2.263321121067523,0.78575878342856731,0.66192763555794976,2.3421502705847637,1.2413419843290521,1.4678239169576031,1.9423153487232936,1.851069453292427,1.0704287687541421,2.200087855207407,0.81726006793441464,0.95841845798617453,1.2034111956805336,-0.0035449618991625353,1.1113583317850106,1.1150608441826764,0.69720814264351572,1.2039191006000305,1.7907406989938268,1.0927647155573785,2.4984600847502678,0.072600665085832072,0.86044551665385416,2.145113871038359,2.1629783083987868,1.8790783149026993,2.7277441621539964,0.69017614930901905,1.205542610151392,1.251997701721367,0.51642756581845917,1.2841864129010148,1.018862521568326,1.9348592355844232,1.5982623704324326,0.4750098160235085,2.0308613178675707,2.4610144753511518,1.3726306494755376,2.1357840493465789,0.54959813761423726,2.1437583869462298,2.7244579262001398,0.9132477197245582,1.1197353319425887,0.80066660982693627,2.4512850373036188,1.9114821196883915,0.96398207613412168,2.2146103410893705,1.0183053062819629,1.8518754132064845,1.3845235698925686,0.99941316059395291,1.4983412565174228,0.52391994818331855,0.9862818544764389,1.876473761806241,1.4089590394342209,1.7918599128662382,1.6152239008170921,1.5206357125568333,2.786493039066964,0.093107756591297663,1.3472148611662988,1.7748168890726992,0.52307867907237027,0.32678662328308394,1.8150392188743751,0.34007956764236213,1.3110847248907298,0.71236742011462173,1.4582060353033583,1.2351964775328652,1.2686089428976095,1.1515243362357266,1.7323793173937043,0.38949160630673862,2.33495640305423,2.1187941415174412,0.58550534572895674],"w":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],"cluster":null,"hc1":{"beta":2.3520502527649163,"se":0.22881156576137548,"alpha":-0.091709184993883863,"se_intercept":0.14577658521460482,"n":200,"se_type":"HC1"},"classical":{"beta":2.3520502527649163,"se":0.2046986920950549,"alpha":-0.091709184993883863,"se_intercept":0.1283151307209478,"n":200,"se_type":"classical"}},"mild_n500":{"name":"mild_n500","n":500,"d_lower":0.29999999999999999,"weight_pattern":"mild","seed":123,"d":[0.92469333107583218,0.89763436166103927,0.35206915931776167,0.97365125550422815,0.81595441654790191,0.50130426408722994,0.52954450349789106,0.79059716123156243,0.29999999999999999,0.58663246682845049,0.5263414952205494,0.65805382193066175,0.369329280545935,0.97854948779568074,0.46213804974686351,0.30660093356855211,0.77193078976124518,0.92267752124462277,0.44734095537569374,0.57760921623557804,0.52426126974169163,0.46047138823196287,0.39557813794817775,0.35775192205328493,0.29999999999999999,0.50712681878358123,0.53687389863189305,0.37776752291247245,0.79712768094614139,0.80818811883218578,0.83923385199159384,0.73630828240420665,0.88499618591740725,0.57182338060811155,0.40794161057565359,0.33188954957295208,0.53996616117656226,0.53751880612689995,0.70035471979063002,0.45477205647621299,0.97411696277558801,0.85918212782125913,0.29999999999999999,0.45148555508349086,0.338240376370959,0.57271097807679328,0.32441236285958436,0.61396143897436561,0.29999999999999999,0.75074604048859328,0.52186447365675115,0.72095600818283856,0.63491946866270155,0.98396929739974437,0.29999999999999999,0.52387443196494132,0.64048823483753947,0.64202912356704467,0.38358334598597138,0.78816163635347036,0.53297647822182626,0.29999999999999999,0.4349711628165096,0.8487625351175665,0.29999999999999999,0.29999999999999999,0.39933483544737097,0.6485642553307116,0.37251975147519262,0.92653175140731037,0.61672087595798075,0.46312386961653829,0.79279444292187684,0.51615078290924432,0.86239297941792747,0.48551246426068245,0.62653066378552469,0.90455087800510225,0.77206347165629263,0.29999999999999999,0.84970416990108788,0.75899405938107511,0.78349369489587839,0.57665409424807879,0.29999999999999999,0.57681420135777439,0.58180133954156188,0.35043998679611832,0.9995831695618107,0.68322162760887295,0.93371670716442168,0.36638846264686437,0.34093494201079011,0.89981979411095381,0.66822076209355141,0.29999999999999999,0.5227267053443938,0.63690286034252497,0.29999999999999999,0.73284585915971545,0.48005172251723705,0.29999999999999999,0.55486430535092945,0.29999999999999999,0.52503792948555195,0.83843573785852632,0.48552291202358899,0.4720065746223554,0.49323477083817124,0.76188691477291282,0.42869466680567708,0.58748284357134251,0.98031294497195631,0.37779479704331603,0.55819181564729659,0.31401701038703322,0.29999999999999999,0.31722957915626465,0.99016791565809392,0.29999999999999999,0.29999999999999999,0.54881176655180752,0.40671132341958582,0.37379242228344084,0.78878311640582977,0.59031473412178459,0.72893970229197291,0.45755134557839483,0.4029795531183481,0.91617257886100556,0.63803167801816019,0.61857563566882157,0.67358156181871887,0.29999999999999999,0.75717134752776472,0.29999999999999999,0.39960600691847503,0.29999999999999999,0.62674532909877601,0.29999999999999999,0.69217728457879268,0.92397588200401515,0.29999999999999999,0.55864220608491444,0.85173708382062607,0.45812050304375584,0.71589941431302573,0.58892228303011507,0.55915035894140597,0.34250440008472649,0.49206855120137333,0.78046255740337067,0.5923527457751333,0.96340885802637777,0.57800636731553823,0.30441054876428097,0.29999999999999999,0.29999999999999999,0.68600451012607655,0.74313875187654044,0.34544980998616664,0.63010339434258633,0.37266468175221235,0.69266358863096678,0.84534611795097581,0.39597425670363007,0.58462550640106203,0.29999999999999999,0.4538373418850824,0.61733390933368348,0.29999999999999999,0.5199895462952554,0.36647368923295287,0.42253685519099232,0.79455133080482476,0.74109229696914547,0.40964579596184192,0.70341128630097949,0.29999999999999999,0.85924739195033906,0.47053363090381023,0.45137835273053495,0.29999999999999999,0.64826908695977181,0.56229976962786166,0.96786386698950078,0.63821736848913135,0.34057099237106742,0.4893814106704667,0.80441739140078417,0.8309216762660071,0.70943006544839582,0.97993971516843881,0.67363541154190898,0.50039974409155541,0.59989505612757055,0.29999999999999999,0.29999999999999999,0.73206468874122943,0.54632204691879449,0.29999999999999999,0.94097847689408809,0.77429944481700652,0.38927215517032887,0.95470986228901888,0.7326125112827867,0.36373079991899432,0.74098113742657001,0.58626079855021085,0.31243606831412762,0.94320014370605343,0.79929530050139874,0.44457197273150084,0.29999999999999999,0.98946988598909225,0.29999999999999999,0.5404615305829793,0.46710821879096326,0.58628384526818988,0.45062045785598454,0.41764256737660616,0.96815255440305914,0.46181324743665753,0.3387113484321162,0.60442491904832418,0.63449176589492706,0.8182340522762388,0.92324515548534691,0.37050510761328043,0.47251429590396582,0.46642458348069338,0.51086022998206315,0.43138378348667172,0.33699075994081795,0.30732697828207162,0.76558063623961059,0.91811218280345197,0.92052834255155169,0.54773320029489692,0.73533160660881547,0.49191663505043831,0.64479658347554503,0.96511688553728159,0.76146122694481155,0.68084621729794881,0.56877874645870174,0.85682719238102434,0.88783743535168469,0.29999999999999999,0.93131207264959803,0.38532948198262601,0.95832709900569168,0.58960702840704471,0.70983834718354044,0.42970750932581719,0.40293382720556109,0.87024802721571171,0.71604023582767695,0.67174927920568728,0.60954005194362249,0.5605666421586647,0.940544633148238,0.40296628365758802,0.98895344242919236,0.29999999999999999,0.81249464387074111,0.97722864602692416,0.41945166457444427,0.29999999999999999,0.29999999999999999,0.54779027183540163,0.56212394312024117,0.78300497105810785,0.92771707784850144,0.29999999999999999,0.7334795383736491,0.48618084825575347,0.82259760289452966,0.29999999999999999,0.29999999999999999,0.3195948003558442,0.29999999999999999,0.9298774792812764,0.44621344399638474,0.87526382210198783,0.70722506984602651,0.90653832026291636,0.35033286805264652,0.75973068957682699,0.51568368801381437,0.68277830958832053,0.29999999999999999,0.29999999999999999,0.9237800187198445,0.55431038045790049,0.50841924783308057,0.79580322555266314,0.41414923046249896,0.9251357800792902,0.97717885943129656,0.84998148398008189,0.29999999999999999,0.49755289216991511,0.71999227148480704,0.66479500804562119,0.51871371543966227,0.61133760141674431,0.70100694075226777,0.80718804248608644,0.58315719722304493,0.29999999999999999,0.5183088227408007,0.3707409458467737,0.88383801642339677,0.50176749648526309,0.39996001566760242,0.29999999999999999,0.420520222466439,0.61486173889134077,0.74835476963780811,0.76779925480950617,0.29999999999999999,0.37204727786593139,0.87466592676937571,0.29999999999999999,0.82731550501193851,0.9401067308383062,0.29999999999999999,0.4977698310744017,0.88234929398167872,0.45408321961294856,0.51819154145196078,0.42018894571810961,0.61963031471241259,0.29999999999999999,0.72611448762472719,0.8899105232208967,0.43536507594399154,0.50741152276750656,0.29999999999999999,0.97469033233355729,0.6763740281341597,0.63445717976428562,0.46771624032407999,0.63742982386611402,0.40664263179060067,0.94799556909129024,0.56089657610282295,0.93923175611998877,0.29999999999999999,0.83101521865464745,0.77109936482738695,0.48726500249467786,0.70084338136948643,0.63663758097682144,0.70356331490911539,0.46736996895633637,0.96813167924992738,0.54625853647012257,0.29999999999999999,0.79597132771741597,0.58478281528223308,0.86867265666369342,0.42867994355037808,0.87162412132602185,0.66055026552639895,0.32944167347159237,0.59324112783651795,0.8098760997876524,0.7404547920916229,0.33336453919764608,0.62657289162743834,0.56587157640606167,0.4149478574981913,0.29999999999999999,0.84221407945733517,0.82797318145167076,0.91630839814897624,0.8492026865715161,0.77655128585174671,0.36551649069879205,0.38253968888893725,0.32360944109968842,0.45525155426003039,0.62526615932583807,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.85603962477762252,0.54312724198680373,0.29999999999999999,0.53543184413574629,0.47707544995937495,0.88209801863413295,0.95611986240837721,0.98749826156999909,0.71513195617590097,0.68449925924651323,0.76763891121372574,0.99598884363658724,0.40067193054128436,0.29999999999999999,0.45259346810635176,0.76919742769096044,0.29999999999999999,0.52535616299137466,0.54834816826041788,0.300437341327779,0.96978334174491465,0.7549896111711859,0.58307942671235646,0.98834512655157591,0.9938655890990048,0.29999999999999999,0.98449738337658343,0.40702105597592886,0.3720052509801462,0.90767476481851184,0.57153632089029993,0.88467872396577141,0.91738429151009759,0.43842131567653264,0.33208181667141617,0.5024118161061778,0.73399531713221222,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.75352543552871787,0.71738448124378917,0.57925787726417177,0.91227764000650491,0.8325765377609059,0.60760207537095989,0.80869177731219677,0.85039708619005971,0.85181359481066465,0.66151142597664148,0.4518249339656904,0.63272160186897963,0.47226141404826194,0.7509681543800979,0.90973038955125951,0.99023800811264662,0.29999999999999999,0.7681400622241199,0.93160933158360415,0.35368381573352964,0.84760601092129939,0.89321721557062117,0.57299610467161977,0.67945625705178825,0.65196913194376971,0.6605208090739324,0.90047940074000499,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.60788218132220206,0.76362234940752383,0.82813261104747649,0.65160969428252424,0.98587534215766937,0.29999999999999999,0.81645818210672583,0.93979681476484977,0.8619685910409316,0.96543488483875983,0.52520787643734368,0.6708849800983443,0.59018243507016444,0.39148698409553617,0.29999999999999999,0.39594722997862841,0.69521330434363326,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.66967384163290256,0.62617371517699205,0.29999999999999999,0.86704504713416097,0.48598068079445511,0.305750863882713,0.6446053301217034,0.48259979994036256,0.78496238430961962,0.45748973044101149,0.57712488181423394,0.7490236477926373,0.39716424441430714,0.51540400760713967,0.9899486190639436,0.81057345634326339,0.97277335559483613,0.8146290140692144,0.5952914651250466,0.43441632310859857,0.75417268702294671],"dy":[1.8830177963358565,2.3749546540556938,0.4284433882705766,2.6759861084930647,1.9315732040316369,1.7387664743813374,0.55962592993646421,1.748188329417202,0.4162299295497609,1.1976002813675513,0.88396210424399779,1.1124806614145886,1.0110687461790233,1.8093344199801473,1.5819599452233906,0.16692047275113631,1.7630563833350281,2.3139508996828742,1.1894102254953691,1.1346094889286877,1.162778301300095,1.369058607001878,0.25551452803254948,0.44120389081358291,0.75516092572664417,0.91349412933123242,1.7082194697618487,1.0676490815771067,1.813745824777357,1.2092237212761519,1.9002018316122788,1.5086951835507258,1.9640192411530828,0.76911766397697579,1.0652713459749945,0.28124172495781197,1.0769125663408865,1.3142858841163672,1.2344521440359444,1.2047859622299328,1.70630092204064,0.81591256268854151,0.81298719631434102,1.300338802886096,0.59646454896763479,1.4454718173131329,0.21803113003854169,1.2901480297827084,-0.14960735041624718,2.1430503227453856,1.8693960505902782,2.027450190198953,1.3798369702659847,2.2450651313567249,0.020572950739560336,1.4462363276251509,1.3197503510879609,1.1450214956421394,0.24649722990395728,1.642777903554246,0.8115476803382915,0.46818779186886533,0.43966230427216407,2.5886802157191715,0.62059898911681244,1.0569780309089545,-0.19416970396778532,1.2420400566512924,0.51647787588529859,1.6214313569780248,1.9661888052583132,0.42448008690785943,1.9015018986253065,1.4507995692597131,2.019178529487863,0.69163944340890282,1.7472898520597753,2.1228006760164799,1.297552382039558,0.071780380959175494,2.7506944665036239,1.4194084552475044,1.0089174392070195,1.4663709169215402,0.7510921024243582,0.7119090552929116,0.48796795325805054,0.69120142203249357,2.754674809668761,1.7609304070203604,1.9318065114354432,0.43937378432819751,0.82516757712696853,2.1054836256705345,1.72228191893206,0.46868081988444144,1.487167994514089,1.0631746858042974,0.49478211995201477,1.9231364437960552,1.425226985372849,-0.14840188058135173,1.2449670927140908,0.87051160469385203,0.55244558179439396,2.0800140616226508,0.7104958653184188,1.4189514160565011,1.2748465162665339,2.0055362488147259,0.96081080053045698,1.6239658882529877,2.8011357422936047,1.5849074728707855,1.1984990403116547,-0.2420044105151723,0.63961040183759665,0.74687402346001741,2.2123274428180366,0.85431544831082973,1.031271185805706,0.98078886756820105,0.7454087461560408,0.94863796057397398,1.5441303776340722,1.3216778672352454,0.832602291903078,0.52994877274991969,0.32357467206448509,1.7427073373081621,1.1208668715104548,1.504864073469117,1.8761219607731374,0.3360465883649858,1.2875996369594307,0.21032445640419617,0.68128201152107903,0.53138837273897432,1.5647805839969999,0.49847006313264408,0.69669167959963729,2.067497482945504,1.1018747242606226,1.6875492538997849,1.6055256983833395,0.36009270722520248,2.5687766167962809,1.2169246257032833,1.1731149550118558,0.88843125487193131,0.41116075747952441,1.4523740113828092,0.67379306206246481,1.9280268587656244,1.3037799160300922,0.090737048993488223,0.86299307174692519,0.7427376115269344,1.1515036663778928,1.742484042067693,1.0259327563357143,1.803753976258484,0.70185374155345692,1.491807313802054,1.8703895996496172,1.4155718994208757,1.7218158645337311,0.96076062710281573,0.85452886370068293,1.4982947390178918,0.78831613248827836,0.70442651335168538,0.081916461807611807,1.1553669396410391,1.166771994322596,1.6476234071200762,0.96973362390777962,1.7808057600329921,0.70277049500592426,1.6468450855538659,1.402034175109262,1.6593329374176875,0.97947152357017719,0.64515365612225017,1.779284322693965,2.1943334950116555,1.6085968715383361,0.96475185934394303,1.0119366415911961,1.7728557056184906,2.2766354298909874,1.8544882052199236,2.6440688823638903,2.4365769014809198,1.3416858048268139,1.3907047987191541,-0.25725324423024831,1.7036856012984463,1.4318342613144737,2.132078313560609,0.77685742720199513,2.7629611815402693,1.6845766502366948,1.0285924554571937,2.2684441927805179,1.5517930703225966,0.71899409843190465,2.0518119229974339,1.195048855486841,-0.16091597211298769,2.0749325412063118,2.0061687364383936,1.195019503685091,0.87362712699097367,1.595814360214332,0.77449682320447766,1.5556963449568131,1.5103049365876093,1.1857018062854698,0.83340152035034976,1.482747861729905,1.5503297010735406,0.81287594676046249,0.89482514187422502,0.67133917677412991,1.5016086170590959,2.5884658022646732,2.1005805347343061,0.6708108156713849,1.2019742053716396,0.88648586626900561,1.4253740619689501,1.2803693433498042,0.70912701605933126,0.17231205493824908,1.1797070946710999,1.8519044134084131,2.4142206141552496,0.40218782833861066,0.87834692122798308,0.79491593294863927,1.5720798963618445,1.8442425326333209,2.0515690287701362,1.6341058941406483,1.166354424348899,2.2614314906558253,2.1674975293146734,0.44862598911513757,2.2152727720653269,1.0742079500617883,2.3346847845628016,1.0203009080183936,1.9129187201324356,1.3759840719463798,0.96508417776041311,2.0253374080781494,1.5556445251406652,2.3434390251664978,1.4410680392185924,1.1520861609276638,1.1433094075007604,0.22853440903240518,2.2402462780019237,0.70951761793723922,1.9337825351817206,2.5695527522179802,0.81402427509498754,1.1128355177930249,0.25839358285216879,0.70222571006882761,0.72744841344767108,2.0468577872013354,2.0804638717208412,0.9429271687281997,1.5212741780883108,0.8065164007895872,1.7008542206248047,-0.11404672969669183,0.15915389583435408,0.093017993092655904,1.0487289087901395,1.8802245599597831,1.2679427603897759,2.5869499123649327,1.4877904058736913,2.1731717208983063,0.037058663878259512,1.3651506843511974,1.1336322156058229,1.6250472465243873,0.3232407530804472,1.7009435996907196,1.9202148386985443,1.2264961829328309,1.3543022819977149,1.7711898293357349,0.62232738164195633,2.5251566892187776,2.8870394051413086,1.9048259364729243,0.85190693816122653,1.0304084490907721,2.0020832913498348,0.99970877920401457,2.0464906960526807,1.093382802589947,0.96589733502415476,1.6694747148747011,1.3270194831060018,1.2764484830720915,1.4816947269094158,0.83969991919007014,1.4462335150341026,0.73265108126040701,0.78259646268333583,1.6482104453842372,0.15000059249647901,1.7955618259068351,1.4538262947539449,2.3788495783197896,0.1713197440106381,0.84306960268719666,1.5390236218930502,0.98840657054467007,2.4534780889690588,2.9256420638473193,0.94604026303580746,1.8071786053982835,2.4968302344050062,0.91727394559650943,1.3077547203075093,0.50454783187695473,1.2803622797376086,1.1153854833395287,1.8269153073839504,2.2003462008937125,0.51234056081861623,0.85025768943253832,0.32115759449857251,2.3925053858558645,1.0937895518620246,1.6144916894545274,0.55449346031361091,2.1281668650925694,1.0471292723693189,1.8851983729862882,1.3125930159312962,2.0021291200600668,0.77545918390386281,1.9666194129041046,1.3149313297213441,0.7292326082584093,1.6688786701976228,2.0504881483078732,1.9894738347123706,0.75044335483010927,2.5478162036690377,1.1626092484376738,0.74752546068736181,1.886158358093923,2.302336818368647,1.4896074444230447,0.9528577789066055,1.2591759101282054,1.6879328962224001,1.1300862804976757,1.8703276628405203,1.0464638879426342,1.8104993728002925,1.3374166337470905,1.2053175145946664,1.1429461322746279,0.86693534344718781,0.77300750057321432,2.1632894759039081,2.3887365931488946,2.0463080842754584,1.993260152196104,2.7292112924594782,0.94355324413540864,0.88448160558238131,0.14173855319665318,0.97382172086580343,1.2792891883843491,0.62258166807625048,0.39683294349153336,0.35227373902291975,0.33869054727195125,1.846118595152159,1.7220037034815157,1.0466346506237312,1.0128798190086519,0.34806461358943597,1.6597917401973634,2.0033850689080954,2.3089975698124507,1.3186451164946673,2.3122325655984901,1.6031516580629293,1.8039980321719347,0.79300055694666738,0.22484896736263443,1.0291014733345947,1.8093477058477856,0.76923504465135384,0.48476875173096934,1.275186566999462,0.75213349369144766,1.6532672234658143,2.0631282721274617,1.581821690419011,3.1895858243184052,2.3467430159068341,0.64569341100720024,2.2983996297487432,0.89164844631430595,0.0461377638522662,1.3940607497866284,1.2100531848931644,1.771727429096255,2.1091413745360104,0.090023257060715456,0.097767831421265661,0.63995554652579112,2.0240386603601284,0.1876039971639536,0.30719441856938767,0.6589495274696211,1.5482925009240154,1.6477279821335895,2.1812024538583987,1.6243889592464669,1.7509203269268712,1.119254455442956,2.4185364425650828,1.6099527644841714,1.8884683482023221,1.769155507691661,0.54145738483795103,2.0476145176762577,1.2817370472458813,1.241439615615969,2.24957471722754,2.1893245527453367,0.75229150879773377,1.677301792200077,2.5517938618866407,0.20445514963927447,1.7016961278892078,1.9261092582966028,0.84617008019336115,1.0814287381711465,1.4242652923591281,1.3990578867710757,1.0244806369269608,1.0432293822877505,0.72689029439407604,1.5934829492593314,1.6006998817755544,1.5233967029846505,2.9809627673222852,2.5634883562361415,1.7758510148816173,0.81461278238216961,1.7483987856992973,2.2193795040639519,2.0378512290917152,1.7057289395889876,1.2474045790132593,2.1764949024098739,1.2192234604860126,0.76378190152655634,1.1864288025349867,1.1982852189087609,0.87602514289567068,0.7184227893048748,1.2884188938710934,1.1931105399676571,1.6418672410417079,1.6584638096727087,0.14822591512452665,2.0796728551488353,0.66103495414541391,0.4563395777561558,1.788106943241641,0.58031312527501122,1.8615418524027754,1.1491013327083373,1.276136490327048,2.3952337518052174,0.43271138851087054,1.3529525071057693,2.2383244916636027,1.7139228135914366,2.4150696051371963,1.4201839210338707,0.77151414217794845,0.72765555855878628,2.379681166503496],"w":[1.0167294323837561,1.0994303194683659,0.94304600869673816,1.1411478195861475,0.71449613709485627,1.3473731399705922,1.1754115780724055,0.75806415319248854,1.0163659737435977,1.2148994858497368,1.1673192946163808,1.4445802047630643,0.81610367353154489,1.3348409846471179,1.3109644029823657,0.95125506042914365,0.70722199215343451,0.67325644273464513,1.1373360873834912,0.97866197988373171,1.5337308000554351,1.1605413881488613,0.88841653745095139,0.69233732545086579,0.82527949761842423,1.1028665178691994,0.86471960588300489,1.1542690360692072,0.89969858434906547,0.96833202736165735,0.78084709815765174,1.5715130754726112,1.0997865194411183,1.0691900921498352,0.4924412755347799,1.197937569864798,0.69291292333608645,0.73254352769371067,1.2755023513064947,0.86418980604753093,0.47548831599904584,1.5309712329668066,0.28677792238224376,1.1718434589578755,1.305174774725383,0.81070964000183965,1.1332861154234661,1.1317391165165667,1.3121869458743538,1.1452298163857566,0.92653486627224257,1.2747976173820632,1.24018670695293,0.71902928975926228,0.57976376980128075,1.0480832619979534,0.91781128756744523,0.70433826623131124,1.0251792038545098,0.60400104205392835,1.0483679053978596,0.81252148373742406,1.2871492822577442,1.7273467423484599,0.72520622689396241,1.3172992512828925,1.247544918330485,0.97894173270839235,0.86390608778289568,1.4725923120512454,0.39836265452912589,0.80704156252190107,0.56894696690266722,1.418594031683823,0.94278897020654284,0.84259864015909858,1.9552133422198987,0.98498881963371154,0.86687520644004301,1.0899595750408435,0.5294726137735094,1.1470907928016203,0.97115103967600991,1.1405575367590437,0.70528880921864379,0.69310484735561706,0.79197560101714459,0.76960312807216535,1.3897149900050767,1.473743668542427,0.95293241408827989,0.89231903182459582,0.90128835087349946,1.0207709433559049,1.0290712701103164,1.0870103163296712,0.77599631768612332,0.74593108335390435,1.3591232991238253,0.83541179166900315,1.0909137085676353,0.98290883985900035,0.71264518235479923,1.1773185728234523,1.0519314620478863,1.4199350068637524,1.0352378753880964,0.90053627253974411,1.0834884739916093,0.64432250528939838,0.74923178398192092,1.1530819753418291,0.90006372963281545,0.98021171609426094,0.96543348717341515,0.80484621436764126,0.39439340227527486,1.1046504910529777,1.2284918522940576,0.61338512939496259,1.4447208155375819,1.1155464471061705,1.4024920884981615,0.71284885862731351,1.0500343865062753,0.96999581231835752,1.2305522295645328,0.82724212962619204,0.99697069841426167,0.46640225537681657,0.76671356713884986,1.0375101650995331,0.78810355228353302,0.98692915425655603,0.85962220782243404,1.1820790408484472,1.3505464926248689,0.75324957615774324,0.90788903242158148,1.4319283775109226,0.34032302374488799,0.90404866247777949,1.6194112852430027,1.6580770207192765,1.0469785970050449,0.7409173148159085,1.0496372258471314,0.80416768121589421,1.4358451846874452,0.75805520328908627,1.1118734785626245,1.1859550223706077,0.77274695179124531,1.2554574044518549,0.77562100872510809,1.1890719488961941,1.3289984899310658,0.70346712328503824,1.3323985122938793,0.85314013863627636,1.0883060165310905,1.060551241686631,0.87184108409853944,1.0804308609993356,0.63087072237284869,0.95915893834451071,1.2477372494995511,0.34776260523120572,0.5536221437925235,0.65141873272090822,0.52327309175637116,1.1258749117889078,0.70212149507319688,0.35063587440130783,0.80872937047586513,0.88280942455755829,1.257035642133433,0.66887435920035132,1.3483867767216087,1.1195088146885814,1.1087056472906633,0.7442422989056493,1.5861003639694113,0.9507187503708534,0.45253072474650013,0.93884305858225126,0.41966677878038483,0.90684696526399799,0.87333189904311437,1.2045489071210231,1.3028488557212472,0.78216851052965364,1.2418326605125076,1.4272969329006291,0.76475679854720613,0.80427869005020025,1.1952335089461046,1.0549143909831806,1.164632488133285,1.4214052883642687,1.1161249358729186,1.3155103810526119,1.1868716387011582,1.1300861169426564,1.1158253314553237,1.387396991337424,0.69932203788680358,0.66844518125275165,1.1775838010191628,0.96409310076006582,1.0222015635054882,1.2223832131514196,1.2259885143547229,0.92119884899146676,0.90623683930086929,1.0220795827023608,1.3189053367884231,1.1278061460037012,1.4299022532469583,0.99770893887400847,1.3377002815662258,1.2649006933051741,1.1836250382700211,1.1244102120335393,0.91603528085724462,0.96728874724923042,1.0688186487125402,1.0146666672037858,1.2829673405158661,0.96720486304407061,0.9788869235236517,0.85470427373030056,0.95849910143700146,0.97937030730884667,0.30587926783017538,0.59055049083570477,0.97825392701441227,0.92041486754065038,0.6397392015377692,0.40253854718709436,0.893689234025972,1.1960487297082829,1.5319715895782695,0.98846296188719851,1.4479554521883149,1.0249066472907904,1.034659631277868,1.097447594004624,0.73882682642172437,0.98448453613153497,1.2725343096376753,0.77508226394434332,0.90351817903361176,0.65566877486222364,1.1063056589348064,1.1274399347167066,1.1945042053672246,0.63405699054492948,1.0321705104264292,0.71678269250504645,0.99988460538519297,1.402787176001824,0.84894241391971736,1.2150049962816474,0.77509942477489147,0.85644153686490843,1.1316165251759849,0.79626631886263965,0.48911054945636945,1.379550530563999,1.1081071713639539,0.82490816783197307,0.40177636380081982,1.5706629314197915,2.0171112463814627,1.062244122206637,1.2549419942533566,1.3673680936222483,0.7894586699384174,0.894641131133038,0.48186368900644116,0.77902653031843827,1.1867229348658344,0.91278520322773882,0.93573653974507698,0.96623213454924517,0.44089990525131861,1.2512889802695011,0.56695213331937366,0.93742895127530546,0.86843096138235087,0.93442185494314056,1.4379897834207358,0.82538202462538912,0.76507072128643117,0.54410380151684645,0.75829057551662526,0.65014458778967854,1.1223838588744888,0.74109872619763695,1.0912126105084408,0.9560717536456238,0.56993134602675866,0.76281766428319064,1.2655337365173032,1.2709228257863703,1.6016719822901884,0.99892590748006571,0.55125195580535813,0.76947489189545215,1.1225465514520445,1.5700409004684166,1.0330027370319168,1.3421160475385407,1.2304243914076782,0.64957251336480004,0.94866620432054238,1.3915784609043049,1.262828832878774,1.1391388424703393,1.1431342736130639,0.85257840993717937,0.60418440599673939,1.3886277372493923,0.5739341524999213,0.71833122408825112,1.1886894977559521,0.62134163518311958,0.83443887601816247,0.64516014796525667,1.1861990673186851,1.1338939049914287,1.1265654079821064,1.132739431643691,1.1671737239136817,1.191806947591155,0.40940153299773352,0.95535509159203957,1.0337391437655703,1.2174028607943277,0.64375417719841455,0.85011994305522609,0.67790710277465238,1.3171720638129039,1.3837217749628001,1.2363030176077698,0.63327898522690274,1.1355856350209961,1.345134755914275,1.0503822942209378,0.83016720013120904,0.67416452781659497,0.80040916133195317,1.2144545067848016,0.87050166988542166,1.0682844819820263,1.3884837387010913,1.1735004821408366,1.4094018344420618,0.48952605920187808,0.91579711608586223,1.0195204058476579,1.1735767874825545,0.64923801354130906,1.2418554566341866,1.0922170228487,1.0791418040814151,1.1525454374815138,0.96509246803690163,1.2776638295497342,1.1944689321194468,0.95493718774969016,1.3121131058022097,1.0877676054838545,1.2006254198318393,0.8217467075127336,1.4741295510957761,0.99880331669500488,1.2543528306680427,0.96996504223219271,0.91611102790582022,1.235331473584002,0.52461500662264759,1.1435098443382092,1.1180699118984443,0.19140118926264515,1.110513198559977,0.3494746757958983,1.1979413130678158,0.86382587998564297,0.79151895242953174,0.99794610904820047,1.4119156134951969,0.80940307683463186,1.167430988184633,1.102347360526563,0.64614441125753286,0.4776933947998091,0.40222426862825711,1.1653822634403666,0.98957738154061048,1.5551715110831865,1.1721025324789134,1.2549087673247825,1.4003150756004858,0.84978427061050787,1.1530293784685468,1.2606379810732462,1.4108055064009082,1.2287953438979107,1.1263441519111999,0.73953278581863302,1.2188681082873081,1.1500797617254863,1.1902750761120731,1.1270935136716769,0.9394485865869987,0.97694023046979572,1.206209233984455,1.0514894520773621,0.7509674277019035,0.91295226405829977,0.60426228274704719,0.70989042919932643,0.95661667895934477,0.46056023308752236,0.49343725762252644,1.3307695598308253,0.82701432273656794,0.44449248113455364,0.96614102817130754,1.3963207801664543,1.1986762890803924,1.1324149595060913,1.3551237736839523,0.76854956766973181,1.2189067574056112,0.82387431526634192,1.0002292559087542,1.6643395958020375,1.2908303187127781,1.2304023141151643,0.66750162645455213,0.764129224001586,1.6852349440784802,0.67200977080800683,1.0643438125855218,1.2677713178911219,1.3056273917026773,1.3267336032757715,0.95106130303443559,0.75370399086400086,0.90782283010340814,0.72937059743816102,1.1881206229344952,1.3361065085278321,1.6381640657480578,1.1098343148795158,0.73756558684189433,1.3073424588979683,1.271427668108593,0.92852539122915201,0.53264352883830579,1.2283929686181743,1.338743318692488,0.91146765078821979,1.1608728455074075,0.91723285757484674,1.204694573541254,0.9648127855855454,0.8965972408239814,1.0334861494591632,0.91497840550531129,0.82269485066371451,0.90521892062728049,0.99755435430233941,1.0622485422155421,1.4597270864877516,0.59260065071395474,0.9401142848341173,1.1894569383342479,1.5286062710328601,1.1278043087846485,0.99587397505738007,0.90773292712109221,1.1242924492324868,1.2967173760589168,0.9448425068361842,1.0491284220539672,1.0650809032433153,1.2187832902628968,1.3334141221074067,1.0837482450818894,0.97714879853584602,1.4183989396625729,1.0493602352816753,1.4733555936380438,0.98142320271413508,1.1841768892581057,0.53617342172642835],"cluster":null,"hc1":{"beta":2.1056521298184605,"se":0.1667107311155584,"alpha":0.064947176891093417,"se_intercept":0.10272122435411074,"n":500,"se_type":"HC1"},"classical":{"beta":2.1056521298184605,"se":0.15208621263292985,"alpha":0.064947176891093417,"se_intercept":0.093071845763706515,"n":500,"se_type":"classical"}},"informative_n500":{"name":"informative_n500","n":500,"d_lower":0.29999999999999999,"weight_pattern":"informative","seed":7,"d":[0.60574292151723053,0.52160936286672943,0.29999999999999999,0.80504326184745878,0.48835913885850457,0.46952386335469781,0.43091454310342669,0.29999999999999999,0.95236597226466979,0.49817264347802848,0.38211664608679713,0.35779836492147293,0.29999999999999999,0.29999999999999999,0.39164002367760986,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.85971060104202479,0.85340990147087714,0.56231480189599092,0.57387463189661503,0.43089416977018113,0.41434135234449054,0.29999999999999999,0.73458593338727951,0.99211740083992472,0.88692471038084475,0.95924581615254279,0.6593040351988747,0.8514120785985142,0.29999999999999999,0.63317328474950041,0.33550564737524835,0.4487477869726717,0.30674998108297585,0.77778578330762682,0.59247864373028281,0.40772447593044486,0.73369247575756158,0.57604373148642474,0.82670870535075658,0.60692452106159178,0.76715001950506112,0.29999999999999999,0.73792549860663703,0.47062457341235131,0.80609253903385247,0.39743758973199872,0.69246610694099209,0.60492250793613489,0.41609883918426926,0.59902997519820922,0.61741343906614932,0.54220759600866586,0.33281358219683171,0.9077505376655608,0.54352581801358613,0.9091795360436663,0.77034387507010249,0.80596241431776428,0.29999999999999999,0.56947917847428464,0.36638488504104316,0.93421492418274277,0.29999999999999999,0.6697152649983763,0.50665626476984471,0.91177377258427439,0.86666383841075001,0.81407957898918537,0.31942514285910872,0.62151315188966694,0.49604140007868408,0.90306858289986847,0.29999999999999999,0.29999999999999999,0.47554799367208034,0.43086133503820745,0.83167445743456481,0.33902091735508294,0.29999999999999999,0.38378388602286578,0.69189290166832507,0.48980467333458361,0.3952831553760916,0.29999999999999999,0.5009105916833505,0.57842181730084119,0.8885196284390986,0.81537777904886743,0.62137256604619318,0.5996463775634765,0.57791031387168912,0.8627927059307694,0.99460791712626806,0.38098844513297081,0.29999999999999999,0.81816861457191403,0.41218648869544267,0.70037433151155704,0.81838636365719131,0.80678697284311052,0.37582014312501993,0.61598002421669662,0.29999999999999999,0.57676678076386445,0.5236744397087022,0.75027328752912581,0.79568997332826252,0.91649512404110278,0.48281121752224859,0.9453556239139288,0.97697648031171402,0.60943648701068009,0.99301425744779404,0.29999999999999999,0.89021370310802006,0.39561077612452206,0.7207285819109529,0.29999999999999999,0.8741042732959613,0.65118635697290295,0.52678257126826789,0.29999999999999999,0.81213397129904474,0.86095432129222893,0.29999999999999999,0.43434242373332377,0.83076169497799124,0.9207583290990442,0.29999999999999999,0.35618300703354178,0.43359169955365356,0.39418613342568276,0.9804437504615634,0.6567892938386648,0.33342002036515622,0.29999999999999999,0.29999999999999999,0.50494535623583936,0.91497839274816206,0.44978249182458963,0.42188228941522538,0.78054748752620062,0.99769258571323005,0.33435980514623226,0.85963307027705005,0.55272429012693458,0.7375875314697623,0.92166935645509507,0.65930535127408796,0.29999999999999999,0.47113332261797036,0.41781659489497541,0.76425045074429354,0.74383134774398052,0.90643714659381658,0.33675924658309669,0.46953709141816941,0.29999999999999999,0.31868324836250395,0.36741107916459442,0.29999999999999999,0.61811694230418646,0.51269186115823684,0.64304450533818447,0.56623276351019736,0.59173669626470649,0.54290236588567498,0.64035041397437453,0.62786869455594563,0.86776967407204209,0.38408766160719093,0.78556820950470863,0.63119093175046148,0.73739671881776303,0.75675165415741497,0.70745313924271613,0.93342951266095042,0.82734133088961237,0.95617508639115834,0.59716841117478903,0.48460395121946931,0.29999999999999999,0.52483486339915542,0.83694250425323835,0.55355459325946865,0.88745953678153455,0.43105979836545882,0.76136105838231738,0.93294358851853754,0.3503637985093519,0.87984862769953898,0.29999999999999999,0.59701561508700252,0.42885929241310805,0.43022188937757161,0.57135941935703161,0.29999999999999999,0.88288190807215861,0.86839635339565568,0.56434348612092433,0.35526621714234352,0.72083407246973363,0.89593618148937815,0.29999999999999999,0.36471253172494472,0.68083793180994689,0.34324602924752978,0.53594451891258355,0.29999999999999999,0.57746220973785967,0.38543617632240057,0.8259710175683721,0.77599542278330769,0.29999999999999999,0.33538696526084094,0.43514497340656816,0.29999999999999999,0.64865329340100286,0.48152847560122608,0.86476065348833797,0.65355997937731436,0.90462146950885647,0.57072273623198266,0.6939343836857006,0.3689864464569837,0.65482416863087556,0.29999999999999999,0.94888159639667713,0.81242746536154298,0.75550336292944842,0.86939184132497749,0.29999999999999999,0.29999999999999999,0.87686428674496708,0.779328195261769,0.50489942301064727,0.92247765464708209,0.35929049905389548,0.4139535445021465,0.86385014734696597,0.81780632506124673,0.71540545762982211,0.53995696168858553,0.92740198271349072,0.8075946592027321,0.6672467256197705,0.75452655642293387,0.56550772259943183,0.94700982288923108,0.73943256037309757,0.84505727684590959,0.29999999999999999,0.90555846893694247,0.50304829974193122,0.7476142593426629,0.55525190865155305,0.42383546009659767,0.5370009104022756,0.29999999999999999,0.6476342274341732,0.68345269619021565,0.54846319078933448,0.33805929094087334,0.29999999999999999,0.68938956623896952,0.29999999999999999,0.67626438969746228,0.49353393192868678,0.73486347743310032,0.93810249350499353,0.99001596348825838,0.88564036977477367,0.5502232696861028,0.73892713093664497,0.85670498872641465,0.74041804890148333,0.836650751158595,0.98496260023675852,0.57308567389845844,0.69094757796265183,0.29999999999999999,0.80331059985328457,0.93508682979736468,0.61983270540367807,0.85252435414586214,0.55084395634476091,0.83349991177674376,0.91024210534524164,0.53691729244310404,0.74054524027742441,0.78869798157829785,0.65307426450308403,0.43029573285020883,0.57821255433373153,0.42759940212126818,0.63066210290417068,0.90642952623311424,0.3750133288791403,0.66195392387453467,0.72442894650157541,0.43107095248997207,0.29999999999999999,0.79990996303968132,0.60658279589843001,0.57731742744799697,0.29999999999999999,0.29999999999999999,0.54907975466921921,0.35661983643658457,0.29999999999999999,0.38008254484739151,0.57092829348985108,0.29999999999999999,0.92378210683818907,0.76030765352770679,0.29999999999999999,0.93295240451116113,0.64785863154102119,0.29999999999999999,0.81901765796355896,0.96591757216956464,0.95285733933560546,0.93794680205173786,0.81168449388351283,0.32594077796675264,0.77379950166214251,0.42960025514475997,0.65433044338133184,0.59610971692018211,0.57221813537180422,0.34091915562748909,0.83179086204618213,0.8418410369195044,0.29999999999999999,0.29999999999999999,0.64899139581248166,0.6942587131867185,0.29999999999999999,0.89215516389813265,0.70114821812603623,0.5707818952389061,0.7432811378501355,0.49173084679059681,0.52399567745160314,0.63418901562690733,0.60808179432060572,0.63000582191161814,0.29999999999999999,0.49642969132401049,0.84536937719676641,0.60618765437975519,0.80038188805337995,0.29999999999999999,0.96927285825368015,0.84096836203243575,0.97654747841879719,0.97709276301320636,0.80609701694920655,0.29999999999999999,0.93256880745757365,0.69614271095488212,0.65189709463156753,0.29999999999999999,0.97637980619911102,0.84925540504045782,0.44186126638669521,0.4620339712593704,0.95708495315629982,0.75582964103668926,0.79596889782696956,0.5872568891849369,0.59498175668995823,0.74460442857816811,0.29999999999999999,0.53145376273896539,0.29999999999999999,0.8398014896549284,0.68959091433789577,0.29999999999999999,0.60066198571585117,0.74762388176750383,0.63424838781356807,0.79092613162938497,0.8590418910840526,0.61532825699541716,0.67130675474181767,0.30588764590211209,0.29999999999999999,0.53804364698007701,0.85440729807596649,0.33652213024906813,0.50199058859143408,0.49498198267538096,0.81070305968169121,0.82809062425512814,0.29999999999999999,0.33634032036643474,0.80959130942355839,0.99223650849889955,0.32406797474250199,0.87941377395763987,0.29999999999999999,0.64333252860233181,0.4548517726594582,0.45714663509279485,0.80817089772317552,0.349779203440994,0.69593267773743717,0.7732340645743534,0.69740570066496721,0.6469535626238212,0.73856213463004672,0.92194480830803505,0.36863086395896971,0.5977623043349013,0.42467517964541912,0.979717089026235,0.94678178576286876,0.33448933432810007,0.54081136146560305,0.73653206278104333,0.5751743248663842,0.30609321994706989,0.53447270139586178,0.48061277638189492,0.79466700428165493,0.83577117742970586,0.63258485831320288,0.96434108682442454,0.80510554797947398,0.75057482423726463,0.78111322922632098,0.89810491502284995,0.87140235556289547,0.42754379301331935,0.29999999999999999,0.5406811726978048,0.96093105869367712,0.50726446590851992,0.44263902984093872,0.7662324305158108,0.87697273427620526,0.34200461828149853,0.43894523845519867,0.88599818316288292,0.29999999999999999,0.50210252343676987,0.43216462533455341,0.29999999999999999,0.29999999999999999,0.85420523285865779,0.92893335446715353,0.5354821695480495,0.29999999999999999,0.29999999999999999,0.77965788815636183,0.29999999999999999,0.62810986603144559,0.34882407509721813,0.42022365375887599,0.89220719833392648,0.63726387729402634,0.7860838564112782,0.99008055368904024,0.80405564755201331,0.59194309287704527,0.5673601428745314,0.42999998296145348,0.86405420419760048,0.89291707447264335,0.54076787654776126,0.31139384317211805,0.61039843172766262,0.47701366434339432,0.68048941993620238,0.91024951236322516,0.73019867483526468,0.65815875818952918,0.77978276633657506,0.49231803049333389,0.29999999999999999,0.89282872923649848,0.57612741529010236,0.86646515685133629,0.67802548951003694,0.91674837456084779,0.29999999999999999,0.29999999999999999,0.78564227512106299,0.83954027243889862,0.3459520176053047,0.83468824978917833,0.29999999999999999,0.91294939087238158,0.32989821906667199,0.29999999999999999,0.98493680679239326],"dy":[1.9461203805814042,0.72841036073284904,0.62867759434597392,1.7865929830125127,0.77280520459521385,0.95308166473398237,0.97957240430555059,1.1379366207922947,2.5982515129811694,0.92211146325843085,0.76678423117991479,1.7532320732636091,0.31609565359515268,0.929953830949664,0.83831263391040245,-0.033012611169865314,0.27135351358834975,0.3836521112160669,1.8559267356416682,2.5137968432295419,1.4060443744323892,1.9738922362912603,1.686847760127741,0.66458490584358276,0.47248847956415618,1.2863337484815549,2.1187212494332255,2.2494251811722856,2.2272723056900405,1.5814229159129496,1.7357961411066163,0.77061569612361402,1.1803026203154812,0.93969099596316674,1.6057523573040213,0.81982111911101141,2.239161264654018,1.0603039107555081,0.99175652146581028,0.75372812025912139,2.2004759050297071,2.2490835436726377,1.5701369789920612,1.5847507231061386,0.71941517979508096,2.4694032459838402,1.2687693529530186,1.7921621132673116,0.55970556783470227,1.0376523246817864,1.3732877682937841,0.73484575589362255,1.3213351738481069,1.4064364136892691,1.214590815713539,0.93973492516346757,2.015346189484827,1.3993455476760506,2.0830858520817102,1.7441651023298215,2.3932789303933095,0.4379476526222692,1.3016401327239664,0.89558120063408941,2.4472875930726801,0.24736929851582334,1.835900883778111,0.95427853764743886,2.6853499683577304,2.1144208949998653,2.0253696183705805,0.9274034828622697,1.3414039741548593,0.52315242990155042,2.2873727831045838,-0.168318894645483,0.99444445359700939,0.72742675131058077,0.82917204039523895,1.9925952228242765,1.1095832472200542,0.5976928652684248,0.80705718638805246,1.9293495937954146,1.1529193038576713,0.43799952595874292,0.30643721660743739,1.5494363836532528,1.5863810612725326,2.3934354283480528,1.3293292173522313,0.72378639144689494,1.2985347291022775,0.78905395744060924,1.7295578147243518,1.8695409578897335,0.81143856462700892,-0.0087597334758873213,1.3663109933951567,1.1897572403641918,2.3071118995764799,1.6721803727915963,2.0799116321019371,0.45111439820618698,1.2973660036486181,0.34013177126184735,1.0690425342875156,1.1080070637288444,2.4087051168389926,2.0133061607682388,2.1206441490455177,1.4232534085394177,2.5743910149462774,1.9902975952055721,1.3072207912734204,1.9442664392395188,0.70202128522726137,1.8454578002674924,0.1158550860123333,1.5286211319427034,0.33200134882348054,1.9760418933810928,1.397836146964532,1.0584424785279327,-0.041189319499412957,1.4500768954542296,1.6429785676202637,0.58231847192461716,0.73700365465711104,1.5222032815608393,1.4673576783933182,0.83942780459887179,0.59599671518445041,0.34612398838208402,1.2668585961130212,2.3995235279490785,1.0458422339861677,1.1394719467952816,0.367860823499296,0.62492405649956395,1.032179083635304,2.6008357528432064,0.1996338679088403,1.3819275353097622,1.5071199513162461,2.4292083009014362,1.2769063158523213,2.1022891430960686,0.89850422761934934,2.1756349463392595,2.0240232002585072,1.8380562183005524,0.9954653998834736,0.98796448284988403,0.31399728513167147,2.1241455899350394,2.5498861139538613,2.3395161675883864,0.23830809181526075,0.80721090640740834,0.73847063316064565,0.95411943829359025,-0.089997767099326942,0.44619851513804565,0.96073853668762688,0.88956117445195082,1.5334958894571817,1.3973689072504647,1.4639838379101913,0.77966972931369072,1.1371104704679487,1.4757184899749105,1.6394196217828225,0.95761259015275424,2.0570331714595054,1.0063186230409884,1.5130415568686155,1.652834226667762,1.8134689392995214,2.2777016553621618,1.6905360672638143,2.0709827173624706,1.1481344225292498,1.4071449877057307,1.0338196019176231,1.4041099200154958,2.4286257684668087,1.1095141762330361,2.2517725277142682,0.86103316163299848,1.9421835167107075,2.5498071901680737,1.9221512781059755,2.2503128004775577,0.76629088808768631,0.88021444795540593,1.1128702981544993,0.86222949160432305,0.75332064624469797,1.4205210153513246,1.3019706732409404,2.0918417494592259,0.4324929968196507,1.0384485885200592,1.4438798550071352,2.0083283852811347,0.045572468064380423,0.81292884409460386,1.444086644504897,1.3602127763890541,1.7400571958545854,0.59960857991117555,1.1265040116030756,0.80800074399952815,1.3279314887251297,1.6295216049625343,0.81622053114766713,1.0733902282824652,0.5855509081939646,-0.1431102935736297,2.0481575275185846,0.63059791493003736,1.7775365804643943,0.57591342336677609,2.0740596067379182,1.1507249347809023,1.4016585802133925,0.93890259825022293,1.0251126339426133,0.012541730736920553,2.8083796417676363,1.977770326685238,2.0402923401669848,2.165244629433376,0.39840907432322659,0.36281508608919349,1.8839333181072044,1.8418380198148951,1.1773390080194814,2.20979094488135,1.1857377818240293,0.76645028193654363,0.99937569273986893,2.0743626363435204,1.5675087307523883,1.4203169998763834,2.5708113301692528,1.3781897687124207,1.476999423157914,1.7313248025846901,1.00700881253615,1.6086503332591919,1.5862418083292009,1.5193681190878037,0.94884244327866685,1.5527044629804516,0.85497570301722492,1.4256711183205946,0.95036750667067116,1.29171218846366,1.4497303043853056,0.91848208216473104,2.0810282051767386,1.7524563864809264,1.6158574090773481,1.0787773313697078,0.50467476005705836,1.5096226004424711,0.59357651552241719,1.448701446014206,0.62878422944806012,2.0298037965718589,2.4849960727989577,2.0040062537466654,1.8823206645003958,0.95106577367612444,1.2268899633723396,2.3762594409338096,1.7182275092949473,1.8951827464582618,2.4465658961974288,1.1505169248348106,1.3418076574477393,0.065475660940394032,1.8195052669496699,1.9346091935091796,1.4157014043328171,1.8398877335937132,1.2110613571172912,2.6546674880789665,2.0245098337610603,0.77677675624555742,1.3349106603098972,1.7955783840046367,0.84570146625385278,0.77083760353486652,0.85668622623942436,1.1316772290888899,2.1563919846002113,1.8809036683055966,1.0447629210158029,0.40678100061127087,1.722622948694253,0.4973999936907284,1.037697932107418,1.5656592915106886,1.3518025395447273,0.79940555408360958,0.26904202663026089,0.39339110642320207,0.87968049988278718,0.83933721366716751,1.145555182843325,1.2477605690927824,0.98872532423165294,1.2740039936638714,2.3928510270610994,1.8521530478061017,0.18854923654359174,2.5207371398543366,1.0732862606899787,-0.268298746099633,2.2081001533975755,1.9120975318354618,2.4523028266115947,2.9624449765164673,1.3822165028873108,0.49607594468491889,1.8011560580793013,0.3547931433857564,1.4503304824813572,1.3106965639205899,0.91029630593276745,0.72356104694704992,1.6275443091275401,1.273154563845013,0.56186431970191253,0.52838996222788159,1.3662444656793,1.763749328410734,-0.34520185819647864,1.3000482156976338,1.4470421509576448,0.88607416185369114,1.2401939080275008,0.42464931098246617,1.113857790844998,2.0171796904071941,0.60874420024423037,0.97660256527220501,0.5957550090442616,0.48804616319701433,1.9512973553175676,0.84007361741680242,2.5615373390062546,0.58497181525864261,2.0252330761726629,1.3836999781004424,1.7243749685884864,1.7041422823024082,1.9657468872593133,0.83933623739747876,1.768474109654659,1.9631993533730849,1.4028368960026738,-0.16200444096840561,2.5679531292072499,1.8944509610944642,1.5864398019716999,0.93186155756886868,2.3900717127648563,1.4177946993625019,2.1443983403515166,1.4018565124061775,1.2818498351300456,2.0991705621818566,0.78516768395654402,0.81161995201412196,1.1003986724083208,1.952165029359451,2.1377388280542657,0.83077554680574961,1.3337023119280049,1.5462892013560834,1.9118804645581784,1.776893970077998,1.7184878177617939,1.2447505300456287,1.6960721570050048,0.74479165474069242,0.70561845762172992,1.7292846244689624,1.706578081167915,0.89274762322243839,0.87801392393136024,1.1808419547102418,1.5123711787531082,3.0486689656272961,0.44864278545940306,0.5505109990793392,1.7400722103444279,2.6517450531265863,0.08045839346268846,1.8957113978703231,0.76219264377939622,1.4295231056039066,1.2060333076257828,1.6522339283420142,1.8075803455317698,1.0853390946961738,2.3174788706339755,1.4130233375461685,1.6069755092703755,1.4441168920555258,1.6176924286782106,1.7286803109181907,0.65410235607051936,1.2918110051604152,1.3313613018629109,2.1157909939613377,1.708657851704386,0.63756546300708039,0.71618671108211363,1.766706716802003,0.7782474468744498,1.0758086516235377,1.4724481659305968,1.1412995654545821,2.2540667841179625,1.5205321067718383,1.8559241375773285,1.9325477629562755,1.7653656254896535,0.7499910538810971,1.5977011573016722,1.696655996336915,1.8000080712771125,0.75213823163639593,0.36270901337568301,1.391689551463426,2.6855122863657899,0.86303313231876777,0.29516164072047002,2.0928025498369198,2.1270537849174072,0.60974413549330964,1.5338640421126777,1.2494325422715791,1.1632093391139082,1.5784716508211007,0.61415557330453474,0.713470754298272,0.48125308648421772,1.5990413566085284,2.3496861445131909,0.65522287725777606,0.012783711738667547,0.37132316898565326,2.1027958413060261,-0.14045904283658339,1.3714139932844198,1.0285711391068131,1.2273260164208635,2.5034884570231135,1.0064312262140613,1.8206788675004995,2.5473961633394198,1.9762588742759601,1.5468819079250327,1.7327671498476034,1.089512813815515,2.2301357876131815,2.1642838888636655,1.339800159903545,1.012044105638374,1.245781261560402,1.2131227726192195,2.0821921013495426,1.8134674742581161,1.2557098412654701,1.0954316499750578,1.9049347098686356,0.3455654689332206,0.93575533225668339,2.0037476042259281,0.83493730433931068,1.8316320898481229,1.0680992533586087,2.0021337917103934,0.38946911865831624,-0.012932155029216297,2.4732938958199755,1.7715354845967197,0.36325340414443164,2.0861334960286162,0.81363694012965537,2.3771272995243606,0.83747859920542711,0.88483347357906506,2.9844885621417125],"w":[1.2742526467889546,1.1361814820673315,1.5913269073236733,1.6130051532760263,1.09517365610227,1.0970025280956177,1.2215203358326108,1.4171796167269348,2.0639990226831286,1.0349836130160839,1.2809207184240223,1.3941496083978566,1.5134926443453878,1.4766391772776841,1.2761594734154642,1.479395659547299,1.4074092335999011,1.4211191671900449,1.75138551723212,1.7314598965924231,1.3040038391482085,1.2640859150327741,1.2673374844715,1.1965742202010006,1.4897517644800244,1.6456405657343567,1.9945254841353743,1.9398208581376821,1.926166055118665,1.4129745236597955,1.7274649869650602,1.4129836552310735,1.3254948095884174,1.3312696802429855,1.166194519866258,1.5809812562074512,1.5983912640251219,1.3335376679431648,1.3141880787909033,1.5932736346498131,1.2004011667333543,1.6602363350335507,1.2180162403732537,1.7149885496590285,1.4325100137852131,1.5411972107831386,1.2017980912700295,1.7284509015269576,1.2606918962672353,1.4412576548289506,1.2792739456985145,1.19150206213817,1.2488527804147451,1.346075073676184,1.1306347315665335,1.5319005314726384,2.0075042447540907,1.2585801286622882,1.8579370742663741,1.6249221856705844,1.8047948039602488,1.4540530383121222,1.2327089284546673,1.2853050244506448,1.8948677767999469,1.5820679156109689,1.4364569451194256,1.1769533014390618,1.8328136756084858,1.8694873462431132,1.7281964926049114,1.4166922564618289,1.3223318106494844,1.0527700561564415,1.966513167321682,1.4350818563252687,1.5142824565526096,1.1461036908905953,1.2310157134663313,1.8299324364401399,1.4107119638472796,1.4493562925606964,1.3696656327228993,1.4231129328720271,1.0991324066650123,1.4053410473279655,1.4332612793892621,1.1178749502636491,1.1829662236850709,1.8376706030219794,1.830586245562881,1.3366843485273419,1.2325200445018707,1.2588571874890475,1.7321984626818447,2.0365865633822975,1.2580925436224788,1.5896780814509839,1.8332920711953191,1.3729059692006558,1.5992008403409272,1.7040873154532163,1.8108701180666684,1.3259074411354961,1.3957655392121524,1.4880489941220729,1.3166076217778027,1.2044325180817395,1.5477367338724435,1.768047376582399,1.8778755424078553,1.1077689557336272,1.8977384626399725,2.1109480844344941,1.2730935378931463,2.0387585549615324,1.4781099454965441,1.7939679162111133,1.2726183097809556,1.5307648221496493,1.5465473205316811,1.8633667972404508,1.4895256116054951,1.2233155026566236,1.5658807247411459,1.7163865962065756,1.7827538263984024,1.49293373869732,1.2976684315130116,1.8427502262871711,1.8549622540362178,1.5264593267347664,1.3695720781572163,1.3303442861419172,1.2478874146007002,2.0086549442727115,1.4777838793583213,1.3783758907113224,1.5612484477460384,1.5306588943116366,1.0814510229043661,1.8627462262287735,1.2433095395099372,1.1648135199211538,1.6005741797387598,2.0282588542904705,1.4546445584390313,1.7403981477953492,1.2264412993565201,1.5649520992301404,1.8630945106502621,1.4973051993642001,1.5545643299352376,1.2093863390851767,1.2316454356070607,1.6477726412471383,1.4956734505482017,1.9703556868247687,1.4909036992583424,1.1215814673341813,1.437772072199732,1.4981738698668778,1.4048917253501714,1.5097178782802074,1.3078858875669537,1.1915323474444448,1.4262477844953536,1.2840925018303095,1.3650464154779911,1.1210296124219894,1.4547499353066087,1.3743787739891558,1.902891282690689,1.3283685760106894,1.6653693577740341,1.3771963988430798,1.6706425429787486,1.6519306443631647,1.4987930056639016,2.0645191106013954,1.6750286020338534,2.0331309034954756,1.2631378348451108,1.1251082615926862,1.4696712350472807,1.2043940300121903,1.7363895727321503,1.2477423272561281,1.8337661390192808,1.2125089137814939,1.5544606020208447,1.8989045154303312,1.4137541661504656,1.8008603326976298,1.5370625511743128,1.3682311306707562,1.3094099510926753,1.2240568099077793,1.1931153109297157,1.597074564313516,1.8508611312601713,1.740667600603774,1.3177797520533203,1.2971698332577943,1.6002673832233996,1.9504335565958171,1.4277488597203045,1.3300713389180601,1.4289463746827096,1.463341947691515,1.0779499526135623,1.5201550772879271,1.2928838435094803,1.250456915376708,1.7298713884316383,1.6857763095293192,1.5914728187490255,1.3418238453567026,1.2063132610172032,1.4908449592534452,1.472964925924316,1.2061815641354769,1.809850109135732,1.3637375701218841,1.9817034826613962,1.3374421453569083,1.4699904055334627,1.2918925530277194,1.3824018932413309,1.5077157383318989,2.0110569598618895,1.6462906201835721,1.534020611597225,1.7800346099305897,1.4801961943507194,1.5465818662196398,1.8667432099115102,1.6051395605318248,1.1010436078067869,2.0081030162051321,1.4173248167149723,1.172704116953537,1.7821924347430467,1.6966378448996693,1.4369135225191711,1.1622205873951315,1.9812661525793374,1.7186112705618142,1.5055310895666478,1.700211380701512,1.3107454282697288,2.0747603736352174,1.623846620414406,1.6960087669547645,1.5994913497474044,1.9922476285137236,1.1424447390716521,1.5109767152462152,1.128088093549013,1.3277515354100615,1.1703573708422481,1.4017062940634786,1.335767247993499,1.5610646053683013,1.2194559625349939,1.5153750546742231,1.5838806139770893,1.4685087945312261,1.4035981458146125,1.532985090278089,1.1269452233798802,1.5572983049787581,2.0212829568423327,2.0961654889397323,1.8184312262572346,1.2172777782659976,1.5860612495336681,1.8722487510181962,1.5455169192980973,1.6862933254335073,2.0518326365388928,1.2280566992238162,1.5024319771211594,1.4379640004131942,1.6224835142493246,2.0325467376969755,1.2947905275039373,1.7745656183920799,1.170764940464869,1.8394707198254763,1.8576608116738496,1.1057269719894975,1.659160733129829,1.6576435116585342,1.3164428072515872,1.1453751693014056,1.2560712672304362,1.1481698302086445,1.3971328061539678,1.9205259289126841,1.3585509567055851,1.4302277171052993,1.5128845619037747,1.1976411267649383,1.5991048173047602,1.6924205714836718,1.3159039641730486,1.2992542061954735,1.5824910054914652,1.5942494835238903,1.1282363245263696,1.3633684887085111,1.5078878664411604,1.3298332263715567,1.1572025027126074,1.4758077735546975,1.8621496837120504,1.6823451034724712,1.4171302598435431,1.8718803705647586,1.3479541951324792,1.4467417096253483,1.6467312629800288,2.0106288860552013,2.0471098101232199,2.010725657781586,1.7445546073839067,1.5002497017383574,1.5511248933151363,1.1738172674085945,1.4158589721191674,1.2271780474577099,1.2128720468375831,1.3708736172877252,1.729286398505792,1.8678877231664957,1.5561799546703696,1.5628420661669225,1.4341989275533706,1.4200239872094242,1.4688954051584004,1.9496527065988629,1.5323143421206624,1.2729959858115762,1.6473841402679681,1.1538188537117096,1.1757710398174823,1.347068855818361,1.3233657366596163,1.3024664364755154,1.5880026726983487,1.1676116425544023,1.7166650461964308,1.3950725175440311,1.6507150118704885,1.4012557957787066,2.0435947864316404,1.7583587581757454,2.0076711152214557,1.9947774888481944,1.7537147674243896,1.5316678768489509,1.9864535165484993,1.5764663571491835,1.3366996755357829,1.5162804627791047,2.0410124109126628,1.8910943964496254,1.1385311130434275,1.2415181234013291,2.0088372969068584,1.6094684675335884,1.7312360585201529,1.1869524076581002,1.299883031612262,1.6651140955742447,1.4261335276067255,1.1043518244288861,1.5014504319522528,1.8204303601756691,1.4936248059384525,1.4973218341358006,1.2885389131959528,1.6045149686746298,1.4317588276229798,1.7030497938860205,1.8108330233022569,1.4164561116136611,1.3960158373694866,1.4146799325942996,1.4973307518754153,1.2414329001680016,1.741528187924996,1.4069554164540021,1.1113112579099835,1.1982796229422092,1.7807343443855643,1.7008555090054869,1.4492589435074477,1.363313299883157,1.8017743257805705,2.1200529801193624,1.4910169146489354,1.7856529036536812,1.408622293639928,1.4808026655577122,1.0933935417328031,1.0937324446160348,1.7646568164695053,1.4509970946703108,1.3980124072171747,1.6249236284755171,1.5178432710003107,1.356350152613595,1.5923451612703501,1.8593798418994991,1.4090616105124354,1.3725277916528285,1.2971706566866488,2.1170764682814478,2.0894701560027897,1.5263034739065915,1.1167092327959833,1.5460949053522199,1.2147158680949359,1.396599150216207,1.1848302719648927,1.0856295146048069,1.6069650137331337,1.7149497631005943,1.306756275612861,1.9731789242010562,1.6684266885742542,1.6977179211564362,1.7094402604736387,1.8195521480869501,1.8908980629872529,1.1522591825574637,1.4017051848117261,1.2345617370679973,2.0535837319213894,1.1145543218590319,1.2241619459353388,1.6758339819964021,1.9036215700674801,1.4251834666822105,1.1423122200183573,1.7754863191395998,1.4745399019680916,1.128466695174575,1.3324630651623011,1.4344885132741183,1.5059664668515325,1.7571191915776581,1.9578609813936054,1.1310450959950684,1.5878510125912726,1.5294281714595854,1.6538670767564325,1.4573225330561399,1.3799198749475181,1.3992554441094398,1.3391195888631047,1.8005411907564848,1.2913389344699682,1.7415052414406089,2.0376696622464805,1.7274303599726406,1.363978970469907,1.245885451696813,1.3008072555065155,1.8515801107976586,1.8709261827170847,1.2659705340862275,1.576703977258876,1.3708756269887088,1.1763153241947293,1.4978104756213724,1.9497305627912282,1.598511114809662,1.4643030018545686,1.7457963661290703,1.0209676291793586,1.4838415407110004,1.9322972531896085,1.1779363754205405,1.8247054460924117,1.3672735544387249,2.0142514328472316,1.5309448480140417,1.5434301993343977,1.5714729408733548,1.8172490722965449,1.3567039255984128,1.7785242586862295,1.424416871368885,1.8803351766429841,1.3443797805346551,1.5831200993154197,2.0165153003763407],"cluster":null,"hc1":{"beta":2.6179947625556763,"se":0.12471736455812639,"alpha":-0.25759059701071024,"se_intercept":0.081504419469858674,"n":500,"se_type":"HC1"},"classical":{"beta":2.6179947625556763,"se":0.12188389141289566,"alpha":-0.25759059701071024,"se_intercept":0.07949918297897271,"n":500,"se_type":"classical"}},"heavy_n1000":{"name":"heavy_n1000","n":1000,"d_lower":0.29999999999999999,"weight_pattern":"heavy_tailed","seed":321,"d":[0.29999999999999999,0.97222712619695806,0.82121674630325281,0.81957501345314077,0.94823891832493246,0.67438485631719225,0.76261289608664806,0.88885756705421948,0.48454328496009108,0.54256513917353,0.29999999999999999,0.90841895116027438,0.58957661199383438,0.72373050465248523,0.67213489739224308,0.38239903915673495,0.29999999999999999,0.88541547590866676,0.59572848831303415,0.5259995567146688,0.7393457219004631,0.42194295176304875,0.35209727426990867,0.61590649872086944,0.39587272887583819,0.51404441960621627,0.71842351586092257,0.51183696868829431,0.63985332755837587,0.29999999999999999,0.78634906553197648,0.29999999999999999,0.76482657028827816,0.29999999999999999,0.38707047901116309,0.29999999999999999,0.41920060778502372,0.66528238598257294,0.93502576146274796,0.80720878855790934,0.61066020373255014,0.35264251420740039,0.3488638083450496,0.90847127749584611,0.73330690434668211,0.94274074072018266,0.29999999999999999,0.3953929478302598,0.87753855062182984,0.92581199533306058,0.8195433767978102,0.54436222447548055,0.98703342983499165,0.33645564620383084,0.6201590558746829,0.29999999999999999,0.51158494688570499,0.86738230823539197,0.65487330232281238,0.83951414227485655,0.88295853461604557,0.4497785039944574,0.30008628498762846,0.35425143807660786,0.56399697028100493,0.65502487376797935,0.95211373385973275,0.47855154476128514,0.76604755218140774,0.59329399918206038,0.40511856714729216,0.55351444848347453,0.74621865765657269,0.29999999999999999,0.77060473226010795,0.39887307803146538,0.70467307630460707,0.49328322163783012,0.48391065925825383,0.95427271295338867,0.59590245618019255,0.64478617662098259,0.83897398051340133,0.34625857023056594,0.29999999999999999,0.3022137578111142,0.70228247856721282,0.65101903043687337,0.40535384798422452,0.93597536620218302,0.94235718669369817,0.69627132532186797,0.96912563319783651,0.29999999999999999,0.91047593711409713,0.8789251545909792,0.92094090303871778,0.30588468909263611,0.93064301048871123,0.85936522542033345,0.62579884757287796,0.45225815789308399,0.80433445421513161,0.49077531604561953,0.50140264050569383,0.53404025554191314,0.39066459778696294,0.63774755424819884,0.29999999999999999,0.588542337086983,0.74250400024466212,0.40333165270276367,0.72122744482476264,0.34498637793585657,0.60141300831455735,0.86554688643664113,0.29999999999999999,0.73844558708369723,0.97108340761624268,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.46643116874620316,0.90894146545324472,0.91001899808179587,0.53101902455091476,0.35662139209453014,0.76171191686298689,0.97999887319747359,0.77074878048151729,0.49337645224295557,0.97512620242778203,0.94818043275736263,0.7242446076357737,0.63597809756174684,0.85291837174445384,0.50335238580591979,0.37886628387495874,0.50137343863025308,0.87748630333226174,0.30438888086937366,0.55612595144193622,0.33156086276285351,0.98974581358488645,0.96186526950914408,0.37630931255407629,0.56423320309258995,0.83936076548416161,0.42538783233612776,0.29999999999999999,0.62201562190894033,0.29999999999999999,0.43010991672053933,0.65321234662551431,0.9349609998054802,0.84491254198364907,0.86041485904715953,0.52113775289617481,0.71343405502848323,0.6556547198211774,0.64925025908742096,0.56815403285436328,0.72945515736937516,0.79004978965967887,0.29999999999999999,0.59039373220875857,0.30180282632354644,0.54616175906267017,0.57346038618125017,0.66403283213730901,0.82567077900748698,0.53978686267510056,0.29999999999999999,0.47025402481667694,0.80687440489418805,0.29999999999999999,0.90251092377584419,0.43088376373052595,0.53640655437484386,0.60106094132643195,0.88739448941778387,0.79125298571307212,0.73223513262346385,0.65499916393309832,0.71770340839866542,0.84792157837655391,0.36460092121269555,0.50058008958585554,0.39020466413348909,0.80217815600335596,0.82556247978936881,0.83026052946224804,0.4148576656822115,0.98293980127200475,0.48388557347934691,0.96985441274009643,0.86209459819365286,0.3596636349800974,0.8211536744376644,0.37990296357311309,0.90900248049292709,0.76492473531980065,0.99428879993502051,0.4699020096566528,0.85171106383204453,0.87901175820734345,0.51800436459016053,0.81282295370474456,0.54939893542323259,0.34126312260050323,0.29999999999999999,0.88985101380385456,0.44668104217853394,0.39922615941613909,0.65806663439143442,0.58000330796930932,0.78182808624114841,0.42010303884744643,0.54280676259659233,0.29999999999999999,0.82586677414365106,0.43817946033086624,0.35244924232829361,0.29999999999999999,0.43407700746320188,0.32546653372701256,0.45021698211785405,0.45792903080582614,0.74517742898315187,0.7651269092457369,0.45164110637269911,0.65169739248231051,0.41366385491564867,0.708943325583823,0.78642609349917614,0.60400268691591918,0.76329463445581491,0.60325088722165676,0.29999999999999999,0.45308458891231562,0.91571548874489961,0.47651950051076708,0.89718580541666593,0.72417121415492147,0.91653050715103745,0.29999999999999999,0.56679269068408755,0.54839834307786073,0.91953980023972681,0.87836160955484954,0.69970664849970488,0.38432033213321121,0.98700269283726805,0.95605746316723517,0.81345218874048442,0.80152949455659828,0.48762795892544086,0.29999999999999999,0.74346686385106286,0.47923784547019749,0.77854586001485582,0.83254793451633302,0.29999999999999999,0.29999999999999999,0.37767988306004552,0.6810396343935281,0.29999999999999999,0.90508073575329029,0.80763286368455733,0.84904119020793578,0.43517857578117397,0.95559155249502503,0.32821826576255259,0.85529405879788101,0.29999999999999999,0.76812235852703448,0.510135444952175,0.95126641578972337,0.42525040050968527,0.99503354269545519,0.62532746496144676,0.41183966726530341,0.91192779820412395,0.81459246629383408,0.29999999999999999,0.5849884066032246,0.72400474864989517,0.88195162820629769,0.88081250863615423,0.68346713520586488,0.84592404414433986,0.29999999999999999,0.71008912934921675,0.52201600419357419,0.43528150524944065,0.31790816220454871,0.29999999999999999,0.64552217216696584,0.85877805198542767,0.95609986360650501,0.29999999999999999,0.94778626391198484,0.59944029077887528,0.54418670909944922,0.78438139064237467,0.58294350653886795,0.94518021610565484,0.34540457318071277,0.30843000984750685,0.73928540018387134,0.58064229809679091,0.97799213507678351,0.90564395156688982,0.46675431597977873,0.78706309769768268,0.81266414718702429,0.88706089067272842,0.38705551226157692,0.29999999999999999,0.95182180670090011,0.64894745862111447,0.59060489521361881,0.97513640832621595,0.9880928130121901,0.99775517547968773,0.7380868184613063,0.71920155670959496,0.69320613057352598,0.3071424061898142,0.34432091864291575,0.51009079357609155,0.38371783616021277,0.5496129476698115,0.92910454529337583,0.91419177630450577,0.65119761361274864,0.89025236133020369,0.43587005767039955,0.46506397114135323,0.82063351979013532,0.72461802975740275,0.83426643505226816,0.40514002398122101,0.41139068531338124,0.96422420076560222,0.96917527539189896,0.89840651513077319,0.29999999999999999,0.9682317096041515,0.41573722816538061,0.7837157375644892,0.59668683847412463,0.99924481355119488,0.80035483280662445,0.85434015200007707,0.53321181251667438,0.80580549545120439,0.98065994030330328,0.76848435238935053,0.96926861959509547,0.71598220432642845,0.79382450296543539,0.43771415285300463,0.29999999999999999,0.29999999999999999,0.39795515818987037,0.90869700810872012,0.54051175722852352,0.29999999999999999,0.7844628856051713,0.82708545082714402,0.32385102182161063,0.39168318132869895,0.47398001824039965,0.47474479679949577,0.3775563242146745,0.83643693220801651,0.29999999999999999,0.42958054400514811,0.29999999999999999,0.59582221144810321,0.48291462045162914,0.76151841208338733,0.29999999999999999,0.56897176832426333,0.99200126710347825,0.85222303029149771,0.39610369363799691,0.29999999999999999,0.68680812276434144,0.29999999999999999,0.44951975871808825,0.96838846581522375,0.97315507282037284,0.6653949719388037,0.80570600538048887,0.96702065279241645,0.72327066422440112,0.57555948817171154,0.29999999999999999,0.29999999999999999,0.48622916932217775,0.59218374150805175,0.29999999999999999,0.36292705330997704,0.37761407815851272,0.42791381159331648,0.29999999999999999,0.4525769732426852,0.29999999999999999,0.69123444617725904,0.83784014007542273,0.49865118570160116,0.69151649600826204,0.29999999999999999,0.67895192301366469,0.49104348719120022,0.55398176389280707,0.99664196555968365,0.31044872156344355,0.34353238504845646,0.66380765843205147,0.65179805813822889,0.50295297449920329,0.93543081730604172,0.3513086137128994,0.78151098513044415,0.81591692559886719,0.63784038734156634,0.29999999999999999,0.29999999999999999,0.34624308894854039,0.29999999999999999,0.37075743321329352,0.72563472206238655,0.53870421438477933,0.29999999999999999,0.46629390968009826,0.46999236429110169,0.993984784442,0.41019601041916753,0.67069347705692051,0.99564069537445898,0.66518267246428875,0.4935662962961942,0.45517448391765353,0.45911700341384853,0.29999999999999999,0.62159391343593595,0.29999999999999999,0.86461700391955665,0.71940486645326018,0.29999999999999999,0.29999999999999999,0.44874135002028193,0.29999999999999999,0.54297413444146514,0.49632543257903305,0.99873932446353131,0.94512526332400737,0.94976225981954476,0.8646503844764083,0.31078043167944996,0.29999999999999999,0.52225410265382377,0.66855951903853561,0.86763959499076004,0.37632925740908829,0.83245776682160788,0.81655743992887431,0.62995236595161253,0.5360769307473674,0.44084467154461887,0.29999999999999999,0.87585012130439277,0.29999999999999999,0.29999999999999999,0.58108416786417361,0.29999999999999999,0.77308090943843122,0.29999999999999999,0.34178074239753187,0.60983487071935083,0.84282199228182431,0.95046443361788979,0.74794911141507325,0.85164813245646653,0.40514592065010219,0.32075524751562623,0.66885287635959678,0.29999999999999999,0.9138884466374293,0.46317516525741664,0.41232398245483637,0.81538314211647955,0.29999999999999999,0.92629656102508307,0.88165510089602317,0.49786400974262501,0.43729950897395609,0.46618112830910829,0.44126187635120001,0.43674898729659617,0.29999999999999999,0.88909892856609074,0.41679835719987746,0.99490566337481134,0.29999999999999999,0.29999999999999999,0.47649759009946135,0.52342210612259799,0.61121476213447745,0.75912018534727388,0.47703435875009742,0.52037903692107645,0.70422769852448253,0.30301343314349649,0.46009291433729227,0.33095587284769862,0.90474376182537519,0.74832130742724978,0.69121371575165536,0.70544367069378489,0.7230262746801599,0.61800788391847161,0.78826734172180291,0.6550016938941553,0.85037040286697441,0.29999999999999999,0.29999999999999999,0.39095829883590338,0.30730567106511442,0.62504112154711033,0.45577168657910078,0.74658229323104019,0.65431935396045438,0.77063097455538809,0.68217140268534415,0.56837597431149334,0.56061856886371964,0.72709866887889796,0.73705707818735389,0.42705163743812591,0.75775720260571688,0.96963510946370657,0.73380996191408476,0.88695405682083217,0.42588234934955832,0.39303978432435538,0.29999999999999999,0.29999999999999999,0.30909879233222454,0.70119508262723684,0.29999999999999999,0.81538092948030672,0.78860228343401095,0.40167497820220888,0.78264414023142304,0.70847997893579295,0.51049411545973267,0.30691494764760135,0.29999999999999999,0.60569566944614051,0.78685287183616304,0.42148197209462523,0.86195704478304824,0.44443124362733211,0.55398876133840524,0.72562122687231745,0.92079153934027991,0.29999999999999999,0.36514361440204085,0.42810058246832339,0.38071795313153417,0.29999999999999999,0.87370848213322461,0.50325675350613885,0.92274821689352393,0.78357316204346716,0.49101475861389188,0.29999999999999999,0.83515321060549463,0.57477764626964922,0.58970349191222338,0.83569278432987626,0.29999999999999999,0.87645472951699044,0.40955513068474825,0.41722821458242831,0.83255811743438235,0.69892380638048046,0.46385454398114234,0.29999999999999999,0.69639917579479516,0.41148520379792897,0.59297807610128073,0.29999999999999999,0.84100254266522823,0.8261403096606954,0.90532478759996593,0.46205347981303929,0.44572543434333056,0.68764539123512802,0.54751953845843671,0.29999999999999999,0.29999999999999999,0.60709420756902543,0.62688798855524508,0.84516784199513495,0.29999999999999999,0.93622716683894391,0.33309693424962461,0.39555397774092849,0.36734256746713073,0.29999999999999999,0.34000025871209799,0.70010991562157865,0.50872597468551251,0.29999999999999999,0.77372160940431056,0.29999999999999999,0.48333268221467729,0.48859299311880022,0.40994268720969557,0.53220494734123347,0.29999999999999999,0.477137062815018,0.98478029163088643,0.53987919867504386,0.73261952502652994,0.98159324168227613,0.81243780963122836,0.98113226103596385,0.39190098957624286,0.29999999999999999,0.99674042901024218,0.32099393042735752,0.77918465978000306,0.52013071349356321,0.7435216556070372,0.34322468047030269,0.88264577223453666,0.40283296939451246,0.65169031566474578,0.95720095552969719,0.66223394491244103,0.71452031254302706,0.53882590155117216,0.80275936513207846,0.29999999999999999,0.52568542545195662,0.45217405799776311,0.70410599848255506,0.51637508010026067,0.37929312202613802,0.77670185039751227,0.90583830124232911,0.66887115163262933,0.30435115897562354,0.96434078400488943,0.29999999999999999,0.84959933420177547,0.5825129962526262,0.47302804994396863,0.42369933603331444,0.56746911944355816,0.78554762559942892,0.35091587486676873,0.76773536249529561,0.87147108321078115,0.51763868425041437,0.29999999999999999,0.65080241293180729,0.6070078637450933,0.61534586665220559,0.82774060920346526,0.49228164912201461,0.66754969037137923,0.86621674639172852,0.45949995554983614,0.58665273406077179,0.69133736700750881,0.36776541322469708,0.3857925507472828,0.54844844145700333,0.29999999999999999,0.31207677032798525,0.52684956642333414,0.34407016222830861,0.46668286800850178,0.75579742058180266,0.65583108693826941,0.29999999999999999,0.29999999999999999,0.34511328963562843,0.3656165802152827,0.99291491780895735,0.45873715346679089,0.57458758708089586,0.95387313817627728,0.34338890758808699,0.69397879254538564,0.29999999999999999,0.43467530095949769,0.75512409287039184,0.29999999999999999,0.74252426601015031,0.40210403355304147,0.29999999999999999,0.95519200591370457,0.67649001111276441,0.95038485906552517,0.91243415207136414,0.72050338881090281,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.8369191190227866,0.42425836713518944,0.29999999999999999,0.8417647200403735,0.38410226816777138,0.51790419032331558,0.63089818202424797,0.29999999999999999,0.93963949659373602,0.71558575113303957,0.9849915538914501,0.73666572435759003,0.52303079760167748,0.29999999999999999,0.48902638785075392,0.33234480549581347,0.75622756839729843,0.87496721264906219,0.35580665010493245,0.50364529786165801,0.66108561558648937,0.93060110632795834,0.54068790334276851,0.29999999999999999,0.96988071158993983,0.96519080691505221,0.39918756084516643,0.60160506924148649,0.47382710676174611,0.87723672934807828,0.29999999999999999,0.92290965148713433,0.39297461619134988,0.6578929098555818,0.96805717162787908,0.29999999999999999,0.65275854547508061,0.97138012268114826,0.81119456577580418,0.85650392584502688,0.75230602559167892,0.47404810683801768,0.95233818506821988,0.29999999999999999,0.29999999999999999,0.36667282958514985,0.99829830708913503,0.9657845214474946,0.37331317190546542,0.66917258158791804,0.59713006620295339,0.91858093407936392,0.93912049096543337,0.99224863888230175,0.33333524817135185,0.76283346067648372,0.48802536751609293,0.60442542200908067,0.94366753359790889,0.61547125594224783,0.61666641749907281,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.74450097871012977,0.40548462935257701,0.29999999999999999,0.8130034507717937,0.78885205299593508,0.46689335936680432,0.58502557761967178,0.52597230344545098,0.29999999999999999,0.29999999999999999,0.31941114373039453,0.29999999999999999,0.44114253188017755,0.58870373403187837,0.89557337337173515,0.95703806600067765,0.89476481603924185,0.56360547987278553,0.82669095569290219,0.47331504561007021,0.50771959584672,0.57269486263394354,0.59081812661606814,0.81317596582230178,0.95355718331411476,0.89481377045158295,0.97341196516063055,0.29999999999999999,0.76401512019801876,0.79717731906566769,0.68191644004546104,0.71770417897496364,0.91386841947678477,0.45871598543599246,0.93382434467785058,0.32890619107056407,0.98233672964852303,0.45695464522577822,0.75549678809475151,0.29999999999999999,0.74910778256598853,0.43898138643708079,0.70969152143225067,0.87434693605173375,0.97504229811020193,0.50602185996249316,0.98377297515980899,0.44735320506151766,0.96340062958188355,0.91542837247252462,0.83539828418288375,0.66946156367193899,0.75872671129181979,0.92746051130816332,0.40451866339426484,0.29999999999999999,0.74292007591575382,0.30875771925784645,0.62723908841144294,0.69728516920004036,0.47682083039544521,0.46229475755244492,0.81831052936613558,0.9440986467525363,0.56096567583736034,0.29999999999999999,0.65305645389016709,0.93416907929349691,0.51887405838351697,0.73981025717221194,0.53420420347247266,0.41807799106463789,0.47703498769551511,0.74291661793831731,0.88094921959564088,0.5623566732974723,0.29999999999999999,0.29999999999999999,0.31532079288735987,0.32935074677225201,0.49646151703782376,0.32890328445937483,0.67695415245834734,0.79510117121972135,0.98638165935408317,0.29999999999999999,0.29999999999999999,0.5607746120309457,0.8347693131770938,0.29999999999999999,0.88964662675280115,0.60455917387735092,0.86761365225538611,0.29999999999999999,0.71749284581746897,0.29999999999999999,0.75099994831252836,0.91231022716965526,0.91187383194919669,0.49673033847939219,0.96011709657032041,0.77424494430888435,0.82611082631628951,0.86535575906746087,0.29999999999999999,0.99974264837801452,0.95215783028397705,0.7826833177125081,0.47252012901008128,0.3545822123531252,0.36777898045256735,0.46792155196890234,0.81220460468903177,0.36069947564974425,0.66278297856915735,0.84086098773404949,0.35006251654122023,0.77413474423810835,0.58612214925233275,0.83292741563636807,0.66904242802411318,0.82651366512291125,0.29999999999999999,0.37458910609129814,0.96568732354789966,0.83065947920549654,0.87804049078840762,0.74434889159165318,0.48250842976849523,0.62108354419469836,0.50757810838986184,0.52892781363334507,0.74886646359227593,0.37906515286304054,0.33072699571494013,0.58187171476893129,0.67866719539742915,0.40632071588188406,0.29999999999999999,0.37305932079907506,0.92356701255775986,0.97305400851182633,0.43106509183999148,0.29999999999999999,0.81850741242524228,0.39926447244361041,0.68570983151439568,0.3539451344870031,0.29999999999999999,0.94969223940279335,0.5733583775581792,0.44861823383253063,0.49032471438404168,0.71545295368414363,0.89369166532997035,0.29999999999999999,0.77924138286616651,0.41653227694332595,0.29999999999999999,0.80090943048708141,0.31709580007009208,0.5043196994578466,0.29999999999999999,0.7662066919961944,0.72982241699937722,0.70808527697809032,0.38672109893523154,0.55682473669294263,0.43930957484990357,0.52855216192547227,0.75609538212884209,0.84655186629388479,0.66891609621234238,0.67180410763248799,0.44758828771300613,0.29999999999999999,0.71448137008119372,0.50536501065362238,0.49869627566076813,0.51747921439819033,0.29999999999999999,0.29999999999999999,0.59754521402064709,0.35448665942531077,0.93441140598151828,0.29999999999999999,0.62595561274792999,0.58274649048689753,0.29999999999999999,0.72029706195462495,0.83579666544683273,0.29999999999999999,0.87482253727503112,0.29999999999999999,0.51470559281297024,0.85427502118982368,0.72145141817163672,0.36146477460861204,0.29999999999999999,0.65440340496134008,0.41374086968135088,0.90771335475146764,0.71725438863504676,0.94579694084823129,0.29999999999999999,0.82435712032020092,0.98878560576122254,0.29999999999999999,0.84752690559253097,0.7170465768547728,0.64612574805505574,0.59042400503531101,0.84358532633632421,0.79796558537054807,0.7189619040582329,0.29999999999999999,0.58479582690633836,0.6240330889355391,0.29999999999999999],"dy":[0.30175990372659256,2.9394076373206492,1.7002421761903805,1.358909618481758,2.236878205554754,1.2829525695423496,1.3105806573870034,1.6138975112423415,1.3427775583491353,1.3390500587600664,0.91985435352328948,1.9337986601863375,1.200213011051664,1.9450229475055147,0.91686095348926611,1.2232068012007491,-0.14803847078227217,2.2611436301845536,0.64396683359577933,1.9342961828993399,2.4709645854476769,1.1698365837200639,0.86325109512252507,0.95874715741771332,0.57772819532081332,0.84634329486706372,0.85682345110160707,1.1462336516376226,1.5337365431135768,1.0135794608357567,1.9149307783213181,0.20816216852849651,2.2912542478245119,0.7575288110850118,0.99628174668435254,0.72966656294957855,0.89447883384913274,1.8354482534134433,1.9757001305540194,2.6092076706678875,1.2988453403342093,1.1498245819718069,1.0071152049110497,1.2426812301157062,1.9691712970733146,1.6737836377439752,0.82416679072653287,2.0920236441170217,1.8514420178757709,1.6522000979308458,1.8001526535727641,0.83034069827281787,2.4953985206750544,0.75055442653325533,1.615002557874093,0.810501808299538,1.202244028365457,1.8689687865930751,0.96298112073932129,2.1220138550595489,1.6599883683854917,1.4855307249305381,0.73769525670919078,0.96626084742025897,0.7809664562005123,1.2826829976981855,3.232536179670066,1.722375880259825,1.7030389375081691,0.94525986560664688,0.6001083227079822,1.8809475709034322,1.0523903299058697,0.56293857727029251,1.5247341314872542,0.21358791926510035,1.3502769561983632,0.75707169348174319,1.7109662309944629,1.8700311582378126,2.0083731138118068,1.029089300758055,1.9158907148133648,0.93272576652150896,1.0151637681072392,0.1449559854238357,1.2229291114950782,1.9944780265647071,0.29935298891958217,2.570739575946547,1.8719145671450972,1.4670270677352941,2.2033942487122506,0.92382947625052925,1.6516953019452068,2.3157769690752481,2.5466631630331742,1.0302987407027651,1.226942301028223,2.0926943866902743,1.7898084438440836,1.2506533698748981,1.4086623848220408,1.0695989362770193,1.5615928952287177,1.1047705750191235,0.77454829076212484,1.8683953777554094,0.6337848936573659,1.7136049498899195,1.8101535510465843,1.5352512011921902,2.2197397760228652,1.3301525768785938,1.3986137446624938,1.8322769219091268,1.0119283557501846,1.8718351684914034,2.5112957836450165,0.27393912637602502,0.68180546773890915,0.38229767393268732,0.65430396682229164,1.8662244835372161,1.7979982425179495,1.1558823863989611,1.1124217087053334,2.0782333680137191,2.2653558266232396,0.82188611816151358,0.96403219618539393,2.3423735671961361,1.97660889112853,1.9451537514616031,1.4585908613521525,1.8189394334744295,1.0574043557727471,0.18584360170519043,1.4289603927958603,2.1904231022273004,0.69657337824668175,1.4493931399383788,0.5018398486971567,2.5857999342664502,1.9852315254399202,1.0300419541669701,2.2818362614905974,0.98611766858264316,1.1596823217807513,1.081038280084488,1.6940394322286276,0.1233138373877376,1.3227418506384421,1.1222073488465643,2.6558891844292818,2.289786325607408,1.9567069232156074,0.85719561725112148,2.3473406907424157,1.641402918940766,2.0163761910637144,1.5603033374096857,2.0486402612304859,1.4637144346941722,0.56800430967987248,1.9725145153762904,0.35007392332354881,0.66449450075377114,0.74373339584682008,1.3823311220222072,1.9825390719989309,1.046444553469978,0.81932875043726527,1.0095093030101716,1.8922675872640859,0.47147348670039779,1.9121082431261065,1.1213966267394992,1.0927583417320428,1.8465802474882012,1.6862324635482271,0.75437761982880258,1.8624918291417869,1.0609378875777717,1.0848175371393238,1.9089185683718524,0.40632890923973997,0.74476232824916044,0.48927694202042449,1.9996611373811723,1.523834738522472,2.1217268797954776,0.92914049300246115,1.9733870068936237,0.56529021416583225,1.9560890311056434,1.6462701879551542,0.67642576824481515,2.0946993611281326,1.2280802594032527,2.2776942398061961,2.1207616192517009,2.2178244172165509,1.2921964016080187,2.3712909376360711,1.9715596947732794,0.86041041304781252,2.2554568714265999,1.6075215055653878,0.11014365508089163,1.0274991986080546,2.2603620961549771,1.3241600102560824,0.60546878852999519,1.9669331405660113,0.97778876219124533,1.060287516341949,1.5475685981214373,1.1656613226098347,0.98604382270268687,2.5498274398394281,1.1420248607029315,0.99441515838738959,0.011890896895697889,0.94994619679157088,0.97474984837131684,0.50037509859629314,0.50680994323020834,2.4635057299560481,1.9801794508642545,0.84914171830333474,1.1011341050857912,0.30355489546120862,0.69359203601418395,1.4087456571112738,1.5964488899152567,1.626901281639908,1.4758169532802676,-0.23667412456298043,1.2650513046394891,2.2360213944835889,0.30383035764285249,2.1716553759802091,0.89329059620276752,2.4319929389974573,0.5673899863309082,1.2003597355271809,1.2801682440319384,1.9022966770795455,1.5517893880149367,1.1565569188665861,1.1809488486297814,1.8994580099825193,1.7311280392969615,1.5305710719146357,2.1021430478765302,0.68770493851672176,0.72726004352019813,1.817118070053624,1.0935703750532841,2.4417700500150969,2.4117273187958608,0.92700900750924431,0.1289216505534107,0.63869673476602895,2.1335214768726019,0.72150363543462193,2.1355054930141142,1.9616768542420613,1.9963215466097173,1.0651613629590913,2.0637920113876236,0.44951008009930982,1.9297291382495252,1.409156220029407,2.1953320466858024,1.3970966435585583,1.6705366614017589,0.64570730893888961,2.2267066988767952,0.96110990182828759,0.71017658547075335,2.2799724812003506,1.5143661695248023,0.31616202553768269,1.4474289128017488,1.5863231556180653,2.0278036863955973,1.4487616779344026,1.394024555050732,1.5829307950664397,0.70119483568759944,1.7105866645123726,0.40658294687864316,1.0462764809284379,0.8054404528748158,0.35651446330397102,0.53717836173372324,2.0131463741704412,1.8288430060941896,0.88366403207867306,2.1506242491721266,1.6346333095496919,0.97613260250297362,1.3474754156573978,1.7458945762265881,1.8162576815463032,1.5166941430511318,0.6951984301180486,1.4607894912534971,0.96426308528501625,1.9643776579724543,2.4013600620947431,0.24089007751000069,1.3022196484838444,1.8962355071425057,1.754869463912224,-0.12614604105702132,-0.37126702053449456,1.8338617246054736,0.57228086761507169,1.5118441496927602,1.921204164346483,1.7367015771997913,2.4589922295776918,1.4051891001658523,2.031119716897102,1.3983897365538229,0.98562224656897535,0.78400932208147156,1.2714376528872902,1.1044092849095222,1.9841557458930261,1.9384692500469924,1.7882304037087409,1.6139279614424942,1.1274099656497998,0.47425350557318902,0.083251581049465173,1.2889818392284575,1.4775706450926296,2.0986597461542034,0.96499966009245997,0.85002226742691067,2.5798928050116787,1.9342568182256903,1.7462408971548089,0.55808610052564667,2.6369379313378025,1.0221008855609035,2.0861873804105526,1.2234453495258986,2.7169210037860583,1.9737329001151351,1.734730464155211,1.0202940292481724,1.5087202949147869,2.0238553109117259,2.6484638928124271,1.5996053263570327,1.1681912642409178,1.9013413026979051,1.1099957284617716,0.46803070514723055,1.00720431525202,0.28108749975286262,2.0972567673982421,1.073314612299991,0.089628167460951591,1.4280532942826651,1.3004395651370064,0.80060364765143954,0.73773053500741304,1.6530013364480776,1.6003153721707992,1.8631856124330817,2.791489131351979,0.76248495842348996,0.76752522176648597,-0.085632315286411131,1.0038959593335708,0.48941014363117086,1.9898328250763038,1.0158749620186702,1.532685498167571,3.0254203024582091,2.0942534002488311,0.82026680264280483,0.84089021149414278,1.5279628728729484,0.69445446511311992,0.59369674380226845,2.1626072570321564,2.562966375702052,1.0349242132127019,2.2611330987489437,2.1371999220200792,1.5326698945849553,1.1404361403698231,0.53645020508316443,0.67487356606856264,0.97695424347999049,1.3152650284055318,0.94447247399570611,1.34903392070155,0.59511333332968708,0.94676561089217992,-0.024349227865251843,2.1371487578529491,1.4377465252577071,1.8161755217190534,2.9362666479150583,1.3743473457914388,2.1929721657614789,0.9286821458256953,1.1798609021554367,0.51955693625332167,0.9923126493965333,2.5490707325306294,0.48948290593428923,0.64118277560871639,1.9208371262709758,1.2520635649501295,1.0319511617915125,1.2045037226897493,1.5116278431323336,1.2477001328973303,1.6716168534537983,1.1925689790173666,1.1069485370704293,0.5754190069171663,-0.25639167528825924,1.469988452237154,0.47200913850562526,1.2424725706111965,1.2061624807015394,0.77735605990884415,1.304006415546745,1.0289651872447709,2.5124081704187451,0.93306843909877957,2.0933638619996087,2.2773443429183469,1.6439378402109979,1.3440845117691924,0.81840550569984483,1.077944655827372,0.78378141005843749,1.7576830538967618,1.4319432643422996,1.9056718928766083,1.572037094854734,0.93972973555005179,0.45057800082425614,0.69216278964743005,0.42541862813794984,1.4378644623283523,1.1675219702768453,1.8933846131692957,2.2471847103219966,2.2072889486638627,2.0343137005472953,-0.46482127191767542,0.80293726551266553,1.437721760809598,1.4665771052430858,1.2323509588818862,1.2095974882033187,2.3271823666881608,1.4862613532834184,1.7120721822547489,0.96692884371257959,0.56239197816280873,0.12661148407121103,2.1776739769882347,1.1508687153914123,0.57473882419602051,1.0836674622801845,0.45758586034990545,2.2903332469109117,0.83611966371345359,1.224561633584476,0.78788357266488784,1.5654168202860663,2.1195298642508913,2.2451458770062107,2.1287165023226655,0.41440615431765926,0.13048778774055525,1.6023499359369577,0.48979224581678515,1.9194095505368065,0.66730252846435234,0.44741255337965335,2.2732308951467588,0.99129410478696323,2.1949538061047686,1.5765231130746828,1.4549339280563867,1.1216333756279289,1.8231034467072968,1.3620946388266848,1.1783751234403801,0.84485647089942817,2.4585886433203008,0.52314287811307203,2.8543551461774812,0.83842619073137703,0.84779013709147288,1.6901311996146129,1.0833237143990329,0.74458396485088008,1.7418468441756738,0.48732858773903243,1.3188138486457657,1.3982462594979117,-0.34917589609408539,-0.0059613235485999061,0.87745563649485903,1.4239662743341934,1.2567306555870326,1.3964940371154251,1.490399403350259,1.8974188920428905,0.69116375753812798,1.8895646425823727,1.8984453734720095,1.7623694224680528,0.3161346213757375,0.91181854119614081,1.0175613051236148,0.71756788268275562,1.6562566458232721,1.2160958623339719,2.1398080764437228,1.5005629680340649,2.1035653697384014,1.3330678211022524,1.9155253233519731,1.1667338336417739,1.3689374838614532,1.8435947536322745,1.2569502312542145,1.6395726291209161,2.7593818074417902,1.1222475998563584,2.5364685353027077,1.1765039644818784,0.2710641879031237,-0.23883300376070582,0.8786490557481037,0.63271214383828045,1.3670417956544128,0.42716482180182402,1.7203198368293138,1.332540961950248,0.41103648331356613,1.3231578189995967,1.3630766963254544,0.68322589200300032,0.44387860360735409,0.19895676231807302,1.3114541685749614,1.4385146459217328,0.80039128708448648,2.3906699192234613,0.20037411331588062,1.0871152708828733,1.26461545088676,2.1123003175752784,0.87411376441161548,0.47328357234322638,1.0147689772271375,1.2306159016741589,0.53434715441502301,1.9407141842313558,0.85334507390930814,2.2334380665439384,1.0530839605731925,1.0006683417588682,0.32101430431973421,1.717980527213737,1.7403622774173004,1.2664350829581916,1.5083193888828184,1.0979060890157508,2.2906889945478506,0.32210792389193399,0.52234887922279283,1.7166062767297652,1.1332784843455226,1.0913775003705222,0.80615195487015234,1.4919270684924,1.0258623349850051,1.5883248353862198,0.90155663296128141,1.8780910627161655,2.5179520394234793,2.1035557963402263,0.63479752464167027,0.76120847307684247,1.0167749599184925,1.7550555892080544,0.28304939833848114,0.3000291231646075,2.2184454045830488,1.1652999361103262,1.7348460981807425,0.5548124585658496,1.5482829869246695,0.76503116022178885,1.3260283940261754,0.83021385431122485,1.2856096782513737,0.43072312331058404,0.34466057531155281,1.9843616709983489,1.0046029829090346,1.8715281511550643,0.28657245847200963,0.44123734396781999,1.1217011751609849,0.20041801860098485,1.8528423413484483,0.85442124080165271,0.63261365701571259,2.9525349530125071,1.3455821354876845,2.1097351100524722,1.6619939031383559,2.8326247534907281,2.5045153417393307,0.49387058939085726,0.48063521684763411,2.0389912443631801,0.06077068286910936,1.2988328703487784,1.6355028956473141,1.4697461485572219,0.26609123483318065,2.082085072664102,1.3172264071512614,1.6510234435945121,2.8779020147869576,1.2853104938806028,1.8254041606941782,0.62770904274951755,2.1120987449881659,0.11444348874486765,1.6718756220886952,0.70202136070626908,1.8483827041093173,0.772842174697705,1.4309719097349265,1.3542884042690708,2.6923633356087842,1.7445002023631953,0.61833165232575416,3.0969951114685639,0.86680907701091803,2.0237556262622909,1.3712549696538359,1.0049897036291182,1.2431025989654774,1.5552836523053304,1.8420014688896522,0.40265959985550748,1.6882919930316247,1.623541013394687,1.4490868549031466,0.88185774092095315,1.5438456076139686,1.0441396346357188,1.3257828092004009,2.0902158992522941,1.4174511745969915,1.8491640672903003,1.5598048571589984,1.1068327180191622,1.844605309621361,1.3191539554008584,0.92855267554255061,0.72840435886872157,1.1906729600009462,0.27225492868804874,0.28088816100449499,1.365613893342746,0.13654838802640168,1.7769098397751906,2.2302545788680432,0.95978150890488445,1.1688110231177657,0.7960385298835877,0.47727595981936155,0.28431958460832263,2.324791832947215,0.2967887344373773,0.51292703955460539,2.1803610047194351,0.52492243722958509,0.93601889444361264,0.55073581502340341,0.61414671673682697,1.552274469975907,0.69949321362763206,1.5160232128618774,1.3034183578397007,1.3591779373587225,2.1205897835728056,1.6554308183910256,1.7663386547340454,2.1273465706735477,1.5164505408427051,0.51516325586256517,0.83833649827475276,0.4109126199629175,1.5627099626485934,1.2478598721361926,0.78136690931079644,2.2487029161099761,1.1597029548307063,1.7828159895937152,0.95560284504382231,1.3266610071379299,2.0742107557302405,1.7792387102645795,2.0667403672756977,1.3368460576075152,0.66122232471348708,0.92477146157303758,0.92004145468404985,1.0769489783153503,1.5115519290190296,2.2467177173985275,1.1564493982374175,0.49783322720035461,1.2837004789394428,1.9883186264964323,1.4392766704939133,0.63277214055394193,2.7930071744612275,1.7022342976348095,0.99084273544465451,1.1883550225165616,1.0325618871232254,2.2780575799958345,0.39827099615576439,1.6475672850396288,0.49027180782472962,1.3181095438807289,1.6145655196811597,0.91545943434103449,2.0443261176075898,1.8063081981871132,1.7075197406751026,1.4862819911548837,1.7549567711242848,0.71193750152900659,2.3192588636052003,0.36847411175180739,0.56731829318278759,0.56203809375323155,1.5282060842191427,2.2338930536426487,0.79135115265705125,2.0816830259688897,1.6740532890681719,2.7647357005940782,2.2509201651855317,2.0263361199484677,0.97380610693810399,1.6523385276838474,1.0877130696849409,1.7326360411628117,2.3557224084715993,1.6236025931058662,0.74345730135243238,0.53916918343937703,1.2144451743687588,0.18438538909174684,1.7409653584986069,0.78734142110890482,0.20254833387139626,1.1418269010636521,2.0569706571500306,0.96166308543890699,1.1035279794592019,0.85738917699755612,0.8284779507450204,0.036354356843017577,1.0922495688191853,1.0999942122770545,1.6000099064436972,1.7823901763407275,2.2483357452155786,2.4303592501766427,2.8359904496766051,1.784294260214986,0.98119530965011126,1.3371659053489551,0.84671414802306555,1.1596601064881122,1.3040930883752528,2.1081067934702751,2.235956480480592,1.9658589121114187,2.3031215566969099,1.3016509766985613,2.0935189529002405,2.3004786287876247,0.40819331529573799,0.66023891415463198,2.3865764267911134,0.73409646954558549,1.7208582386350453,0.61431263611518738,2.19885240288367,0.30832523113907306,1.7511815353016831,1.1169020682846436,1.2591493301543011,1.0599184895208151,0.69494364049121571,2.0805937785519091,2.2995289449327645,1.6838599343218417,2.7841213751592324,0.57910354640501538,1.8609670102982556,1.6847602406257685,1.7773533895871463,1.5308965106472889,1.5899106889476944,2.3163846556205332,0.36088091599757055,0.92106960741109822,1.7804586765148458,0.19622313316818735,1.027908034119581,1.4188707623731012,0.46304967409698339,0.21914420160717174,1.1417306975057984,2.2434653336298576,1.4298430007228644,0.11290224979892804,1.4375344916661379,2.6271812261440632,1.59149626456324,2.0553711195755149,1.1448294721702212,0.40009113376737537,1.8798447807567045,1.3643451325321945,2.6231153164895735,1.0013068937623064,0.95195369266112195,0.8533121890431975,0.54401396281936121,0.49126635093352922,1.3676585889313282,0.30204297153574422,1.4561588033109825,1.6119957079307452,2.5222410822195283,0.42152901390881026,0.44667116744363206,0.87303433744275072,2.5398738663576959,0.40777812198127239,2.4210654028246976,0.72102720804993514,2.0195438156475176,0.8356007766479947,1.5390035292431061,0.45211541776287928,1.862495892546965,2.3246493868026303,2.0242057498962267,1.5282056955443875,1.624392333716977,1.9671212448788951,2.2038879109554896,2.2795052341486022,0.22064525297851462,2.5832165419130249,2.4186100646456841,2.3022216077175561,0.42408373727789106,1.0863894507137675,1.2146143819188453,1.2299081166133194,2.5165937812153008,1.3681092070550527,1.7323339269258224,2.0792565292165639,0.6560946282193143,1.7153128421154742,1.3524155260337019,2.2809980972874402,1.3126934389764742,2.7094351484275352,0.36392027875425154,1.1144764069394499,2.5345790502031766,2.2254853076951142,1.7967232430312357,2.1674829125550765,0.41323715197999644,1.3003567493359902,1.7998496894079423,1.1148811318337362,2.2523160059701635,0.56192082934701904,0.61492623739932939,1.101747192805683,1.7900258574071777,0.87830230910447893,-0.26622729862179229,0.55701413022298341,2.0652544298103308,1.8703044508612345,0.40825509256338144,1.02648278367983,1.649098250987618,1.4497599976232705,1.5304274601318859,1.1963503505133133,1.2400328068965305,1.5872869000980712,1.0902146584832082,0.68782186315409977,1.1444568419283874,1.7409777387489016,2.3200164667382257,0.66461213695149424,2.0130522946934648,0.95959405550485954,0.79492229359387767,1.9512891873298999,0.43093906352153399,1.0862766706599438,0.17969978143047927,2.0345817601456027,1.8896305985029811,1.545161543600774,0.8314533847738359,1.3751248226179837,1.2798175665810896,1.8839235098347773,1.3163821240005218,2.0433513154665075,1.7290022374585532,1.4105167135223025,0.67226945535124316,1.3062718164364508,1.9193343336057715,1.455381433135484,1.5863714650716054,1.2827584106442689,0.39479242164632844,0.51834602188030698,1.6574678725790948,1.6068962573775631,2.3675367093529616,-0.34797293576620802,1.2767475771429109,1.590300364805916,0.1197460788915502,0.9342247857424717,1.4727605248054156,0.31676376244095389,1.95676296286008,0.083192836701014516,1.1941297100541153,2.1026048176544219,1.4505723482657447,0.54300983209366249,0.35135026915067347,0.87636646983433308,0.37656295588295108,1.6948179951608806,2.2907181132552576,1.860961517299248,0.71872450490873152,2.0911242360985725,2.3527155414427368,0.83005106182417332,1.572626418060151,0.82542976369724341,2.1357780714508658,1.2433462866311142,1.578609664184059,1.9000169285802795,2.0933640355751564,0.53121870687940764,1.6343865154778519,1.1488269349208049,0.32441954086481034],"w":[0.41345433163394191,0.31813721385712662,1.2617289285339262,0.82095744512846525,0.77049716875665897,1.1125670771744032,1.5703958623063747,1.2751906457878917,1.6704167171550082,1.7278583485940033,1.1421965195768562,3.4623457087237632,0.97496806290739735,1.7771476789398701,0.2506766296194583,0.93094393030128475,0.68787707667750697,0.41081482531987851,1.6519903268036749,0.85021669719503257,0.57026766381378646,0.65295501074977802,1.1726223989637872,1.2947972205816449,0.79410060620625356,1.5533767119160691,0.61829708377623971,0.85679202833917123,1.6934249394777112,0.61092441029845324,1.1421107013824914,1.012951682377278,0.79724041287757497,1.0166925064199275,1.1007292003056661,1.6523221887618915,1.3968461712026952,1.1608792262230747,1.9207824330453538,0.75183995561060646,0.70604346036465027,1.9135130231779214,1.1469856702666659,1.2460596175804892,0.77891681083696274,1.1785126267043502,0.80139223804689808,2.5237353754191183,2.2680894369119642,0.32802586355935387,0.83130191470423731,0.65945266572062022,2.3810910608895552,0.86356050800311945,0.49730665102966776,0.48326988175082308,1.0949801631790228,1.6150870676000435,0.586833715441478,1.3503807616416288,0.97257317848735991,0.71443050919050521,1.7147425272132999,0.29762370900143281,1.0830626948949857,0.59056369814230514,0.65604571722842409,0.52165616065857456,0.82968844238289796,2.0818474439550534,2.1876318298201638,0.48822310694922522,1.203301188682502,1.2497801310929271,0.78264839855109403,0.8536934013384867,0.64431547628615637,0.62431411236893375,1.1932444283817996,1.0697859408048733,0.53300552014518054,0.22801239856955466,0.82036776286154489,1.2667156126011141,1.4372822493225161,0.53516117714952993,0.73866563685279718,0.85445401416488087,0.55666962989104218,1.3217188862456424,0.50762671824521721,2.6039251790732676,0.71102767999783134,0.61696051856955891,1.6097282463733675,1.0198097708735436,0.80383999371928117,0.8267348446309315,0.93821794348685894,0.9554742688954041,0.7142448143561827,1.7083973953078511,1.8865917768447915,1.4379912571082094,1.2885522933577005,0.95754264150290958,1.2973754374375512,0.360842971088632,1.4773327564781507,0.71563714797111988,2.5398227394721515,1.1924334619559622,1.2231055716478001,0.56893099237583489,1.1496902288372186,1.2407014805365471,1.4421879646409348,0.47776392726797445,1.4879639664879385,0.53754546378293844,1.0122049245812694,2.8209993046103006,1.5059847805639408,0.51812749795704005,2.5652967546733354,1.4128309052026495,0.75377098343790772,0.51068708118819184,2.1447198320240437,1.7051141314382057,0.75536225788883016,1.2590095082505366,2.5738757883323604,1.2227263180880035,1.6450603744658845,0.7750275114554156,1.0257406632054,2.2488020955925161,1.3376082555777375,1.1082735005902788,1.153799801805506,1.3901386085897607,0.72746186715446703,0.67993578876457594,0.77648966922581186,0.96061077162245789,0.31420371197042141,0.58195368822352422,1.1345110674395262,0.78899277515623645,0.88210456369377355,1.4371731801616054,0.87561782023253609,0.41762743402822755,0.85593444758373705,0.49701139130409688,1.6283307963866773,0.96906866885067888,0.97924143474833936,0.39152385453630811,1.102768714150876,0.78130651934245454,2.6489225347328555,0.76313605057416323,0.79233312629092911,1.1384906349881003,0.51875322963028703,1.3829982808266856,0.60854368901387601,0.5011651133495747,0.89118501424162333,1.4259962505567163,0.8123014111264365,0.55965999677889922,0.86223547003606749,1.0252080845917995,0.52179207957036011,0.72346460262137735,1.8846757859690269,1.5620224370930962,0.7879830896982547,1.6134303371759751,1.0667608061615841,2.7874544115880941,0.89823828571850972,0.51129205560560087,0.82649523385070034,1.1315044073182361,3.1012244857687441,0.85242460844567036,0.55357098209207189,1.1969765775768937,1.6583729435933976,2.559481621019414,0.87981203393150964,0.65662627959453757,0.63153718988202923,0.61189925176940141,0.69321548304382985,0.91004715466704222,0.49630915380186741,0.87007416075460287,1.2635924957833602,0.66685995908707885,1.1767464081858274,1.2546117897464926,1.2162033455154648,1.2180552550114758,1.0943168881150709,0.49782206378634886,1.2783926888730517,0.61954401787207458,2.0196878694831097,0.92157275859832466,0.68123896165080444,0.48074269942151959,0.77278452625130489,1.1407802778680685,1.2201518961076216,1.0816113859930609,1.0553027571331113,0.77891820387671828,0.69455912235660788,0.95967770389426943,0.7776616413417794,1.1206435246560322,1.0554197832916745,2.7767311220154793,0.39636429029856413,1.6568127159535719,0.64368802649555612,0.83138789738619345,0.97580704504847304,3.3723285953229598,1.3919215990599949,1.4073963035449126,1.0208075842278328,0.95206884374838729,1.0516725854631475,0.29946342669409526,0.82074811119668867,1.0122787081093545,1.2192699562566027,0.79468309543070881,1.1825086654759243,0.93488071859298205,1.8536869476313402,1.013801103910017,1.1676214150341573,1.4339869660955971,1.4984129312543963,1.6155478307130928,0.64393852125221063,1.1283651304564084,0.79433005169214188,0.48680352686215084,0.86381002610002777,0.75436512996014193,1.119603523381709,0.94591276794870094,1.1152207911974097,0.95278691856650777,1.5835058126816017,0.80500658462636021,0.65767716381313723,0.39926643932912165,1.0811456237433679,0.50371132810802244,1.4941235888095696,1.1488064002854883,1.8255977334843916,1.0536256061473075,0.75677886824586471,0.91406556954362728,0.28947970195313666,0.7003463245115239,1.8002098929915502,1.0660158054475399,1.158152156898036,0.49424402302165366,1.3495807994456019,0.59714725461270179,1.1869949583351063,1.5032487720036933,2.1065069687690743,1.3366080060758787,1.7736869793157375,0.89071837363612494,0.54131367158907462,1.5085245605672206,1.4256511998004986,0.6506910422313531,0.40494725606426024,0.96773122885377538,0.98388833493268613,1.2850635903735563,0.73131791532439316,1.806269794173412,0.69954280185055817,0.81431570447825663,0.96373518244116985,0.82576691920359124,1.1463775566572232,0.7013306792895373,0.56763601924010365,1.3236990030861404,1.570617952934932,0.99582171942722308,0.51728116993177398,1.1440170171351869,0.4257935444017435,2.1912278441389508,0.60446386751837355,1.1671673630229977,1.5642139287351255,0.74774326149340309,1.1025447855609403,1.3263040256651222,1.5280192411570785,1.3487073005227526,0.64520202719491015,2.5886193087355038,1.6727282899399505,1.0241835291006178,1.3596077751482878,1.5204783133408826,0.87538372565750322,1.0923687907408952,0.90015067061273502,1.6697464474409225,0.62273571845399467,2.4937381392785762,1.2244685734296676,1.1198631223699105,0.38091415442991999,1.3752140797389976,0.58698437685306293,0.77176342874243975,1.3196474551603832,0.53941477465777921,2.2089828356485328,1.8540565270123637,0.75459987302039355,1.0459920399158904,1.4363196211753528,3.8491299018459224,1.3006790570633957,1.0192426838652899,1.0623408484029224,2.0838752835337351,1.9236570773576165,0.61864738935578567,1.1969267404679875,0.7477193775676314,0.88210651376946692,1.0617789824423907,1.9020871547617493,0.50075267900964859,0.57157070264344201,0.84865431218813259,0.77942466613784478,0.50002954752050821,0.58181524903415072,1.1778215717876945,0.66216917481618764,0.73563094798502049,0.51077029789113448,0.56581918158094258,1.3021932535847711,1.0866510322689455,0.94356425342699202,1.4048989399953296,2.9052227798624171,2.4824580018375952,0.33621920576165998,0.8979622851875535,0.96046034390399526,0.39362567118062158,0.6454289059346765,0.73553596636473517,0.71557646492552007,1.2625032306711101,0.82212797953565231,1.6176929503355604,1.9323293416479597,1.1664267276747746,0.86177391215113119,0.39715219582740052,1.0443456507019735,0.73844587895799818,0.93536904996084302,1.5562559489995991,1.8879435591041893,1.1139686662380499,0.42682554502027104,0.83409169682668172,0.97755310205435975,1.2403263735348256,0.37709885943733651,0.55458619798884456,1.4077697662026074,1.0191110912963155,1.0557699527909605,0.46946944458523271,0.92170807901754737,1.7011307693400866,1.154751540133099,2.1971812496535379,0.44367596942899395,1.2868278827941866,0.66490583799535286,1.0739470425816531,1.5419825215726093,0.71992992936426736,1.3935683519450797,1.1528523807557078,1.0591788609359292,0.4147939764711735,0.86384719624697426,0.53601702601028056,0.86675477076588014,0.63595158064148227,0.81714244041185458,0.91457569935140481,0.45064436323185653,1.1795348457814696,0.84591489067062153,2.4293608704779324,0.61249977333388139,1.5735358599826013,3.0448307395130283,0.66802307189085386,1.0567483826334709,0.64189909254491051,0.90474692562190495,0.37090755389380919,1.2802942186385906,1.1216212536121386,1.2140310518619448,0.38726465849219438,1.1897792345731009,1.4854342219075394,0.65740725103956899,0.90229963571612415,2.9193428531757779,1.1792186415562647,1.5332815638663655,0.76316631501020582,0.62251998298883904,0.50526058236686167,0.83497814278098426,1.8318396550635696,0.87038496961725109,0.92287544728798898,1.358951249871776,1.3868437475906876,1.0949143634132312,0.68908586329785415,1.5644579314880922,0.7803197170267141,1.1477232463379883,0.80319166755851024,0.33694105322969442,1.4775461422098892,0.58089886381217282,0.63892852527132349,1.5538486629614681,3.8898148545446705,0.88035722953298934,0.4888514074179085,1.7605073261549911,0.61437038539134869,1.3486838320348271,0.75943921266188263,0.61756125240465765,0.54447812456410316,1.495169836775718,2.1472236784874776,2.1628856366229612,0.86801678470668864,0.55386794728828781,1.0756589950625863,1.20231312708892,0.64485262798165122,1.016346025159206,0.78786922946056115,0.93562172323266779,0.99043820759474943,1.3649948071711457,1.3198608176847366,0.94170399860696941,0.94291777678980848,1.5416339950009157,0.71447647718627272,1.1811885011527576,2.1200870312670936,1.5091937611556068,3.1794123465487969,1.2583495587225024,0.99714000806811176,2.1789669380031556,0.30055566083829621,1.0659401531374249,1.4009480983318672,0.79278296600267883,1.0639665667682361,0.48016135096050971,0.62432659225436915,0.98227767621567474,0.67371181850076567,1.076280013789493,0.6421151522610502,0.41296456845342538,1.4096886484249083,1.6181245113600862,1.1978108262767335,1.539773565316219,1.4665292631077755,1.0149361849890719,1.0860051799285402,0.66500792331536096,3.3416871156268679,2.3554169024030234,2.1160915628440042,0.76890139570745575,2.5645717978712677,0.95547994921558022,0.35536610168295435,0.93493771386160818,1.0409361736371723,0.91646274379086679,1.5933819572007193,0.60939900856387574,1.491147364184062,1.453355792265103,0.70271114642678267,0.63267601483304237,1.1293671261484097,0.88371777542097463,2.3132618659440771,0.51461900662519711,0.58861343123917198,0.6025771419498116,0.96093847845722136,0.79883557990218934,0.66545325940543631,1.7220534859921077,1.381199805617997,0.68008560311281219,0.5376150065585017,1.150116947222878,2.0561923106467801,0.6853634498595228,1.1184053137407746,1.2717317292685653,2.0493464678391473,0.85244458907927134,0.90124834597253967,0.80078423905099794,1.6003675866729543,1.0913233319615374,1.4019961042017546,0.98394617220969793,0.93020958343511961,1.4377680006075981,1.5065229262272204,1.1016372519519109,0.7979324458009035,0.87970598852178927,0.72087264182412636,0.77240606202601014,1.0577811595461897,1.1696211806270402,0.89408557805593314,2.5849423349394978,1.188534490426062,0.47049778930654068,0.46658111549494696,0.72670773163797597,0.60828181552834693,1.7702764612752853,1.5051950148025877,1.5202538571495676,0.94473032388166533,0.56770053833511869,0.83902503872775369,3.1824088496891751,1.8481108419997809,1.0375000190530137,1.5479293353086601,1.4724275261938928,1.5125675308153799,1.5535648078842508,1.0192479190384596,1.6375388915623548,0.6600394274586372,0.81719059994297549,1.2789014970148795,1.3981423814476204,1.5699817022981464,0.37346098308573472,1.6886437761250752,2.0994492488100973,0.62155861533441814,0.79717905816329304,0.5769794510691727,0.425689241791055,1.0660465323003641,1.4044348966244495,0.99389898505064445,0.32366823067081057,1.1105509176974717,0.50970461102269027,0.63039480121660085,0.76858485465681026,1.7837078819114802,0.75727235645677582,0.53679500784503176,1.0419298077733674,1.4478493896653861,1.4013940216335261,0.41268392929304548,0.99776599926449838,0.89582235068234006,0.74474312202279402,0.36967648811696735,0.91589846323517676,1.8247274888040572,0.69952910028932602,1.0651904533224164,0.9245144454618156,2.2999242869187717,1.9385720437759828,0.7930895106404392,2.8492133324326363,0.93566620930201583,0.6038639940638354,1.3858720084209402,0.60170325945794034,0.46118579444819141,1.162681515906731,1.3024319201609962,1.2485610446567803,1.8860082474210693,0.61988512387685923,1.0450128148240319,1.3077782742645849,1.2164552830517616,1.0392102540623545,0.83331040858142824,0.63892929033816293,2.2633385021792107,2.8618104278599366,1.8577591512767857,2.4746592322367849,2.2899302699808635,0.4410671219596764,0.94687142106525191,1.1879469956384074,1.0326295745352019,0.97816770898186611,1.093092842359525,0.73398009342915327,0.76516104198080992,0.73671920780739319,0.68311905997878331,1.0806518926508395,0.71660400109579658,0.90593484288848836,0.8811832580952228,2.9642536568506217,0.32741935228757929,0.9311294510436342,1.4545198418746079,0.96419899527758279,0.68423481528074448,0.90960846932811967,1.1341577330796748,0.94379030622923665,0.53192635888940609,2.7184229891587073,0.82529108183391775,0.62654342962954601,0.67898098054559208,1.1248668467028438,0.52604904754681669,0.56484796257080028,0.39245486762121851,0.44921875843801207,1.7073302048451235,0.69201144720844265,1.6096107226909375,1.1716719285886927,1.7980476271221595,0.79342477530448241,1.03737433966377,0.51809670038173494,2.7006352438253911,0.68502029617241622,1.8334040552023334,0.38757848959575331,1.2560252332864319,0.82402522537963341,1.0235828346359532,0.46391690317706241,0.59120986358078487,1.7131703738776256,0.96355117788078892,1.2423845503342086,1.1525430093040285,0.96748433287951285,1.2423649044457885,1.522657038834657,1.0779418059744983,1.1351598922408368,1.3818104898729042,0.49454598099760755,1.0409467843227704,0.93320128020234261,0.98630562545077449,2.325397770523876,0.95444065404733214,1.1601839830070289,1.2765288962720052,0.37100732932740421,1.1035220794488021,0.81395894424144344,0.41558056554956335,0.85200483245178793,1.3030434352232325,1.1356722126939638,1.0339726745061939,0.79053561225770319,0.73890967190777845,1.4037246963036525,1.0194978479101247,1.6874842754073389,2.3382015632976572,0.94483286474889816,0.77933822470263969,1.5500550899993566,1.2349988421717666,1.6937743062097839,1.2759236904133249,1.3511587170748358,0.84002062189783511,0.7732923161623152,0.68450637206227039,0.87386365603857119,1.2243827418424298,2.3352866589542041,1.3318875256729372,1.1891103590200831,0.42833333354761582,1.0653296566615904,0.70295511758598384,0.6483555189977388,2.4114596632987779,2.1843230731160497,0.64814011641828762,0.66751983888808542,1.3751542947282229,0.54403358285750802,1.0256213528204983,1.0660311353634635,1.5935195494994236,0.85760077700316883,0.56260254097428308,2.2215189095920422,0.81090900208107808,1.3070347069194563,0.85581400307934963,2.1636648901312654,0.83346227764369429,1.0326775808009481,0.7242418191311164,2.243065265918109,0.53436057381797208,0.76072637975025625,1.0750136132553094,1.6886768134607379,0.89534891932160732,0.87579657142703826,4.4348735099691634,2.2780271895053277,1.9670609936365839,1.4757709433336132,2.1485906087011779,1.8391775700848207,0.66149521595075578,1.2987994229634785,1.9288973195171004,0.48993213154362758,1.5778825227884594,0.63058284017279131,1.466890397630225,0.89418768250767577,0.27617566175902769,0.58347920486349814,1.0045277995894173,0.40323152885145097,0.73579960504313657,0.73171418330523719,0.94097280431869579,0.82525313890237062,0.63556687506844611,0.77889718186698709,1.7159935154742934,1.5790869692074265,0.58522399793758251,0.87506941266258731,1.1788534271973574,2.4366938990014324,0.53549240419387001,3.2947671788572204,1.0130193905302873,0.81546423388548417,1.2519067357641078,2.0990537973058534,3.1092367578009195,1.7126050076191484,1.1411895547690181,0.74223674106258153,0.62956414280398987,2.0611713944945422,0.99366071213830087,1.531138314990947,0.6542246016990001,1.6896214627871422,1.5555811590951942,0.54294304674181648,0.35237094630328625,1.5986613941584698,1.5852972832654593,1.2323969496725513,2.1089140319721054,1.4493871730250962,0.84117331800973127,1.018843117549654,1.9750046659777507,3.9313335957773412,1.1778425409913265,0.61037320801292361,0.787902662522866,1.5626617908844256,1.1478860449523083,1.0567062080532326,0.45748704282600838,2.0449875933389174,0.89846193029300347,0.67656679945618459,1.2418095949642185,2.424046912486026,0.52520504085972142,0.55433786902765914,0.46824770599692317,0.60805687416390053,0.36638000133374271,0.32170737723410275,2.1178207937091931,1.1007457825849729,0.34331085458790045,0.678503878536172,0.80496960552625763,0.50995515592079821,0.98725702819567995,1.7204902425556894,1.6447337828928976,0.86031079236946051,1.4647634624090276,0.47851157408234896,0.54037962542215023,0.80821123634089787,0.74965015429727111,1.3474615252807358,0.43766809521317135,0.55705326660376309,0.61396732556418165,0.68768883225778255,1.021351928397906,1.3425880500579972,0.39131802705484792,1.630594038066439,0.36095109257896568,1.0094549422874237,0.6355185903802506,1.4425615421213338,1.1309347947562518,2.1295784534737039,0.47731531583634196,0.94696440346932897,1.4554099754079506,1.0311091652849251,1.3404095743178932,1.0961534102785595,1.7989739682404673,1.0201021026062733,0.64035439057433752,1.2318352483943344,1.0224517051838826,1.1537300635576322,2.1376773995957499,0.73582209870575421,0.74267677386172704,0.76012950913161859,0.45766975896459516,1.2350190119687097,1.4325468214474504,0.3264490882747772,2.6240422358588993,2.8815532739862926,0.96416033496473119,0.96268943145443597,1.3102825591990366,0.69711316405916979,1.8350694717553548,0.85712662288791597,0.8206231093805636,0.69140021404698671,1.7587232383021765,1.5253139941340135,0.41995392200815318,0.61650658284877113,1.8466283771893823,0.88870532551021808,1.8995298349282823,0.76018704187436981,1.0239256165782677,1.3177156911851202,0.94449137697411922,1.3989570545344614,1.4323572303992316,1.5892766865436028,0.64585591247201679,1.3594349952587907,2.4542829668940946,0.83460135530928004,0.53725762298750179,1.229886572390223,0.53186958739514434,0.65973613061621139,1.1537584103287011,0.81439416837280032,0.77001638279772,2.7660215709174198,1.3022175345124807,0.57026644438659524,1.2378737155764763,1.2907272715166958,0.69527316119268545,0.64763801233035134,0.74028525212125396,0.84848879342528405,1.0180121627524608,1.4170219651661111,0.57896905644313734,1.3513681323104028,0.53256918421748889,0.82262989056790092,1.6194526484205267,1.5791142475891591,2.2814017911227005,1.6762441851647794,1.012740532546059,1.7738506869305648,1.5212457389371905,0.49413765159541656,1.0201055893912132,1.1088595097902323,1.1099125203423126,1.0741788071794152,1.5154327613731633,0.99349523849694654,1.5696632025971551,0.68049576642407272,1.1295095303411813,1.3191285112800499,1.4306739756233811,1.5393919801196096,0.72782567345814597,1.8323511801539676,0.27610088261142879,0.69887451276402413,0.58552487048971513,2.0002043137918086,0.89568782980394279,0.61941589009661835,0.52595016311314047,1.0517203187493294,3.9343393931705393,0.81212156267736002,0.72709481374742857,0.31153240788560499,1.0794896884316085,0.48494575715830823,1.5629820321433625,1.3971109497783198,2.1594447259639571,0.83187792196089871,0.60334827576654082],"cluster":null,"hc1":{"beta":2.3484727747900673,"se":0.10584564044900167,"alpha":-0.090222702605383842,"se_intercept":0.064809497290692408,"n":1000,"se_type":"HC1"},"classical":{"beta":2.3484727747900673,"se":0.099792551687901382,"alpha":-0.090222702605383842,"se_intercept":0.061714059424009475,"n":1000,"se_type":"classical"}},"informative_cluster_n600":{"name":"informative_cluster_n600","n":600,"d_lower":0.29999999999999999,"weight_pattern":"informative","seed":99,"d":[0.50577210620976987,0.92611504499800501,0.52645233026705684,0.44312826397363092,0.29999999999999999,0.51573193606454881,0.83181628568563604,0.29999999999999999,0.75073346630670124,0.44732044627889989,0.50635205092839897,0.91226330872159445,0.45631149450782682,0.82928217675071203,0.76999929219018659,0.4141906274249777,0.29999999999999999,0.66010530334897333,0.52535742397885765,0.39721993501298125,0.29999999999999999,0.47130266833119094,0.9883004975039511,0.64003259346354746,0.58623493844643237,0.9164932949002832,0.91888420279137784,0.29999999999999999,0.60929129531141368,0.60404276927001777,0.74407251970842481,0.58228247822262347,0.68936411407776177,0.36002102103084327,0.57322548541706053,0.9434757714159786,0.49823762222658841,0.35537531056907029,0.88511499059386545,0.29999999999999999,0.59520830106921496,0.99244831430260083,0.52979807916563004,0.77543701671529552,0.29999999999999999,0.81542733567766845,0.29999999999999999,0.60297538517042992,0.69401781796477735,0.29999999999999999,0.70682202698662877,0.3783388736192137,0.29999999999999999,0.94523327569477256,0.35636690487153827,0.68441614913754156,0.30424292592797425,0.33625874766148628,0.89005607478320592,0.88315744092687964,0.29999999999999999,0.76121284207329154,0.40449471212923527,0.4974193617934361,0.29999999999999999,0.61991999435704204,0.39711822676472364,0.42127142630051817,0.29999999999999999,0.87859494781587266,0.46603221525438127,0.31206835804041472,0.57767801787704232,0.29999999999999999,0.5768447193084284,0.42081813479308039,0.29999999999999999,0.73859303444623947,0.72771837161853903,0.85681367518845941,0.84319107257761061,0.29999999999999999,0.71167221674695602,0.81395438495092087,0.5674132930813357,0.29999999999999999,0.62141025485470891,0.54168902342207725,0.77222918255720285,0.35043883419129995,0.88893812960013741,0.83568454594351349,0.75541543248109511,0.4491270224796608,0.70030146869830778,0.39804880847223101,0.9363449400058016,0.8127152923960238,0.29999999999999999,0.29999999999999999,0.77021032073535023,0.5204963217955082,0.36845364526379853,0.51063640543725342,0.67327477009966963,0.29999999999999999,0.47650877763517197,0.37819776998367161,0.29999999999999999,0.8443676001392304,0.89546227364335207,0.78784324426669627,0.29999999999999999,0.84432875530328599,0.68539121581707141,0.46044064178131516,0.5987385112326592,0.67523948915768406,0.44671253410633649,0.70888690189458425,0.426273286761716,0.58259208903182291,0.77571524463128294,0.30082546013873068,0.29999999999999999,0.31441285207401959,0.86265784350689501,0.5505197518505156,0.96288789303507649,0.94286649541463696,0.6467005114303902,0.42152864541858431,0.34021448707208035,0.38126679104752836,0.70885379384271796,0.8086796858813613,0.63410671569872645,0.41474137972109015,0.56269387197680765,0.96163843974936747,0.75440471491310745,0.68567478854674846,0.45904311137273907,0.57154836147092281,0.29999999999999999,0.9842092738952487,0.58956966931000354,0.90188428780529639,0.8529079632600769,0.49885870476718991,0.92031853245571249,0.86970720505341881,0.90202385289594522,0.35171634554862974,0.58997201656457032,0.73246308320667586,0.87305159827228629,0.71306188721209762,0.29999999999999999,0.62853994066827001,0.68417217030655586,0.55085408922750501,0.74813353861682108,0.69744562068954108,0.57131061684340234,0.80090282566379756,0.68312546040397137,0.6792305937502533,0.42494258494116366,0.39405862211715426,0.83676183817442507,0.84189017859753212,0.33495696575846523,0.80669576157815748,0.3747214509407058,0.29999999999999999,0.5318872903008014,0.48422481219749897,0.32837159545160827,0.7814012938877567,0.55651613066438588,0.31179537845309824,0.86563233875203871,0.38462410429492594,0.71377978383097795,0.44965959452092646,0.75654589952900997,0.39783220530953256,0.76104426367674016,0.80322573753073812,0.63341707745566966,0.65985228475183244,0.34453462644014504,0.44457572782412169,0.30086489040404557,0.50114826117642219,0.29999999999999999,0.93683282139245416,0.29999999999999999,0.95124522934202105,0.58325762737076725,0.6375760601134971,0.83392203699331724,0.29999999999999999,0.37720292680896816,0.34219737155362961,0.58470001793466508,0.46855746337678283,0.29999999999999999,0.36797863305546341,0.84342547412961721,0.57941633549053218,0.81502021062187846,0.91070884396322072,0.76504556520376354,0.6181735340273008,0.80609388102311641,0.42684261747635899,0.96149102742783721,0.71305169337429097,0.80647144613321864,0.89530209270305927,0.96665473319590089,0.33860223202500489,0.29999999999999999,0.86744535434991121,0.93711275034584096,0.5991480308119207,0.68184108817949884,0.42802037913352248,0.36318589539732782,0.70929829608649009,0.70755012053996325,0.45891396324150263,0.40276231139432639,0.80759563432075077,0.63272868846543129,0.36116712745279073,0.68702639844268554,0.56172358195763084,0.35342961584683508,0.29999999999999999,0.55195898383390152,0.98514018531423053,0.33258518814109267,0.78146006613969798,0.91593789355829358,0.61069307415746155,0.81997956193517885,0.44048312783706933,0.99012745730578899,0.30094905484002082,0.87537843734025955,0.41105806364212183,0.32528994854073973,0.44304654344450678,0.81342663911636914,0.87725964861456296,0.99348215975332999,0.98536266167648134,0.8406470837071538,0.65809100484475491,0.32280501290224489,0.29999999999999999,0.67449550915043799,0.29999999999999999,0.46404905240051447,0.69822550404351202,0.7294695685151964,0.94188661850057542,0.33331347645726056,0.54927111673168838,0.37877118419855832,0.48817734022159126,0.79040672995615746,0.29999999999999999,0.29999999999999999,0.79258952520322046,0.98965952647849909,0.41042697990778831,0.58787349625490604,0.82991813705302775,0.78368633359204976,0.6034807523479685,0.55592501731589439,0.80454861184116444,0.85047655773814768,0.96538276646751908,0.72516172055620698,0.72097639797721058,0.34590492313727733,0.43196732390206305,0.61839461347553881,0.29999999999999999,0.29999999999999999,0.74583287811838084,0.37964717266149817,0.70813131451141087,0.85700382417999199,0.47936529712751508,0.96328439789358522,0.49683951877523214,0.31834505549632014,0.70476059392094603,0.29999999999999999,0.29999999999999999,0.82579686944372943,0.58765617108438162,0.33370700543746351,0.77898532059043646,0.43404121429193765,0.6465071373619139,0.76608000879641613,0.66361177428625517,0.29999999999999999,0.95956469441298387,0.89318050465080878,0.37825799130368976,0.29999999999999999,0.97662984484340987,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.9784310035407543,0.98723403946496546,0.98947493173182011,0.29999999999999999,0.39093757900409398,0.40565067762508988,0.76191387695725998,0.8458894454641267,0.58895708722993734,0.96941013296600431,0.77161608904134482,0.29999999999999999,0.60686546897049987,0.64374922653660172,0.43363203273620454,0.50448669025208803,0.36011616080068048,0.89493610237259413,0.44307313957251604,0.51026273379102349,0.35683803339488801,0.4675355603918433,0.29999999999999999,0.99634940277319395,0.63432876407168803,0.55584532557986677,0.29999999999999999,0.30183525604661554,0.72394451592117548,0.79661683759186408,0.60781999167520551,0.87755039855837813,0.76525636899750671,0.29999999999999999,0.89517748637590555,0.29999999999999999,0.87677401751279826,0.79205192665103819,0.81862841376569118,0.57452964375261217,0.29999999999999999,0.77236387531738726,0.68938598309177901,0.7335033488227054,0.29999999999999999,0.76277113493997595,0.60360474227927619,0.29999999999999999,0.69523380675818769,0.518163308664225,0.54506018389947708,0.50749512475449587,0.51797811577562236,0.36328783736098558,0.29999999999999999,0.29999999999999999,0.53125372305512431,0.96644487632438536,0.52163595720194278,0.86660755015909663,0.9598811744013801,0.29999999999999999,0.43455102352891117,0.76863956875167783,0.56185788405127823,0.96584301320835941,0.54739736674819139,0.91690425996202973,0.79923403756693001,0.40750713266897942,0.53256869700271636,0.39351709962356834,0.29999999999999999,0.29999999999999999,0.7243520027957856,0.32365606063976882,0.48797778736334291,0.29999999999999999,0.89829530799761415,0.50620439853519195,0.94461574908345936,0.68642669678665691,0.29999999999999999,0.38233918030746278,0.44093514303676784,0.68089964000973846,0.8878976931795477,0.81497851670719679,0.97602360844612113,0.7857524613384157,0.92811933138873426,0.29999999999999999,0.93914915679488331,0.29999999999999999,0.29999999999999999,0.38874677338171748,0.61820112515706571,0.58058032244443891,0.84930511764250693,0.9947561440290883,0.33098965433891864,0.83168418677523726,0.37466172166168688,0.44232689510099588,0.4460358106764033,0.29999999999999999,0.38245696683879943,0.54596500035841011,0.90486919332761317,0.47384338029660283,0.66899892811197781,0.47202021975535896,0.97821569771040229,0.73249986714217807,0.36347639027517287,0.35868774198461323,0.98546282029710708,0.77262241572607304,0.509723568172194,0.79732346355449402,0.63830999329220506,0.3303240635199472,0.47367613101378081,0.75889983654487869,0.6920893699396401,0.83241528945509335,0.80153017353732137,0.85199595752637824,0.68786084407474846,0.68451770727988326,0.37180949917528777,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.66022183427121484,0.29999999999999999,0.65007422761991618,0.5362256311113015,0.83932084904517978,0.69289535542484371,0.29999999999999999,0.99780314955860372,0.29999999999999999,0.53079783639404921,0.35787364263087512,0.72475069421343497,0.33431579461321231,0.29999999999999999,0.35101500328164548,0.38777003760915246,0.9971245345659554,0.32159271303098647,0.82194798963610083,0.40732381863053885,0.54612176569644355,0.48681942604016509,0.74056836995296171,0.4356855307240039,0.7534505470190197,0.90782794097904107,0.87513289551716289,0.95336357308551667,0.43843306875787674,0.84240073934197424,0.48779882362578064,0.43511466195341197,0.55581929548643527,0.50480209130328146,0.65381619208492336,0.87669592006132002,0.76690399982035151,0.29999999999999999,0.42778361418750133,0.51714459196664386,0.98212139334063975,0.8064472089987248,0.29999999999999999,0.73335913419723509,0.94696385951247064,0.70248000409919764,0.77959988093934951,0.67062707482837136,0.56439847557339817,0.81798471722286192,0.83308174880221486,0.4350052086636424,0.39915706717874855,0.63213477232493454,0.71790689562913024,0.87342147196177389,0.49317643619142471,0.75294269633013755,0.29999999999999999,0.94059254741296172,0.29999999999999999,0.82304096003063021,0.66744504813104866,0.44613427966833114,0.64095127035398036,0.29999999999999999,0.29999999999999999,0.86723218292463566,0.35748419552110133,0.49979681491386141,0.5388915983960032,0.82691280017606905,0.96077305469661944,0.32363777705468239,0.89052305445075031,0.66960390841122708,0.54274845630861812,0.95202274082694194,0.48204137480352072,0.29999999999999999,0.84137231318745753,0.94182765621226272,0.57792039003688844,0.69974418100900948,0.32834301926195619,0.74608816965483127,0.68402164308354252,0.36157659830059857,0.94963777035009111,0.85653223001863799,0.4595324317226186,0.4255515615455806,0.81323046355973927,0.41492672075983134,0.43975075308699157,0.29999999999999999,0.73002634069416672,0.29999999999999999,0.29999999999999999,0.29999999999999999,0.39494533166289325,0.57187843248248094,0.83774145627394314,0.5556464979890734,0.94204445967916395,0.29999999999999999,0.87957561942748719,0.41836740113794801,0.57522125146351755,0.33205957768950611,0.29999999999999999,0.813599331327714,0.87015435590874401,0.29999999999999999,0.29999999999999999,0.60470407824032002,0.29999999999999999,0.29999999999999999,0.57568682965356854,0.92351779118180266,0.83687450203578917,0.44519691565074027,0.98753697162028398,0.50231979743111876,0.77123149225953957,0.89659180892631407,0.8270297377370297,0.39681540445890273,0.66758359898813069,0.40747157223522662,0.7160013965331018,0.30761219304986298,0.29999999999999999,0.31206392250023779,0.79296436738222831,0.42272032715845853,0.8607495988253504,0.89383411307353522,0.45489858451765031,0.97531333300285039,0.48497973363846536,0.90434099424164738,0.6610813306411728],"dy":[0.65853109055083081,2.7641493698462716,1.3759896257254791,1.1119292075186871,1.6938504758053545,1.0683668504738473,1.7086662097434104,0.61704301185013721,0.86349991561982331,0.95991535424732988,0.89876970462568517,2.3816056063090723,0.81672165828703103,1.5712499598074081,1.5631764943415722,-0.045292258312603639,-0.064268793127851831,1.6183462868761143,1.0507052665191687,1.3776958979042013,1.3849759816731781,1.0468224994588491,3.0177074816702132,0.87916346584335481,0.84722053653324259,2.4960305058978873,1.6245115132094712,0.4960586536170265,1.5461829072497355,1.2142579257415866,1.1024868054662709,1.6523667783936091,1.9758846247867032,1.4176768328215958,1.2947957232077543,2.5975406992744747,1.351340498793165,0.69760415097257966,1.5402514183923053,0.59285702416024755,1.273712177935167,2.2257219428864485,0.43708684573489065,2.3487416665644725,0.60580165667889885,1.4341087494008986,0.45231948899234076,1.7578599112917439,1.1904185343095404,0.81594596074554659,1.4263850481755498,0.20780687191463376,0.77990804545459158,2.2058615265970416,1.1574578708570897,2.0569962004506936,0.83232266788253773,0.79540918072038835,2.0218402499531214,1.7311712785848001,0.061803332834792446,1.8237562442296107,0.7512488011462638,1.0476496386631995,0.14592969877664175,1.045993223913924,0.65747297143872863,0.70219341739593111,0.8031989934682886,2.038182538709814,0.75535280911242753,1.1619629837616583,1.1187576925781708,0.67130310109016866,1.6312633503461333,1.2355338664980524,0.8365388223589123,1.5137060725619857,2.2227986535582853,2.0760366898936309,1.8976548066270547,0.71912919047766577,1.9528113890597572,1.9632632297923052,1.4916547739025365,1.2775812224027567,1.6244985561793925,1.8421586646834491,1.8608563894831633,0.85604075568470883,2.2114139382223192,2.0052528595112498,1.8070222289305724,1.2777641628202385,1.2186131411076084,0.99443314581564046,1.9574216799875437,2.1316902269027214,0.44748753402805586,0.83208855728463926,2.2742558788227303,0.77389152358488256,0.64952181457465408,1.1266965602566912,1.5492380683854041,1.1550669363495949,0.71286223482216182,0.40397958513006899,0.89590957507181646,1.8737152015858705,1.9181053794814482,1.3283147795594241,0.98981624970265869,1.3434032554613751,1.8516922371640276,0.90665639609689486,0.92255013806241459,1.3778924028711463,1.2090233570277751,2.2886858891893485,1.415324174140931,1.1588624111490531,2.0744098437735907,0.38223068176070174,0.62732951715466678,1.0346026336759313,2.2091541868238846,0.38481891366377197,2.8454098937005723,1.9244694363300254,0.91396070661881734,1.4199519461063637,0.8192626074694036,0.73153646873137812,1.2048793542169038,1.7356700144080861,1.3021757460493613,1.2018609216887306,0.47799994452502415,2.4937179830061575,2.0904217147412272,1.1522412243037325,0.80484449031739369,1.0200173210452006,0.8760403679672164,1.8405195435805772,1.0886190600108325,2.6900168904360577,1.8226664317687515,1.5196083115137762,2.3739256036910188,1.8595875307644132,2.2043018979146511,1.2128954763129001,1.0812413837071908,2.4857604750478379,1.8834087688440178,1.9082281281248596,0.32042682533760108,1.9410081688125929,1.8376457339422951,0.58255307602963813,0.74984486485475754,1.9699487008807577,1.0245700580646706,1.1585516166757046,1.3199649817252483,1.676036973168485,0.77650434377023836,0.44256193326315152,2.3801184971858924,1.2470033473128528,1.6644294055156301,1.2509698524532358,0.78380778117132255,0.39531903155272152,0.98056411032109425,0.85126365330226417,0.83001263434724915,2.1554941785985302,1.0342606335990714,0.21406019993606978,1.2434414555516284,1.5393390831568534,1.7653415341243055,1.6888861676280698,0.44934358558600707,0.053732153467056953,2.1486030709502231,2.1508816425738755,1.3969827229005005,1.8382566354034267,1.4361587858181353,0.66091560088607482,0.78997446800577553,1.9045875490707314,0.52416583700530262,2.5900457818561535,0.76230775209173807,1.4966596272772565,1.6960806766657588,1.9222647169882454,1.7519850113584319,0.56552999471073861,0.3859764511000065,0.6883619611696643,1.5137905210764766,0.77141519906086964,0.96843696325832251,0.66096003735752107,1.5516106528748368,1.1394786576270313,1.7693349892252574,2.1877238675467057,1.582857389303546,1.3973436059383539,1.6565774380579865,0.96231257695692762,1.8246341685241714,1.1601411787735507,1.5182501935816914,1.6172103324091402,2.7522812062194433,1.4499419932926059,0.92864578259993447,1.9584699991818064,2.4334859803367803,0.72580107104986491,1.8389441072764885,1.3087005895318691,1.1887368123543416,1.0720390292012976,1.6346557833036433,1.7514177541558087,1.1043014621422116,1.7830010555038707,0.93558142033159719,1.0955239847965146,1.000272675220657,1.0739747074243375,1.6836998941945924,0.42180009975610722,0.52329621826289885,2.1331720280790001,0.46344844769295002,2.539222230186494,1.9118714198627638,0.88554987936494611,1.3680448713166256,1.0142049512989635,2.3826083816319059,0.70803326060577243,1.8313798552234095,0.67945953797214009,0.9281637631411872,1.4889613264681487,1.9359349362362597,1.3328325508826939,3.1662044265971163,2.2059490587938217,1.7445893101040513,1.4244534261606869,1.0016559031021037,0.30900797117632106,1.3860222841899412,1.3263936379846364,0.40760856917204225,1.3652525507108502,1.6506642758397549,1.7853983988676601,0.95798495940210782,1.6395612153303396,0.0093870194220149195,0.45470727290276824,1.3727910422992866,1.1061486869474553,0.52198316212534945,1.047173991126598,2.7189979479737474,0.88137686277403471,1.0561612422626607,2.0476185031571457,1.1735112459342059,0.88792785872214686,0.75893253369072089,1.5947943153577402,2.8509335459597409,2.7874019024932104,1.8756605027611297,1.3900089387721737,0.80442922123248672,1.0506651682663428,0.6063840597026029,0.61266969963254958,1.3937762718283011,1.4557023604470367,0.9079611953890443,1.9597364865320177,1.9002074415267389,0.92150504078904349,2.1005403498331581,0.71626757452414669,0.43909405320056705,2.0182777778178056,0.20139016845117147,1.0309989498314109,1.8187917845400887,1.374526766108378,0.69625783822372111,1.977660366619149,1.0245797576114875,1.4026416604161374,1.622777444103515,1.7183352382151407,1.3597333996081964,2.9579821528620576,1.598867033762061,0.96604608463731978,0.77895145179288094,2.1500845061090152,0.89423649915944314,0.39732950967389424,0.38443802615939671,2.1754442325739678,2.1504820224320476,2.3716653695409948,0.10343448643239184,0.62719440055635134,1.3613512733551223,1.1841880259043618,1.9111507706094468,0.83682734176168205,2.3836212859231733,1.3511637876171723,0.94659987024446091,1.1127200079977781,1.135034892670878,0.76049417019756738,1.3277043216890991,0.74423342455305996,1.9354136075151844,1.5743960419191587,1.5118761267753011,0.6793270674204922,0.91099830179632169,0.30896068689213141,2.1445836306565282,1.2910799506325081,0.60064007014222931,0.2297016999153485,0.2227840756975713,1.381891718043599,1.5420074855137584,1.6484635912903889,2.1414138190671568,1.8264747815016218,0.56852362201631612,2.3340906864284974,1.1368873449037502,2.1288134495900923,1.1197491860117879,1.8615183893361937,1.5728046047239057,1.3490442268821365,1.2965638885308071,1.3653314668488141,1.5294374599184566,0.66514663880882041,1.4738728305263105,1.533553826955059,1.3226555649483598,1.4294235963488819,0.82043668257441182,0.72831710936819816,1.8205087084349918,0.50992596665359768,0.91093478866816635,1.2345525954144709,0.42058153849095026,0.99109240169221513,2.6781663748788582,1.326867113846709,1.0016233633984375,1.4953327660866815,0.7556780387198162,0.47742781495734726,1.9671842523804552,1.883562846073757,2.6693804726481822,0.79172489310150373,1.6292928009529573,1.490017317155796,1.083219985717883,0.56919402672385044,1.636349283019451,0.85082835931441936,0.91874850777368322,1.8944575917439308,1.4254724122405507,1.1480341781242884,1.364021547615569,1.9317104163891636,1.2130653999724716,1.9583669614175649,1.4150517730140131,1.3927293722364857,0.33790821977435537,1.2677591050885444,1.7912426034763362,1.6604534018573318,2.1461599463074137,2.610678727926846,2.1290642333678855,2.3309968103664103,1.5059906360624993,1.1700699542944424,0.45516466421906904,1.6002341233439408,1.190613790769441,1.6690093648910755,0.75852368852116658,1.5123709953360132,1.8656494245204069,1.2358088877589621,2.0246788816256247,0.70468978286568573,0.83124123468262401,1.2093576665031454,0.63293517124244991,0.64753134170654558,1.4792894512689054,1.9449166442877308,1.0154140999719685,1.6127124049446773,1.2656021799660981,2.5400598722549046,2.0777637120235708,1.01361286522755,1.112474978224397,1.8388553535137506,1.9344302188902758,0.6195618820877844,1.9331016049749585,1.1881843063072015,0.90375547676613199,0.94486284591719272,1.1543432658417516,1.5044431848514046,1.8238234465952314,1.7702956164791532,2.2353901413958068,1.1891084642624206,0.50356432256432537,0.72216478508103243,0.5484588484626064,0.79736219796035013,-0.29414020636628169,1.1601993584597177,0.71600192711148269,1.891611102660447,1.5403037677196134,1.9580776309367198,1.3161834053570718,0.95106191246943772,1.9254246839016118,0.087491355310104102,0.9598961757304344,0.99943873587492893,1.6948666130619401,-0.53408384387434049,0.87121751264917147,0.66012776933171768,0.85759756625738248,2.7198269951968537,0.69350621511191413,2.1922462599972534,0.84459646208547656,1.0070777898785745,1.8594514692613067,1.9716514540267585,1.3210154813083634,1.4167479086775348,1.9110606122048437,2.217310990569703,1.7108914790696221,0.96095306789251467,2.3691804850934224,0.81942489502512195,1.3657349362509761,1.2588664368906104,1.5802774624387008,0.97981764871313803,2.3770226085845017,2.5953487331489704,0.27682461638399175,0.53990138620365702,0.43331023265952628,2.2645286415984573,1.8677008256423819,0.46114986514726775,1.0234528472900806,2.0045989858438507,2.3063310252617071,0.531552750049475,2.0837598885074939,0.86328650867023748,1.8028253547830173,2.2657815295003769,1.1926691002853698,1.031446832595261,1.780321272072364,2.0360157944400976,1.9643638283775919,1.9662421756523041,1.7693059187308573,0.0016916140534510848,2.1065295197235363,0.58455673454996682,2.1639778056680266,1.9723759182394835,0.51706719207397545,1.427835430414405,0.44957546513131308,1.3472051884112373,1.4469977134518972,1.4579316845932617,1.3276703670449785,1.8544669005769867,1.3655458610652977,2.1693361167160319,0.58163505078998035,1.9056560780582306,2.4984282437817962,1.307241423179957,1.4033719926676369,1.0643352676160542,0.27099186917004858,1.8217985881536822,2.3528095412143699,1.2426095646041921,1.5505923873083627,0.79082863365622424,2.341599375042227,1.7205868811448337,1.3873072189735698,2.5028036991000482,1.9267016580842931,0.20290233633437416,0.86021172039049509,2.1463832815450301,0.84815481916518898,1.1692201180337451,0.15683178868332193,2.1883128180623728,0.41580654025256925,0.061993290494242625,0.55684733570054434,0.61001852857266059,0.95195767260534292,2.1262651346759966,1.0556449314327918,2.0730166183792944,0.93191268392564819,1.8251799083632716,0.218123710979237,0.331909639476063,0.36645325008189911,0.99098101128085991,1.3230151821528202,2.0449707374149875,1.0898248745401076,0.91152211683614437,1.5273462593888101,1.0213649457412832,0.2059877802302002,0.72381919832113051,2.5455541658295169,1.9849773200123342,1.0391983824997277,1.8174905491688425,0.62105070832024745,1.8761118105676289,2.6754797086783859,1.608144870007806,1.0479912861280716,1.4404118494788709,0.46391959978225372,0.85262969448421977,0.28997296631709912,1.3235674257920955,0.59016998325158465,1.4582991601336992,0.56723403364834235,1.8119259986603442,2.4556779467545193,0.87745649242789026,2.2233965060023166,1.1868653430018719,2.0933751934998996,1.5550234481459098],"w":[1.1122920838184656,1.9886466409079731,1.0707209346815945,1.2732419229578227,1.5372277870308606,1.1531621330417694,1.7048172499053178,1.4076200312934815,1.5021432841662317,1.2813996665645391,1.0364935123827308,1.8587790838908402,1.1397450401447715,1.810431948956102,1.718340992182493,1.1763558458071202,1.4429247103165834,1.3470981163438409,1.1310854635667056,1.3868914844468236,1.5570607818197457,1.1133616644423454,2.1225772347301244,1.2955455320421605,1.2040178860537709,1.9626576498616486,1.8635740872938185,1.5860542527399957,1.2411090328823775,1.2548253399319946,1.612014601798728,1.2151881408412009,1.4368423693813384,1.4286960528697819,1.1815315578132868,1.9511630562599747,1.1599935207515957,1.4833866648375988,1.780677380505949,1.4747491500806063,1.2242470585741103,2.0658828824292867,1.1120811426080763,1.735696467850357,1.5193963562604038,1.814967621397227,1.5607881772797554,1.3421959528233856,1.4939383652526885,1.4204193617217242,1.5544636188074947,1.2706355017609894,1.4956568467896432,1.9976706693414599,1.4325058607850223,1.4841846432071177,1.402205517003313,1.4516723761335015,1.8688311775680631,1.7855812006164342,1.5880263931583611,1.5401847202796488,1.28462569960393,1.0767547836992888,1.5308139987289904,1.2568846655543895,1.2265072799753398,1.2920303243212403,1.5417563791852444,1.8753344195894897,1.2258465291932228,1.5391512175556274,1.3014919284265489,1.4560382551513611,1.2147188055794684,1.2029866515658796,1.4426132119726389,1.5995452147442848,1.620264690136537,1.7148003321606664,1.7378242351580411,1.5033297742251306,1.442738129897043,1.7209618215914815,1.2899763059802354,1.534903177851811,1.3096532285679132,1.1271038800012319,1.6107935404404998,1.4056745972018689,1.8346938243135809,1.7237913579680024,1.7033002087380735,1.1091764949262142,1.413842527149245,1.3401662488933652,1.9733910460025073,1.7098716486711054,1.4715098299086093,1.4297239549458025,1.565937198139727,1.2202580225653945,1.3769753526896238,1.0231709459330887,1.514539018832147,1.4081891194451599,1.1926078895106913,1.2964330649003386,1.5798146708402783,1.7305506517644971,1.8866927686613053,1.6302136671263725,1.471408044733107,1.7362957715056837,1.5329360647592691,1.2707673917524518,1.2939177985303103,1.3702868737280367,1.209283241070807,1.4393622735515237,1.1749779928475617,1.1975615049246697,1.6734258104115725,1.4952673261519522,1.5623729123733936,1.4848167021758856,1.8236343045718968,1.1851009867619724,2.0395690926350651,1.9258792962878941,1.470364048331976,1.2135202447883786,1.4050996516365559,1.2449572244659066,1.4369545531459151,1.6506391033064574,1.312500510411337,1.2517946601379664,1.2295083633158355,2.039979179482907,1.6905972781591116,1.4786724358331411,1.139136886410415,1.2654478265438229,1.4508059136103837,2.0359439946245401,1.3055225852876902,1.8610317667946217,1.7712958208285272,1.0183945439755915,1.9430764564778655,1.7769242038950324,1.9105634605512021,1.3978752447292209,1.1954107188154011,1.4775375665631143,1.8639541334006935,1.4273642242886126,1.5376795978751032,1.3184702806174755,1.4139100612606852,1.234084054455161,1.5126781750470397,1.5180325419642031,1.2598420544993132,1.6983150708023458,1.5456093998625875,1.3894818063359706,1.2646989424247295,1.3609216514043512,1.7783651578705757,1.8001362384296953,1.4523223984520883,1.7516175749711691,1.2784447371494028,1.4467332661617547,1.2350871864240616,1.0584544877521693,1.3887409917544575,1.6658385933842508,1.129306597681716,1.5360425283201038,1.746579988207668,1.2313513230066748,1.4809626045636832,1.2211827162653206,1.5841855770908295,1.3112550852820277,1.5396969292312859,1.7136251277755945,1.2915666507091375,1.3808283558115362,1.3754205089993776,1.2755521675571799,1.528674804791808,1.1631240522954611,1.5988920810166745,1.9938656706828624,1.4550995707046239,1.9077828734647482,1.2558619145769625,1.2888420464005321,1.8656007766723632,1.4865573454182595,1.3434249487239869,1.464039672864601,1.2333426309749485,1.1502299311105162,1.5433704055845736,1.2866991926450282,1.8562032122164964,1.2744520485401152,1.7854012893047182,1.9123488922137768,1.5445890257600694,1.3688607406336815,1.684688664600253,1.3142581596504899,2.0938503471203149,1.4837185128126291,1.6466633955482393,1.8732655179221183,2.0615421855356546,1.3837947513908149,1.5097841724287717,1.9062883831560611,1.9121481193229555,1.2353084085509181,1.5303485858254133,1.34050091477111,1.2926593653392047,1.590780013334006,1.4778686275705695,1.1025277325883507,1.3254251012112945,1.7676919084042311,1.4533476164564489,1.4616940692532807,1.5736365599557756,1.241047596000135,1.3144713711459191,1.530052376538515,1.1105437954422086,1.9718359252903608,1.3688396394718438,1.6517588385380804,2.0181328026577829,1.3392636086326093,1.6475220799911765,1.2021935949567706,2.0408186016604306,1.413764833845198,1.9087133368011564,1.2041816608980298,1.4310815925709903,1.2551844576373696,1.7292925399262458,1.8517074240371583,2.161707598622888,2.056469105789438,1.8743813168257475,1.3525626388844103,1.4924839693587275,1.4473274246789514,1.4778148090932517,1.4986201757565141,1.1877672764007001,1.5691023976076395,1.5130111146252601,1.9727385878097266,1.4670205629430713,1.105120310233906,1.3599263551179319,1.1166267148684712,1.6941310187801717,1.4684227689169347,1.4119050914887339,1.7480830565094947,2.1524747814517466,1.2185596950817854,1.1987713600508869,1.7809535308275373,1.6926682537421585,1.370097785210237,1.2320929807145149,1.771906042145565,1.8251924386247993,2.1239844630006699,1.5917849427554756,1.6228839518502354,1.4060816521290691,1.3164307696744801,1.4119061346165838,1.4935773533303289,1.464795721322298,1.5271483685355631,1.3548088301438839,1.5525103454478084,1.7224464314524082,1.1282797084189951,2.0007652373984457,1.0266571675427258,1.5014435617718846,1.4118112867232411,1.4946637785527854,1.4541638921946287,1.7897014477290212,1.1893694987520576,1.3618526360485701,1.5795135153923183,1.1804270076565444,1.4606328784953804,1.6370024603791533,1.3647016208618878,1.411405253317207,2.0072559272870421,1.8556882249657063,1.3370579758193344,1.4654775304254144,1.969917977368459,1.5003044973127544,1.5976266552694141,1.5522305514197796,2.0286176564171909,2.0567608336452397,2.0391527690459044,1.5900256044231353,1.2850269122980535,1.2774691008031369,1.6144966503139584,1.8550500873941926,1.2987886798102408,2.1208093917462976,1.6050659413915127,1.5655727406498043,1.3591725202742964,1.3822295214515179,1.2423615534324199,1.2003583205863835,1.3182024441193791,1.9193795045837758,1.3104372933506967,1.1784005356021225,1.3517031877301635,1.2642648787237705,1.551265709940344,2.0438988015055655,1.4593398165889084,1.2132735448423773,1.459978838218376,1.5395006237551569,1.4807909165974706,1.7111296971328556,1.3957868029829115,1.9371383706107734,1.6867717632092534,1.5270868541672824,1.9249802734237165,1.5163632974959909,1.8767132788896559,1.7307834002189337,1.7563298384193331,1.2652256717905401,1.4804131092503665,1.7035792007111012,1.5510405354201793,1.5355286111589521,1.5968684536870568,1.6804310546722261,1.3912843795958905,1.5224248142912984,1.5389511207118629,1.082701124344021,1.2112208104692399,1.1388871770352125,1.1468274523038415,1.3770092990715057,1.4331595862284301,1.5800043495837599,1.2230530763976277,1.9409249403979629,1.2410108400974422,1.7996692178305236,1.9468003665562719,1.5041874636895953,1.3078977854922413,1.6013315943535416,1.1344030627515167,2.0086827398743479,1.1889885369688271,1.8926093467045575,1.7974356493912635,1.2746377588715405,1.098203984554857,1.3285948039032518,1.5136585671454668,1.483439329592511,1.567573035461828,1.3712218557484448,1.0344549618661405,1.4089533681049942,1.9721949480008334,1.2026507161557674,1.9676477282773703,1.4654855241533367,1.5001302944961934,1.4229072880465539,1.2692051047459245,1.5036967033520341,1.8455154391471296,1.7019351895432919,2.0885874702129512,1.6155012867879122,1.9134183389600365,1.4710911740548909,1.9450169171672314,1.5749363990500569,1.4110025444999337,1.3686585609335453,1.3984582129865886,1.248380939150229,1.7556561497505754,2.0110410245601087,1.4377347168512644,1.8340621809009461,1.4367553521413357,1.3036630841903389,1.1204013283364476,1.4822286088950931,1.2870898713823409,1.1363299588672815,2.0010909384582192,1.111506676673889,1.4029497685376553,1.2380495917983354,2.1291759521700442,1.5392561602406203,1.3836569062899799,1.4157859210390598,1.985125258518383,1.717270199395716,1.0685921970289198,1.6730589588638394,1.4006455634720625,1.4455486739054324,1.1967802369501441,1.5351500445045529,1.5206875809002667,1.795874486863613,1.684034196473658,1.8732791521120815,1.4126933778636157,1.4667009594384579,1.3243468232918529,1.5849597282242029,1.5599425485823302,1.5589164183009414,1.3929553541820494,1.5615062485914677,1.3799551678821445,1.2394066092092544,1.8408418287988753,1.4687427525408565,1.4959748750086872,2.0173115643206985,1.4502865711692721,1.2317338813096286,1.4511420363560319,1.5640092294663188,1.3388657473027707,1.4596330727450548,1.4716437180526554,1.3216723231133076,2.1864741485100239,1.471872438536957,1.6946827455423772,1.2745485464110973,1.1901621303055434,1.0702283869963141,1.6708996308036146,1.2414318814873695,1.6648659429512918,2.0137390519026663,1.8415804409887639,2.0276242330670358,1.2712610590271651,1.7024924201425164,1.1453513481654227,1.2501418135128917,1.2851925413124263,1.1498638150282203,1.4452353675384073,1.7701585274655371,1.5400823882315307,1.4086959138046948,1.1506487353704871,1.139424136513844,1.969152083294466,1.8100570679642261,1.5717067427933216,1.5091819669585675,2.0790681450627746,1.5408761306200176,1.7458436070475727,1.4154126349370926,1.2537414334248751,1.8266032704617827,1.7800892884843051,1.1681299011688679,1.2939900342375039,1.449376640189439,1.5351709643844513,1.9102821337990461,1.1807959179393948,1.6015061654616147,1.4580468383617697,2.0192756195086985,1.4060004426166415,1.8092701946850867,1.3774100198876111,1.1856287352740764,1.446300084516406,1.4250768141355366,1.5474962308071554,1.7850394035689532,1.3488710172940046,1.1238134772982449,1.2328719572164117,1.7289639526978133,1.936280140466988,1.4060824209358544,1.875557951722294,1.428058790694922,1.2568838497158139,1.9554749013856052,1.1575939460191875,1.5048606832511722,1.8803570762276647,2.0726001408882437,1.2312414140440524,1.5521039610262961,1.4417080291546882,1.5537815819960088,1.3830258637201041,1.3883295041043313,2.0378119308967144,1.8624202874954787,1.0862564424984158,1.1675392532721163,1.820877360412851,1.3152230022475124,1.1400260799098763,1.4398298133630305,1.6449973215814679,1.4955265134572981,1.4571224728133529,1.4250746290199459,1.4036233659368009,1.2227830264717339,1.7947516935877501,1.1655718688853085,2.0559136438649146,1.4590987446252255,1.8897228545043616,1.1717001181095839,1.2765486400108785,1.3695958552882075,1.521164563205093,1.7123173207044602,1.7493019131943583,1.4343660948332397,1.4297585078515112,1.3065673675388096,1.5959803219418971,1.5093543542549013,1.2419300712645054,1.9674968327861277,1.7182011580560357,1.2855833329260349,2.1330721639562396,1.1342732727527618,1.6340927917044608,1.9860860453452913,1.7112017056904731,1.2625144398771226,1.3415858065243811,1.2032760654110461,1.4479508339427412,1.4680166437290609,1.5114946915768086,1.446803583437577,1.5990621405187992,1.2001600130461156,1.7405371483881027,1.9390190134290606,1.1746264217887075,1.9575707594864069,1.1517955282237382,1.9833356274291871,1.4434315620455891],"cluster":[6,3,21,21,26,5,23,10,11,8,16,29,17,12,9,29,9,25,30,14,1,3,19,26,29,29,17,17,8,2,4,15,1,27,19,29,30,2,2,10,6,21,26,29,3,18,12,21,19,25,13,4,8,19,10,16,7,14,6,18,10,5,17,6,1,16,5,11,6,7,2,15,22,27,8,20,24,15,26,22,15,19,24,16,9,16,13,9,7,24,1,17,2,17,3,6,27,7,29,8,15,4,17,29,19,25,29,11,8,23,27,10,5,4,28,13,10,10,14,25,27,22,19,27,11,21,23,5,7,5,1,12,18,10,23,11,13,23,9,12,11,14,25,28,18,25,22,25,24,19,24,5,18,10,2,21,8,1,24,23,24,25,3,2,30,6,23,3,14,4,9,27,28,23,14,30,28,11,16,26,12,10,11,9,28,7,24,26,5,26,11,20,2,13,19,15,2,2,15,21,16,2,29,26,30,4,7,23,25,19,4,26,21,6,24,16,24,29,20,10,3,14,25,23,4,29,9,25,24,21,3,17,28,2,26,9,16,20,30,5,18,24,7,27,23,20,7,21,15,8,29,13,12,1,2,29,15,23,1,5,17,25,3,3,6,20,10,6,30,23,29,7,6,5,11,5,13,14,8,21,11,5,18,8,4,14,11,16,26,3,3,26,18,18,17,9,29,6,6,2,26,12,11,28,30,9,19,24,10,6,9,6,24,28,4,30,28,17,18,4,14,1,4,2,15,22,30,9,10,28,19,5,24,23,1,8,11,23,27,9,18,29,5,18,27,26,28,29,20,8,10,19,22,18,22,15,24,21,13,15,13,4,16,5,18,7,25,20,11,27,15,30,11,16,1,23,25,9,20,14,21,26,20,21,24,6,12,5,13,13,12,19,8,9,9,25,15,4,9,15,30,13,17,8,12,10,27,18,21,27,30,20,10,21,11,21,2,17,16,23,17,25,14,12,16,3,14,6,17,14,30,11,8,25,14,8,12,3,1,10,7,29,12,23,14,19,4,9,6,7,23,28,1,1,11,7,22,28,19,23,27,30,20,20,14,11,25,2,11,27,25,29,6,8,6,11,7,19,14,15,11,17,1,19,18,2,3,12,8,5,26,27,19,5,16,26,23,11,9,12,28,27,19,18,16,12,13,23,16,19,15,5,29,17,7,26,2,4,2,21,19,16,1,13,9,26,21,9,7,26,14,10,21,12,12,28,17,9,22,22,22,21,1,13,19,10,11,3,30,15,7,4,5,6,27,8,1,2,9,23,2,14,24,1,25,28,17,13,16,14,7,19,9,15,1,12,18,14,5,12,3,20,1,18,27,14,8,27,2,23,1,22,21,23,8,19,8,25,22,28],"cr1":{"beta":2.0832939899410157,"se":0.11225876017668746,"alpha":0.099673351657256923,"se_intercept":0.068563315835700175,"n":600,"se_type":"stata"}}}} diff --git a/diff_diff/had.py b/diff_diff/had.py index f72afba6..9ffef4ff 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -420,8 +420,7 @@ def summary(self) -> str: lines.append(f"{'Effective sample size:':<30} {sm.effective_n:>20.6g}") if self.effective_dose_mean is not None: lines.append( - f"{'Weighted D̄ (denominator):':<30} " - f"{self.effective_dose_mean:>20.6g}" + f"{'Weighted D̄ (denominator):':<30} " f"{self.effective_dose_mean:>20.6g}" ) if sm.df_survey is not None: lines.append(f"{'Survey df:':<30} {sm.df_survey:>20}") @@ -639,12 +638,49 @@ class HeterogeneousAdoptionDiDEventStudyResults: # Staggered auto-filter metadata filter_info: Optional[Dict[str, Any]] + # Phase 4.5 B weighted / survey-path extras (optional so unweighted + # fits stay unchanged; all None on unweighted fits). + variance_formula: Optional[str] = None + """Per-horizon variance family label (applied uniformly across all + horizons in the fit). One of ``"pweight"`` / ``"pweight_2sls"`` + (weights= shortcut; continuous / mass-point), ``"survey_binder_tsl"`` + / ``"survey_binder_tsl_2sls"`` (survey= path), or ``None`` on + unweighted fits. Mirrors the static-path ``variance_formula`` field.""" + effective_dose_mean: Optional[float] = None + """Weighted denominator used by the β̂-scale rescaling. For continuous + designs: weighted ``sum(w · d)/sum(w)`` (continuous_at_zero) or + ``sum(w · (d − d_lower))/sum(w)`` (continuous_near_d_lower). For + mass-point: weighted Wald-IV dose gap. ``None`` on unweighted fits.""" + cband_low: Optional[np.ndarray] = None + """Simultaneous confidence-band lower endpoints, shape ``(n_horizons,)``. + ``None`` on unweighted fits and when ``cband=False`` on the weighted + event-study path. Derived from multiplier-bootstrap sup-t critical + value: ``cband_low[e] = att[e] − cband_crit_value * se[e]``.""" + cband_high: Optional[np.ndarray] = None + """Simultaneous confidence-band upper endpoints, shape + ``(n_horizons,)``. See ``cband_low``.""" + cband_crit_value: Optional[float] = None + """Sup-t multiplier-bootstrap critical value at level ``1 - alpha``. + Reduces to ``Φ⁻¹(1 − alpha/2) ≈ 1.96`` at ``H=1`` up to Monte Carlo + error. ``None`` on unweighted fits and when ``cband=False``.""" + cband_method: Optional[str] = None + """``"multiplier_bootstrap"`` on the weighted event-study path with + ``cband=True``, else ``None``.""" + cband_n_bootstrap: Optional[int] = None + """Number of multiplier-bootstrap replicates used to compute the sup-t + critical value. ``None`` on unweighted fits and when ``cband=False``.""" + def __repr__(self) -> str: - return ( + base = ( f"HeterogeneousAdoptionDiDEventStudyResults(" f"n_horizons={len(self.event_times)}, " - f"design={self.design!r}, n_units={self.n_units})" + f"design={self.design!r}, n_units={self.n_units}" ) + if self.variance_formula is not None: + base += f", variance_formula={self.variance_formula!r}" + if self.cband_crit_value is not None: + base += f", cband_crit={self.cband_crit_value:.3f}" + return base + ")" def summary(self) -> str: """Formatted per-horizon summary table.""" @@ -680,6 +716,20 @@ def summary(self) -> str: f"{self.filter_info.get('n_kept', 0)} / " f"{self.filter_info.get('n_dropped', 0):<8}".rjust(51) ) + if self.survey_metadata is not None: + sm = self.survey_metadata + vf_label = self.variance_formula or "unknown" + lines.append(f"{'Variance formula:':<30} {vf_label:>20}") + lines.append(f"{'Effective sample size:':<30} {sm.effective_n:>20.6g}") + if self.effective_dose_mean is not None: + lines.append( + f"{'Weighted D̄ (denominator):':<30} " f"{self.effective_dose_mean:>20.6g}" + ) + if sm.df_survey is not None: + lines.append(f"{'Survey df:':<30} {sm.df_survey:>20}") + if self.cband_crit_value is not None: + lines.append(f"{'Sup-t crit (bootstrap):':<30} " f"{self.cband_crit_value:>20.4f}") + lines.append(f"{'Bootstrap replicates:':<30} " f"{self.cband_n_bootstrap or 0:>20}") lines.extend( [ "", @@ -748,26 +798,43 @@ def to_dict(self) -> Dict[str, Any]: "vcov_type": self.vcov_type, "cluster_name": self.cluster_name, "filter_info": _json_safe_filter_info(self.filter_info), + # Phase 4.5 B weighted/survey-path surfaces (None on + # unweighted fits). The full SurveyMetadata dataclass is + # carried as an object, matching the static-path ``to_dict`` + # contract — consumers read attributes uniformly. + "survey_metadata": self.survey_metadata, + "variance_formula": self.variance_formula, + "effective_dose_mean": self.effective_dose_mean, + "cband_low": (self.cband_low.tolist() if self.cband_low is not None else None), + "cband_high": (self.cband_high.tolist() if self.cband_high is not None else None), + "cband_crit_value": self.cband_crit_value, + "cband_method": self.cband_method, + "cband_n_bootstrap": self.cband_n_bootstrap, } def to_dataframe(self) -> pd.DataFrame: """Return a tidy per-horizon DataFrame. Columns: ``event_time, att, se, t_stat, p_value, conf_int_low, - conf_int_high, n_obs``. One row per event-time horizon. + conf_int_high, n_obs``. One row per event-time horizon. On the + weighted event-study path with ``cband=True``, also includes + ``cband_low`` and ``cband_high`` columns. """ - return pd.DataFrame( - { - "event_time": self.event_times, - "att": self.att, - "se": self.se, - "t_stat": self.t_stat, - "p_value": self.p_value, - "conf_int_low": self.conf_int_low, - "conf_int_high": self.conf_int_high, - "n_obs": self.n_obs_per_horizon, - } - ) + data: Dict[str, Any] = { + "event_time": self.event_times, + "att": self.att, + "se": self.se, + "t_stat": self.t_stat, + "p_value": self.p_value, + "conf_int_low": self.conf_int_low, + "conf_int_high": self.conf_int_high, + "n_obs": self.n_obs_per_horizon, + } + if self.cband_low is not None: + data["cband_low"] = self.cband_low + if self.cband_high is not None: + data["cband_high"] = self.cband_high + return pd.DataFrame(data) # ============================================================================= @@ -1521,23 +1588,18 @@ def _aggregate_unit_weights( w = np.asarray(weights_arr, dtype=np.float64).ravel() if w.shape[0] != n_rows: raise ValueError( - f"weights length ({w.shape[0]}) does not match number of " - f"rows in data ({n_rows})." + f"weights length ({w.shape[0]}) does not match number of " f"rows in data ({n_rows})." ) if not np.all(np.isfinite(w)): raise ValueError("weights contains non-finite values (NaN or Inf).") if np.any(w < 0): raise ValueError("weights must be non-negative.") if np.sum(w) <= 0: - raise ValueError( - "weights sum to zero — no observations have positive weight." - ) + raise ValueError("weights sum to zero — no observations have positive weight.") df = data.reset_index(drop=True).copy() df["_w_tmp__"] = w - w_per_unit = df.groupby(unit_col)["_w_tmp__"].agg( - lambda s: (float(s.min()), float(s.max())) - ) + w_per_unit = df.groupby(unit_col)["_w_tmp__"].agg(lambda s: (float(s.min()), float(s.max()))) varying = w_per_unit.apply(lambda t: not np.isclose(t[0], t[1], rtol=1e-12, atol=0.0)) if bool(varying.any()): n_bad = int(varying.sum()) @@ -1549,9 +1611,7 @@ def _aggregate_unit_weights( f"vary within unit would require an obs-level estimator not " f"available on this path." ) - w_unit = ( - df.groupby(unit_col)["_w_tmp__"].first().sort_index().to_numpy(dtype=np.float64) - ) + w_unit = df.groupby(unit_col)["_w_tmp__"].first().sort_index().to_numpy(dtype=np.float64) return w_unit @@ -1613,8 +1673,7 @@ def _collapse(arr: Optional[np.ndarray], name: str) -> Optional[np.ndarray]: return None if arr.shape[0] != n_rows: raise ValueError( - f"ResolvedSurveyDesign.{name} length does not match " - f"data rows ({n_rows})." + f"ResolvedSurveyDesign.{name} length does not match " f"data rows ({n_rows})." ) df = data.reset_index(drop=True).copy() # Use a stable per-row column so we can take first() per unit and @@ -1645,12 +1704,8 @@ def _collapse(arr: Optional[np.ndarray], name: str) -> Optional[np.ndarray]: fpc_unit = _collapse(resolved.fpc, "fpc") # Recompute n_strata and n_psu at the unit level. - n_strata_unit = ( - int(np.unique(strata_unit).shape[0]) if strata_unit is not None else 1 - ) - n_psu_unit = ( - int(np.unique(psu_unit).shape[0]) if psu_unit is not None else int(w_unit.shape[0]) - ) + n_strata_unit = int(np.unique(strata_unit).shape[0]) if strata_unit is not None else 1 + n_psu_unit = int(np.unique(psu_unit).shape[0]) if psu_unit is not None else int(w_unit.shape[0]) return ResolvedSurveyDesign( weights=w_unit, @@ -1870,6 +1925,160 @@ def _detect_design(d_arr: np.ndarray) -> str: return "continuous_near_d_lower" +# ============================================================================= +# Sup-t multiplier bootstrap (Phase 4.5 B event-study simultaneous CI) +# ============================================================================= + + +def _sup_t_multiplier_bootstrap( + influence_matrix: np.ndarray, + att_per_horizon: np.ndarray, + se_per_horizon: np.ndarray, + resolved_survey: Any, # Optional[ResolvedSurveyDesign] + *, + n_bootstrap: int, + alpha: float, + seed: Optional[int], + bootstrap_weights: str = "rademacher", +) -> Tuple[float, Optional[np.ndarray], Optional[np.ndarray], int]: + """Compute sup-t simultaneous CI via PSU-level multiplier bootstrap. + + Reuses :func:`diff_diff.bootstrap_utils.generate_survey_multiplier_weights_batch` + (survey path) / :func:`generate_bootstrap_weights_batch` (weights= + shortcut) to draw ``n_bootstrap`` replicates of multiplier weights + shaped ``(n_bootstrap, n_units)``; the helpers handle stratum + centering, lonely-PSU, and FPC scaling so this function only + composes them with the per-unit influence function. + + Construction (mirrors `staggered_bootstrap.py:354-373` perturbation + idiom and `:497-533` sup-t quantile idiom; NO ``(1/n)`` prefactor — + ``psi`` is already on the θ̂-scale per the Phase 4.5 B IF scale + convention): + + 1. Draw multiplier weights ``xi`` shape ``(n_bootstrap, n_units)``. + 2. Perturbations: ``delta[b, e] = sum_g xi[b, g] * psi[g, e]``. + 3. t-statistics: ``t[b, e] = delta[b, e] / se[e]``. + 4. Sup-t: ``sup_t[b] = max_e |t[b, e]|``. + 5. Critical value: ``q = quantile(sup_t[isfinite], 1 - alpha)``. + + Under ``H=1`` the sup reduces to the marginal, so + ``q -> Phi^{-1}(1 - alpha/2) ≈ 1.96`` at ``alpha=0.05`` (locked by + the ``TestSupTReducesToNormalAtH1`` reduction-invariant test). + + Parameters + ---------- + influence_matrix : np.ndarray, shape (n_units, n_horizons) + Per-unit per-horizon influence function on the β̂-scale. NaN + columns are treated as degenerate-horizon placeholders and drop + out of the sup via the finite mask. + att_per_horizon : np.ndarray, shape (n_horizons,) + Per-horizon point estimates (used to assemble the simultaneous + band: ``att ± q · se``). + se_per_horizon : np.ndarray, shape (n_horizons,) + Per-horizon analytical SE (Binder-TSL on survey path, HC1 + sandwich on weights= shortcut). + resolved_survey : Optional[ResolvedSurveyDesign] + ``None`` → unit-level Rademacher draw via + ``generate_bootstrap_weights_batch``. Otherwise → + PSU-level draw via ``generate_survey_multiplier_weights_batch`` + (stratum-centered, FPC-scaled, lonely-PSU-aware). + n_bootstrap : int + Number of multiplier replicates. + alpha : float + CI level (``0.05`` for 95% simultaneous band). + seed : int or None + RNG seed for reproducibility. + bootstrap_weights : str + Passed through to the helper: ``"rademacher"``, ``"mammen"``, or + ``"webb"``. Default ``"rademacher"`` (binary ±1 multipliers). + + Returns + ------- + (cband_crit_value, cband_low, cband_high, n_valid) : tuple + ``cband_crit_value`` is the sup-t quantile (float); ``cband_low`` + / ``cband_high`` are simultaneous-band endpoints shape + ``(n_horizons,)``; ``n_valid`` is the count of finite sup-t + draws (<= ``n_bootstrap``). Returns ``(nan, None, None, + n_valid)`` when fewer than half the draws are finite — warns the + caller. + """ + from diff_diff.bootstrap_utils import ( + generate_bootstrap_weights_batch, + generate_survey_multiplier_weights_batch, + ) + + influence_matrix = np.asarray(influence_matrix, dtype=np.float64) + att_per_horizon = np.asarray(att_per_horizon, dtype=np.float64) + se_per_horizon = np.asarray(se_per_horizon, dtype=np.float64) + n_units, n_horizons = influence_matrix.shape + rng = np.random.default_rng(seed) + + use_survey_bootstrap = resolved_survey is not None and ( + resolved_survey.strata is not None + or resolved_survey.psu is not None + or resolved_survey.fpc is not None + ) + + if use_survey_bootstrap: + psu_weights, psu_ids = generate_survey_multiplier_weights_batch( + n_bootstrap, resolved_survey, bootstrap_weights, rng + ) + if resolved_survey.psu is not None: + unit_psu = resolved_survey.psu + psu_id_to_col = {int(p): c for c, p in enumerate(psu_ids)} + unit_to_psu_col = np.array([psu_id_to_col[int(unit_psu[i])] for i in range(n_units)]) + else: + unit_to_psu_col = np.arange(n_units) + all_bootstrap_weights = psu_weights[:, unit_to_psu_col] # (B, G) + else: + all_bootstrap_weights = generate_bootstrap_weights_batch( + n_bootstrap, n_units, bootstrap_weights, rng + ) # (B, G) + + # Perturbations: (B, H) = (B, G) @ (G, H). Matches staggered:373 + # idiom — no (1/n) prefactor; ``psi`` is already on θ̂-scale. + # Silence divide/invalid/overflow warnings from the matmul — NaN / + # inf rows from degenerate horizons propagate and are filtered by + # the finite-mask below, so these are expected at construction time. + with np.errstate(divide="ignore", invalid="ignore", over="ignore"): + perturbations = all_bootstrap_weights @ influence_matrix # (B, H) + + # t-statistics via per-horizon analytical SE. + safe_se = np.where( + (se_per_horizon > 0) & np.isfinite(se_per_horizon), + se_per_horizon, + np.nan, + ) + t_dist = perturbations / safe_se[np.newaxis, :] # (B, H) + # Suppress the all-NaN-slice warning from nanmax on rows with + # every horizon degenerate — the finite mask drops those. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=RuntimeWarning) + sup_t_dist = np.nanmax(np.abs(t_dist), axis=1) # (B,) + + finite_mask = np.isfinite(sup_t_dist) + n_valid = int(finite_mask.sum()) + + if n_valid < max(1, int(0.5 * n_bootstrap)): + warnings.warn( + f"Too few valid sup-t bootstrap samples ({n_valid}/{n_bootstrap}) " + f"— returning NaN simultaneous-band critical value. Possible " + f"causes: near-singular per-horizon SEs, or a survey design " + f"with all-zero PSU multipliers after stratum-level FPC. " + f"Inspect the per-horizon ``se`` array and consider raising " + f"`n_bootstrap` or passing `cband=False` to skip the sup-t " + f"band.", + RuntimeWarning, + stacklevel=2, + ) + return float("nan"), None, None, n_valid + + q = float(np.quantile(sup_t_dist[finite_mask], 1.0 - alpha)) + cband_low = att_per_horizon - q * se_per_horizon + cband_high = att_per_horizon + q * se_per_horizon + return q, cband_low, cband_high, n_valid + + # ============================================================================= # Mass-point 2SLS # ============================================================================= @@ -1881,7 +2090,10 @@ def _fit_mass_point_2sls( d_lower: float, cluster: Optional[np.ndarray], vcov_type: str, -) -> Tuple[float, float]: + *, + weights: Optional[np.ndarray] = None, + return_influence: bool = False, +) -> Tuple[float, float, Optional[np.ndarray]]: """Wald-IV point estimate and structural-residual 2SLS sandwich SE. The just-identified binary instrument ``Z_g = 1{D_{g,2} > d_lower}`` @@ -1890,24 +2102,26 @@ def _fit_mass_point_2sls( ``beta_hat = (Ybar_{Z=1} - Ybar_{Z=0}) / (Dbar_{Z=1} - Dbar_{Z=0})``. The STANDARD ERROR is computed via the 2SLS sandwich - ``V = [Z'X]^{-1} * Omega * [Z'X]^{-T}`` where ``Omega`` is built from - the STRUCTURAL residuals - ``u = dy - alpha_hat - beta_hat * d`` - (NOT the reduced-form residuals). This is the canonical 2SLS - inference path and matches what ``AER::ivreg`` / ``ivreg2`` would - produce. The "OLS-on-indicator + scale by dose-gap" shortcut gives - reduced-form residuals, which diverge from structural residuals in - finite samples and substantively under clustering. + ``V = [Z'WX]^{-1} * Omega * [Z'WX]^{-T}`` where ``Omega`` is built + from the STRUCTURAL residuals + ``u = dy - alpha_hat - beta_hat * d`` (NOT the reduced-form + residuals). This is the canonical 2SLS inference path and matches + what ``estimatr::iv_robust`` / Stata ``ivregress`` would produce. Supported ``vcov_type``: - - ``"classical"``: constant variance ``sigma_hat^2 = sum(u^2) / (n-2)``. + - ``"classical"``: constant variance ``sigma_hat^2 = sum(w² u²) / + (sum(w) - k)`` (weighted) or ``sum(u^2) / (n-k)`` (unweighted); + sandwich form with ``w²`` in the meat when weighted. - ``"hc1"``: heteroskedasticity-robust with small-sample DOF scaling - ``n / (n - 2)``. With ``cluster`` supplied, switches to CR1 - (Liang-Zeger) cluster-robust. + ``n / (n - k)``; meat = ``Z' diag(w² u²) Z`` (pweight convention, + Wooldridge 2010 Eq. 12.37; matches ``estimatr::iv_robust(..., + weights=..., se_type="HC1")`` bit-exactly). With ``cluster`` + supplied, switches to CR1 (Liang-Zeger) with cluster score + ``Z'_c (w · u)_c``. ``"hc2"`` and ``"hc2_bm"`` raise ``NotImplementedError`` (2SLS-specific - leverage derivation pending; queued for follow-up). + leverage derivation pending). Parameters ---------- @@ -1921,90 +2135,180 @@ def _fit_mass_point_2sls( Cluster ids per unit (``None`` for no clustering). vcov_type : str One of ``"classical"``, ``"hc1"``. + weights : np.ndarray or None, shape (n,) + Per-unit sampling weights (pweight convention). ``None`` for the + unweighted path — that branch is numerically bit-exact with + pre-Phase 4.5 B output. Zero-weight units contribute zero to + every sum-weighted expression and zero to the returned IF. + return_influence : bool + When True, returns the per-unit influence function (IF) on the + β̂-scale, shape ``(n,)`` with zeros at zero-weight rows. The IF + is scaled so that under a trivial ``ResolvedSurveyDesign`` + (single stratum, each unit its own PSU, no FPC), + ``compute_survey_if_variance(IF, trivial)`` ≈ ``V_HC1[1, 1]`` + at ``atol=1e-10`` (PR #359 convention; see the "IF scale + convention" section of the Phase 4.5 B plan for derivation). Returns ------- - tuple[float, float] - ``(beta_hat, se_beta)``. NaN for SE when the dose-gap vanishes - (``Dbar_{Z=1} == Dbar_{Z=0}``) or the sandwich is singular. + tuple[float, float, np.ndarray or None] + ``(beta_hat, se_beta, psi)``. ``psi`` is the per-unit IF when + ``return_influence=True``, else ``None``. NaN for SE when the + dose-gap vanishes (``Dbar_{Z=1} == Dbar_{Z=0}``) or the + sandwich is singular; in those cases ``psi`` is returned as a + length-``n`` zero array when ``return_influence=True``. """ d = np.asarray(d, dtype=np.float64) dy = np.asarray(dy, dtype=np.float64) n = d.shape[0] + # Weight validation / normalization. The unweighted branch preserves + # numerical bit-parity vs pre-Phase 4.5 B by skipping np.average and + # using plain sums / means exactly as before. `w_arr`, `w_sum`, and + # `pos_mask` are initialized as sentinels so static typing flows + # cleanly; only the weighted branch populates them with real values. + weighted = weights is not None + w_arr: np.ndarray = np.ones(n, dtype=np.float64) + w_sum: float = float(n) + pos_mask: np.ndarray = np.ones(n, dtype=bool) + if weighted: + w_arr = np.asarray(weights, dtype=np.float64).ravel() + if w_arr.shape[0] != n: + raise ValueError( + f"weights length ({w_arr.shape[0]}) does not match d / dy length ({n})." + ) + if not np.all(np.isfinite(w_arr)): + raise ValueError("weights contains non-finite values (NaN or Inf).") + if np.any(w_arr < 0): + raise ValueError("weights must be non-negative.") + w_sum = float(w_arr.sum()) + if w_sum <= 0: + raise ValueError("weights sum to zero — no observations have positive weight.") + pos_mask = w_arr > 0 + Z = (d > d_lower).astype(np.float64) - n_above = int(Z.sum()) - n_at_or_below = n - n_above - # Degeneracy checks: if either Z-subset is empty, Wald-IV is undefined. + # Degeneracy checks on the positive-weight subset (zero-weight units + # do not drive design resolution; same subpopulation convention as + # PR #359). Under unweighted fits the "positive" subset is the full + # sample so behavior is unchanged. + if weighted: + n_above = int(((Z == 1) & pos_mask).sum()) + n_at_or_below = int(((Z == 0) & pos_mask).sum()) + else: + n_above = int(Z.sum()) + n_at_or_below = n - n_above + + _null_psi = np.zeros(n, dtype=np.float64) if return_influence else None + if n_above == 0 or n_at_or_below == 0: - return float("nan"), float("nan") + return float("nan"), float("nan"), _null_psi + + # Point estimate: weighted Wald-IV ratio (reduces to unweighted at w=1). + if weighted: + Z1_idx = (Z == 1) & pos_mask + Z0_idx = (Z == 0) & pos_mask + w_Z1 = float(w_arr[Z1_idx].sum()) + w_Z0 = float(w_arr[Z0_idx].sum()) + if w_Z1 <= 0.0 or w_Z0 <= 0.0: + return float("nan"), float("nan"), _null_psi + dose_gap = float( + (w_arr[Z1_idx] * d[Z1_idx]).sum() / w_Z1 - (w_arr[Z0_idx] * d[Z0_idx]).sum() / w_Z0 + ) + d_bar = float((w_arr * d).sum() / w_sum) + dy_gap = float( + (w_arr[Z1_idx] * dy[Z1_idx]).sum() / w_Z1 - (w_arr[Z0_idx] * dy[Z0_idx]).sum() / w_Z0 + ) + dy_bar = float((w_arr * dy).sum() / w_sum) + else: + dose_gap = d[Z == 1].mean() - d[Z == 0].mean() + d_bar = float(d.mean()) + dy_gap = dy[Z == 1].mean() - dy[Z == 0].mean() + dy_bar = float(dy.mean()) - dose_gap = d[Z == 1].mean() - d[Z == 0].mean() - if abs(dose_gap) < 1e-12 * max(1.0, abs(float(d.mean()))): + if abs(dose_gap) < 1e-12 * max(1.0, abs(d_bar)): # No dose variation around d_lower -> beta undefined. - return float("nan"), float("nan") + return float("nan"), float("nan"), _null_psi - dy_gap = dy[Z == 1].mean() - dy[Z == 0].mean() + # dy_gap / dy_bar were computed inside the weighted/unweighted blocks above. beta_hat = float(dy_gap / dose_gap) - alpha_hat = float(dy.mean() - beta_hat * d.mean()) + alpha_hat = float(dy_bar - beta_hat * d_bar) - # STRUCTURAL residuals (plan-review CRITICAL #1): u = y - alpha - beta*x. - # The Wald-IV/OLS-on-indicator shortcut would use reduced-form residuals - # u_rf = dy - (alpha_rf + gamma * Z), which differ in finite samples. + # STRUCTURAL residuals: u = y - alpha - beta*x. The Wald-IV / + # OLS-on-indicator shortcut would use reduced-form residuals + # u_rf = dy - (alpha_rf + gamma * Z), which differ in finite + # samples and substantively under clustering. u = dy - alpha_hat - beta_hat * d # Design matrices: X = [1, d] (endogenous), Z_d = [1, Z] (instrument). X = np.column_stack([np.ones(n, dtype=np.float64), d]) Zd = np.column_stack([np.ones(n, dtype=np.float64), Z]) - ZtX = Zd.T @ X # (2, 2) + if weighted: + # Weighted bread: Z' diag(w) X. Zero-weight rows contribute 0. + ZtWX = Zd.T @ (w_arr[:, None] * X) + else: + ZtWX = Zd.T @ X + try: - ZtX_inv = np.linalg.inv(ZtX) + ZtWX_inv = np.linalg.inv(ZtWX) except np.linalg.LinAlgError: - # Z'X singular (e.g., no variation in Z, already handled above). - return beta_hat, float("nan") + return beta_hat, float("nan"), _null_psi vcov_type = vcov_type.lower() if vcov_type in _MASS_POINT_VCOV_UNSUPPORTED: raise NotImplementedError( f"vcov_type={vcov_type!r} is not supported on the " - f"HeterogeneousAdoptionDiD mass-point path in Phase 2a. " - f"HC2 / HC2-BM require a 2SLS-specific leverage derivation " - f"`x_i' (Z'X)^{{-1}}(...)(X'Z)^{{-1}} x_i` that differs from " - f"the OLS leverage `x_i' (X'X)^{{-1}} x_i`. Derivation + R " - f"parity anchor are queued for the follow-up PR. Use " + f"HeterogeneousAdoptionDiD mass-point path. HC2 / HC2-BM " + f"require a 2SLS-specific leverage derivation " + f"`x_i' (Z'WX)^{{-1}}(...)(X'WZ)^{{-1}} x_i` that differs " + f"from the OLS leverage `x_i' (X'WX)^{{-1}} x_i`. Derivation " + f"+ R parity anchor are queued for a follow-up PR. Use " f"vcov_type='hc1' or 'classical' for now." ) + k = 2 # intercept + dose if cluster is not None: # CR1 (Liang-Zeger) cluster-robust sandwich. # Use pd.unique to match R's first-appearance order (stable for # cross-runtime reproducibility). clusters_unique = pd.unique(cluster) Omega = np.zeros((2, 2), dtype=np.float64) + wu = (w_arr * u) if weighted else u for c in clusters_unique: idx = cluster == c - # score per cluster: s_c = Zd[idx]' @ u[idx] - s = Zd[idx].T @ u[idx] + # score per cluster: s_c = Zd[idx]' @ (w · u)[idx] + s = Zd[idx].T @ wu[idx] Omega += np.outer(s, s) - G = len(clusters_unique) - k = 2 - if G < 2: + n_clusters = len(clusters_unique) + if n_clusters < 2: # Cluster-robust SE undefined with a single cluster. - return beta_hat, float("nan") - Omega *= (G / (G - 1)) * ((n - 1) / (n - k)) + return beta_hat, float("nan"), _null_psi + Omega *= (n_clusters / (n_clusters - 1)) * ((n - 1) / (n - k)) elif vcov_type == "classical": - dof = n - 2 - if dof <= 0: - return beta_hat, float("nan") - sigma2 = float((u * u).sum()) / dof - Omega = sigma2 * (Zd.T @ Zd) + if weighted: + dof = w_sum - k + if dof <= 0: + return beta_hat, float("nan"), _null_psi + sigma2 = float((w_arr * w_arr * u * u).sum()) / dof + Omega = sigma2 * (Zd.T @ ((w_arr * w_arr)[:, None] * Zd)) + else: + dof = n - k + if dof <= 0: + return beta_hat, float("nan"), _null_psi + sigma2 = float((u * u).sum()) / dof + Omega = sigma2 * (Zd.T @ Zd) elif vcov_type == "hc1": - dof = n - 2 + dof = n - k if dof <= 0: - return beta_hat, float("nan") - Omega = (n / dof) * (Zd.T @ ((u * u)[:, None] * Zd)) + return beta_hat, float("nan"), _null_psi + if weighted: + # Pweight HC1: meat = Z' diag(w² u²) Z (Wooldridge 2010 Eq 12.37, + # matches linalg.py:1141 convention and estimatr::iv_robust + # HC1 bit-exactly). + Omega = (n / dof) * (Zd.T @ ((w_arr * w_arr * u * u)[:, None] * Zd)) + else: + Omega = (n / dof) * (Zd.T @ ((u * u)[:, None] * Zd)) else: raise ValueError( f"Unsupported vcov_type={vcov_type!r} on the HAD mass-point " @@ -2012,12 +2316,32 @@ def _fit_mass_point_2sls( f"cluster-robust CR1 via cluster=)." ) - V = ZtX_inv @ Omega @ ZtX_inv.T + V = ZtWX_inv @ Omega @ ZtWX_inv.T var_beta = float(V[1, 1]) if not np.isfinite(var_beta) or var_beta < 0: - return beta_hat, float("nan") + return beta_hat, float("nan"), _null_psi se_beta = float(np.sqrt(var_beta)) - return beta_hat, se_beta + + if not return_influence: + return beta_hat, se_beta, None + + # Per-unit influence function on β̂-scale, scaled so that + # compute_survey_if_variance(psi, trivial_resolved) ≈ V_HC1[1, 1] + # at atol=1e-10. Derivation (see Phase 4.5 B plan, "IF scale + # convention"): V_HC1[1,1] = (n/(n-k)) · sum_j psi₀_j² with + # psi₀_j = [(Z'WX)^{-1} z_j w_j u_j][1]; trivial + # compute_survey_if_variance reduces to (n/(n-1)) · sum_j psi_j². + # Setting psi_j = psi₀_j · sqrt((n-1)/(n-k)) makes the two agree. + # For the unweighted path w=1 is the degenerate case and psi₀_j + # equals the standard OLS IF. + bread_row = ZtWX_inv[1, :] # shape (2,) + if weighted: + psi0 = (Zd @ bread_row) * (w_arr * u) # shape (n,) + else: + psi0 = (Zd @ bread_row) * u + dof_psi = max(n - k, 1) + psi = psi0 * np.sqrt((n - 1) / dof_psi) + return beta_hat, se_beta, psi # ============================================================================= @@ -2160,6 +2484,8 @@ def __init__( vcov_type: Optional[str] = None, robust: bool = False, cluster: Optional[str] = None, + n_bootstrap: int = 999, + seed: Optional[int] = None, ) -> None: self.design = design self.d_lower = d_lower @@ -2168,6 +2494,16 @@ def __init__( self.vcov_type = vcov_type self.robust = robust self.cluster = cluster + # Phase 4.5 B: event-study survey sup-t simultaneous-CI support. + # ``n_bootstrap`` = number of multiplier-bootstrap replicates for + # the sup-t band on the event-study + weighted path. ``seed`` = + # reproducibility seed for the multiplier draws. Both are + # consulted only when ``aggregate="event_study"`` AND a + # ``survey=`` / ``weights=`` is passed to ``fit()`` with + # ``cband=True`` (default). Unweighted event-study skips the + # bootstrap entirely — pre-Phase 4.5 B numerical output preserved. + self.n_bootstrap = n_bootstrap + self.seed = seed self._validate_constructor_args() def _validate_constructor_args(self) -> None: @@ -2209,6 +2545,17 @@ def _validate_constructor_args(self) -> None: f"of {_MASS_POINT_VCOV_SUPPORTED + _MASS_POINT_VCOV_UNSUPPORTED}, " f"or None." ) + # Phase 4.5 B: n_bootstrap must be a positive int; seed must be + # None or a nonneg int (numpy default_rng contract). + if not isinstance(self.n_bootstrap, (int, np.integer)): + raise ValueError(f"n_bootstrap must be an int; got {type(self.n_bootstrap).__name__}.") + if int(self.n_bootstrap) < 1: + raise ValueError(f"n_bootstrap must be >= 1; got {self.n_bootstrap!r}.") + if self.seed is not None: + if not isinstance(self.seed, (int, np.integer)): + raise ValueError(f"seed must be None or an int; got {type(self.seed).__name__}.") + if int(self.seed) < 0: + raise ValueError(f"seed must be nonneg; got {self.seed!r}.") def get_params(self, deep: bool = True) -> Dict[str, Any]: """Return the raw constructor parameters (sklearn-compatible). @@ -2235,6 +2582,8 @@ def get_params(self, deep: bool = True) -> Dict[str, Any]: "vcov_type": self.vcov_type, "robust": self.robust, "cluster": self.cluster, + "n_bootstrap": self.n_bootstrap, + "seed": self.seed, } def set_params(self, **params: Any) -> "HeterogeneousAdoptionDiD": @@ -2282,6 +2631,7 @@ def fit( aggregate: str = "overall", survey: Any = None, weights: Optional[np.ndarray] = None, + cband: bool = True, ) -> HeterogeneousAdoptionDiDResults: """Fit the HAD estimator. @@ -2372,14 +2722,6 @@ def fit( "shortcut, use weights=; it is internally equivalent to " "survey=SurveyDesign(weights=w)." ) - # Event-study + survey/weights: Phase 4.5 B deferral. - if aggregate == "event_study" and (survey is not None or weights is not None): - raise NotImplementedError( - "survey= / weights= are not yet supported on " - "aggregate='event_study' (deferred to Phase 4.5 B — " - "event-study survey composition). The continuous-design " - "overall path supports survey= and weights= as of this PR." - ) # Dispatch the event-study path to a dedicated method so the # single-period path stays unchanged (Phase 2a contract preserved). # Note: event_study returns HeterogeneousAdoptionDiDEventStudyResults @@ -2396,6 +2738,9 @@ def fit( time_col=time_col, unit_col=unit_col, first_treat_col=first_treat_col, + survey=survey, + weights=weights, + cband=cband, ) # ---- Resolve effective fit-time state (local vars only, per @@ -2483,15 +2828,9 @@ def fit( # drift from the docstring/test contract in ``survey.py``). weights_col_name = survey.weights # known non-None from guard above if weights_col_name not in data.columns: - raise ValueError( - f"survey.weights column {weights_col_name!r} not found in data." - ) - raw_weights_row = np.asarray( - data[weights_col_name].values, dtype=np.float64 - ) - raw_weights_unit = _aggregate_unit_weights( - data, raw_weights_row, unit_col - ) + raise ValueError(f"survey.weights column {weights_col_name!r} not found in data.") + raw_weights_row = np.asarray(data[weights_col_name].values, dtype=np.float64) + raw_weights_unit = _aggregate_unit_weights(data, raw_weights_row, unit_col) # Resolve the SurveyDesign against the long-panel data. This # validates column names, applies pweight/aweight normalization # to mean=1, and extracts numpy arrays for all design columns. @@ -2502,9 +2841,7 @@ def fit( resolved_survey_unit = _aggregate_unit_resolved_survey( data, resolved_survey_row, unit_col ) - weights_unit = np.asarray( - resolved_survey_unit.weights, dtype=np.float64 - ) + weights_unit = np.asarray(resolved_survey_unit.weights, dtype=np.float64) # Zero-weight units (e.g. SurveyDesign.subpopulation() output, or # a user-supplied pweight column with excluded observations) must @@ -2805,28 +3142,43 @@ def fit( vcov_label: Optional[str] = None cluster_label: Optional[str] = None elif resolved_design == "mass_point": - # Phase 4.5 B deferral: weighted 2SLS + survey composition on - # the mass-point path is not yet wired. Reject explicitly - # rather than silently ignoring survey/weights. - if weights_unit is not None: - raise NotImplementedError( - "survey= / weights= on design='mass_point' is deferred " - "to Phase 4.5 B (weighted 2SLS + sandwich variance). " - "This PR ships survey support only on the continuous-" - "dose paths (continuous_at_zero, continuous_near_d_lower)." - ) if vcov_type_arg is None: # Backward-compat: robust=True -> hc1, robust=False -> classical. vcov_requested = "hc1" if robust_arg else "classical" else: vcov_requested = vcov_type_arg.lower() - att, se = _fit_mass_point_2sls( - d_arr, - dy_arr, + # Phase 4.5 B: accept weights_unit (None on unweighted fits). + # return_influence=True only on the survey= path because + # Binder-TSL composition consumes the IF; the weights= + # shortcut and unweighted paths use the analytical sandwich + # SE directly. ``psi_mp`` is per-unit IF on β̂-scale or None. + # Fit on FULL (unfiltered) arrays so the IF aligns with the + # full survey design (subpopulation convention: zero-weight + # units contribute 0 to all sums; IF zero-padded back to full + # length). Under unweighted fits d_arr_full == d_arr and + # weights_unit_full is None, so behavior is unchanged. + att, se, psi_mp = _fit_mass_point_2sls( + d_arr_full, + dy_arr_full, d_lower_val, cluster_arr, vcov_requested, + weights=weights_unit_full, + return_influence=resolved_survey_unit_full is not None, ) + # Survey path: compose Binder-TSL variance from per-unit IF + # (replaces analytical sandwich SE). Mirrors continuous-path + # branch at lines 3082-3099. Under trivial resolved (single + # stratum, no PSU/FPC, uniform w), this reduces to analytical + # HC1 within the IF-scale-convention tolerance (atol=1e-10). + if resolved_survey_unit_full is not None and psi_mp is not None: + from diff_diff.survey import compute_survey_if_variance + + v_survey = compute_survey_if_variance(psi_mp, resolved_survey_unit_full) + if np.isfinite(v_survey) and v_survey > 0.0: + se = float(np.sqrt(v_survey)) + else: + se = float("nan") bc_fit = None bw_diag = None inference_method = "analytical_2sls" @@ -2849,9 +3201,7 @@ def fit( df_infer: Optional[int] = None if resolved_survey_unit is not None: df_infer = resolved_survey_unit.df_survey - t_stat, p_value, conf_int = safe_inference( - att, se, alpha=float(self.alpha), df=df_infer - ) + t_stat, p_value, conf_int = safe_inference(att, se, alpha=float(self.alpha), df=df_infer) # Build survey metadata (repo-standard SurveyMetadata from # diff_diff.survey.compute_survey_metadata) when weights/survey @@ -2878,7 +3228,13 @@ def fit( survey_metadata = compute_survey_metadata( resolved_survey_unit_full, raw_weights_unit_full ) - variance_formula_label = "survey_binder_tsl" + # Design-specific label — continuous uses bias-corrected CCT + # IF, mass-point uses 2SLS IF; both route through Binder TSL. + variance_formula_label = ( + "survey_binder_tsl_2sls" + if resolved_design == "mass_point" + else "survey_binder_tsl" + ) else: # weights= shortcut: construct a minimal resolved # SurveyDesign with the FULL user-supplied weights @@ -2901,9 +3257,7 @@ def fit( ) # weights_unit_full is already the raw user-supplied # array (no SurveyDesign.resolve() normalization here). - survey_metadata = compute_survey_metadata( - minimal_resolved, weights_unit_full - ) + survey_metadata = compute_survey_metadata(minimal_resolved, weights_unit_full) # On the ``weights=`` shortcut, inference stays Normal # (df=None in safe_inference) — no PSU / strata / FPC # composition. Clear the survey-only fields that @@ -2918,22 +3272,40 @@ def fit( survey_metadata.n_strata = None survey_metadata.n_psu = None survey_metadata.df_survey = None - variance_formula_label = "pweight" + variance_formula_label = ( + "pweight_2sls" if resolved_design == "mass_point" else "pweight" + ) # Expose the effective weighted denominator used by the - # beta-scale rescaling. Use FULL arrays (same numerical - # result — zero-weight units contribute 0 to both numerator - # and denominator — but preserves symmetry with the FULL- - # array fit path above). + # beta-scale rescaling. Continuous paths use a weighted sample + # mean of d (or d − d_lower). Mass-point uses the weighted + # Wald-IV dose-gap (the denominator of β̂ = + # dy_gap_w / dose_gap_w), computed from the FULL arrays + # (zero-weight units contribute 0 to both subgroup sums). if resolved_design == "continuous_at_zero": - effective_dose_mean_value = float( - np.average(d_arr_full, weights=weights_unit_full) - ) + effective_dose_mean_value = float(np.average(d_arr_full, weights=weights_unit_full)) elif resolved_design == "continuous_near_d_lower": effective_dose_mean_value = float( np.average(d_arr_full - d_lower_val, weights=weights_unit_full) ) - # else (mass_point): unreachable here because mass_point with - # weights raises NotImplementedError upstream. + elif resolved_design == "mass_point": + # Weighted Wald-IV dose gap: mean(d | Z=1, w) - mean(d | Z=0, w). + # Surface this as the "effective denominator" so downstream + # reporting displays the β̂-scale denominator consistently + # across designs. Guard against empty subgroups (handled + # upstream by _fit_mass_point_2sls returning NaN, so we + # only reach here on a successful fit with positive mass + # on both sides). + Z_mp = (d_arr_full > d_lower_val).astype(np.float64) + pos_mp = weights_unit_full > 0 + Z1_mp = (Z_mp == 1) & pos_mp + Z0_mp = (Z_mp == 0) & pos_mp + w_Z1_mp = float(weights_unit_full[Z1_mp].sum()) + w_Z0_mp = float(weights_unit_full[Z0_mp].sum()) + if w_Z1_mp > 0.0 and w_Z0_mp > 0.0: + effective_dose_mean_value = float( + (weights_unit_full[Z1_mp] * d_arr_full[Z1_mp]).sum() / w_Z1_mp + - (weights_unit_full[Z0_mp] * d_arr_full[Z0_mp]).sum() / w_Z0_mp + ) return HeterogeneousAdoptionDiDResults( att=float(att), @@ -2973,6 +3345,7 @@ def _fit_continuous( d_lower_val: float, weights_arr: Optional[np.ndarray] = None, resolved_survey_unit: Any = None, # ResolvedSurveyDesign (G,) or None + force_return_influence: bool = False, ) -> Tuple[float, float, Optional[BiasCorrectedFit], Optional[BandwidthResult]]: """Fit Phase 1c ``bias_corrected_local_linear`` and form the WAS estimate. @@ -3057,7 +3430,7 @@ def _fit_continuous( # for survey-composed variance (compute_survey_if_variance). # Unconditional IF computation would add a small O(G) cost # to every fit; gate it on the survey path. - return_influence=resolved_survey_unit is not None, + return_influence=(resolved_survey_unit is not None or force_return_influence), # No cluster / vce threading in Phase 2a (see UserWarning # in fit()). ) @@ -3088,6 +3461,7 @@ def _fit_continuous( # design, V(mu_hat) = compute_survey_if_variance(psi, # resolved) with PSU aggregation + strata sum + FPC. from diff_diff.survey import compute_survey_if_variance + v_survey = compute_survey_if_variance( bc_fit.influence_function, resolved_survey_unit ) @@ -3112,6 +3486,9 @@ def _fit_event_study( time_col: str, unit_col: str, first_treat_col: Optional[str], + survey: Any = None, + weights: Optional[np.ndarray] = None, + cband: bool = True, ) -> HeterogeneousAdoptionDiDEventStudyResults: """Multi-period event-study fit (paper Appendix B.2). @@ -3121,9 +3498,12 @@ def _fit_event_study( on the period-F dose distribution, and then fits the chosen design path independently on each event-time horizon's first differences. - Per-horizon sandwich independence is the paper-faithful convention - (Pierce-Schott Figure 2 pointwise CIs). Joint cross-horizon - covariance is deferred to a follow-up PR. + On the weighted path (``survey=`` or ``weights=``), per-horizon + variance is Binder-TSL via :func:`compute_survey_if_variance` and + the simultaneous confidence band (when ``cband=True``) is + constructed by a shared-PSU multiplier bootstrap over the stacked + per-horizon influence-function matrix (see + :func:`_sup_t_multiplier_bootstrap`). """ # ---- Resolve effective fit-time state (local vars only, # feedback_fit_does_not_mutate_config). ---- @@ -3132,12 +3512,91 @@ def _fit_event_study( vcov_type_arg = self.vcov_type robust_arg = self.robust cluster_arg = self.cluster + n_bootstrap_eff = int(self.n_bootstrap) + seed_eff = None if self.seed is None else int(self.seed) + + # ---- Survey/weights mutex + contract validation (front-door). + # Mirrors the static-path gates at fit(): exactly one knob + # (survey= xor weights=); survey= without a weights column is + # unsupported; non-pweight SurveyDesigns are rejected with an + # NotImplementedError pointing at Phase 4.5 C. ---- + if survey is not None and weights is not None: + raise ValueError( + "Pass exactly one of survey= or weights=, not both. " + "For SurveyDesign-composed inference (PSU, strata, FPC, " + "replicate weights), use survey=. For a simple pweight-" + "only shortcut, use weights=; it is internally equivalent " + "to survey=SurveyDesign(weights=w)." + ) + if survey is not None: + if not hasattr(survey, "weights"): + raise TypeError( + f"survey= must be a SurveyDesign-like object with a " + f".weights attribute; got {type(survey).__name__}. " + f"Construct a SurveyDesign via diff_diff.survey." + ) + if getattr(survey, "weights", None) is None: + raise NotImplementedError( + "survey= without weights is not yet supported. Pass " + "survey=SurveyDesign(weights='', ...) with a " + "per-row weight column." + ) + weight_type = getattr(survey, "weight_type", "pweight") + if weight_type != "pweight": + raise NotImplementedError( + f"survey=SurveyDesign(weight_type={weight_type!r}) is " + f"not yet supported on HeterogeneousAdoptionDiD. " + f"HAD's weighted local-linear treats weights as " + f"sampling (probability) weights only. Replicate " + f"designs (BRR/Fay/JK1/JKn/SDR) and frequency / " + f"analytic weights are deferred to Phase 4.5 C." + ) # ---- Validate multi-period panel and apply staggered filter ---- F, t_pre_list, t_post_list, data_filtered, filter_info = _validate_had_panel_event_study( data, outcome_col, dose_col, time_col, unit_col, first_treat_col ) + # ---- Filter weights / resolve survey on the FILTERED frame. + # ``data_filtered`` preserves the original row index (via + # ``.loc[keep_mask].copy()`` in the validator), so the filtered + # weights array is ``weights[data_filtered.index]``. Survey + # resolution runs against ``data_filtered`` directly — the + # SurveyDesign reads its columns by name from the DataFrame. ---- + weights_unit: Optional[np.ndarray] = None + raw_weights_unit: Optional[np.ndarray] = None + resolved_survey_unit: Any = None # ResolvedSurveyDesign (G,) or None + if weights is not None: + w_full = np.asarray(weights, dtype=np.float64).ravel() + if w_full.shape[0] != int(data.shape[0]): + raise ValueError( + f"weights length ({w_full.shape[0]}) does not match " + f"data rows ({int(data.shape[0])})." + ) + # Filter to rows surviving the staggered last-cohort filter. + # data_filtered.index is the original integer positional index + # into `data`; use positional slicing via `.iloc` elsewhere, + # but here `.index` carries the row labels to match. + w_filtered = w_full[data_filtered.index.to_numpy()] + weights_unit = _aggregate_unit_weights(data_filtered, w_filtered, unit_col) + raw_weights_unit = weights_unit + elif survey is not None: + from diff_diff.survey import ResolvedSurveyDesign # noqa: F401 + + resolved_survey_row = survey.resolve(data_filtered) + # Capture RAW pre-normalization per-row weights for + # compute_survey_metadata (matches PR #359 static-path + # contract: sum_weights / weight_range surface the user- + # supplied scale, not the resolver's mean=1 normalization). + weight_col_name = getattr(survey, "weights", None) + if isinstance(weight_col_name, str): + raw_row_w = np.asarray(data_filtered[weight_col_name], dtype=np.float64) + raw_weights_unit = _aggregate_unit_weights(data_filtered, raw_row_w, unit_col) + resolved_survey_unit = _aggregate_unit_resolved_survey( + data_filtered, resolved_survey_row, unit_col + ) + weights_unit = np.asarray(resolved_survey_unit.weights, dtype=np.float64) + # ---- Aggregate to per-horizon first differences ---- # Cluster extraction is deferred until after design resolution. d_arr, dy_dict, _, _, _ = _aggregate_multi_period_first_differences( @@ -3159,6 +3618,39 @@ def _fit_event_study( f"got n_units={n_units} after aggregation." ) + # ---- Zero-weight subpopulation convention (mirrors static path). + # Design decisions (auto-detect, d_lower, mass-point threshold) + # run on the positive-weight subset; variance composition runs + # on the FULL design (zero contributions but preserved frame). ---- + d_arr_full = d_arr # unfiltered; pass to per-horizon fits + dy_dict_full = dy_dict + weights_unit_full = weights_unit + resolved_survey_unit_full = resolved_survey_unit + raw_weights_unit_full = raw_weights_unit + if weights_unit is not None: + positive_mask = weights_unit > 0.0 + if not bool(positive_mask.all()): + n_dropped = int((~positive_mask).sum()) + warnings.warn( + f"HAD event-study: {n_dropped} unit(s) have weight == 0 " + f"and are excluded from design resolution (auto-detect, " + f"d_lower, mass-point threshold). Retained in the survey " + f"design for variance + SurveyMetadata (subpopulation " + f"convention).", + UserWarning, + stacklevel=2, + ) + d_arr = d_arr[positive_mask] + dy_dict = {e: v[positive_mask] for e, v in dy_dict.items()} + weights_unit = weights_unit[positive_mask] + n_units = int(d_arr.shape[0]) + if n_units < 3: + raise ValueError( + f"HAD event-study requires at least 3 positive-" + f"weight units for inference; got n_units={n_units} " + f"after the zero-weight filter." + ) + # ---- Resolve design (once, from D_{g, F} distribution) ---- if design_arg == "auto": resolved_design = _detect_design(d_arr) @@ -3299,15 +3791,36 @@ def _fit_event_study( cluster_label = None # ---- Per-horizon loop ---- + # On the weighted path, every horizon uses the FULL arrays + # (zero-weight units padded to 0 contribution) so the stacked IF + # matrix aligns with the full survey design. On unweighted fits, + # `d_arr_full == d_arr` and `dy_dict_full == dy_dict`, so this + # branch is a no-op. event_times_sorted = sorted(dy_dict.keys()) n_horizons = len(event_times_sorted) + # Use the full arrays when weighted so the IF matrix aligns with + # the survey design; unweighted uses the same d_arr either way. + weighted_es = weights_unit_full is not None + d_arr_loop = d_arr_full if weighted_es else d_arr + dy_dict_loop = dy_dict_full if weighted_es else dy_dict + G_full = int(d_arr_full.shape[0]) + att_arr = np.full(n_horizons, np.nan, dtype=np.float64) se_arr = np.full(n_horizons, np.nan, dtype=np.float64) t_arr = np.full(n_horizons, np.nan, dtype=np.float64) p_arr = np.full(n_horizons, np.nan, dtype=np.float64) ci_lo_arr = np.full(n_horizons, np.nan, dtype=np.float64) ci_hi_arr = np.full(n_horizons, np.nan, dtype=np.float64) - n_obs_arr = np.full(n_horizons, n_units, dtype=np.int64) + n_obs_arr = np.full(n_horizons, G_full if weighted_es else n_units, dtype=np.int64) + + # Per-horizon IF matrix on the weighted path (shape (G, H)); drives + # both per-horizon Binder-TSL variance (already composed inside + # ``_fit_continuous`` for continuous, or explicitly below for + # mass-point) AND the shared-PSU multiplier bootstrap for sup-t. + if weighted_es: + Psi = np.full((G_full, n_horizons), np.nan, dtype=np.float64) + else: + Psi = np.zeros((0, 0), dtype=np.float64) # sentinel, not used # Collect per-horizon diagnostics on continuous paths. Entries may be # None for horizons where ``_fit_continuous`` caught a degenerate @@ -3319,24 +3832,90 @@ def _fit_event_study( [] if resolved_design in ("continuous_at_zero", "continuous_near_d_lower") else None ) + # df_survey for t-inference on survey= path (mirrors static path). + df_infer: Optional[int] = None + if resolved_survey_unit_full is not None: + df_infer = resolved_survey_unit_full.df_survey + + # On the weighted event-study path, the sup-t multiplier bootstrap + # operates on the per-horizon IF matrix, so we must force the IF + # computation even on the ``weights=`` shortcut (no survey + # structure → _fit_continuous normally skips IF). Pass through + # the actual ``resolved_survey_unit_full`` (None on shortcut) so + # the per-horizon analytical SE still matches the static-path + # convention (bc_fit.se_robust on shortcut; Binder-TSL on + # survey=). IF return is gated on `force_return_influence=True`. + + # Track the Binder-TSL den for continuous paths so we can + # reconstruct the per-unit IF (psi / den) for the sup-t bootstrap + # where both numerator IF and denominator divide are needed. for i, e in enumerate(event_times_sorted): - dy_e = dy_dict[e] + dy_e = dy_dict_loop[e] if resolved_design in ("continuous_at_zero", "continuous_near_d_lower"): att_e, se_e, bc_fit_e, bw_diag_e = self._fit_continuous( - d_arr, dy_e, resolved_design, d_lower_val + d_arr_loop, + dy_e, + resolved_design, + d_lower_val, + weights_arr=weights_unit_full, + resolved_survey_unit=resolved_survey_unit_full, + # Force IF return on the weighted event-study path + # (needed for the sup-t bootstrap). Does NOT change + # the per-horizon SE formula — that still follows + # the static-path convention (Binder-TSL under + # survey=, bc_fit.se_robust under weights= shortcut). + force_return_influence=weighted_es, ) if bc_fits is not None: bc_fits.append(bc_fit_e) if bw_diags is not None: bw_diags.append(bw_diag_e) + # Collect per-unit IF on β̂-scale (psi_bc / den) so the + # sup-t bootstrap operates on the same θ̂-scale IF that + # the analytical variance sees. Per continuous-path + # construction in _fit_continuous, bc_fit.influence_function + # is the numerator IF; dividing by |den| yields the β̂ IF. + if weighted_es and bc_fit_e is not None and bc_fit_e.influence_function is not None: + if resolved_design == "continuous_at_zero": + den_e = float(np.average(d_arr_full, weights=weights_unit_full)) + else: + den_e = float( + np.average( + d_arr_full - d_lower_val, + weights=weights_unit_full, + ) + ) + if abs(den_e) > 1e-12: + Psi[:, i] = bc_fit_e.influence_function / abs(den_e) elif resolved_design == "mass_point": - att_e, se_e = _fit_mass_point_2sls( - d_arr, dy_e, d_lower_val, cluster_arr, vcov_requested + att_e, se_e, psi_e = _fit_mass_point_2sls( + d_arr_loop, + dy_e, + d_lower_val, + cluster_arr, + vcov_requested, + weights=weights_unit_full, + return_influence=resolved_survey_unit_full is not None or weighted_es, ) + # Survey path: override analytical sandwich SE with + # Binder-TSL via compute_survey_if_variance (matches + # continuous-path convention from PR #359). + if resolved_survey_unit_full is not None and psi_e is not None: + from diff_diff.survey import compute_survey_if_variance + + v_survey = compute_survey_if_variance(psi_e, resolved_survey_unit_full) + if np.isfinite(v_survey) and v_survey > 0.0: + se_e = float(np.sqrt(v_survey)) + else: + se_e = float("nan") + if weighted_es and psi_e is not None: + Psi[:, i] = psi_e else: raise ValueError(f"Internal error: unhandled design={resolved_design!r}.") - t_stat_e, p_value_e, conf_int_e = safe_inference(att_e, se_e, alpha=float(self.alpha)) + t_stat_e, p_value_e, conf_int_e = safe_inference( + att_e, se_e, alpha=float(self.alpha), df=df_infer + ) att_arr[i] = float(att_e) se_arr[i] = float(se_e) t_arr[i] = float(t_stat_e) @@ -3344,6 +3923,83 @@ def _fit_event_study( ci_lo_arr[i] = float(conf_int_e[0]) ci_hi_arr[i] = float(conf_int_e[1]) + # ---- Sup-t simultaneous confidence band (weighted + cband only) ---- + cband_low_arr: Optional[np.ndarray] = None + cband_high_arr: Optional[np.ndarray] = None + cband_crit_value: Optional[float] = None + cband_method_label: Optional[str] = None + cband_n_bootstrap_eff: Optional[int] = None + if weighted_es and cband and n_horizons >= 1: + q, cband_low_arr, cband_high_arr, _n_valid = _sup_t_multiplier_bootstrap( + influence_matrix=Psi, + att_per_horizon=att_arr, + se_per_horizon=se_arr, + resolved_survey=resolved_survey_unit_full, + n_bootstrap=n_bootstrap_eff, + alpha=float(self.alpha), + seed=seed_eff, + ) + cband_crit_value = q + cband_method_label = "multiplier_bootstrap" + cband_n_bootstrap_eff = n_bootstrap_eff + + # ---- Build survey metadata + variance_formula + effective_dose_mean + # (mirrors static-path branch). ---- + survey_metadata: Optional[SurveyMetadata] = None + variance_formula_label: Optional[str] = None + effective_dose_mean_value: Optional[float] = None + if weights_unit_full is not None: + if resolved_survey_unit_full is not None: + assert raw_weights_unit_full is not None + survey_metadata = compute_survey_metadata( + resolved_survey_unit_full, raw_weights_unit_full + ) + variance_formula_label = ( + "survey_binder_tsl_2sls" + if resolved_design == "mass_point" + else "survey_binder_tsl" + ) + else: + from diff_diff.survey import ResolvedSurveyDesign + + minimal_resolved = ResolvedSurveyDesign( + weights=weights_unit_full, + weight_type="pweight", + strata=None, + psu=None, + fpc=None, + n_strata=1, + n_psu=int(weights_unit_full.shape[0]), + lonely_psu="remove", + combined_weights=True, + mse=False, + ) + survey_metadata = compute_survey_metadata(minimal_resolved, weights_unit_full) + survey_metadata.n_strata = None + survey_metadata.n_psu = None + survey_metadata.df_survey = None + variance_formula_label = ( + "pweight_2sls" if resolved_design == "mass_point" else "pweight" + ) + if resolved_design == "continuous_at_zero": + effective_dose_mean_value = float(np.average(d_arr_full, weights=weights_unit_full)) + elif resolved_design == "continuous_near_d_lower": + effective_dose_mean_value = float( + np.average(d_arr_full - d_lower_val, weights=weights_unit_full) + ) + elif resolved_design == "mass_point": + Z_mp = (d_arr_full > d_lower_val).astype(np.float64) + pos_mp = weights_unit_full > 0 + Z1_mp = (Z_mp == 1) & pos_mp + Z0_mp = (Z_mp == 0) & pos_mp + w_Z1_mp = float(weights_unit_full[Z1_mp].sum()) + w_Z0_mp = float(weights_unit_full[Z0_mp].sum()) + if w_Z1_mp > 0.0 and w_Z0_mp > 0.0: + effective_dose_mean_value = float( + (weights_unit_full[Z1_mp] * d_arr_full[Z1_mp]).sum() / w_Z1_mp + - (weights_unit_full[Z0_mp] * d_arr_full[Z0_mp]).sum() / w_Z0_mp + ) + return HeterogeneousAdoptionDiDEventStudyResults( event_times=np.asarray(event_times_sorted, dtype=np.int64), att=att_arr, @@ -3359,12 +4015,19 @@ def _fit_event_study( d_lower=d_lower_val, dose_mean=dose_mean, F=F, - n_units=n_units, + n_units=G_full if weighted_es else n_units, inference_method=inference_method, vcov_type=vcov_label, cluster_name=cluster_label, - survey_metadata=None, + survey_metadata=survey_metadata, bandwidth_diagnostics=bw_diags, bias_corrected_fit=bc_fits, filter_info=filter_info, + variance_formula=variance_formula_label, + effective_dose_mean=effective_dose_mean_value, + cband_low=cband_low_arr, + cband_high=cband_high_arr, + cband_crit_value=cband_crit_value, + cband_method=cband_method_label, + cband_n_bootstrap=cband_n_bootstrap_eff, ) diff --git a/docs/methodology/REGISTRY.md b/docs/methodology/REGISTRY.md index 44da7543..31693825 100644 --- a/docs/methodology/REGISTRY.md +++ b/docs/methodology/REGISTRY.md @@ -2291,7 +2291,28 @@ Under `survey=SurveyDesign(weights, strata, psu, fpc)`, the variance composes vi - **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. - **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. - **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). -- **Note:** `HeterogeneousAdoptionDiD.fit()` dispatch matrix — survey / weights are supported on `continuous_at_zero` + `continuous_near_d_lower`; `mass_point` and `aggregate='event_study'` paths raise `NotImplementedError` pointing to Phase 4.5 B (weighted 2SLS + event-study survey composition). The HAD pretests (`qug_test`, `stute_test`, `yatchew_hr_test`, joint Stute variants, `did_had_pretest_workflow`) do NOT gain `survey=` / `weights=` kwargs in this PR — deferred to Phase 4.5 C per reciprocal-guard discipline (`feedback_reciprocal_guards_in_dispatch`). +- **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. + +*Weighted 2SLS (Phase 4.5 B):* `_fit_mass_point_2sls(..., weights=, return_influence=)` extends the Wald-IV / 2SLS sandwich with pweight semantics: +- **Weighted bread**: `Z'WX = Z'·diag(w)·X` (`w¹`, matches `estimatr::iv_robust(..., weights=)` weighted-bread convention). +- **HC1 pweight meat**: `Ω_HC1 = (n/(n-k)) · Z'·diag(w² u²)·Z` (`w²` squared, Wooldridge 2010 Eq. 12.37; matches `linalg.py:1141` pweight convention). Bit-exact with `estimatr::iv_robust(..., weights=, se_type="HC1")` at `atol=1e-10`. +- **CR1 pweight-cluster meat**: for each cluster c, `s_c = Z'_c·(w·u)_c`; `Ω_CR1 = (G/(G-1))·((n-1)/(n-k))·Σ_c s_c s_c'` (`w¹` inside cluster score). Bit-exact with `estimatr::iv_robust(..., weights=, clusters=, se_type="stata")` at `atol=1e-10`. +- **Classical**: sandwich form `Ω_cl = σ²·Z'·diag(w²)·Z` with `σ² = Σw²u²/(Σw-k)`. Deviates from `estimatr` classical (projection-form + `n-k` DOF) by `O(1/n)` at non-uniform weights; unweighted path is bit-exact by equivalence. Skipped in cross-language parity tests. +- **Per-unit IF on β̂-scale** (for Binder-TSL survey composition): `psi_g = [(Z'WX)^{-1} · z_g · w_g · u_g][1] · sqrt((n-1)/(n-k))`. The scaling factor absorbs DOF / small-sample differences so `compute_survey_if_variance(psi, trivial_resolved) ≈ V_HC1[1,1]` at `atol=1e-10` (mirrors PR #359 convention; asserted by `TestIFScaleInvariant` and bit-exact against estimatr HC1 on 4 DGPs). Fixture: `benchmarks/R/generate_estimatr_iv_robust_golden.R` → `benchmarks/data/estimatr_iv_robust_golden.json`. + +*Event-study survey composition (Phase 4.5 B):* The per-horizon loop in `_fit_event_study` threads `weights_unit_full` + `resolved_survey_unit_full` through to both `_fit_continuous` and `_fit_mass_point_2sls` (the latter with `return_influence=True` under weighted fits). The returned IF matrix `Psi ∈ R^{G × H}` has a shared construction contract across paths — each column on the β̂-scale, such that `compute_survey_if_variance(Psi[:, e], resolved) ≈ V_β[e]`. Per-horizon analytical variance uses Binder-TSL via `compute_survey_if_variance` (survey= path) or the weighted-robust HC1 sandwich (weights= shortcut). `survey_metadata`, `variance_formula` (`"survey_binder_tsl"` / `"survey_binder_tsl_2sls"` / `"pweight"` / `"pweight_2sls"`), and `effective_dose_mean` populate identically to the static path. Pre-PR numerical output is preserved bit-exactly on the unweighted path when `cband=False` (stability invariant; Phase 2b convention unchanged for unweighted fits). + +*Sup-t multiplier bootstrap (Phase 4.5 B):* Simultaneous confidence band on the weighted event-study path via `_sup_t_multiplier_bootstrap`: + +1. **Multiplier draws**: reuse `diff_diff.bootstrap_utils.generate_survey_multiplier_weights_batch` (survey= path: PSU-level draws with stratum centering, FPC scaling, lonely-PSU handling) or `generate_bootstrap_weights_batch` (weights= shortcut: unit-level Rademacher). Default `n_bootstrap=999` (CS parity); `seed` exposed on `HeterogeneousAdoptionDiD.__init__` for reproducibility. +2. **Perturbations**: `delta = xi @ Psi` — shape `(B, H)` matrix-matrix product, NO `(1/n)` prefactor (matches `staggered_bootstrap.py:373` idiom; `Psi` is already on the β̂-scale). +3. **t-statistics**: `t[b, e] = delta[b, e] / se[e]` where `se[e]` is the per-horizon analytical Binder-TSL / HC1 SE from the loop above. +4. **Sup-t distribution**: `sup_t[b] = max_e |t[b, e]|` with finite-mask filtering of degenerate horizons. +5. **Critical value**: `q = quantile(sup_t[finite], 1 - alpha)`. Simultaneous band: `cband_low[e] = att[e] - q · se[e]`. + +**Reduction invariant**: at `H=1`, the sup collapses to the marginal and `q → Φ⁻¹(1 - alpha/2) ≈ 1.96` at `alpha=0.05` up to MC noise. Locked by `TestSupTReducesToNormalAtH1` (G=500, B=5000, seed=42, `atol=0.15` on the quantile). + +**Scope**: sup-t bootstrap runs only when `aggregate="event_study"` AND `weights=` or `survey=` is supplied AND `cband=True` (default). Unweighted event-study skips the bootstrap entirely — pre-Phase 4.5 B numerical output bit-exactly preserved. Setting `cband=False` on the weighted path disables the bootstrap (useful for smoke-test bit-parity assertions against the unweighted path at uniform weights). - 2SLS (Design 1 mass-point case): standard 2SLS inference (details not elaborated in the paper). - TWFE with small `G`: HC2 standard errors with Bell-McCaffrey (2002) degrees-of-freedom correction, following Imbens and Kolesar (2016). Used in the Pierce and Schott (2016) application with `G=103`. Added library-wide to `diff_diff/linalg.py` as a new `vcov_type` dispatch (Phase 1a), exposed on `DifferenceInDifferences` and `TwoWayFixedEffects`. diff --git a/tests/test_estimatr_iv_robust_parity.py b/tests/test_estimatr_iv_robust_parity.py new file mode 100644 index 00000000..22ca883e --- /dev/null +++ b/tests/test_estimatr_iv_robust_parity.py @@ -0,0 +1,206 @@ +"""Cross-language weighted 2SLS parity tests against `estimatr::iv_robust`. + +HAD Phase 4.5 B mass-point path: verifies that ``_fit_mass_point_2sls`` +weighted HC1 and CR1 (Stata) sandwich SE match R's `estimatr::iv_robust` +(Blair et al. 2019) at ``atol=1e-10``. Classical SE diverges by O(1/n) +due to a different DOF / projection convention — documented in +`docs/methodology/REGISTRY.md` and skipped in parity assertions. + +Fixture generated by ``benchmarks/R/generate_estimatr_iv_robust_golden.R``. +Guard per ``feedback_golden_file_pytest_skip``: CI isolated-install jobs +copy ``tests/`` only, not ``benchmarks/data/``, so a missing fixture +downgrades to pytest.skip rather than fail. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import numpy as np +import pytest + +from diff_diff.had import _fit_mass_point_2sls + +FIXTURE_PATH = ( + Path(__file__).parent.parent / "benchmarks" / "data" / "estimatr_iv_robust_golden.json" +) + + +def _load_fixture(): + if not FIXTURE_PATH.exists(): + pytest.skip( + f"Golden fixture {FIXTURE_PATH} missing — regenerate via " + f"`Rscript benchmarks/R/generate_estimatr_iv_robust_golden.R`." + ) + with open(FIXTURE_PATH) as f: + return json.load(f) + + +@pytest.fixture(scope="module") +def fixture(): + return _load_fixture() + + +class TestEstimatrIVRobustHC1Parity: + """HC1 pweight sandwich: bit-exact match vs estimatr at atol=1e-10.""" + + @pytest.mark.parametrize( + "dgp_name", + ["uniform_n200", "mild_n500", "informative_n500", "heavy_n1000"], + ) + def test_hc1_beta_matches_estimatr(self, fixture, dgp_name): + dgp = fixture["fixtures"][dgp_name] + d = np.asarray(dgp["d"], dtype=np.float64) + dy = np.asarray(dgp["dy"], dtype=np.float64) + w = np.asarray(dgp["w"], dtype=np.float64) + d_lower = float(dgp["d_lower"]) + + beta, se, _psi = _fit_mass_point_2sls( + d, + dy, + d_lower, + None, + "hc1", + weights=w, + return_influence=False, + ) + + np.testing.assert_allclose( + beta, + dgp["hc1"]["beta"], + atol=1e-10, + rtol=1e-10, + err_msg=f"HC1 beta mismatch on DGP {dgp_name}", + ) + + @pytest.mark.parametrize( + "dgp_name", + ["uniform_n200", "mild_n500", "informative_n500", "heavy_n1000"], + ) + def test_hc1_se_matches_estimatr(self, fixture, dgp_name): + dgp = fixture["fixtures"][dgp_name] + d = np.asarray(dgp["d"], dtype=np.float64) + dy = np.asarray(dgp["dy"], dtype=np.float64) + w = np.asarray(dgp["w"], dtype=np.float64) + d_lower = float(dgp["d_lower"]) + + _beta, se, _psi = _fit_mass_point_2sls( + d, + dy, + d_lower, + None, + "hc1", + weights=w, + ) + + np.testing.assert_allclose( + se, + dgp["hc1"]["se"], + atol=1e-10, + rtol=1e-10, + err_msg=f"HC1 SE mismatch on DGP {dgp_name}", + ) + + +class TestEstimatrIVRobustCR1Parity: + """CR1 pweight-cluster: bit-exact match vs estimatr (se_type='stata').""" + + def test_cr1_beta_matches_estimatr(self, fixture): + dgp = fixture["fixtures"]["informative_cluster_n600"] + d = np.asarray(dgp["d"], dtype=np.float64) + dy = np.asarray(dgp["dy"], dtype=np.float64) + w = np.asarray(dgp["w"], dtype=np.float64) + cluster = np.asarray(dgp["cluster"], dtype=np.int64) + d_lower = float(dgp["d_lower"]) + + beta, _se, _psi = _fit_mass_point_2sls( + d, + dy, + d_lower, + cluster, + "hc1", + weights=w, + ) + np.testing.assert_allclose( + beta, + dgp["cr1"]["beta"], + atol=1e-10, + rtol=1e-10, + ) + + def test_cr1_se_matches_estimatr(self, fixture): + dgp = fixture["fixtures"]["informative_cluster_n600"] + d = np.asarray(dgp["d"], dtype=np.float64) + dy = np.asarray(dgp["dy"], dtype=np.float64) + w = np.asarray(dgp["w"], dtype=np.float64) + cluster = np.asarray(dgp["cluster"], dtype=np.int64) + d_lower = float(dgp["d_lower"]) + + _beta, se, _psi = _fit_mass_point_2sls( + d, + dy, + d_lower, + cluster, + "hc1", + weights=w, + ) + np.testing.assert_allclose( + se, + dgp["cr1"]["se"], + atol=1e-10, + rtol=1e-10, + ) + + +class TestIFScaleInvariant: + """IF scale invariant: compute_survey_if_variance(psi, trivial) ≈ V_HC1.""" + + @pytest.mark.parametrize( + "dgp_name", + ["uniform_n200", "mild_n500", "informative_n500", "heavy_n1000"], + ) + def test_if_reduces_to_hc1_variance(self, fixture, dgp_name): + from diff_diff.survey import ResolvedSurveyDesign, compute_survey_if_variance + + dgp = fixture["fixtures"][dgp_name] + d = np.asarray(dgp["d"], dtype=np.float64) + dy = np.asarray(dgp["dy"], dtype=np.float64) + w = np.asarray(dgp["w"], dtype=np.float64) + d_lower = float(dgp["d_lower"]) + + _beta, se, psi = _fit_mass_point_2sls( + d, + dy, + d_lower, + None, + "hc1", + weights=w, + return_influence=True, + ) + + n = d.shape[0] + trivial = ResolvedSurveyDesign( + weights=w, + weight_type="pweight", + strata=None, + psu=None, + fpc=None, + n_strata=1, + n_psu=n, + lonely_psu="remove", + combined_weights=True, + mse=False, + ) + V_survey = compute_survey_if_variance(psi, trivial) + np.testing.assert_allclose( + V_survey, + se**2, + atol=1e-10, + rtol=1e-10, + err_msg=( + "compute_survey_if_variance(psi, trivial_resolved) should " + "equal the analytical HC1 variance under trivial survey " + "design (PR #359 IF scale convention)." + ), + ) diff --git a/tests/test_had.py b/tests/test_had.py index c5908299..2721c570 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -774,7 +774,7 @@ def test_helper_returns_nan_on_empty_z_one(self): """_fit_mass_point_2sls returns NaN when no units above d_lower.""" d = np.full(50, 0.5) dy = np.random.default_rng(0).standard_normal(50) - beta, se = _fit_mass_point_2sls(d, dy, 0.5, None, "hc1") + beta, se, _ = _fit_mass_point_2sls(d, dy, 0.5, None, "hc1") assert np.isnan(beta) assert np.isnan(se) @@ -782,7 +782,7 @@ def test_helper_returns_nan_on_empty_z_zero(self): """_fit_mass_point_2sls returns NaN when no units at d_lower.""" d = np.full(50, 0.6) # all strictly above d_lower=0.5 dy = np.random.default_rng(0).standard_normal(50) - beta, se = _fit_mass_point_2sls(d, dy, 0.5, None, "hc1") + beta, se, _ = _fit_mass_point_2sls(d, dy, 0.5, None, "hc1") assert np.isnan(beta) assert np.isnan(se) @@ -814,6 +814,8 @@ def test_get_params_returns_all_constructor_args(self): vcov_type="hc1", robust=True, cluster="state", + n_bootstrap=500, + seed=42, ) params = est.get_params() assert params == { @@ -824,6 +826,8 @@ def test_get_params_returns_all_constructor_args(self): "vcov_type": "hc1", "robust": True, "cluster": "state", + "n_bootstrap": 500, + "seed": 42, } def test_clone_round_trip(self): @@ -3379,23 +3383,29 @@ def test_survey_and_weights_mutex(self): survey=sd, weights=row_w, ) - # ---------- Deferred paths ---------- + # ---------- Previously deferred paths (Phase 4.5 B supported) ---------- - def test_weights_on_mass_point_raises(self): + def test_mass_point_weights_smoke(self): + """Mass-point + uniform weights fits and is bit-parity with + unweighted (Phase 4.5 B).""" d, dy = _dgp_mass_point(500, seed=42) panel = _make_panel(d, dy) with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) - est = HeterogeneousAdoptionDiD(design="mass_point") - with pytest.raises(NotImplementedError, match="Phase 4.5 B"): - est.fit( - panel, "outcome", "dose", "period", "unit", - weights=np.ones(panel.shape[0]), - ) - - def test_weights_on_event_study_raises(self): - """Multi-period + event-study + weights → NotImplementedError - (Phase 4.5 B deferral).""" + est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1") + r_unw = est.fit(panel, "outcome", "dose", "period", "unit") + r_uniform = est.fit( + panel, "outcome", "dose", "period", "unit", + weights=np.ones(panel.shape[0]), + ) + assert np.isclose(r_unw.att, r_uniform.att, atol=1e-10) + assert np.isclose(r_unw.se, r_uniform.se, atol=1e-10) + assert r_uniform.variance_formula == "pweight_2sls" + + def test_event_study_weights_smoke(self): + """Multi-period + event-study + uniform weights fits and + preserves pre-PR numerical output on att/se at cband=False + (Phase 4.5 B).""" rng = np.random.default_rng(7) G = 150 d = rng.uniform(0, 1, G) @@ -3409,12 +3419,23 @@ def test_weights_on_event_study_raises(self): rows.append((g, t, dose, y)) panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) est = HeterogeneousAdoptionDiD() - with pytest.raises(NotImplementedError, match="Phase 4.5 B"): - est.fit( - panel, "outcome", "dose", "period", "unit", - aggregate="event_study", - weights=np.ones(panel.shape[0]), - ) + r_unw = est.fit( + panel, "outcome", "dose", "period", "unit", + aggregate="event_study", + ) + r_w = est.fit( + panel, "outcome", "dose", "period", "unit", + aggregate="event_study", + weights=np.ones(panel.shape[0]), + cband=False, # skip bootstrap to allow att/se bit-parity check + ) + # Uniform-weights + cband=False recovers the unweighted output at + # atol=1e-10 (composition through np.average introduces O(ULP) + # reductions differing from raw mean()). + np.testing.assert_allclose(r_unw.att, r_w.att, atol=1e-10, rtol=1e-10) + np.testing.assert_allclose(r_unw.se, r_w.se, atol=1e-10, rtol=1e-10) + assert r_w.variance_formula == "pweight" + assert r_w.cband_crit_value is None # cband=False # ---------- Result-object contract ---------- @@ -4262,3 +4283,373 @@ def test_survey_no_psu_no_strata_se_matches_weights_hc1(self): f"still uses the classical scale (V_Y_cl), the SE will be " f"materially smaller than the bias-corrected SE here." ) + + +# ============================================================================= +# Phase 4.5 B: mass-point weighted + event-study survey + sup-t bootstrap +# ============================================================================= + + +class TestMassPointWeighted: + """Weighted 2SLS on the mass-point path (Phase 4.5 B).""" + + @staticmethod + def _dgp_mp(n, seed=0): + rng = np.random.default_rng(seed) + d = np.concatenate([np.full(n // 5, 0.3), rng.uniform(0.3, 1.0, n - n // 5)]) + rng.shuffle(d) + dy = 2.0 * d + 0.3 * rng.standard_normal(n) + return d, dy + + @staticmethod + def _make_panel(d, dy): + G = d.shape[0] + return pd.DataFrame({ + "unit": np.repeat(np.arange(G), 2), + "period": np.tile([1, 2], G), + "dose": np.column_stack([np.zeros(G), d]).ravel(), + "outcome": np.column_stack([np.zeros(G), dy]).ravel(), + }) + + def test_uniform_weights_bit_parity_all_vcov_variants(self): + """Direct helper call: weights=np.ones ≡ unweighted at atol=1e-14 + across classical, hc1, and CR1 sandwich branches.""" + from diff_diff.had import _fit_mass_point_2sls + + d, dy = self._dgp_mp(400, seed=7) + cluster = np.arange(d.shape[0]) // 10 + for vcov in ("classical", "hc1"): + for use_cluster in (False, True): + cluster_arg = cluster if use_cluster else None + b0, s0, _ = _fit_mass_point_2sls(d, dy, 0.3, cluster_arg, vcov) + b1, s1, _ = _fit_mass_point_2sls( + d, dy, 0.3, cluster_arg, vcov, + weights=np.ones(d.shape[0]), return_influence=False, + ) + np.testing.assert_allclose(b0, b1, atol=1e-14, rtol=1e-14) + np.testing.assert_allclose(s0, s1, atol=1e-14, rtol=1e-14) + + def test_weights_none_path_unchanged(self): + """Unweighted path returns (beta, se, None) — third slot is None + when return_influence=False (the default).""" + from diff_diff.had import _fit_mass_point_2sls + + d, dy = self._dgp_mp(200, seed=1) + _b, _s, psi = _fit_mass_point_2sls(d, dy, 0.3, None, "hc1") + assert psi is None + + def test_negative_weights_rejected(self): + """Front-door reject negative weights with a clear ValueError.""" + from diff_diff.had import _fit_mass_point_2sls + + d, dy = self._dgp_mp(200, seed=2) + w = np.ones(d.shape[0]) + w[0] = -0.1 + with pytest.raises(ValueError, match="non-negative"): + _fit_mass_point_2sls(d, dy, 0.3, None, "hc1", weights=w) + + def test_non_finite_weights_rejected(self): + from diff_diff.had import _fit_mass_point_2sls + + d, dy = self._dgp_mp(200, seed=3) + w = np.ones(d.shape[0]) + w[0] = np.nan + with pytest.raises(ValueError, match="non-finite"): + _fit_mass_point_2sls(d, dy, 0.3, None, "hc1", weights=w) + + def test_zero_sum_weights_rejected(self): + from diff_diff.had import _fit_mass_point_2sls + + d, dy = self._dgp_mp(200, seed=4) + w = np.zeros(d.shape[0]) + with pytest.raises(ValueError, match="weights sum to zero"): + _fit_mass_point_2sls(d, dy, 0.3, None, "hc1", weights=w) + + def test_weights_length_mismatch_rejected(self): + from diff_diff.had import _fit_mass_point_2sls + + d, dy = self._dgp_mp(200, seed=5) + w = np.ones(d.shape[0] - 5) + with pytest.raises(ValueError, match="length"): + _fit_mass_point_2sls(d, dy, 0.3, None, "hc1", weights=w) + + def test_fit_mass_point_survey_variance_formula(self): + """`fit(design='mass_point', survey=...)` sets + variance_formula='survey_binder_tsl_2sls' and populates + survey_metadata with the full SurveyMetadata dataclass.""" + from diff_diff.survey import SurveyDesign + + d, dy = self._dgp_mp(300, seed=6) + panel = self._make_panel(d, dy) + panel["w"] = np.random.default_rng(0).uniform(0.5, 2.0, panel.shape[0]) + # Constant-within-unit for the aggregator. + panel["w"] = panel.groupby("unit")["w"].transform("first") + sd = SurveyDesign(weights="w") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1") + r = est.fit(panel, "outcome", "dose", "period", "unit", survey=sd) + assert r.variance_formula == "survey_binder_tsl_2sls" + assert r.survey_metadata is not None + assert r.survey_metadata.weight_type == "pweight" + assert r.effective_dose_mean is not None + + def test_fit_mass_point_weights_shortcut_variance_formula(self): + """`fit(design='mass_point', weights=...)` shortcut sets + variance_formula='pweight_2sls' and clears survey-only metadata.""" + d, dy = self._dgp_mp(300, seed=7) + panel = self._make_panel(d, dy) + # Per-unit weights (constant within unit, as HAD requires). + rng = np.random.default_rng(0) + w_per_unit = rng.uniform(0.5, 2.0, d.shape[0]) + w = panel["unit"].map(lambda g: w_per_unit[g]).to_numpy() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1") + r = est.fit( + panel, "outcome", "dose", "period", "unit", weights=w, + ) + assert r.variance_formula == "pweight_2sls" + assert r.survey_metadata is not None + # Weights= shortcut clears survey-only fields (inference is Normal). + assert r.survey_metadata.n_psu is None + assert r.survey_metadata.n_strata is None + assert r.survey_metadata.df_survey is None + + def test_mass_point_non_pweight_rejected(self): + """Non-pweight SurveyDesigns rejected at fit() with a clear + NotImplementedError — mirrors static continuous path.""" + from diff_diff.survey import SurveyDesign + + d, dy = self._dgp_mp(200, seed=8) + panel = self._make_panel(d, dy) + panel["w"] = np.ones(panel.shape[0]) + sd = SurveyDesign(weights="w", weight_type="aweight") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point") + with pytest.raises(NotImplementedError, match="aweight"): + est.fit(panel, "outcome", "dose", "period", "unit", survey=sd) + + +class TestSupTReducesToNormalAtH1: + """At H=1 the sup-t critical value must reduce to the Normal + quantile (up to MC error). Catches the (1/n) prefactor / scale- + convention drift in _sup_t_multiplier_bootstrap in isolation from + the full event-study pipeline.""" + + def test_sup_t_h1_reduces_to_normal_quantile(self): + import scipy.stats + from diff_diff.had import _sup_t_multiplier_bootstrap + + rng = np.random.default_rng(42) + G = 500 + # Well-scaled IF under unit-level iid multipliers: any i.i.d. psi + # with bounded variance. Analytical "SE" = sqrt(sum(psi^2)) since + # Var_xi(sum(xi * psi)) = sum(psi^2) under Rademacher xi ∈ {-1,+1}. + psi = rng.standard_normal((G, 1)) + se = np.array([float(np.sqrt(np.sum(psi[:, 0] ** 2)))]) + q, _low, _high, n_valid = _sup_t_multiplier_bootstrap( + psi, + np.zeros(1), + se, + None, + n_bootstrap=5000, + alpha=0.05, + seed=42, + ) + assert n_valid >= 4500 # almost all draws finite + expected = float(scipy.stats.norm.ppf(0.975)) + # B=5000 MC noise on the 97.5%-tile quantile is ~0.03-0.05. + assert abs(q - expected) < 0.15, ( + f"H=1 sup-t quantile should reduce to Phi^-1(0.975)={expected:.4f}; " + f"got q={q:.4f}. |diff|={abs(q - expected):.4f}. Likely a " + f"scale-convention drift in _sup_t_multiplier_bootstrap (check " + f"that perturbations = weights @ psi has no (1/n) prefactor and " + f"that sum(psi^2) matches the claimed 'analytical' variance)." + ) + + def test_sup_t_h5_greater_than_pointwise(self): + """At H=5 with i.i.d. psi, sup-t > 1.96. Catches degenerate + constructions where sup collapses to the marginal.""" + from diff_diff.had import _sup_t_multiplier_bootstrap + + rng = np.random.default_rng(0) + G = 500 + H = 5 + psi = rng.standard_normal((G, H)) + se = np.sqrt(np.sum(psi**2, axis=0)) + q, _, _, _ = _sup_t_multiplier_bootstrap( + psi, np.zeros(H), se, None, + n_bootstrap=1000, alpha=0.05, seed=42, + ) + assert q > 1.96 + 0.15, ( + f"H=5 sup-t should exceed pointwise Normal quantile by a " + f"material margin; got q={q:.4f}." + ) + + def test_sup_t_seed_reproducibility(self): + """Same seed → same critical value (across repeated calls).""" + from diff_diff.had import _sup_t_multiplier_bootstrap + + rng = np.random.default_rng(0) + G = 200 + H = 3 + psi = rng.standard_normal((G, H)) + se = np.sqrt(np.sum(psi**2, axis=0)) + q1, _, _, _ = _sup_t_multiplier_bootstrap( + psi, np.zeros(H), se, None, + n_bootstrap=500, alpha=0.05, seed=17, + ) + q2, _, _, _ = _sup_t_multiplier_bootstrap( + psi, np.zeros(H), se, None, + n_bootstrap=500, alpha=0.05, seed=17, + ) + assert q1 == q2 + + +class TestEventStudySurveyCband: + """Event-study + weights / survey + sup-t cband scope (Phase 4.5 B).""" + + @staticmethod + def _multi_period_panel(G=150, T=4, seed=0): + rng = np.random.default_rng(seed) + d_post = rng.uniform(0.0, 1.0, G) + rows = [] + for t in range(T): + for g in range(G): + dose = d_post[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) + return panel + + def test_unweighted_es_cband_fields_none(self): + """Unweighted event-study: all cband_* fields are None (pre-PR + numerical output preserved).""" + panel = self._multi_period_panel(G=200) + est = HeterogeneousAdoptionDiD(design="continuous_at_zero", seed=0) + r = est.fit(panel, "outcome", "dose", "period", "unit", aggregate="event_study") + assert r.cband_low is None + assert r.cband_high is None + assert r.cband_crit_value is None + assert r.cband_method is None + assert r.variance_formula is None + + def test_weighted_es_cband_false_skips_bootstrap(self): + """`cband=False` under weighted event-study: no bootstrap, cband_* + fields are None; att/se bit-exact to unweighted at uniform + weights.""" + panel = self._multi_period_panel(G=200, seed=3) + est = HeterogeneousAdoptionDiD(design="continuous_at_zero", seed=0) + r = est.fit( + panel, "outcome", "dose", "period", "unit", + aggregate="event_study", + weights=np.ones(panel.shape[0]), + cband=False, + ) + assert r.cband_low is None + assert r.cband_high is None + assert r.cband_crit_value is None + # variance_formula IS set (pweight shortcut active). + assert r.variance_formula == "pweight" + + def test_weighted_es_cband_true_populates_band(self): + """Weighted event-study + cband=True populates cband_* fields, + with cband_crit_value in a plausible range.""" + panel = self._multi_period_panel(G=200, seed=5) + est = HeterogeneousAdoptionDiD( + design="continuous_at_zero", seed=42, n_bootstrap=500, + ) + r = est.fit( + panel, "outcome", "dose", "period", "unit", + aggregate="event_study", + weights=np.ones(panel.shape[0]), + cband=True, + ) + assert r.cband_low is not None and r.cband_high is not None + assert r.cband_crit_value is not None and np.isfinite(r.cband_crit_value) + assert r.cband_method == "multiplier_bootstrap" + assert r.cband_n_bootstrap == 500 + # Sup-t should be >= pointwise Normal quantile (1.96) to cover + # all horizons simultaneously. + assert r.cband_crit_value >= 1.5 # loose lower bound given MC noise + # Band strictly wider than pointwise CI (centered on att). + for i in range(len(r.event_times)): + if np.isfinite(r.se[i]) and r.se[i] > 0: + pointwise_width = r.conf_int_high[i] - r.conf_int_low[i] + sim_width = r.cband_high[i] - r.cband_low[i] + assert sim_width >= pointwise_width * 0.99 # allow tiny MC slack + + def test_event_study_filter_info_stable_across_weight_patterns(self): + """filter_info is identical whether the fit is unweighted, + uniform-weighted, or informatively-weighted (staggered-filter + is identification-theory, not sampling-domain).""" + rng = np.random.default_rng(0) + G = 120 + # Staggered panel: cohort 3 and cohort 4. + d_post = rng.uniform(0.0, 1.0, G) + first_treat = rng.choice([0, 3, 4], size=G, p=[0.4, 0.3, 0.3]) + rows = [] + for t in range(5): + for g in range(G): + ft = first_treat[g] + dose = d_post[g] if (ft > 0 and t >= ft) else 0.0 + y = 0.2 * t + (2.0 * dose if dose > 0 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y, ft)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome", "first_treat"]) + + est = HeterogeneousAdoptionDiD(design="continuous_at_zero") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + r_unw = est.fit( + panel, "outcome", "dose", "period", "unit", + aggregate="event_study", first_treat_col="first_treat", + ) + r_uni = est.fit( + panel, "outcome", "dose", "period", "unit", + aggregate="event_study", first_treat_col="first_treat", + weights=np.ones(panel.shape[0]), cband=False, + ) + # Informative per-row weights (constant within unit). + w_unit = 1.0 + 0.5 * rng.standard_normal(G) + w_unit = np.clip(w_unit, 0.1, None) + w_row = panel["unit"].map(lambda g: w_unit[g]).to_numpy() + r_inf = est.fit( + panel, "outcome", "dose", "period", "unit", + aggregate="event_study", first_treat_col="first_treat", + weights=w_row, cband=False, + ) + # filter_info must agree across all three fits (same dropped cohorts). + assert r_unw.filter_info == r_uni.filter_info == r_inf.filter_info + + def test_event_study_mass_point_weighted_smoke(self): + """Mass-point + weighted event-study smoke: variance_formula = + 'pweight_2sls' and cband populated.""" + rng = np.random.default_rng(10) + G = 200 + T = 4 + d_mp = np.concatenate( + [np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)] + ) + rng.shuffle(d_mp) + rows = [] + for t in range(T): + for g in range(G): + dose = d_mp[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD( + design="mass_point", vcov_type="hc1", seed=0, n_bootstrap=200, + ) + r = est.fit( + panel, "outcome", "dose", "period", "unit", + aggregate="event_study", + weights=np.ones(panel.shape[0]), + ) + assert r.design == "mass_point" + assert r.variance_formula == "pweight_2sls" + assert r.cband_crit_value is not None and np.isfinite(r.cband_crit_value) From 082368a5e8982e78f780425cf4885397dd14534d Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 16:56:43 -0400 Subject: [PATCH 2/9] Address PR #363 R1 review (1 P0 + 2 P1 + 1 P3) R1 P0 (methodology): cband_low/high now NaN-gated for horizons with se <= 0 or non-finite, matching the safe_inference contract for pointwise output. Previously a horizon with se=0 produced a finite band equal to the point estimate, overstating precision. R1 P1 (methodology): sup-t multiplier bootstrap now applies stratum-centered + small-sample-corrected PSU aggregation before the matmul, so Var_xi(xi @ Psi_psu_scaled) reproduces the analytical Binder-TSL variance term-for-term (V = sum_h (1-f_h)(n_h/(n_h-1)) sum_j (psi_hj - psi_h_bar)^2). Previously the bootstrap omitted centering and the small-sample correction; under stratified designs the critical value diverged from the analytical target. Verified at H=1 G=400 n_strata=4: bootstrap q=1.985 matches Phi^-1(0.975)=1.960. R1 P1 (code quality): event-study weights= filtering now translates data_filtered.index LABELS to POSITIONAL offsets via data.index.get_indexer, so non-RangeIndex inputs work correctly. Previous code treated index labels as positions and silently broke on custom int / string indices. Raises a clear ValueError when index alignment fails (duplicate or malformed labels). R1 P3 (docs): HeterogeneousAdoptionDiDEventStudyResults docstring updated to describe Phase 4.5 B weighted/survey support + new cband_* fields + variance_formula / effective_dose_mean; fit() docstring updated to reflect the Phase 4.5 B dispatch matrix and document the new cband kwarg. Regression tests: - test_zero_se_horizon_nan_gates_cband locks the P0 NaN gate - test_weights_nonrange_index_aligned_positionally locks the P1 row-order contract across RangeIndex vs custom int indices - Existing TestSupTReducesToNormalAtH1 covers the stratum-centered path via the unit-PSU reduction case Co-Authored-By: Claude Opus 4.7 (1M context) --- diff_diff/had.py | 204 +++++++++++++++++---- tests/test_had.py | 453 +++++++++++++++++++++++++++++++++------------- 2 files changed, 491 insertions(+), 166 deletions(-) diff --git a/diff_diff/had.py b/diff_diff/had.py index 9ffef4ff..d6213d33 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -537,12 +537,18 @@ class HeterogeneousAdoptionDiDEventStudyResults: :class:`HeterogeneousAdoptionDiDResults.att` for the per-design formula, applied to ``ΔY_t = Y_{g,t} - Y_{g,F-1}``). se : np.ndarray, shape (n_horizons,) - Per-horizon standard error on the beta-scale. Each horizon uses - the INDEPENDENT per-period sandwich from the chosen design path - (continuous: CCT-2014 robust divided by ``|den|``; mass-point: - structural-residual 2SLS sandwich). Pointwise CIs only — joint - cross-horizon covariance is not computed in Phase 2b (paper - reports pointwise CIs per Pierce-Schott). + Per-horizon standard error on the beta-scale. On unweighted fits + each horizon uses the INDEPENDENT per-period sandwich from the + chosen design path (continuous: CCT-2014 robust divided by + ``|den|``; mass-point: structural-residual 2SLS sandwich). On + weighted fits (``weights=`` shortcut or ``survey=``) each horizon + uses the Binder (1983) Taylor-series linearization via + :func:`compute_survey_if_variance` on the per-unit β̂-scale IF + (continuous + mass-point both route through the same helper). + Pointwise CIs are always populated; a simultaneous confidence + band is available only on the weighted path via ``cband_*`` + below. Joint cross-horizon analytical covariance is not computed + in this release (tracked in TODO.md). t_stat, p_value : np.ndarray, shape (n_horizons,) Per-horizon inference triple element. conf_int_low, conf_int_high : np.ndarray, shape (n_horizons,) @@ -587,8 +593,44 @@ class HeterogeneousAdoptionDiDEventStudyResults: cluster_name : str or None Column name of the cluster variable when cluster-robust SE is requested. ``None`` otherwise. - survey_metadata : object or None - Always ``None`` in Phase 2b. Field shape kept for future-compat. + survey_metadata : SurveyMetadata or None + Repo-standard survey metadata dataclass from + :class:`diff_diff.survey.SurveyMetadata`. ``None`` when + ``fit()`` was called without ``survey=`` or ``weights=``; + populated on the weighted event-study path (Phase 4.5 B). See + :class:`HeterogeneousAdoptionDiDResults.survey_metadata` for + the attribute contract. + variance_formula : str or None + Per-horizon variance family (applied uniformly across horizons). + ``"pweight"`` / ``"pweight_2sls"`` on the ``weights=`` shortcut, + ``"survey_binder_tsl"`` / ``"survey_binder_tsl_2sls"`` on the + ``survey=`` path. ``None`` on unweighted fits. + effective_dose_mean : float or None + Weighted denominator used by the β̂-scale rescaling (continuous + paths: weighted sample mean of ``d`` or ``d - d_lower``; + mass-point: weighted Wald-IV dose gap). ``None`` on unweighted + fits. + cband_low, cband_high : np.ndarray or None, shape (n_horizons,) + Simultaneous confidence-band endpoints constructed by the + multiplier-bootstrap sup-t procedure. ``None`` on unweighted + fits and when ``fit(..., cband=False)`` is passed. Horizons + with ``se <= 0`` or non-finite ``se`` are NaN (matches the + pointwise inference gate from ``safe_inference``). + cband_crit_value : float or None + Sup-t multiplier-bootstrap critical value at level + ``1 - alpha``. Under a trivial resolved design (no strata / + PSU / FPC) at ``H=1`` reduces to ``Φ⁻¹(1 − alpha/2) ≈ 1.96`` + up to Monte Carlo error; under stratified designs the helper + applies PSU-aggregation + stratum-demeaning + ``sqrt(n_h / + (n_h - 1))`` small-sample correction so the bootstrap + variance matches the analytical Binder-TSL target term-for- + term. + cband_method : str or None + ``"multiplier_bootstrap"`` on the weighted event-study path + with ``cband=True``, else ``None``. + cband_n_bootstrap : int or None + Number of multiplier-bootstrap replicates used to compute the + sup-t critical value. bandwidth_diagnostics : list[BandwidthResult] or None Per-horizon bandwidth diagnostics on the continuous paths; ``None`` on the mass-point path. When non-None, aligned with @@ -2023,27 +2065,79 @@ def _sup_t_multiplier_bootstrap( psu_weights, psu_ids = generate_survey_multiplier_weights_batch( n_bootstrap, resolved_survey, bootstrap_weights, rng ) + # Aggregate Psi to PSU level, stratum-demean, and apply the + # small-sample correction so Var_xi(xi @ Psi_psu_scaled) matches + # the analytical Binder-TSL variance exactly (review R1 P1). + # Target: + # V = sum_h (1 - f_h) (n_h / (n_h - 1)) sum_j (psi_hj - psi_h_bar)² + # ``generate_survey_multiplier_weights_batch`` already bakes the + # (1 - f_h) FPC factor into the multipliers, so we only need to + # pre-process Psi at the PSU level (aggregate → stratum-demean → + # sqrt(n_h / (n_h - 1)) rescale). + n_psu = int(psu_weights.shape[1]) + psu_id_to_col = {int(p): c for c, p in enumerate(psu_ids)} + Psi_psu = np.zeros((n_psu, n_horizons), dtype=np.float64) if resolved_survey.psu is not None: - unit_psu = resolved_survey.psu - psu_id_to_col = {int(p): c for c, p in enumerate(psu_ids)} - unit_to_psu_col = np.array([psu_id_to_col[int(unit_psu[i])] for i in range(n_units)]) + unit_psu = np.asarray(resolved_survey.psu) + for i in range(n_units): + col = psu_id_to_col[int(unit_psu[i])] + Psi_psu[col] += influence_matrix[i] + else: + # Each unit is its own PSU (psu_ids = np.arange(n_units)). + Psi_psu = influence_matrix.copy() + + if resolved_survey.strata is not None: + strata = np.asarray(resolved_survey.strata) + # Build PSU -> stratum map (strata constant-within-PSU by + # SurveyDesign.resolve contract). + psu_stratum = np.empty(n_psu, dtype=strata.dtype) + if resolved_survey.psu is not None: + seen = np.zeros(n_psu, dtype=bool) + unit_psu = np.asarray(resolved_survey.psu) + for i in range(n_units): + col = psu_id_to_col[int(unit_psu[i])] + if not seen[col]: + psu_stratum[col] = strata[i] + seen[col] = True + else: + psu_stratum = strata.copy() + + for h in np.unique(psu_stratum): + mask_h = psu_stratum == h + n_h = int(mask_h.sum()) + if n_h < 2: + # Singleton / empty stratum contributes 0 variance + # regardless; the helper's lonely-PSU logic already + # zeros those multipliers. Skip centering to avoid + # a divide-by-zero on sqrt(n_h / (n_h - 1)). + continue + Psi_psu[mask_h] -= Psi_psu[mask_h].mean(axis=0, keepdims=True) + Psi_psu[mask_h] *= np.sqrt(n_h / (n_h - 1)) else: - unit_to_psu_col = np.arange(n_units) - all_bootstrap_weights = psu_weights[:, unit_to_psu_col] # (B, G) + # Single implicit stratum — demean across all PSUs, scale by + # sqrt(n_psu / (n_psu - 1)). + if n_psu >= 2: + Psi_psu -= Psi_psu.mean(axis=0, keepdims=True) + Psi_psu *= np.sqrt(n_psu / (n_psu - 1)) + + # PSU-level perturbations: (B, H) = (B, n_psu) @ (n_psu, H). + # No (1/n) prefactor — Psi_psu_scaled is already on the θ̂-scale + # matched to the analytical variance. + with np.errstate(divide="ignore", invalid="ignore", over="ignore"): + perturbations = psu_weights @ Psi_psu # (B, H) else: all_bootstrap_weights = generate_bootstrap_weights_batch( n_bootstrap, n_units, bootstrap_weights, rng ) # (B, G) - - # Perturbations: (B, H) = (B, G) @ (G, H). Matches staggered:373 - # idiom — no (1/n) prefactor; ``psi`` is already on θ̂-scale. - # Silence divide/invalid/overflow warnings from the matmul — NaN / - # inf rows from degenerate horizons propagate and are filtered by - # the finite-mask below, so these are expected at construction time. + # Unit-level iid multipliers: no stratum centering needed. + # Var(xi @ Psi) = sum_g psi_g² matches the trivial analytical + # variance from compute_survey_if_variance at the IF-scale- + # invariant tolerance (PR #359 convention). + with np.errstate(divide="ignore", invalid="ignore", over="ignore"): + perturbations = all_bootstrap_weights @ influence_matrix # (B, H) + + # t-statistics via per-horizon analytical SE. with np.errstate(divide="ignore", invalid="ignore", over="ignore"): - perturbations = all_bootstrap_weights @ influence_matrix # (B, H) - - # t-statistics via per-horizon analytical SE. safe_se = np.where( (se_per_horizon > 0) & np.isfinite(se_per_horizon), se_per_horizon, @@ -2074,8 +2168,13 @@ def _sup_t_multiplier_bootstrap( return float("nan"), None, None, n_valid q = float(np.quantile(sup_t_dist[finite_mask], 1.0 - alpha)) - cband_low = att_per_horizon - q * se_per_horizon - cband_high = att_per_horizon + q * se_per_horizon + # NaN-gate simultaneous-band endpoints for degenerate horizons the + # same way ``safe_inference`` gates pointwise output: a horizon with + # ``se <= 0`` or non-finite ``se`` gets a NaN band instead of the + # point estimate ± 0, avoiding misleading precision (review R1 P0). + se_valid_mask = (se_per_horizon > 0) & np.isfinite(se_per_horizon) + cband_low = np.where(se_valid_mask, att_per_horizon - q * se_per_horizon, np.nan) + cband_high = np.where(se_valid_mask, att_per_horizon + q * se_per_horizon, np.nan) return q, cband_low, cband_high, n_valid @@ -2693,17 +2792,36 @@ def fit( FPC) must be constant within unit (sampling-unit-level assignment); within-unit variance raises ``ValueError``. Replicate-weight designs raise ``NotImplementedError`` - (Phase 4.5 C). ``design="mass_point"`` and - ``aggregate="event_study"`` raise ``NotImplementedError`` on - survey/weights (Phase 4.5 B). + (Phase 4.5 C). Phase 4.5 B support matrix: survey / weights + are now accepted on ALL design × aggregate combinations + (continuous × {overall, event-study}, mass-point × {overall, + event-study}); HAD pretests (``qug_test``, ``stute_test``, + ``yatchew_hr_test``, joint variants, + ``did_had_pretest_workflow``) still don't accept + survey/weights — deferred to Phase 4.5 C / C0. weights : np.ndarray or None Per-row sampling weights as a lightweight shortcut equivalent to ``survey=SurveyDesign(weights=)``. Produces the same - ATT; the SE uses lprobust's weighted-robust CCT-2014 formula - rather than Binder-TSL (no PSU/strata composition). Mutually + ATT; the SE uses the analytical weighted HC1 sandwich + (continuous: CCT-2014 weighted-robust; mass-point: pweight + 2SLS sandwich) rather than Binder-TSL. Must be constant + within each unit; row-order aligned with ``data`` (index + labels are resolved to positional offsets via + ``data.index.get_indexer``, so custom non-RangeIndex inputs + work as long as ``data.index`` is unique). Mutually exclusive with ``survey=`` — passing both raises - ``ValueError``. Must be constant within each unit (same - invariant as ``survey=``). + ``ValueError``. + cband : bool, default True + Phase 4.5 B: controls the multiplier-bootstrap simultaneous + confidence band on the weighted event-study path. When + ``True`` (default) and ``aggregate="event_study"`` AND + ``weights=`` or ``survey=`` is supplied, the fit populates + ``cband_low`` / ``cband_high`` / ``cband_crit_value`` / + ``cband_method`` / ``cband_n_bootstrap`` on the result. When + ``False`` those fields stay ``None``. No effect on + ``aggregate="overall"`` or on unweighted event-study. + ``n_bootstrap`` and ``seed`` (constructor params) control + replicate count and RNG; defaults are 999 / ``None``. Returns ------- @@ -3573,11 +3691,25 @@ def _fit_event_study( f"weights length ({w_full.shape[0]}) does not match " f"data rows ({int(data.shape[0])})." ) - # Filter to rows surviving the staggered last-cohort filter. - # data_filtered.index is the original integer positional index - # into `data`; use positional slicing via `.iloc` elsewhere, - # but here `.index` carries the row labels to match. - w_filtered = w_full[data_filtered.index.to_numpy()] + # Public ``weights`` contract is ROW-ORDER aligned with + # ``data``, NOT index-label aligned, so we must translate + # ``data_filtered``'s surviving index LABELS back to + # POSITIONAL offsets via ``data.index.get_indexer`` (handles + # custom int, string, or MultiIndex inputs uniformly; raises + # on duplicate labels that would make the mapping ambiguous). + # Review R1 P1: using ``data_filtered.index.to_numpy()`` as + # positions was a silent-failure vector on non-RangeIndex + # inputs. + positional_idx = data.index.get_indexer(data_filtered.index) + if np.any(positional_idx < 0): + raise ValueError( + "Cannot align weights to filtered panel: some " + "data_filtered rows could not be located in the " + "original data.index (possible duplicate / malformed " + "index labels). Pass a DataFrame with a unique index " + "or reset the index before calling fit()." + ) + w_filtered = w_full[positional_idx] weights_unit = _aggregate_unit_weights(data_filtered, w_filtered, unit_col) raw_weights_unit = weights_unit elif survey is not None: diff --git a/tests/test_had.py b/tests/test_had.py index 2721c570..1aef7ae6 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -3264,19 +3264,19 @@ def test_uniform_weights_continuous_at_zero_bit_parity(self): est = HeterogeneousAdoptionDiD(design="continuous_at_zero") base = est.fit(panel, "outcome", "dose", "period", "unit") w1 = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", weights=np.ones(panel.shape[0]), ) np.testing.assert_allclose(w1.att, base.att, atol=1e-12, rtol=1e-12) np.testing.assert_allclose(w1.se, base.se, atol=1e-12, rtol=1e-12) np.testing.assert_allclose(w1.t_stat, base.t_stat, atol=1e-12, rtol=1e-12) np.testing.assert_allclose(w1.p_value, base.p_value, atol=1e-12, rtol=1e-12) - np.testing.assert_allclose( - w1.conf_int[0], base.conf_int[0], atol=1e-12, rtol=1e-12 - ) - np.testing.assert_allclose( - w1.conf_int[1], base.conf_int[1], atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(w1.conf_int[0], base.conf_int[0], atol=1e-12, rtol=1e-12) + np.testing.assert_allclose(w1.conf_int[1], base.conf_int[1], atol=1e-12, rtol=1e-12) def test_uniform_weights_continuous_near_d_lower_bit_parity(self): with warnings.catch_warnings(): @@ -3287,7 +3287,11 @@ def test_uniform_weights_continuous_near_d_lower_bit_parity(self): est = HeterogeneousAdoptionDiD(design="continuous_near_d_lower") base = est.fit(panel, "outcome", "dose", "period", "unit") w1 = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", weights=np.ones(panel.shape[0]), ) np.testing.assert_allclose(w1.att, base.att, atol=1e-12, rtol=1e-12) @@ -3357,7 +3361,11 @@ def test_zero_sum_weights_raise(self): est = HeterogeneousAdoptionDiD(design="continuous_at_zero") with pytest.raises(ValueError, match="sum to zero"): est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", weights=np.zeros(panel.shape[0]), ) @@ -3366,7 +3374,11 @@ def test_weights_length_mismatch_raises(self): est = HeterogeneousAdoptionDiD(design="continuous_at_zero") with pytest.raises(ValueError, match="length"): est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", weights=np.ones(panel.shape[0] + 1), ) @@ -3379,8 +3391,13 @@ def test_survey_and_weights_mutex(self): est = HeterogeneousAdoptionDiD(design="continuous_at_zero") with pytest.raises(ValueError, match="OR weights"): est.fit( - panel_with_w, "outcome", "dose", "period", "unit", - survey=sd, weights=row_w, + panel_with_w, + "outcome", + "dose", + "period", + "unit", + survey=sd, + weights=row_w, ) # ---------- Previously deferred paths (Phase 4.5 B supported) ---------- @@ -3395,7 +3412,11 @@ def test_mass_point_weights_smoke(self): est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1") r_unw = est.fit(panel, "outcome", "dose", "period", "unit") r_uniform = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", weights=np.ones(panel.shape[0]), ) assert np.isclose(r_unw.att, r_uniform.att, atol=1e-10) @@ -3420,11 +3441,19 @@ def test_event_study_weights_smoke(self): panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) est = HeterogeneousAdoptionDiD() r_unw = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", aggregate="event_study", ) r_w = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", aggregate="event_study", weights=np.ones(panel.shape[0]), cband=False, # skip bootstrap to allow att/se bit-parity check @@ -3517,11 +3546,19 @@ def test_survey_with_strata_produces_different_se(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) r_basic = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w"), ) r_strat = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w", strata="strata"), ) np.testing.assert_allclose(r_basic.att, r_strat.att, atol=1e-14, rtol=1e-14) @@ -3537,11 +3574,19 @@ def test_survey_with_psu_clustering(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) r_strat = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w", strata="strata"), ) r_psu = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w", strata="strata", psu="psu"), ) np.testing.assert_allclose(r_strat.att, r_psu.att, atol=1e-14, rtol=1e-14) @@ -3557,7 +3602,11 @@ def test_survey_metadata_records_binder_tsl_method(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) r = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w", strata="strata", psu="psu"), ) sm = r.survey_metadata @@ -3584,7 +3633,11 @@ def test_survey_design_column_varies_within_unit_raises(self): warnings.simplefilter("ignore", UserWarning) with pytest.raises(ValueError, match="strata varies within"): est.fit( - panel_bad, "outcome", "dose", "period", "unit", + panel_bad, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w", strata="strata"), ) @@ -3631,23 +3684,17 @@ def test_survey_if_uses_bias_corrected_scale(self): # Nonlinear m(d) = 2d + 0.8 d² — the 0.8 quadratic term drives a # nontrivial bias correction so V_Y_bc != V_Y_cl. y = 2.0 * x + 0.8 * x**2 + rng.normal(0.0, 0.25, n) - r = lprobust( - y, x, eval_point=0.0, h=0.3, b=0.3, vce="hc1", return_influence=True - ) + r = lprobust(y, x, eval_point=0.0, h=0.3, b=0.3, vce="hc1", return_influence=True) sum_if_sq = float((r.influence_function**2).sum()) # Bias-corrected scale: sum(IF^2) should equal V_Y_bc[0,0] to # floating-point precision. NOT equal to V_Y_cl[0,0]. - np.testing.assert_allclose( - sum_if_sq, r.V_Y_bc[0, 0], atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(sum_if_sq, r.V_Y_bc[0, 0], atol=1e-12, rtol=1e-12) # The classical SE is DIFFERENT from the bias-corrected SE under # nonlinear m(d) — the two differ by the bias-correction inflation. assert not np.isclose( r.V_Y_cl[0, 0], r.V_Y_bc[0, 0], atol=0.0, rtol=1e-6 ), "DGP chosen to drive V_Y_cl != V_Y_bc; check nonlinearity" - assert not np.isclose( - sum_if_sq, r.V_Y_cl[0, 0], atol=0.0, rtol=1e-6 - ), ( + assert not np.isclose(sum_if_sq, r.V_Y_cl[0, 0], atol=0.0, rtol=1e-6), ( "sum(IF^2) must track V_Y_bc (not V_Y_cl) — if this fails, " "the IF is computed with classical res_h instead of " "bias-corrected res_b, silently underestimating survey SE." @@ -3786,14 +3833,10 @@ def test_effective_dose_mean_matches_weighted_mean_continuous_at_zero(self): r = est.fit(panel, "outcome", "dose", "period", "unit", weights=row_w) assert r.effective_dose_mean is not None expected = float(np.average(d, weights=w_unit)) - np.testing.assert_allclose( - r.effective_dose_mean, expected, atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(r.effective_dose_mean, expected, atol=1e-12, rtol=1e-12) # dose_mean stays as raw-sample mean — orthogonal to the # weighted denominator actually used in the fit. - np.testing.assert_allclose( - r.dose_mean, float(d.mean()), atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(r.dose_mean, float(d.mean()), atol=1e-12, rtol=1e-12) def test_effective_dose_mean_matches_weighted_mean_near_d_lower(self): """For ``continuous_near_d_lower``, the estimator auto-resolves @@ -3811,9 +3854,7 @@ def test_effective_dose_mean_matches_weighted_mean_near_d_lower(self): # Use the estimator's auto-resolved d_lower (== d.min()), not the # DGP's theoretical lower bound. expected = float(np.average(d - r.d_lower, weights=w_unit)) - np.testing.assert_allclose( - r.effective_dose_mean, expected, atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(r.effective_dose_mean, expected, atol=1e-12, rtol=1e-12) def test_effective_dose_mean_none_when_unweighted(self): """On unweighted fits, ``effective_dose_mean`` is ``None`` — @@ -3849,9 +3890,7 @@ def test_zero_weight_unit_at_d_min_does_not_flip_design(self): warnings.simplefilter("ignore", UserWarning) # Full panel with zero-weight unit at d=0: auto-detect. est = HeterogeneousAdoptionDiD(design="auto") - r_full = est.fit( - panel, "outcome", "dose", "period", "unit", weights=row_w - ) + r_full = est.fit(panel, "outcome", "dose", "period", "unit", weights=row_w) # Physically drop the zero-weight unit and refit. panel_dropped = panel[panel["unit"] != 0].reset_index(drop=True) w_dropped = row_w[panel["unit"].to_numpy() != 0] @@ -3874,9 +3913,7 @@ def test_zero_weight_unit_at_d_min_does_not_flip_design(self): # d_lower set by the positive-weight subpopulation (d.min() of # the kept units), NOT the contaminated full d.min()=0. assert r_full.d_lower > 0.0 - np.testing.assert_allclose( - r_full.d_lower, r_dropped.d_lower, atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(r_full.d_lower, r_dropped.d_lower, atol=1e-12, rtol=1e-12) def test_zero_weight_filter_warns_user(self): """Dropping zero-weight units from design resolution should @@ -3894,9 +3931,7 @@ def test_zero_weight_filter_warns_user(self): row_w[panel["unit"].to_numpy() == g] = w_unit[g] est = HeterogeneousAdoptionDiD(design="continuous_at_zero") with pytest.warns(UserWarning, match="weight == 0"): - est.fit( - panel, "outcome", "dose", "period", "unit", weights=row_w - ) + est.fit(panel, "outcome", "dose", "period", "unit", weights=row_w) def test_zero_weight_survey_metadata_preserves_full_design(self): """Round 6 P1a: on the ``survey=`` path, zero-weight units @@ -3932,19 +3967,25 @@ def test_zero_weight_survey_metadata_preserves_full_design(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) r_full = est.fit( - panel_sd, "outcome", "dose", "period", "unit", + panel_sd, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w", strata="strata", psu="psu"), ) # Reference fit: physically drop the zero-weight units and # refit on the positive-weight subsample. SurveyMetadata # values SHOULD DIFFER because dropping loses sampling frame # structure. - keep_rows = panel_sd["unit"].isin( - [g for g in range(G) if w_unit[g] > 0] - ) + keep_rows = panel_sd["unit"].isin([g for g in range(G) if w_unit[g] > 0]) panel_sub = panel_sd.loc[keep_rows].reset_index(drop=True) r_sub = est.fit( - panel_sub, "outcome", "dose", "period", "unit", + panel_sub, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w", strata="strata", psu="psu"), ) # Point estimate IDENTICAL — zero-weight units contribute 0 to @@ -3984,17 +4025,27 @@ def test_bias_corrected_local_linear_zero_weight_matches_filtered(self): y_ref = y_full[w_full > 0] # Explicit-h/b mode: r_weighted = bias_corrected_local_linear( - d=d_full, y=y_full, boundary=float(d_pos.min()), - h=0.3, b=0.3, weights=w_full, return_influence=True, + d=d_full, + y=y_full, + boundary=float(d_pos.min()), + h=0.3, + b=0.3, + weights=w_full, + return_influence=True, ) r_dropped = bias_corrected_local_linear( - d=d_ref, y=y_ref, boundary=float(d_pos.min()), - h=0.3, b=0.3, return_influence=True, + d=d_ref, + y=y_ref, + boundary=float(d_pos.min()), + h=0.3, + b=0.3, + return_influence=True, ) np.testing.assert_allclose( r_weighted.estimate_bias_corrected, r_dropped.estimate_bias_corrected, - atol=1e-12, rtol=1e-12, + atol=1e-12, + rtol=1e-12, ) np.testing.assert_allclose( r_weighted.se_robust, r_dropped.se_robust, atol=1e-12, rtol=1e-12 @@ -4003,14 +4054,13 @@ def test_bias_corrected_local_linear_zero_weight_matches_filtered(self): assert r_weighted.influence_function is not None assert r_weighted.influence_function.shape[0] == G # Zero-weight unit at index 0 has IF=0. - np.testing.assert_allclose( - r_weighted.influence_function[0], 0.0, atol=1e-14, rtol=1e-14 - ) + np.testing.assert_allclose(r_weighted.influence_function[0], 0.0, atol=1e-14, rtol=1e-14) # Positive-weight positions match the dropped-sample IF. np.testing.assert_allclose( r_weighted.influence_function[1:], r_dropped.influence_function, - atol=1e-12, rtol=1e-12, + atol=1e-12, + rtol=1e-12, ) def test_bias_corrected_local_linear_zero_weight_auto_bandwidth(self): @@ -4024,26 +4074,19 @@ def test_bias_corrected_local_linear_zero_weight_auto_bandwidth(self): G = 300 d_pos = rng.uniform(0.0, 1.0, G - 1) d_full = np.concatenate([d_pos, [1.0]]) # zero-weight unit at d=1.0 - y_full = np.concatenate( - [2.0 * d_pos + rng.normal(0, 0.25, G - 1), [0.0]] - ) + y_full = np.concatenate([2.0 * d_pos + rng.normal(0, 0.25, G - 1), [0.0]]) w_full = np.concatenate([np.ones(G - 1), [0.0]]) d_ref = d_full[w_full > 0] y_ref = y_full[w_full > 0] - r_weighted = bias_corrected_local_linear( - d=d_full, y=y_full, boundary=0.0, weights=w_full - ) - r_dropped = bias_corrected_local_linear( - d=d_ref, y=y_ref, boundary=0.0 - ) + r_weighted = bias_corrected_local_linear(d=d_full, y=y_full, boundary=0.0, weights=w_full) + r_dropped = bias_corrected_local_linear(d=d_ref, y=y_ref, boundary=0.0) # Auto-selected h identical between the two paths. - np.testing.assert_allclose( - r_weighted.h, r_dropped.h, atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(r_weighted.h, r_dropped.h, atol=1e-12, rtol=1e-12) np.testing.assert_allclose( r_weighted.estimate_bias_corrected, r_dropped.estimate_bias_corrected, - atol=1e-12, rtol=1e-12, + atol=1e-12, + rtol=1e-12, ) def test_zero_weight_counts_reflect_positive_subset(self): @@ -4082,11 +4125,13 @@ def test_survey_metadata_raw_weights_match_shortcut(self): est = HeterogeneousAdoptionDiD(design="continuous_at_zero") with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) - r_w = est.fit( - panel_with_w, "outcome", "dose", "period", "unit", weights=row_w - ) + r_w = est.fit(panel_with_w, "outcome", "dose", "period", "unit", weights=row_w) r_sd = est.fit( - panel_with_w, "outcome", "dose", "period", "unit", + panel_with_w, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w"), ) sm_w = r_w.survey_metadata @@ -4094,9 +4139,7 @@ def test_survey_metadata_raw_weights_match_shortcut(self): assert sm_w is not None and sm_sd is not None # sum_weights at unit-level: G unit-constant weights aggregated # per-unit via .first() give identical arrays on both paths. - np.testing.assert_allclose( - sm_sd.sum_weights, sm_w.sum_weights, atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(sm_sd.sum_weights, sm_w.sum_weights, atol=1e-12, rtol=1e-12) np.testing.assert_allclose( sm_sd.weight_range[0], sm_w.weight_range[0], atol=1e-12, rtol=1e-12 ) @@ -4105,12 +4148,8 @@ def test_survey_metadata_raw_weights_match_shortcut(self): ) # design_effect and effective_n are scale-invariant so they also # agree (secondary lock). - np.testing.assert_allclose( - sm_sd.design_effect, sm_w.design_effect, atol=1e-12, rtol=1e-12 - ) - np.testing.assert_allclose( - sm_sd.effective_n, sm_w.effective_n, atol=1e-12, rtol=1e-12 - ) + np.testing.assert_allclose(sm_sd.design_effect, sm_w.design_effect, atol=1e-12, rtol=1e-12) + np.testing.assert_allclose(sm_sd.effective_n, sm_w.effective_n, atol=1e-12, rtol=1e-12) def test_repr_surfaces_weighted_fields_when_present(self): """Round 4 P3: ``__repr__`` must name ``variance_formula`` and @@ -4159,7 +4198,11 @@ def test_survey_path_populates_df_survey(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) r = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w", strata="strata", psu="psu"), ) sm = r.survey_metadata @@ -4205,13 +4248,15 @@ def test_effective_dose_mean_equals_dose_mean_under_uniform_weights(self): panel, _, _, _, _, _ = self._panel_with_unit_weights(G=200) est = HeterogeneousAdoptionDiD(design="continuous_at_zero") r = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", weights=np.ones(panel.shape[0]), ) assert r.effective_dose_mean is not None - np.testing.assert_allclose( - r.effective_dose_mean, r.dose_mean, atol=1e-14, rtol=1e-14 - ) + np.testing.assert_allclose(r.effective_dose_mean, r.dose_mean, atol=1e-14, rtol=1e-14) # ---------- P1 fix: SurveyMetadata contract for downstream consumers ---------- @@ -4230,11 +4275,13 @@ def test_survey_metadata_is_surveymetadata_instance(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) # Both entry paths produce SurveyMetadata (not dict). - r_w = est.fit( - panel_with_w, "outcome", "dose", "period", "unit", weights=row_w - ) + r_w = est.fit(panel_with_w, "outcome", "dose", "period", "unit", weights=row_w) r_sd = est.fit( - panel_with_w, "outcome", "dose", "period", "unit", + panel_with_w, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w"), ) assert isinstance(r_w.survey_metadata, SurveyMetadata) @@ -4264,11 +4311,19 @@ def test_survey_no_psu_no_strata_se_matches_weights_hc1(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) r_w = est.fit( - panel_with_w, "outcome", "dose", "period", "unit", + panel_with_w, + "outcome", + "dose", + "period", + "unit", weights=row_w, ) r_sd = est.fit( - panel_with_w, "outcome", "dose", "period", "unit", + panel_with_w, + "outcome", + "dose", + "period", + "unit", survey=SurveyDesign(weights="w"), ) # ATT matches (same weighted lprobust fit). @@ -4304,12 +4359,14 @@ def _dgp_mp(n, seed=0): @staticmethod def _make_panel(d, dy): G = d.shape[0] - return pd.DataFrame({ - "unit": np.repeat(np.arange(G), 2), - "period": np.tile([1, 2], G), - "dose": np.column_stack([np.zeros(G), d]).ravel(), - "outcome": np.column_stack([np.zeros(G), dy]).ravel(), - }) + return pd.DataFrame( + { + "unit": np.repeat(np.arange(G), 2), + "period": np.tile([1, 2], G), + "dose": np.column_stack([np.zeros(G), d]).ravel(), + "outcome": np.column_stack([np.zeros(G), dy]).ravel(), + } + ) def test_uniform_weights_bit_parity_all_vcov_variants(self): """Direct helper call: weights=np.ones ≡ unweighted at atol=1e-14 @@ -4323,8 +4380,13 @@ def test_uniform_weights_bit_parity_all_vcov_variants(self): cluster_arg = cluster if use_cluster else None b0, s0, _ = _fit_mass_point_2sls(d, dy, 0.3, cluster_arg, vcov) b1, s1, _ = _fit_mass_point_2sls( - d, dy, 0.3, cluster_arg, vcov, - weights=np.ones(d.shape[0]), return_influence=False, + d, + dy, + 0.3, + cluster_arg, + vcov, + weights=np.ones(d.shape[0]), + return_influence=False, ) np.testing.assert_allclose(b0, b1, atol=1e-14, rtol=1e-14) np.testing.assert_allclose(s0, s1, atol=1e-14, rtol=1e-14) @@ -4407,7 +4469,12 @@ def test_fit_mass_point_weights_shortcut_variance_formula(self): warnings.simplefilter("ignore", UserWarning) est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1") r = est.fit( - panel, "outcome", "dose", "period", "unit", weights=w, + panel, + "outcome", + "dose", + "period", + "unit", + weights=w, ) assert r.variance_formula == "pweight_2sls" assert r.survey_metadata is not None @@ -4440,6 +4507,7 @@ class TestSupTReducesToNormalAtH1: def test_sup_t_h1_reduces_to_normal_quantile(self): import scipy.stats + from diff_diff.had import _sup_t_multiplier_bootstrap rng = np.random.default_rng(42) @@ -4480,8 +4548,13 @@ def test_sup_t_h5_greater_than_pointwise(self): psi = rng.standard_normal((G, H)) se = np.sqrt(np.sum(psi**2, axis=0)) q, _, _, _ = _sup_t_multiplier_bootstrap( - psi, np.zeros(H), se, None, - n_bootstrap=1000, alpha=0.05, seed=42, + psi, + np.zeros(H), + se, + None, + n_bootstrap=1000, + alpha=0.05, + seed=42, ) assert q > 1.96 + 0.15, ( f"H=5 sup-t should exceed pointwise Normal quantile by a " @@ -4498,12 +4571,22 @@ def test_sup_t_seed_reproducibility(self): psi = rng.standard_normal((G, H)) se = np.sqrt(np.sum(psi**2, axis=0)) q1, _, _, _ = _sup_t_multiplier_bootstrap( - psi, np.zeros(H), se, None, - n_bootstrap=500, alpha=0.05, seed=17, + psi, + np.zeros(H), + se, + None, + n_bootstrap=500, + alpha=0.05, + seed=17, ) q2, _, _, _ = _sup_t_multiplier_bootstrap( - psi, np.zeros(H), se, None, - n_bootstrap=500, alpha=0.05, seed=17, + psi, + np.zeros(H), + se, + None, + n_bootstrap=500, + alpha=0.05, + seed=17, ) assert q1 == q2 @@ -4543,7 +4626,11 @@ def test_weighted_es_cband_false_skips_bootstrap(self): panel = self._multi_period_panel(G=200, seed=3) est = HeterogeneousAdoptionDiD(design="continuous_at_zero", seed=0) r = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", aggregate="event_study", weights=np.ones(panel.shape[0]), cband=False, @@ -4559,10 +4646,16 @@ def test_weighted_es_cband_true_populates_band(self): with cband_crit_value in a plausible range.""" panel = self._multi_period_panel(G=200, seed=5) est = HeterogeneousAdoptionDiD( - design="continuous_at_zero", seed=42, n_bootstrap=500, + design="continuous_at_zero", + seed=42, + n_bootstrap=500, ) r = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", aggregate="event_study", weights=np.ones(panel.shape[0]), cband=True, @@ -4603,22 +4696,39 @@ def test_event_study_filter_info_stable_across_weight_patterns(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) r_unw = est.fit( - panel, "outcome", "dose", "period", "unit", - aggregate="event_study", first_treat_col="first_treat", + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + first_treat_col="first_treat", ) r_uni = est.fit( - panel, "outcome", "dose", "period", "unit", - aggregate="event_study", first_treat_col="first_treat", - weights=np.ones(panel.shape[0]), cband=False, + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + first_treat_col="first_treat", + weights=np.ones(panel.shape[0]), + cband=False, ) # Informative per-row weights (constant within unit). w_unit = 1.0 + 0.5 * rng.standard_normal(G) w_unit = np.clip(w_unit, 0.1, None) w_row = panel["unit"].map(lambda g: w_unit[g]).to_numpy() r_inf = est.fit( - panel, "outcome", "dose", "period", "unit", - aggregate="event_study", first_treat_col="first_treat", - weights=w_row, cband=False, + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + first_treat_col="first_treat", + weights=w_row, + cband=False, ) # filter_info must agree across all three fits (same dropped cohorts). assert r_unw.filter_info == r_uni.filter_info == r_inf.filter_info @@ -4629,9 +4739,7 @@ def test_event_study_mass_point_weighted_smoke(self): rng = np.random.default_rng(10) G = 200 T = 4 - d_mp = np.concatenate( - [np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)] - ) + d_mp = np.concatenate([np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)]) rng.shuffle(d_mp) rows = [] for t in range(T): @@ -4643,13 +4751,98 @@ def test_event_study_mass_point_weighted_smoke(self): with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) est = HeterogeneousAdoptionDiD( - design="mass_point", vcov_type="hc1", seed=0, n_bootstrap=200, + design="mass_point", + vcov_type="hc1", + seed=0, + n_bootstrap=200, ) r = est.fit( - panel, "outcome", "dose", "period", "unit", + panel, + "outcome", + "dose", + "period", + "unit", aggregate="event_study", weights=np.ones(panel.shape[0]), ) assert r.design == "mass_point" assert r.variance_formula == "pweight_2sls" assert r.cband_crit_value is not None and np.isfinite(r.cband_crit_value) + + def test_zero_se_horizon_nan_gates_cband(self): + """Review R1 P0: a horizon with se <= 0 or non-finite must NOT + produce a finite simultaneous-band endpoint — gating matches + the pointwise ``safe_inference`` contract.""" + from diff_diff.had import _sup_t_multiplier_bootstrap + + rng = np.random.default_rng(0) + G = 200 + H = 3 + psi = rng.standard_normal((G, H)) + se = np.array([np.sqrt(np.sum(psi[:, 0] ** 2)), 0.0, np.nan]) + att = np.array([1.0, 2.0, 3.0]) + q, low, high, n_valid = _sup_t_multiplier_bootstrap( + psi, + att, + se, + None, + n_bootstrap=500, + alpha=0.05, + seed=1, + ) + assert n_valid > 250 + # Horizon 0: finite se → finite band. + assert np.isfinite(low[0]) and np.isfinite(high[0]) + # Horizons 1 and 2: zero / NaN se → NaN band (not `att ± q * 0`). + assert np.isnan(low[1]) and np.isnan(high[1]) + assert np.isnan(low[2]) and np.isnan(high[2]) + + def test_weights_nonrange_index_aligned_positionally(self): + """Review R1 P1: ``weights=`` is row-order aligned, not + index-label aligned. A DataFrame with a custom non-RangeIndex + (here shifted int labels) must produce the same fit as the + same data with a RangeIndex + the same row-order weights.""" + rng = np.random.default_rng(3) + G, T = 150, 3 + d_post = rng.uniform(0.0, 1.0, G) + rows = [] + for t in range(T): + for g in range(G): + dose = d_post[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y)) + panel_range = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) + # Row-order-aligned unit-constant weights. + w_unit = 1.0 + 0.3 * rng.standard_normal(G) + w_row = panel_range["unit"].map(lambda g: w_unit[g]).to_numpy() + + # Same DataFrame but with a non-positional index (offset labels + # starting at 1000; same row order). + panel_shifted = panel_range.copy() + panel_shifted.index = panel_shifted.index + 1000 + + est = HeterogeneousAdoptionDiD(design="continuous_at_zero", seed=0) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + r_range = est.fit( + panel_range, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=w_row, + cband=False, + ) + r_shifted = est.fit( + panel_shifted, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=w_row, + cband=False, + ) + np.testing.assert_allclose(r_range.att, r_shifted.att, atol=1e-12, rtol=1e-12) + np.testing.assert_allclose(r_range.se, r_shifted.se, atol=1e-12, rtol=1e-12) From 4ecc6f437fbdaea479fec838f7d700a2ce452ee5 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 17:06:23 -0400 Subject: [PATCH 3/9] Address PR #363 R2 review (2 P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R2 P1 (methodology): reject SurveyDesign(lonely_psu='adjust') with singleton strata in _sup_t_multiplier_bootstrap. The bootstrap helper pools singletons into a pseudo-stratum with NONZERO multipliers, but compute_survey_if_variance centers singleton PSU scores around the global mean — without the matching pseudo-stratum centering transform in the bootstrap, the simultaneous band target diverges from the analytical Binder-TSL variance. Clear NotImplementedError points users to lonely_psu='remove' (matches the 'remove' analytical target) or cband=False (skips bootstrap). 'remove' / 'certainty' continue to work unchanged. Deferred transform tracked for follow-up. R2 P1 (code quality): reject cluster= + weights/survey= on design='mass_point' on both static and event-study paths. The weighted path composes Binder-TSL variance via compute_survey_if_variance which was silently overriding the CR1 sandwich while result metadata still advertised vcov_type='cr1' and cluster_name=. Clean NotImplementedError with a pointer to the two supported combinations: cluster= alone (unweighted CR1) or survey/weights alone (Binder-TSL). Combined cluster-robust survey inference requires a derivation not yet in scope. Regression tests (+4): - test_mass_point_survey_plus_cluster_rejected_static - test_mass_point_survey_plus_cluster_rejected_event_study - test_lonely_psu_adjust_with_singletons_rejected_on_cband - test_stratified_h1_sup_t_matches_analytical (H=1 quantile lock under n_strata=4, psu_per_unit: q=1.985, matches Phi^-1(0.975)) Co-Authored-By: Claude Opus 4.7 (1M context) --- diff_diff/had.py | 74 ++++++++++++++++++++++++ tests/test_had.py | 144 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) diff --git a/diff_diff/had.py b/diff_diff/had.py index d6213d33..5b460a37 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -2062,6 +2062,45 @@ def _sup_t_multiplier_bootstrap( ) if use_survey_bootstrap: + # Review R2 P1: lonely_psu="adjust" pools singleton strata into a + # pseudo-stratum with NONZERO multipliers in the bootstrap helper, + # but the analytical compute_survey_if_variance target for + # singletons is centered at the global mean of PSU scores. Since + # this PR's stratum-demean loop only matches the within-stratum + # Binder-TSL target (and skips singletons assuming zero + # contribution), pooled singleton multipliers would diverge from + # the analytical variance without an additional pseudo-stratum + # centering step. Reject with a clear pointer until the matching + # transform is derived; "remove" / "certainty" (singleton + # multipliers forced to zero) are fine. + _lonely = getattr(resolved_survey, "lonely_psu", "remove") + if _lonely == "adjust": + strata_arr = resolved_survey.strata + psu_arr = resolved_survey.psu + _has_singleton = False + if strata_arr is not None: + for h in np.unique(strata_arr): + mask_h = np.asarray(strata_arr) == h + if psu_arr is not None: + n_psu_h = int(np.unique(np.asarray(psu_arr)[mask_h]).shape[0]) + else: + n_psu_h = int(mask_h.sum()) + if n_psu_h < 2: + _has_singleton = True + break + if _has_singleton: + raise NotImplementedError( + "HeterogeneousAdoptionDiD event-study sup-t bootstrap " + "does not yet support SurveyDesign(lonely_psu='adjust') " + "with singleton strata: the bootstrap helper pools " + "singletons with nonzero multipliers while the " + "analytical Binder-TSL target centers singleton PSU " + "scores at the global mean, and the matching " + "pseudo-stratum centering transform has not been " + "implemented. Use lonely_psu='remove' (drops singleton " + "contributions; matches the 'remove' analytical target) " + "or pass cband=False to skip the simultaneous band." + ) psu_weights, psu_ids = generate_survey_multiplier_weights_batch( n_bootstrap, resolved_survey, bootstrap_weights, rng ) @@ -3260,6 +3299,25 @@ def fit( vcov_label: Optional[str] = None cluster_label: Optional[str] = None elif resolved_design == "mass_point": + # Review R2 P1: mass-point + weights + cluster is a silent + # inference mismatch because the weighted path overrides the + # CR1 SE with Binder-TSL while result metadata still reports + # CR1. Reject the combination front-door until a combined + # cluster + survey variance is derived. Unweighted CR1 + # continues to work unchanged; weighted pweight sandwich + # without cluster continues to work unchanged. + if cluster_arg is not None and weights_unit_full is not None: + raise NotImplementedError( + f"cluster={cluster_arg!r} + survey=/weights= on " + f"design='mass_point' is not yet supported: the " + f"weighted path composes Binder-TSL variance and " + f"would silently override the CR1 cluster-robust " + f"sandwich. Pass either cluster= alone (unweighted " + f"CR1) or survey=/weights= alone (weighted 2SLS " + f"pweight sandwich → Binder-TSL under survey=); " + f"combined cluster-robust survey inference is " + f"deferred to a follow-up PR." + ) if vcov_type_arg is None: # Backward-compat: robust=True -> hc1, robust=False -> classical. vcov_requested = "hc1" if robust_arg else "classical" @@ -3858,6 +3916,22 @@ def _fit_event_study( # ---- Extract cluster IDs on mass-point path only ---- cluster_arr: Optional[np.ndarray] = None if resolved_design == "mass_point" and cluster_arg is not None: + # Review R2 P1: reject cluster= + weights/survey on + # mass-point (mirrors the static-path rejection) — + # the weighted path would compose Binder-TSL variance + # and silently override CR1 while result metadata still + # claims cluster-robust inference. + if weights_unit_full is not None: + raise NotImplementedError( + f"cluster={cluster_arg!r} + survey=/weights= on " + f"design='mass_point' (event-study) is not yet " + f"supported: the weighted path composes Binder-TSL " + f"variance and would silently override the CR1 " + f"cluster-robust sandwich. Pass either cluster= " + f"alone (unweighted CR1) or survey=/weights= alone; " + f"combined cluster-robust survey inference is " + f"deferred to a follow-up PR." + ) _, _, cluster_arr, _, _ = _aggregate_multi_period_first_differences( data_filtered, outcome_col, diff --git a/tests/test_had.py b/tests/test_had.py index 1aef7ae6..f2ea45e1 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -4846,3 +4846,147 @@ def test_weights_nonrange_index_aligned_positionally(self): ) np.testing.assert_allclose(r_range.att, r_shifted.att, atol=1e-12, rtol=1e-12) np.testing.assert_allclose(r_range.se, r_shifted.se, atol=1e-12, rtol=1e-12) + + def test_mass_point_survey_plus_cluster_rejected_static(self): + """Review R2 P1: mass-point + (weights= or survey=) + cluster= + must raise NotImplementedError on the static path. Previously + the weighted path silently overrode the CR1 SE with Binder-TSL + while the result still reported vcov_type='cr1'.""" + rng = np.random.default_rng(0) + G = 200 + d = np.concatenate([np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)]) + rng.shuffle(d) + dy = 2.0 * d + 0.3 * rng.standard_normal(G) + panel = pd.DataFrame( + { + "unit": np.repeat(np.arange(G), 2), + "period": np.tile([1, 2], G), + "dose": np.column_stack([np.zeros(G), d]).ravel(), + "outcome": np.column_stack([np.zeros(G), dy]).ravel(), + "state": np.repeat(np.arange(G) // 20, 2), + } + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1", cluster="state") + with pytest.raises(NotImplementedError, match="cluster"): + est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + weights=np.ones(panel.shape[0]), + ) + + def test_mass_point_survey_plus_cluster_rejected_event_study(self): + """Review R2 P1 (event-study arm): same rejection must fire on + the multi-period dispatch.""" + rng = np.random.default_rng(1) + G, T = 150, 4 + d_mp = np.concatenate([np.full(30, 0.3), rng.uniform(0.3, 1.0, G - 30)]) + rng.shuffle(d_mp) + rows = [] + for t in range(T): + for g in range(G): + dose = d_mp[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y, g // 25)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome", "state"]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1", cluster="state") + with pytest.raises(NotImplementedError, match="cluster"): + est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=np.ones(panel.shape[0]), + ) + + def test_lonely_psu_adjust_with_singletons_rejected_on_cband(self): + """Review R2 P1: sup-t bootstrap rejects lonely_psu='adjust' + when there are singleton strata, because the bootstrap helper + pools singletons with nonzero multipliers but the analytical + target centers them at the global mean — mismatch.""" + from diff_diff.had import _sup_t_multiplier_bootstrap + from diff_diff.survey import ResolvedSurveyDesign + + rng = np.random.default_rng(0) + G = 80 + # 3 strata, two with multiple PSUs, one singleton. + strata = np.array([1] * 30 + [2] * 30 + [3] * 20) + # PSUs: 10 in stratum 1, 10 in stratum 2, 1 in stratum 3 (singleton). + psu = np.concatenate( + [np.arange(10).repeat(3), (10 + np.arange(10)).repeat(3), np.full(20, 20)] + ) + adjust_resolved = ResolvedSurveyDesign( + weights=np.ones(G), + weight_type="pweight", + strata=strata, + psu=psu, + fpc=None, + n_strata=3, + n_psu=21, + lonely_psu="adjust", + combined_weights=True, + mse=False, + ) + psi = rng.standard_normal((G, 2)) + with pytest.raises(NotImplementedError, match="lonely_psu='adjust'"): + _sup_t_multiplier_bootstrap( + psi, + np.zeros(2), + np.array([1.0, 1.0]), + adjust_resolved, + n_bootstrap=200, + alpha=0.05, + seed=0, + ) + + def test_stratified_h1_sup_t_matches_analytical(self): + """Review R2 P1 coverage: stratum-centered H=1 bootstrap variance + matches the analytical Binder-TSL target (q ≈ 1.96 at H=1).""" + from diff_diff.had import _sup_t_multiplier_bootstrap + from diff_diff.survey import ResolvedSurveyDesign, compute_survey_if_variance + + rng = np.random.default_rng(7) + G = 400 + strata = np.repeat(np.arange(4), G // 4) + psu = np.arange(G) + resolved = ResolvedSurveyDesign( + weights=np.ones(G), + weight_type="pweight", + strata=strata, + psu=psu, + fpc=None, + n_strata=4, + n_psu=G, + lonely_psu="remove", + combined_weights=True, + mse=False, + ) + psi = rng.standard_normal((G, 1)) + V_analytical = compute_survey_if_variance(psi[:, 0], resolved) + se_analytical = np.sqrt(V_analytical) + q, _, _, _ = _sup_t_multiplier_bootstrap( + psi, + np.zeros(1), + np.array([se_analytical]), + resolved, + n_bootstrap=5000, + alpha=0.05, + seed=42, + ) + # At H=1 the sup collapses to the marginal; with stratum- + # centered + small-sample-corrected perturbations the bootstrap + # distribution is ~ N(0, 1), so q → Phi^-1(0.975) = 1.96. + # B=5000 MC noise on the tail quantile is ~0.03-0.05. + assert abs(q - 1.96) < 0.15, ( + f"Stratified H=1 sup-t should match Normal quantile 1.96 up to " + f"MC noise; got q={q:.4f}. Likely a stratum-centering bug in " + f"_sup_t_multiplier_bootstrap." + ) From cb8a711e10275e2059c4ad8ce9199859d7ea5b52 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 17:40:40 -0400 Subject: [PATCH 4/9] Address PR #363 R3 review (2 P1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R3 P1 (methodology — trivial-survey sup-t): _sup_t_multiplier_bootstrap now treats ANY non-None resolved_survey as survey-aware bootstrap input, including the trivial SurveyDesign(weights=...) case (no explicit strata / PSU / FPC). Previously the gate required explicit strata / psu / fpc, so the trivial survey path fell through to raw unit-level Rademacher — Var(xi @ Psi) = Σ psi², NOT the centered (n/(n-1))·Σ(ψ−ψ̄)² target that compute_survey_if_variance uses for the analytical SE. The existing "no strata → single implicit stratum" branch in _sup_t_multiplier_bootstrap now handles the trivial case end-to-end. H=1 trivial-survey regression locks q ≈ 1.96. R3 P1 (methodology — classical weighted mass-point): reject vcov_type='classical' on mass-point whenever the IF matrix is used (static survey= path; event-study survey= OR weights= shortcut with cband=True). The IF is HC1-scaled (sqrt((n-1)/(n-k)) factor in _fit_mass_point_2sls targets compute_survey_if_variance(psi, trivial) ≈ V_HC1[1,1]); mixing it with a classical analytical SE either through the survey Binder-TSL override or the sup-t bootstrap normalization would silently report a V_HC1-targeted SE under a classical vcov_type label. Rejection message points users to hc1. weights= shortcut + cband=False + classical continues to work unchanged (no IF consumption; analytical classical sandwich returned as-is). Regression tests (+4): - test_trivial_survey_h1_sup_t_matches_analytical: H=1 trivial SurveyDesign reduces to Normal quantile, confirms stratum-demean + sqrt(n/(n-1)) correction fires on the trivial case. - test_mass_point_classical_survey_rejected_static - test_mass_point_classical_event_study_with_cband_rejected - test_mass_point_classical_event_study_cband_false_accepts (complement: confirms classical+weights+cband=False still works, no IF consumption) Co-Authored-By: Claude Opus 4.7 (1M context) --- diff_diff/had.py | 63 ++++++++++++++++++-- tests/test_had.py | 143 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 5 deletions(-) diff --git a/diff_diff/had.py b/diff_diff/had.py index 5b460a37..e95f4a7e 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -2055,11 +2055,16 @@ def _sup_t_multiplier_bootstrap( n_units, n_horizons = influence_matrix.shape rng = np.random.default_rng(seed) - use_survey_bootstrap = resolved_survey is not None and ( - resolved_survey.strata is not None - or resolved_survey.psu is not None - or resolved_survey.fpc is not None - ) + # Review R3 P1: the survey-aware branch must fire for ANY non-None + # resolved_survey, including the trivial ``SurveyDesign(weights=...)`` + # case (no explicit strata / PSU / FPC). The analytical Binder-TSL + # target under that design still applies the centered + # (n/(n-1)) · sum(psi − psi_bar)² formula, so the bootstrap must + # also use PSU-aggregation + stratum-demeaning + sqrt(n/(n-1)) + # scaling — not raw unit-level Rademacher draws (which skip the + # centering and small-sample factor). The unit-level branch below is + # reserved for the ``weights=`` shortcut (no survey object at all). + use_survey_bootstrap = resolved_survey is not None if use_survey_bootstrap: # Review R2 P1: lonely_psu="adjust" pools singleton strata into a @@ -3318,6 +3323,28 @@ def fit( f"combined cluster-robust survey inference is " f"deferred to a follow-up PR." ) + # Review R3 P1: the weighted mass-point path returns an + # HC1-scaled influence function (the IF scale convention + # locks compute_survey_if_variance(psi, trivial) ≈ V_HC1[1,1] + # via the sqrt((n-1)/(n-k)) factor in _fit_mass_point_2sls). + # On the survey= path the analytical SE is ALWAYS overwritten + # with that HC1-scale Binder-TSL composition, so + # vcov_type="classical" + survey= would silently report an + # HC1-target SE under a classical label. Reject until a + # classical-aligned IF is derived. + _vcov_requested_lc = vcov_type_arg.lower() if vcov_type_arg is not None else None + if _vcov_requested_lc == "classical" and resolved_survey_unit_full is not None: + raise NotImplementedError( + "vcov_type='classical' + survey= on " + "design='mass_point' is not yet supported: the " + "survey path composes Binder-TSL variance via the " + "HC1-scale influence function, which targets V_HC1 " + "rather than the classical sandwich " + "V_cl = σ² · (Z'WX)^{-1}(Z'W²Z)(X'WZ)^{-1}. Use " + "vcov_type='hc1' (or leave vcov_type unset with " + "robust=True) on the weighted path; a classical-" + "aligned IF derivation is queued for a follow-up PR." + ) if vcov_type_arg is None: # Backward-compat: robust=True -> hc1, robust=False -> classical. vcov_requested = "hc1" if robust_arg else "classical" @@ -3983,6 +4010,32 @@ def _fit_event_study( # ---- Resolve vcov label for mass-point ---- if resolved_design == "mass_point": + # Review R3 P1 (event-study arm): reject vcov_type="classical" + # when the weighted path will compute the IF (always on + # survey= path; on weights= shortcut when cband=True the + # bootstrap divides HC1-scale perturbations by per-horizon + # analytical SE, so classical SE would give wrong t-stats). + # Matches the static-path rejection — weighted mass-point + # paths use the HC1-scale IF convention uniformly. + _vcov_requested_lc = vcov_type_arg.lower() if vcov_type_arg is not None else None + _uses_if_matrix = resolved_survey_unit_full is not None or ( + weights_unit_full is not None and cband + ) + if _vcov_requested_lc == "classical" and _uses_if_matrix: + raise NotImplementedError( + "vcov_type='classical' + weights/survey= on " + "design='mass_point' event-study is not yet " + "supported: the per-horizon IF matrix is HC1-scale " + "(targets V_HC1 via compute_survey_if_variance) and " + "mixing it with a classical analytical SE — either " + "through the survey Binder-TSL override or the " + "sup-t bootstrap normalization — would produce an " + "inconsistent variance family. Use " + "vcov_type='hc1' (or leave vcov_type unset with " + "robust=True) on the weighted event-study path, or " + "pass cband=False to skip the bootstrap on the " + "weights= shortcut." + ) if vcov_type_arg is None: vcov_requested = "hc1" if robust_arg else "classical" else: diff --git a/tests/test_had.py b/tests/test_had.py index f2ea45e1..8098b5ae 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -4990,3 +4990,146 @@ def test_stratified_h1_sup_t_matches_analytical(self): f"MC noise; got q={q:.4f}. Likely a stratum-centering bug in " f"_sup_t_multiplier_bootstrap." ) + + def test_trivial_survey_h1_sup_t_matches_analytical(self): + """Review R3 P1: the survey-aware bootstrap branch must fire even + on trivial ``SurveyDesign(weights=...)`` (no explicit strata / + PSU / FPC). The analytical target is still the centered + (n/(n-1)) · Σ(ψ − ψ̄)² Binder formula, so the bootstrap must + also apply stratum-demeaning + small-sample correction — NOT + fall through to raw unit-level Rademacher. + """ + from diff_diff.had import _sup_t_multiplier_bootstrap + from diff_diff.survey import ResolvedSurveyDesign, compute_survey_if_variance + + rng = np.random.default_rng(11) + G = 300 + # Trivial resolved: weights only, no strata / PSU / FPC. + resolved = ResolvedSurveyDesign( + weights=np.ones(G), + weight_type="pweight", + strata=None, + psu=None, + fpc=None, + n_strata=1, + n_psu=G, + lonely_psu="remove", + combined_weights=True, + mse=False, + ) + psi = rng.standard_normal((G, 1)) + V_analytical = compute_survey_if_variance(psi[:, 0], resolved) + se_analytical = np.sqrt(V_analytical) + q, _, _, _ = _sup_t_multiplier_bootstrap( + psi, + np.zeros(1), + np.array([se_analytical]), + resolved, + n_bootstrap=5000, + alpha=0.05, + seed=42, + ) + # q ≈ 1.96 at H=1 confirms the trivial-survey branch applies + # the same stratum-demean + sqrt(n/(n-1)) correction the + # analytical target uses. Pre-R3, use_survey_bootstrap fell + # through to unit-level Rademacher, off by sqrt(n/(n-1)). + assert abs(q - 1.96) < 0.15, ( + f"Trivial-survey H=1 sup-t should match Normal quantile " + f"1.96 up to MC noise; got q={q:.4f}. Likely the survey-" + f"aware bootstrap branch is not firing on trivial " + f"SurveyDesign." + ) + + def test_mass_point_classical_survey_rejected_static(self): + """Review R3 P1: vcov_type='classical' + survey= on + design='mass_point' rejects with a clear pointer to HC1. + Previously the survey path silently overrode classical SE + with Binder-TSL composed from the HC1-scale IF.""" + from diff_diff.survey import SurveyDesign + + rng = np.random.default_rng(20) + G = 200 + d = np.concatenate([np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)]) + rng.shuffle(d) + dy = 2.0 * d + 0.3 * rng.standard_normal(G) + panel = pd.DataFrame( + { + "unit": np.repeat(np.arange(G), 2), + "period": np.tile([1, 2], G), + "dose": np.column_stack([np.zeros(G), d]).ravel(), + "outcome": np.column_stack([np.zeros(G), dy]).ravel(), + "w": np.ones(2 * G), + } + ) + sd = SurveyDesign(weights="w") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="classical") + with pytest.raises(NotImplementedError, match="classical.*survey"): + est.fit(panel, "outcome", "dose", "period", "unit", survey=sd) + + def test_mass_point_classical_event_study_with_cband_rejected(self): + """Review R3 P1 (event-study arm): vcov_type='classical' is + rejected on the event-study path whenever the IF matrix gets + used (survey= composition OR weights= shortcut + cband=True). + With cband=False on the weights= shortcut the classical SE is + returned as-is — no IF consumption — so that combination is + allowed and covered by the complementary test below.""" + rng = np.random.default_rng(30) + G, T = 150, 4 + d_mp = np.concatenate([np.full(30, 0.3), rng.uniform(0.3, 1.0, G - 30)]) + rng.shuffle(d_mp) + rows = [] + for t in range(T): + for g in range(G): + dose = d_mp[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD( + design="mass_point", vcov_type="classical", seed=0, n_bootstrap=100 + ) + with pytest.raises(NotImplementedError, match="classical"): + est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=np.ones(panel.shape[0]), + cband=True, + ) + + def test_mass_point_classical_event_study_cband_false_accepts(self): + """Complement to the above: cband=False with classical weighted + mass-point event-study is accepted — no IF consumption, the + per-horizon classical analytical SE is returned as-is.""" + rng = np.random.default_rng(31) + G, T = 100, 4 + d_mp = np.concatenate([np.full(20, 0.3), rng.uniform(0.3, 1.0, G - 20)]) + rng.shuffle(d_mp) + rows = [] + for t in range(T): + for g in range(G): + dose = d_mp[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="classical", seed=0) + r = est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=np.ones(panel.shape[0]), + cband=False, + ) + assert r.variance_formula == "pweight_2sls" + assert r.cband_crit_value is None From 35794442f3a40045627c26bcd21c0d77acfdbfb1 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 17:54:30 -0400 Subject: [PATCH 5/9] Address PR #363 R4 review (2 P1 + 2 P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R4 P1 (cluster guard overreach): narrow the mass-point cluster+weighted rejection. Previously rejected ANY cluster= + weighted fit, which blocked the valid weights= shortcut + cluster= weighted-CR1 pweight sandwich path (parity-tested vs estimatr::iv_robust se_type='stata'). Narrowed to: - Static path: reject only cluster= + survey= (Binder-TSL override would silently overwrite CR1 while metadata says vcov_type='cr1'). - Event-study path: reject cluster= + survey= OR cluster= + weights= + cband=True (sup-t bootstrap mixes HC1-scale perturbations with CR1 analytical SE). cluster= + weights= + cband=False is allowed (no IF consumption). R4 P1 (lonely_psu="adjust" deviation): added REGISTRY.md Note explicitly documenting the HAD-specific unsupported combo (weighted event-study + cband=True + lonely_psu="adjust" with singleton strata). The shared survey bootstrap helper supports pooled singletons but compute_survey_if_variance centers them at the global mean; matching the two requires a pseudo-stratum centering transform not yet derived. Other survey-bootstrap consumers (CS, dCDH, SDID) retain full "adjust" support. R4 P2 (n_units reporting): weighted event-study now reports the POSITIVE-WEIGHT contributing sample size in n_units and n_obs_per_horizon, matching the static-path n_obs contract. Full- design size still surfaces through survey_metadata.n_psu / effective_n / etc. R4 P2 (docstring drift): HeterogeneousAdoptionDiDEventStudyResults.se docstring now documents the three regimes separately — unweighted analytical sandwich, weights= shortcut analytical (HC1 / classical / CR1), and survey= Binder-TSL. Previously said "Binder-TSL for all weighted fits" which was wrong for the weights= shortcut. Regression tests (+5): - test_mass_point_weights_plus_cluster_shortcut_allowed - test_mass_point_weights_plus_cluster_event_study_cband_false_allowed - test_mass_point_weights_plus_cluster_event_study_cband_true_rejected - test_event_study_zero_weight_units_excluded_from_n_units - Updated test_mass_point_survey_plus_cluster_rejected_static to use survey= instead of weights= (matches narrowed guard) Co-Authored-By: Claude Opus 4.7 (1M context) --- diff_diff/had.py | 130 +++++++++++++++++--------- docs/methodology/REGISTRY.md | 8 +- tests/test_had.py | 173 +++++++++++++++++++++++++++++++++-- 3 files changed, 259 insertions(+), 52 deletions(-) diff --git a/diff_diff/had.py b/diff_diff/had.py index e95f4a7e..7153e093 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -537,18 +537,29 @@ class HeterogeneousAdoptionDiDEventStudyResults: :class:`HeterogeneousAdoptionDiDResults.att` for the per-design formula, applied to ``ΔY_t = Y_{g,t} - Y_{g,F-1}``). se : np.ndarray, shape (n_horizons,) - Per-horizon standard error on the beta-scale. On unweighted fits - each horizon uses the INDEPENDENT per-period sandwich from the - chosen design path (continuous: CCT-2014 robust divided by - ``|den|``; mass-point: structural-residual 2SLS sandwich). On - weighted fits (``weights=`` shortcut or ``survey=``) each horizon - uses the Binder (1983) Taylor-series linearization via - :func:`compute_survey_if_variance` on the per-unit β̂-scale IF - (continuous + mass-point both route through the same helper). + Per-horizon standard error on the beta-scale. Three regimes: + + - **Unweighted**: per-horizon INDEPENDENT analytical sandwich + (continuous: CCT-2014 weighted-robust divided by ``|den|``; + mass-point: structural-residual 2SLS sandwich via + ``_fit_mass_point_2sls``). No cross-horizon covariance. + - **``weights=`` shortcut**: continuous paths still use the + CCT-2014 weighted-robust SE from lprobust (``bc_fit.se_robust + / |den|``); mass-point uses the analytical weighted 2SLS + pweight sandwich (HC1 / classical / CR1 depending on + ``vcov_type`` + ``cluster=``). No Binder-TSL composition + on this path — inference is Normal (``df=None``). + - **``survey=``**: each horizon composes Binder (1983) + Taylor-series linearization via + :func:`compute_survey_if_variance` on the per-unit β̂-scale + IF (continuous + mass-point both route through the same + helper). ``df_survey`` threads into ``safe_inference`` for + t-inference. + Pointwise CIs are always populated; a simultaneous confidence band is available only on the weighted path via ``cband_*`` - below. Joint cross-horizon analytical covariance is not computed - in this release (tracked in TODO.md). + below. Joint cross-horizon analytical covariance is not + computed in this release (tracked in TODO.md). t_stat, p_value : np.ndarray, shape (n_horizons,) Per-horizon inference triple element. conf_int_low, conf_int_high : np.ndarray, shape (n_horizons,) @@ -3304,24 +3315,30 @@ def fit( vcov_label: Optional[str] = None cluster_label: Optional[str] = None elif resolved_design == "mass_point": - # Review R2 P1: mass-point + weights + cluster is a silent - # inference mismatch because the weighted path overrides the - # CR1 SE with Binder-TSL while result metadata still reports - # CR1. Reject the combination front-door until a combined - # cluster + survey variance is derived. Unweighted CR1 - # continues to work unchanged; weighted pweight sandwich - # without cluster continues to work unchanged. - if cluster_arg is not None and weights_unit_full is not None: + # Review R4 P1: narrow the cluster+weighted rejection. Only + # survey= + cluster= is a silent-mismatch case (the + # Binder-TSL override would overwrite the CR1 sandwich while + # result metadata still advertises vcov_type='cr1'). The + # weights= shortcut + cluster= path just returns the + # weighted-CR1 sandwich from _fit_mass_point_2sls directly + # (no survey composition) and matches estimatr::iv_robust + # (se_type="stata") bit-exactly — see + # tests/test_estimatr_iv_robust_parity.py::TestEstimatrIVRobustCR1Parity. + if cluster_arg is not None and resolved_survey_unit_full is not None: raise NotImplementedError( - f"cluster={cluster_arg!r} + survey=/weights= on " + f"cluster={cluster_arg!r} + survey= on " f"design='mass_point' is not yet supported: the " - f"weighted path composes Binder-TSL variance and " - f"would silently override the CR1 cluster-robust " - f"sandwich. Pass either cluster= alone (unweighted " - f"CR1) or survey=/weights= alone (weighted 2SLS " - f"pweight sandwich → Binder-TSL under survey=); " - f"combined cluster-robust survey inference is " - f"deferred to a follow-up PR." + f"survey path composes Binder-TSL variance via " + f"compute_survey_if_variance and would silently " + f"override the CR1 cluster-robust sandwich while " + f"result metadata still advertises " + f"vcov_type='cr1'. Pass cluster= alone " + f"(unweighted CR1), or weights= + cluster= " + f"(weighted-CR1 pweight sandwich; parity-tested vs " + f"estimatr::iv_robust se_type='stata'), or " + f"survey= alone (Binder-TSL). Combined cluster-" + f"robust + survey inference is deferred to a " + f"follow-up PR." ) # Review R3 P1: the weighted mass-point path returns an # HC1-scaled influence function (the IF scale convention @@ -3943,21 +3960,42 @@ def _fit_event_study( # ---- Extract cluster IDs on mass-point path only ---- cluster_arr: Optional[np.ndarray] = None if resolved_design == "mass_point" and cluster_arg is not None: - # Review R2 P1: reject cluster= + weights/survey on - # mass-point (mirrors the static-path rejection) — - # the weighted path would compose Binder-TSL variance - # and silently override CR1 while result metadata still - # claims cluster-robust inference. - if weights_unit_full is not None: + # Review R4 P1: narrow the cluster+weighted guard (mirrors + # the static-path narrowing). Incompatible cases on the + # event-study path: + # (a) survey= + cluster=: Binder-TSL override would + # silently overwrite CR1. + # (b) weights= shortcut + cluster= + cband=True: the + # sup-t bootstrap normalizes HC1-scale perturbations + # by the CR1 analytical SE, producing an inconsistent + # variance family in the bootstrap t-distribution. + # weights= shortcut + cluster= + cband=False is fine: the + # per-horizon CR1 sandwich is returned as-is and no IF is + # consumed. Unweighted + cluster= also unchanged. + if resolved_survey_unit_full is not None: + raise NotImplementedError( + f"cluster={cluster_arg!r} + survey= on " + f"design='mass_point' event-study is not yet " + f"supported: the survey path composes Binder-TSL " + f"variance per horizon and would silently override " + f"the CR1 cluster-robust sandwich. Pass cluster= " + f"alone (unweighted CR1), or weights= + cluster= " + f"+ cband=False (weighted-CR1 per horizon), or " + f"survey= alone (Binder-TSL). Combined cluster-" + f"robust + survey event-study inference is deferred." + ) + if weights_unit_full is not None and cband: raise NotImplementedError( - f"cluster={cluster_arg!r} + survey=/weights= on " - f"design='mass_point' (event-study) is not yet " - f"supported: the weighted path composes Binder-TSL " - f"variance and would silently override the CR1 " - f"cluster-robust sandwich. Pass either cluster= " - f"alone (unweighted CR1) or survey=/weights= alone; " - f"combined cluster-robust survey inference is " - f"deferred to a follow-up PR." + f"cluster={cluster_arg!r} + weights= + cband=True " + f"on design='mass_point' event-study is not yet " + f"supported: the sup-t bootstrap uses an HC1-scale " + f"influence function and normalizes by the CR1 " + f"analytical SE, mixing variance families in the " + f"bootstrap t-distribution. Pass cband=False to " + f"disable the simultaneous band (pointwise CIs " + f"still use the weighted-CR1 sandwich per horizon), " + f"or drop cluster= to use the weighted-HC1 sandwich " + f"with sup-t." ) _, _, cluster_arr, _, _ = _aggregate_multi_period_first_differences( data_filtered, @@ -4070,7 +4108,12 @@ def _fit_event_study( p_arr = np.full(n_horizons, np.nan, dtype=np.float64) ci_lo_arr = np.full(n_horizons, np.nan, dtype=np.float64) ci_hi_arr = np.full(n_horizons, np.nan, dtype=np.float64) - n_obs_arr = np.full(n_horizons, G_full if weighted_es else n_units, dtype=np.int64) + # Review R4 P2: report the POSITIVE-WEIGHT contributing sample + # size, not the full pre-filter design size. Matches the + # static-path n_obs contract where zero-weight units are + # excluded from the reported count (survey_metadata still + # carries the full-design effective_n / n_psu / etc.). + n_obs_arr = np.full(n_horizons, n_units, dtype=np.int64) # Per-horizon IF matrix on the weighted path (shape (G, H)); drives # both per-horizon Binder-TSL variance (already composed inside @@ -4274,7 +4317,10 @@ def _fit_event_study( d_lower=d_lower_val, dose_mean=dose_mean, F=F, - n_units=G_full if weighted_es else n_units, + # Review R4 P2: report positive-weight contributing count + # (matches n_obs_per_horizon; full-design size surfaces + # through survey_metadata.n_psu / effective_n / etc.). + n_units=n_units, inference_method=inference_method, vcov_type=vcov_label, cluster_name=cluster_label, diff --git a/docs/methodology/REGISTRY.md b/docs/methodology/REGISTRY.md index 31693825..19bb14a2 100644 --- a/docs/methodology/REGISTRY.md +++ b/docs/methodology/REGISTRY.md @@ -2310,10 +2310,16 @@ Under `survey=SurveyDesign(weights, strata, psu, fpc)`, the variance composes vi 4. **Sup-t distribution**: `sup_t[b] = max_e |t[b, e]|` with finite-mask filtering of degenerate horizons. 5. **Critical value**: `q = quantile(sup_t[finite], 1 - alpha)`. Simultaneous band: `cband_low[e] = att[e] - q · se[e]`. -**Reduction invariant**: at `H=1`, the sup collapses to the marginal and `q → Φ⁻¹(1 - alpha/2) ≈ 1.96` at `alpha=0.05` up to MC noise. Locked by `TestSupTReducesToNormalAtH1` (G=500, B=5000, seed=42, `atol=0.15` on the quantile). +**Reduction invariant**: at `H=1`, the sup collapses to the marginal and `q → Φ⁻¹(1 - alpha/2) ≈ 1.96` at `alpha=0.05` up to MC noise. Locked by `TestSupTReducesToNormalAtH1` (G=500, B=5000, seed=42, `atol=0.15` on the quantile) and `TestEventStudySurveyCband::test_trivial_survey_h1_sup_t_matches_analytical` / `test_stratified_h1_sup_t_matches_analytical` for the trivial-survey and stratified cases respectively. **Scope**: sup-t bootstrap runs only when `aggregate="event_study"` AND `weights=` or `survey=` is supplied AND `cband=True` (default). Unweighted event-study skips the bootstrap entirely — pre-Phase 4.5 B numerical output bit-exactly preserved. Setting `cband=False` on the weighted path disables the bootstrap (useful for smoke-test bit-parity assertions against the unweighted path at uniform weights). +- **Deviation from shared survey-bootstrap contract:** `_sup_t_multiplier_bootstrap` raises `NotImplementedError` on `SurveyDesign(lonely_psu="adjust")` with singleton strata. The shared `generate_survey_multiplier_weights_batch` helper pools singleton PSUs into a pseudo-stratum with NONZERO multipliers, but `compute_survey_if_variance` centers singleton PSU scores at the GLOBAL mean of PSU scores (rather than the pseudo-stratum mean). Matching the two would require a pooled-singleton pseudo-stratum centering transform in the HAD sup-t path that has not been derived. The HAD-specific limitation is scoped to: weighted event-study + `cband=True` + `lonely_psu="adjust"` + at least one singleton stratum. Practitioners can use `lonely_psu="remove"` or `"certainty"` (matches the analytical target bit-exactly on the HAD sup-t path), or pass `cband=False` to skip the simultaneous band. All other survey-bootstrap consumers (CallawaySantAnna, dCDH, SDID) retain full `lonely_psu="adjust"` support through the shared helper. + +- **Deviation: weighted mass-point `vcov_type="classical"` on survey/sup-t paths:** `vcov_type="classical"` raises `NotImplementedError` whenever the mass-point IF matrix is consumed downstream — specifically on `design="mass_point"` + `survey=` (static + event-study) and `design="mass_point"` + `weights=` + `aggregate="event_study"` + `cband=True`. The per-unit 2SLS IF returned by `_fit_mass_point_2sls` is scaled (`sqrt((n-1)/(n-k))`) to match V_HC1 via `compute_survey_if_variance`; mixing it with a classical analytical SE would silently return a V_HC1-targeted variance under a classical label. A classical-aligned IF derivation is queued for a follow-up PR. The allowed weighted-mass-point combinations are: `vcov_type="hc1"` on every path; `vcov_type="classical"` on `weights=` + `aggregate="overall"`, and `weights=` + `aggregate="event_study"` + `cband=False` (no IF consumption). + +- **Deviation: mass-point `cluster=` + `survey=` rejected:** the `survey=` path composes Binder-TSL variance via `compute_survey_if_variance`, which would silently overwrite the CR1 cluster-robust sandwich while result metadata still reports `vcov_type="cr1"`. Narrowed (R4) to apply only to: `design="mass_point"` + `survey=` + `cluster=` (static and event-study), and `design="mass_point"` + `weights=` + `cluster=` + `aggregate="event_study"` + `cband=True` (where the sup-t bootstrap normalizes HC1-scale perturbations by the CR1 analytical SE). The `weights=` shortcut + `cluster=` on `aggregate="overall"` or `aggregate="event_study"` with `cband=False` continues to work — returns the weighted-CR1 pweight sandwich bit-exact with `estimatr::iv_robust(..., weights=, clusters=, se_type="stata")`. + - 2SLS (Design 1 mass-point case): standard 2SLS inference (details not elaborated in the paper). - TWFE with small `G`: HC2 standard errors with Bell-McCaffrey (2002) degrees-of-freedom correction, following Imbens and Kolesar (2016). Used in the Pierce and Schott (2016) application with `G=103`. Added library-wide to `diff_diff/linalg.py` as a new `vcov_type` dispatch (Phase 1a), exposed on `DifferenceInDifferences` and `TwoWayFixedEffects`. - Bootstrap: wild bootstrap with Mammen (1993) two-point weights is used for the Stute test (see Diagnostics below), NOT for the main WAS estimator. Reuses the existing `diff_diff.bootstrap_utils.generate_bootstrap_weights(..., weight_type="mammen")` helper. diff --git a/tests/test_had.py b/tests/test_had.py index 8098b5ae..3972b85e 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -4851,7 +4851,13 @@ def test_mass_point_survey_plus_cluster_rejected_static(self): """Review R2 P1: mass-point + (weights= or survey=) + cluster= must raise NotImplementedError on the static path. Previously the weighted path silently overrode the CR1 SE with Binder-TSL - while the result still reported vcov_type='cr1'.""" + while the result still reported vcov_type='cr1'. Narrowed in + R4: only survey= + cluster= is rejected (weights= shortcut + + cluster= is the weighted-CR1 pweight sandwich, which is valid + and parity-tested). This test therefore uses survey= to + trigger the narrowed guard.""" + from diff_diff.survey import SurveyDesign + rng = np.random.default_rng(0) G = 200 d = np.concatenate([np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)]) @@ -4864,20 +4870,15 @@ def test_mass_point_survey_plus_cluster_rejected_static(self): "dose": np.column_stack([np.zeros(G), d]).ravel(), "outcome": np.column_stack([np.zeros(G), dy]).ravel(), "state": np.repeat(np.arange(G) // 20, 2), + "w": np.ones(2 * G), } ) + sd = SurveyDesign(weights="w") with warnings.catch_warnings(): warnings.simplefilter("ignore", UserWarning) est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1", cluster="state") with pytest.raises(NotImplementedError, match="cluster"): - est.fit( - panel, - "outcome", - "dose", - "period", - "unit", - weights=np.ones(panel.shape[0]), - ) + est.fit(panel, "outcome", "dose", "period", "unit", survey=sd) def test_mass_point_survey_plus_cluster_rejected_event_study(self): """Review R2 P1 (event-study arm): same rejection must fire on @@ -5133,3 +5134,157 @@ def test_mass_point_classical_event_study_cband_false_accepts(self): ) assert r.variance_formula == "pweight_2sls" assert r.cband_crit_value is None + + def test_mass_point_weights_plus_cluster_shortcut_allowed(self): + """Review R4 P1: weights= shortcut + cluster= is the weighted-CR1 + pweight sandwich (parity-tested vs estimatr::iv_robust + se_type='stata') and must NOT be rejected. Narrowed guard only + rejects survey= + cluster=, not weights= + cluster=.""" + rng = np.random.default_rng(40) + G = 300 + d = np.concatenate([np.full(60, 0.3), rng.uniform(0.3, 1.0, G - 60)]) + rng.shuffle(d) + dy = 2.0 * d + 0.3 * rng.standard_normal(G) + cluster = np.repeat(np.arange(G // 20), 20) + rng.shuffle(cluster) + panel = pd.DataFrame( + { + "unit": np.repeat(np.arange(G), 2), + "period": np.tile([1, 2], G), + "dose": np.column_stack([np.zeros(G), d]).ravel(), + "outcome": np.column_stack([np.zeros(G), dy]).ravel(), + "state": np.repeat(cluster, 2), + } + ) + w_unit = 1.0 + 0.3 * rng.standard_normal(G) + w_unit = np.clip(w_unit, 0.1, None) + w_row = panel["unit"].map(lambda g: w_unit[g]).to_numpy() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point", vcov_type="hc1", cluster="state") + r = est.fit(panel, "outcome", "dose", "period", "unit", weights=w_row) + assert r.vcov_type == "cr1" + assert r.cluster_name == "state" + assert r.variance_formula == "pweight_2sls" + assert np.isfinite(r.se) and r.se > 0 + + def test_mass_point_weights_plus_cluster_event_study_cband_false_allowed(self): + """Review R4 P1: event-study + weights= + cluster= + cband=False + is valid (no IF consumption; per-horizon CR1 sandwich).""" + rng = np.random.default_rng(41) + G, T = 180, 4 + d_mp = np.concatenate([np.full(36, 0.3), rng.uniform(0.3, 1.0, G - 36)]) + rng.shuffle(d_mp) + cluster_per_unit = np.repeat(np.arange(G // 15), 15) + rng.shuffle(cluster_per_unit) + rows = [] + for t in range(T): + for g in range(G): + dose = d_mp[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y, cluster_per_unit[g])) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome", "state"]) + w_unit = 1.0 + 0.3 * rng.standard_normal(G) + w_unit = np.clip(w_unit, 0.1, None) + w_row = panel["unit"].map(lambda g: w_unit[g]).to_numpy() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD( + design="mass_point", + vcov_type="hc1", + cluster="state", + seed=0, + ) + r = est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=w_row, + cband=False, + ) + assert r.vcov_type == "cr1" + assert r.cluster_name == "state" + assert r.variance_formula == "pweight_2sls" + assert r.cband_crit_value is None + + def test_mass_point_weights_plus_cluster_event_study_cband_true_rejected(self): + """Review R4 P1: event-study + weights= + cluster= + cband=True + IS rejected (HC1-scale bootstrap perturbations normalized by + CR1 analytical SE would mix variance families in the bootstrap + t-distribution).""" + rng = np.random.default_rng(42) + G, T = 180, 4 + d_mp = np.concatenate([np.full(36, 0.3), rng.uniform(0.3, 1.0, G - 36)]) + rng.shuffle(d_mp) + cluster_per_unit = np.repeat(np.arange(G // 15), 15) + rng.shuffle(cluster_per_unit) + rows = [] + for t in range(T): + for g in range(G): + dose = d_mp[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y, cluster_per_unit[g])) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome", "state"]) + w_unit = 1.0 + 0.3 * rng.standard_normal(G) + w_unit = np.clip(w_unit, 0.1, None) + w_row = panel["unit"].map(lambda g: w_unit[g]).to_numpy() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD( + design="mass_point", + vcov_type="hc1", + cluster="state", + seed=0, + n_bootstrap=100, + ) + with pytest.raises(NotImplementedError, match="cband=True"): + est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=w_row, + cband=True, + ) + + def test_event_study_zero_weight_units_excluded_from_n_units(self): + """Review R4 P2: weighted event-study reports the POSITIVE-WEIGHT + contributing sample size in n_units / n_obs_per_horizon (matches + the static-path n_obs contract). survey_metadata still carries + the full-design effective_n / n_psu.""" + rng = np.random.default_rng(50) + G, T = 200, 4 + d_post = rng.uniform(0.0, 1.0, G) + rows = [] + for t in range(T): + for g in range(G): + dose = d_post[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) + w_unit = np.ones(G) + w_unit[:30] = 0.0 # 30 zero-weight units; 170 contribute. + w_row = panel["unit"].map(lambda g: w_unit[g]).to_numpy() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="continuous_at_zero", seed=0) + r = est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=w_row, + cband=False, + ) + assert r.n_units == 170, ( + f"n_units should report positive-weight contributing count " + f"(170), not full-design size (200); got {r.n_units}" + ) + assert np.all(r.n_obs_per_horizon == 170) From 8d7cf9401186922e9996c7209cb14ad684b7fc6c Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 18:43:10 -0400 Subject: [PATCH 6/9] Address PR #363 R5 review (1 P1 + 1 P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R5 P1 (classical-guard bypass): the classical-on-IF-consumption rejection was only checking the RAW vcov_type kwarg, which missed the default mapping (vcov_type=None, robust=False → classical). Re-ordered both mass-point branches (static + event-study) to resolve vcov_requested first, then gate the rejection on the resolved value. Default-path users who pass HeterogeneousAdoptionDiD(design='mass_point').fit(..., survey=...) without an explicit vcov_type now hit the same clear NotImplementedError as the explicit vcov_type='classical' path. R5 P2 (regression coverage): added three tests covering the default-path cases that slipped through the previous guard: - test_mass_point_default_vcov_survey_rejected_static: default vcov_type=None + survey= on mass-point → NotImplementedError - test_mass_point_default_vcov_event_study_cband_rejected: default vcov_type=None + weights= + cband=True on event-study → rejects - test_mass_point_default_vcov_robust_true_survey_allowed: complement confirms robust=True (default mapping → hc1) survey= is allowed Co-Authored-By: Claude Opus 4.7 (1M context) --- diff_diff/had.py | 88 +++++++++++++++++++++++++-------------------- tests/test_had.py | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 38 deletions(-) diff --git a/diff_diff/had.py b/diff_diff/had.py index 7153e093..5e01af32 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -3340,19 +3340,31 @@ def fit( f"robust + survey inference is deferred to a " f"follow-up PR." ) - # Review R3 P1: the weighted mass-point path returns an - # HC1-scaled influence function (the IF scale convention + # Resolve the EFFECTIVE vcov family first (vcov_type_arg, + # with default-mapping from robust= when unset). Reject the + # classical-on-IF-consumption combination against the + # resolved value, NOT the raw kwarg, so the default + # `vcov_type=None, robust=False` case (which maps to + # classical) hits the guard too (review R5 P1 — previous + # fix only fired on explicit vcov_type='classical'). + if vcov_type_arg is None: + # Backward-compat: robust=True -> hc1, robust=False -> classical. + vcov_requested = "hc1" if robust_arg else "classical" + else: + vcov_requested = vcov_type_arg.lower() + # Review R3 P1 / R5 P1: the weighted mass-point path returns + # an HC1-scaled influence function (IF scale convention # locks compute_survey_if_variance(psi, trivial) ≈ V_HC1[1,1] # via the sqrt((n-1)/(n-k)) factor in _fit_mass_point_2sls). # On the survey= path the analytical SE is ALWAYS overwritten - # with that HC1-scale Binder-TSL composition, so - # vcov_type="classical" + survey= would silently report an - # HC1-target SE under a classical label. Reject until a + # with that HC1-scale Binder-TSL composition, so effective + # classical + survey= would silently report an HC1-target + # SE under a classical label. Reject until a # classical-aligned IF is derived. - _vcov_requested_lc = vcov_type_arg.lower() if vcov_type_arg is not None else None - if _vcov_requested_lc == "classical" and resolved_survey_unit_full is not None: + if vcov_requested == "classical" and resolved_survey_unit_full is not None: raise NotImplementedError( - "vcov_type='classical' + survey= on " + "vcov_type='classical' (resolved — either explicit or " + "from the default robust=False mapping) + survey= on " "design='mass_point' is not yet supported: the " "survey path composes Binder-TSL variance via the " "HC1-scale influence function, which targets V_HC1 " @@ -3362,11 +3374,6 @@ def fit( "robust=True) on the weighted path; a classical-" "aligned IF derivation is queued for a follow-up PR." ) - if vcov_type_arg is None: - # Backward-compat: robust=True -> hc1, robust=False -> classical. - vcov_requested = "hc1" if robust_arg else "classical" - else: - vcov_requested = vcov_type_arg.lower() # Phase 4.5 B: accept weights_unit (None on unweighted fits). # return_influence=True only on the survey= path because # Binder-TSL composition consumes the IF; the weights= @@ -4048,36 +4055,41 @@ def _fit_event_study( # ---- Resolve vcov label for mass-point ---- if resolved_design == "mass_point": - # Review R3 P1 (event-study arm): reject vcov_type="classical" - # when the weighted path will compute the IF (always on - # survey= path; on weights= shortcut when cband=True the - # bootstrap divides HC1-scale perturbations by per-horizon - # analytical SE, so classical SE would give wrong t-stats). - # Matches the static-path rejection — weighted mass-point - # paths use the HC1-scale IF convention uniformly. - _vcov_requested_lc = vcov_type_arg.lower() if vcov_type_arg is not None else None + # Resolve the EFFECTIVE vcov family first (review R5 P1 — + # previous fix only fired on explicit vcov_type='classical' + # and missed the default vcov_type=None, robust=False → + # 'classical' mapping). + if vcov_type_arg is None: + vcov_requested = "hc1" if robust_arg else "classical" + else: + vcov_requested = vcov_type_arg.lower() + # Review R3/R5 P1 (event-study arm): reject effective + # classical when the weighted path will compute the IF + # (always on survey= path; on weights= shortcut when + # cband=True the bootstrap divides HC1-scale perturbations + # by per-horizon analytical SE, so classical SE would + # give wrong t-stats). Matches the static-path rejection — + # weighted mass-point paths use the HC1-scale IF + # convention uniformly. _uses_if_matrix = resolved_survey_unit_full is not None or ( weights_unit_full is not None and cband ) - if _vcov_requested_lc == "classical" and _uses_if_matrix: + if vcov_requested == "classical" and _uses_if_matrix: raise NotImplementedError( - "vcov_type='classical' + weights/survey= on " - "design='mass_point' event-study is not yet " - "supported: the per-horizon IF matrix is HC1-scale " - "(targets V_HC1 via compute_survey_if_variance) and " - "mixing it with a classical analytical SE — either " - "through the survey Binder-TSL override or the " - "sup-t bootstrap normalization — would produce an " - "inconsistent variance family. Use " - "vcov_type='hc1' (or leave vcov_type unset with " - "robust=True) on the weighted event-study path, or " - "pass cband=False to skip the bootstrap on the " - "weights= shortcut." + "vcov_type='classical' (resolved — either explicit " + "or from the default robust=False mapping) + " + "weights/survey= on design='mass_point' event-study " + "is not yet supported: the per-horizon IF matrix is " + "HC1-scale (targets V_HC1 via " + "compute_survey_if_variance) and mixing it with a " + "classical analytical SE — either through the " + "survey Binder-TSL override or the sup-t bootstrap " + "normalization — would produce an inconsistent " + "variance family. Use vcov_type='hc1' (or leave " + "vcov_type unset with robust=True) on the weighted " + "event-study path, or pass cband=False to skip the " + "bootstrap on the weights= shortcut." ) - if vcov_type_arg is None: - vcov_requested = "hc1" if robust_arg else "classical" - else: - vcov_requested = vcov_type_arg.lower() inference_method = "analytical_2sls" vcov_label: Optional[str] = "cr1" if cluster_arg is not None else vcov_requested cluster_label: Optional[str] = cluster_arg if cluster_arg is not None else None diff --git a/tests/test_had.py b/tests/test_had.py index 3972b85e..51323277 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -5288,3 +5288,93 @@ def test_event_study_zero_weight_units_excluded_from_n_units(self): f"(170), not full-design size (200); got {r.n_units}" ) assert np.all(r.n_obs_per_horizon == 170) + + def test_mass_point_default_vcov_survey_rejected_static(self): + """Review R5 P1: the effective-classical rejection must fire + even when the user does NOT pass vcov_type explicitly — the + default mapping (vcov_type=None, robust=False) resolves to + 'classical', and that default must NOT silently slip through + on the survey= mass-point path.""" + from diff_diff.survey import SurveyDesign + + rng = np.random.default_rng(60) + G = 200 + d = np.concatenate([np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)]) + rng.shuffle(d) + dy = 2.0 * d + 0.3 * rng.standard_normal(G) + panel = pd.DataFrame( + { + "unit": np.repeat(np.arange(G), 2), + "period": np.tile([1, 2], G), + "dose": np.column_stack([np.zeros(G), d]).ravel(), + "outcome": np.column_stack([np.zeros(G), dy]).ravel(), + "w": np.ones(2 * G), + } + ) + sd = SurveyDesign(weights="w") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + # Default vcov_type=None, robust=False → resolves to classical. + est = HeterogeneousAdoptionDiD(design="mass_point") + with pytest.raises(NotImplementedError, match="classical"): + est.fit(panel, "outcome", "dose", "period", "unit", survey=sd) + + def test_mass_point_default_vcov_event_study_cband_rejected(self): + """Review R5 P1 (event-study arm): default vcov_type=None + + weights= + cband=True must hit the effective-classical + rejection. Previous guard only checked explicit + vcov_type='classical'.""" + rng = np.random.default_rng(61) + G, T = 150, 4 + d_mp = np.concatenate([np.full(30, 0.3), rng.uniform(0.3, 1.0, G - 30)]) + rng.shuffle(d_mp) + rows = [] + for t in range(T): + for g in range(G): + dose = d_mp[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + # Default vcov_type=None, robust=False. + est = HeterogeneousAdoptionDiD(design="mass_point", seed=0, n_bootstrap=100) + with pytest.raises(NotImplementedError, match="classical"): + est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=np.ones(panel.shape[0]), + cband=True, + ) + + def test_mass_point_default_vcov_robust_true_survey_allowed(self): + """Complement: robust=True on the default path resolves to + hc1, so the survey= mass-point fit is allowed with no explicit + vcov_type.""" + from diff_diff.survey import SurveyDesign + + rng = np.random.default_rng(62) + G = 200 + d = np.concatenate([np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)]) + rng.shuffle(d) + dy = 2.0 * d + 0.3 * rng.standard_normal(G) + panel = pd.DataFrame( + { + "unit": np.repeat(np.arange(G), 2), + "period": np.tile([1, 2], G), + "dose": np.column_stack([np.zeros(G), d]).ravel(), + "outcome": np.column_stack([np.zeros(G), dy]).ravel(), + "w": np.ones(2 * G), + } + ) + sd = SurveyDesign(weights="w") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="mass_point", robust=True) + r = est.fit(panel, "outcome", "dose", "period", "unit", survey=sd) + assert r.vcov_type == "hc1" + assert r.variance_formula == "survey_binder_tsl_2sls" From 2c325f3f22e98e96552ac4b16dd42d8c1f554c49 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 18:53:01 -0400 Subject: [PATCH 7/9] Address PR #363 R6 review (1 P2 + 1 P3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R6 P2 (event-study opt-out overhead): cband=False on the weighted event-study path no longer allocates the stacked (G, H) IF matrix or forces per-horizon IF return on the weights= shortcut. Split two flags internally: needs_per_horizon_if = survey= path OR (weights= AND cband=True) needs_stacked_if_matrix = weights= AND cband=True (alias for weighted_es AND cband) - Psi allocation gated on needs_stacked_if_matrix. - _fit_continuous force_return_influence gated on (needs_stacked_if_matrix AND resolved_survey_unit_full is None) — under survey= path, _fit_continuous returns the IF anyway via its resolved_survey_unit gate, so no extra cost. - _fit_mass_point_2sls return_influence gated on needs_per_horizon_if — survey= path needs the per-horizon IF for the Binder-TSL override regardless of cband. Net effect: cband=False + weights= shortcut + weighted_es skips the O(GH) Psi allocation and the per-horizon IF work entirely. cband=True paths and survey= paths unchanged. R6 P3 (event-study survey= integration coverage): added two end-to-end integration tests for the previously-unguarded positive-path estimator-level survey= + aggregate='event_study' dispatch: - test_survey_event_study_continuous_end_to_end: continuous_at_zero + SurveyDesign(strata='stratum') — asserts variance_formula= 'survey_binder_tsl', survey_metadata.df_survey=G-n_strata, cband_* populated, PSU dispatch through _aggregate_unit_resolved_survey. - test_survey_event_study_mass_point_end_to_end: mass_point + SurveyDesign(strata=...) — asserts variance_formula= 'survey_binder_tsl_2sls' and that the 2SLS IF flows through per-horizon Binder-TSL + sup-t bootstrap. Co-Authored-By: Claude Opus 4.7 (1M context) --- diff_diff/had.py | 55 ++++++++++++++++--------- tests/test_had.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 19 deletions(-) diff --git a/diff_diff/had.py b/diff_diff/had.py index 5e01af32..748b0fe6 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -4127,11 +4127,18 @@ def _fit_event_study( # carries the full-design effective_n / n_psu / etc.). n_obs_arr = np.full(n_horizons, n_units, dtype=np.int64) - # Per-horizon IF matrix on the weighted path (shape (G, H)); drives - # both per-horizon Binder-TSL variance (already composed inside - # ``_fit_continuous`` for continuous, or explicitly below for - # mass-point) AND the shared-PSU multiplier bootstrap for sup-t. - if weighted_es: + # Two IF-consumption flags (review R6 P2): the PER-HORIZON IF is + # needed when the survey= path composes Binder-TSL variance (via + # compute_survey_if_variance inside _fit_continuous or the + # mass-point override below); the STACKED (G, H) IF matrix is + # needed only when the sup-t multiplier bootstrap runs + # (``cband=True`` on the weighted path). Splitting them avoids + # allocating / filling Psi on the common opt-out path + # ``cband=False`` + weights= shortcut, where no IF consumer + # exists. + needs_per_horizon_if = resolved_survey_unit_full is not None or (weighted_es and cband) + needs_stacked_if_matrix = weighted_es and cband + if needs_stacked_if_matrix: Psi = np.full((G_full, n_horizons), np.nan, dtype=np.float64) else: Psi = np.zeros((0, 0), dtype=np.float64) # sentinel, not used @@ -4173,23 +4180,28 @@ def _fit_event_study( d_lower_val, weights_arr=weights_unit_full, resolved_survey_unit=resolved_survey_unit_full, - # Force IF return on the weighted event-study path - # (needed for the sup-t bootstrap). Does NOT change - # the per-horizon SE formula — that still follows - # the static-path convention (Binder-TSL under - # survey=, bc_fit.se_robust under weights= shortcut). - force_return_influence=weighted_es, + # Force IF return only when the sup-t bootstrap + # needs the stacked matrix AND the survey= gate + # won't already produce it. Under survey= path, + # _fit_continuous returns the IF automatically + # (resolved_survey_unit_full != None); under the + # weights= shortcut + cband=True, force it here; + # otherwise skip the O(G) IF work (review R6 P2). + force_return_influence=( + needs_stacked_if_matrix and resolved_survey_unit_full is None + ), ) if bc_fits is not None: bc_fits.append(bc_fit_e) if bw_diags is not None: bw_diags.append(bw_diag_e) - # Collect per-unit IF on β̂-scale (psi_bc / den) so the - # sup-t bootstrap operates on the same θ̂-scale IF that - # the analytical variance sees. Per continuous-path - # construction in _fit_continuous, bc_fit.influence_function - # is the numerator IF; dividing by |den| yields the β̂ IF. - if weighted_es and bc_fit_e is not None and bc_fit_e.influence_function is not None: + # Collect per-unit IF on β̂-scale (psi_bc / den) into + # Psi ONLY when the sup-t bootstrap will consume it. + if ( + needs_stacked_if_matrix + and bc_fit_e is not None + and bc_fit_e.influence_function is not None + ): if resolved_design == "continuous_at_zero": den_e = float(np.average(d_arr_full, weights=weights_unit_full)) else: @@ -4209,7 +4221,12 @@ def _fit_event_study( cluster_arr, vcov_requested, weights=weights_unit_full, - return_influence=resolved_survey_unit_full is not None or weighted_es, + # Return IF only when a consumer exists: survey= + # path needs it for per-horizon Binder-TSL override; + # weights= shortcut + cband=True needs it for the + # bootstrap. weights= shortcut + cband=False skips + # IF computation entirely (review R6 P2). + return_influence=needs_per_horizon_if, ) # Survey path: override analytical sandwich SE with # Binder-TSL via compute_survey_if_variance (matches @@ -4222,7 +4239,7 @@ def _fit_event_study( se_e = float(np.sqrt(v_survey)) else: se_e = float("nan") - if weighted_es and psi_e is not None: + if needs_stacked_if_matrix and psi_e is not None: Psi[:, i] = psi_e else: raise ValueError(f"Internal error: unhandled design={resolved_design!r}.") diff --git a/tests/test_had.py b/tests/test_had.py index 51323277..7e0211d9 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -5351,6 +5351,108 @@ def test_mass_point_default_vcov_event_study_cband_rejected(self): cband=True, ) + def test_survey_event_study_continuous_end_to_end(self): + """Review R6 P3: estimator-level + ``fit(aggregate='event_study', survey=SurveyDesign(...))`` + integration lock for the continuous path. Verifies + variance_formula, survey_metadata.df_survey (t-inference path), + cband_* population, and stratified PSU dispatch through + _aggregate_unit_resolved_survey.""" + from diff_diff.survey import SurveyDesign + + rng = np.random.default_rng(70) + G, T, n_strata = 200, 4, 4 + d_post = rng.uniform(0.0, 1.0, G) + strata_per_unit = np.repeat(np.arange(n_strata), G // n_strata) + rng.shuffle(strata_per_unit) + rows = [] + for t in range(T): + for g in range(G): + dose = d_post[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y, strata_per_unit[g])) + panel = pd.DataFrame( + rows, + columns=["unit", "period", "dose", "outcome", "stratum"], + ) + w_unit = 1.0 + 0.3 * np.abs(rng.standard_normal(G)) + panel["w"] = panel["unit"].map(lambda g: w_unit[g]) + sd = SurveyDesign(weights="w", strata="stratum") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="continuous_at_zero", seed=0, n_bootstrap=200) + r = est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + survey=sd, + ) + assert r.variance_formula == "survey_binder_tsl" + assert r.survey_metadata is not None + assert r.survey_metadata.n_strata == n_strata + assert r.survey_metadata.n_psu == G + assert r.survey_metadata.df_survey == G - n_strata + assert r.cband_crit_value is not None and np.isfinite(r.cband_crit_value) + assert r.cband_method == "multiplier_bootstrap" + assert r.cband_n_bootstrap == 200 + assert r.cband_low is not None and r.cband_high is not None + assert np.all(np.isfinite(r.se)) + + def test_survey_event_study_mass_point_end_to_end(self): + """Review R6 P3: estimator-level + ``fit(design='mass_point', aggregate='event_study', + survey=...)`` integration lock. Verifies + variance_formula='survey_binder_tsl_2sls' and that the + weighted 2SLS IF flows correctly through per-horizon + Binder-TSL + sup-t bootstrap.""" + from diff_diff.survey import SurveyDesign + + rng = np.random.default_rng(71) + G, T = 200, 4 + d_mp = np.concatenate([np.full(40, 0.3), rng.uniform(0.3, 1.0, G - 40)]) + rng.shuffle(d_mp) + strata_per_unit = np.repeat(np.arange(4), G // 4) + rng.shuffle(strata_per_unit) + rows = [] + for t in range(T): + for g in range(G): + dose = d_mp[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y, strata_per_unit[g])) + panel = pd.DataFrame( + rows, + columns=["unit", "period", "dose", "outcome", "stratum"], + ) + w_unit = 1.0 + 0.3 * np.abs(rng.standard_normal(G)) + panel["w"] = panel["unit"].map(lambda g: w_unit[g]) + sd = SurveyDesign(weights="w", strata="stratum") + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD( + design="mass_point", + vcov_type="hc1", + seed=0, + n_bootstrap=200, + ) + r = est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + survey=sd, + ) + assert r.variance_formula == "survey_binder_tsl_2sls" + assert r.survey_metadata is not None + assert r.survey_metadata.n_strata == 4 + assert r.cband_crit_value is not None and np.isfinite(r.cband_crit_value) + assert r.cband_method == "multiplier_bootstrap" + assert np.all(np.isfinite(r.se)) + def test_mass_point_default_vcov_robust_true_survey_allowed(self): """Complement: robust=True on the default path resolves to hc1, so the survey= mass-point fit is allowed with no explicit From 2431cce6e010b0e809ee75cdc6299f7869c3bbc8 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 19:08:00 -0400 Subject: [PATCH 8/9] Address PR #363 R7 review (1 P0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R7 P0 (sup-t under-scaling on weights= shortcut): the per-unit IF returned by _fit_continuous / _fit_mass_point_2sls is HC1-scaled per the PR #359 convention — compute_survey_if_variance(psi, trivial_resolved) ≈ V_HC1. Routing the weights= shortcut through the unit-level ``resolved_survey=None`` branch of _sup_t_multiplier_bootstrap normalized against raw sum(psi²) = ((n-1)/n) · V_HC1, producing silently too-narrow simultaneous bands. Fix: when the weighted event-study + cband=True path runs, always route the sup-t bootstrap through a ResolvedSurveyDesign. On the weights= shortcut (no user-supplied survey), construct a synthetic trivial resolved (pweight, no strata/psu/fpc, lonely_psu='remove') so the centered + sqrt(n/(n-1))-corrected survey-aware branch fires. The no-strata/no-PSU path inside that branch falls through to the "single implicit stratum — demean across all PSUs, scale by sqrt(n_psu/(n_psu-1))" block, which gives Var_xi(xi @ Psi_psu) ≈ V_HC1 as the IF scale convention requires. Net effect: weights= shortcut and survey=SurveyDesign(weights=...) now target the SAME variance family in the bootstrap (~atol=0.05 between the two quantiles at matching seeds, bounded by the bc_fit.se_robust vs Binder-TSL per-horizon SE convergence tolerance from PR #359). Previously the shortcut was under-scaled by sqrt(n/(n-1)) relative to the analytical HC1 target. Regression tests (+2): - test_weights_shortcut_mass_point_h1_cband_matches_normal: helper- level H=1 lock with mass-point HC1-scaled IF + synthetic trivial resolved. q → Phi^-1(0.975) ≈ 1.96 at atol=0.15 (MC noise at B=5000). The pre-fix under-scaling would have produced q ≈ 1.94 (systematic drift outside MC noise). - test_weights_shortcut_cband_matches_trivial_survey: weights= shortcut and survey=SurveyDesign(weights='w') event-study cband quantiles agree within atol=0.05 on the same DGP / seed. Co-Authored-By: Claude Opus 4.7 (1M context) --- diff_diff/had.py | 32 +++++++++++- tests/test_had.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/diff_diff/had.py b/diff_diff/had.py index 748b0fe6..945f3047 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -4261,11 +4261,41 @@ def _fit_event_study( cband_method_label: Optional[str] = None cband_n_bootstrap_eff: Optional[int] = None if weighted_es and cband and n_horizons >= 1: + # Review R7 P0: the per-unit influence function returned by + # _fit_continuous / _fit_mass_point_2sls is HC1-scaled per + # the PR #359 convention — compute_survey_if_variance(psi, + # trivial_resolved) ≈ V_HC1. Routing the weights= shortcut + # through the unit-level ``resolved_survey=None`` branch of + # _sup_t_multiplier_bootstrap would normalize against raw + # sum(psi²) = ((n-1)/n) · V_HC1, producing silently too- + # narrow simultaneous bands. Construct a synthetic trivial + # ResolvedSurveyDesign on the weights= shortcut so the + # bootstrap always fires the survey-aware branch (centered + # + sqrt(n/(n-1))-corrected), matching the variance family + # of the analytical per-horizon SE. + if resolved_survey_unit_full is not None: + resolved_for_bootstrap: Any = resolved_survey_unit_full + else: + from diff_diff.survey import ResolvedSurveyDesign + + assert weights_unit_full is not None # weighted_es invariant + resolved_for_bootstrap = ResolvedSurveyDesign( + weights=weights_unit_full, + weight_type="pweight", + strata=None, + psu=None, + fpc=None, + n_strata=1, + n_psu=int(weights_unit_full.shape[0]), + lonely_psu="remove", + combined_weights=True, + mse=False, + ) q, cband_low_arr, cband_high_arr, _n_valid = _sup_t_multiplier_bootstrap( influence_matrix=Psi, att_per_horizon=att_arr, se_per_horizon=se_arr, - resolved_survey=resolved_survey_unit_full, + resolved_survey=resolved_for_bootstrap, n_bootstrap=n_bootstrap_eff, alpha=float(self.alpha), seed=seed_eff, diff --git a/tests/test_had.py b/tests/test_had.py index 7e0211d9..b499cf3c 100644 --- a/tests/test_had.py +++ b/tests/test_had.py @@ -5453,6 +5453,132 @@ def test_survey_event_study_mass_point_end_to_end(self): assert r.cband_method == "multiplier_bootstrap" assert np.all(np.isfinite(r.se)) + def test_weights_shortcut_mass_point_h1_cband_matches_normal(self): + """Review R7 P0 (helper-level lock): at H=1 with the mass-point + HC1-scaled IF + synthetic trivial ResolvedSurveyDesign (which + matches what the weights= shortcut now routes through in + _fit_event_study), the sup-t critical value must reduce to the + Normal quantile. Previously the shortcut used the unit-level + branch of _sup_t_multiplier_bootstrap (resolved_survey=None) + which normalized against raw sum(psi²) = ((n-1)/n) · V_HC1 on + the HC1-scaled IF, producing silently too-narrow bands.""" + import scipy.stats + + from diff_diff.had import ( + _fit_mass_point_2sls, + _sup_t_multiplier_bootstrap, + ) + from diff_diff.survey import ResolvedSurveyDesign, compute_survey_if_variance + + rng = np.random.default_rng(72) + G = 500 + d = np.concatenate([np.full(100, 0.3), rng.uniform(0.3, 1.0, G - 100)]) + rng.shuffle(d) + dy = 2.0 * d + 0.3 * rng.standard_normal(G) + w = np.ones(G) + # Fit weighted 2SLS; get the HC1-scale per-unit IF. + _beta, se_analytical, psi = _fit_mass_point_2sls( + d, dy, 0.3, None, "hc1", weights=w, return_influence=True + ) + # Synthetic trivial resolved matching what _fit_event_study + # now constructs for the weights= shortcut. + trivial = ResolvedSurveyDesign( + weights=w, + weight_type="pweight", + strata=None, + psu=None, + fpc=None, + n_strata=1, + n_psu=G, + lonely_psu="remove", + combined_weights=True, + mse=False, + ) + # Sanity: bootstrap target variance matches analytical HC1. + V_analytical = compute_survey_if_variance(psi, trivial) + np.testing.assert_allclose(V_analytical, se_analytical**2, atol=1e-10, rtol=1e-10) + # H=1 sup-t with the trivial routing → Normal quantile. + q, _, _, _ = _sup_t_multiplier_bootstrap( + influence_matrix=psi.reshape(-1, 1), + att_per_horizon=np.zeros(1), + se_per_horizon=np.array([se_analytical]), + resolved_survey=trivial, + n_bootstrap=5000, + alpha=0.05, + seed=42, + ) + expected = float(scipy.stats.norm.ppf(0.975)) + # B=5000 MC noise on the tail quantile ~ 0.03-0.05; atol=0.15 + # tolerates that noise but would reject the sqrt((n-1)/n) + # under-scaling that the old unit-level branch produced + # (systematic drift toward smaller q). + assert abs(q - expected) < 0.15, ( + f"weights= shortcut-equivalent H=1 sup-t should match " + f"Phi^-1(0.975)={expected:.4f}; got q={q:.4f}. Likely " + f"sqrt(n/(n-1)) correction missing." + ) + + def test_weights_shortcut_cband_matches_trivial_survey(self): + """Review R7 P0 complement: ``weights=w`` shortcut and + ``survey=SurveyDesign(weights='w')`` must target the same + variance family, so their sup-t critical values should agree + up to small per-horizon SE convergence (bc_fit.se_robust on + the shortcut vs sqrt(compute_survey_if_variance) on survey=, + which match at atol=1e-10 per PR #359 but propagate into the + t-statistic ratio in the bootstrap sup).""" + from diff_diff.survey import SurveyDesign + + rng = np.random.default_rng(73) + G, T = 150, 4 + d_post = rng.uniform(0.0, 1.0, G) + rows = [] + for t in range(T): + for g in range(G): + dose = d_post[g] if t == T - 1 else 0.0 + y = 0.2 * t + (2.0 * dose if t == T - 1 else 0.0) + 0.5 * rng.standard_normal() + rows.append((g, t, dose, y)) + panel = pd.DataFrame(rows, columns=["unit", "period", "dose", "outcome"]) + w_unit = 1.0 + 0.3 * np.abs(rng.standard_normal(G)) + panel["w"] = panel["unit"].map(lambda g: w_unit[g]) + w_row = panel["w"].to_numpy() + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + est = HeterogeneousAdoptionDiD(design="continuous_at_zero", seed=42, n_bootstrap=2000) + r_weights = est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + weights=w_row, + ) + r_survey = est.fit( + panel, + "outcome", + "dose", + "period", + "unit", + aggregate="event_study", + survey=SurveyDesign(weights="w"), + ) + # Under the R7 P0 fix, both paths use the same bootstrap + # target variance; the remaining quantile gap comes from the + # analytical per-horizon SE formula (bc_fit.se_robust on + # shortcut vs Binder-TSL on survey=) which propagates into + # t-stat normalization. The PR #359 IF scale invariant bounds + # that gap at ~0.1-1%, so the quantiles should agree within + # absolute tolerance ~0.05 (the old under-scaled path + # produced ~6-10% systematic drift, well outside this bound). + assert abs(r_weights.cband_crit_value - r_survey.cband_crit_value) < 0.05, ( + f"weights= shortcut q={r_weights.cband_crit_value:.4f} vs " + f"survey= q={r_survey.cband_crit_value:.4f} should agree " + f"within the Binder-TSL vs se_robust convergence tolerance " + f"(~atol=0.05). Larger drift signals the R7 P0 under-" + f"scaling regressed." + ) + def test_mass_point_default_vcov_robust_true_survey_allowed(self): """Complement: robust=True on the default path resolves to hc1, so the survey= mass-point fit is allowed with no explicit From 1b41a547756db91120d1df34b8e8a277dda03f07 Mon Sep 17 00:00:00 2001 From: igerber Date: Fri, 24 Apr 2026 19:16:27 -0400 Subject: [PATCH 9/9] Address PR #363 R8 review (1 P3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R8 P3 (doc-consistency drift): prose in _fit_event_study docstring, REGISTRY sup-t bullet, and CHANGELOG entry still described the pre-R7-fix weighted-path behavior (Binder-TSL for all weighted fits; raw unit-level shortcut bootstrap). Updated all three to match the actual implementation: - _fit_event_study docstring: distinguishes survey= path (Binder-TSL) from weights= shortcut (analytical CCT-2014 / 2SLS pweight sandwich), documents trivial-survey bootstrap routing for cband. - REGISTRY.md: new "weights= shortcut ↔ bootstrap routing" bullet under the sup-t section explaining the synthetic trivial ResolvedSurveyDesign construction and why (centered + sqrt(n/(n-1))-corrected branch targets V_HC1, raw unit-level would give ((n-1)/n) · V_HC1 under-scaling). - CHANGELOG.md: per-horizon variance on weights= shortcut clarified as analytical (not Binder-TSL); sup-t routing through trivial resolved documented. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 +- diff_diff/had.py | 30 ++++++++++++++++++++++++------ docs/methodology/REGISTRY.md | 2 ++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d6bfb9b..5f00c4f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- **`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. +- **`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 per-horizon variance: `survey=` path composes Binder-TSL via `compute_survey_if_variance`; `weights=` shortcut uses the analytical weighted-robust SE (continuous: CCT-2014 `bc_fit.se_robust / |den|`; mass-point: weighted 2SLS pweight sandwich from `_fit_mass_point_2sls` — HC1 / classical / CR1). `survey_metadata` / `variance_formula` / `effective_dose_mean` populated in both regimes (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` for PSU-level draws with stratum centering + sqrt(n_h/(n_h-1)) small-sample correction + FPC scaling + lonely-PSU handling. On the `weights=` shortcut, sup-t calibration is routed through a synthetic trivial `ResolvedSurveyDesign` so the centered + small-sample-corrected branch fires uniformly — targets the analytical HC1 variance family (`compute_survey_if_variance(IF, trivial) ≈ V_HC1` per the PR #359 IF scale invariant) rather than the raw `sum(ψ²) = ((n-1)/n) · V_HC1` that unit-level Rademacher multipliers would produce on the HC1-scaled IF. Perturbations: `delta = weights @ IF` with NO `(1/n)` prefactor (matching `staggered_bootstrap.py:373` idiom), normalized by per-horizon analytical SE, `(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. - **`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=` (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)". - **`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). - **`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"`. diff --git a/diff_diff/had.py b/diff_diff/had.py index 945f3047..84ac8963 100644 --- a/diff_diff/had.py +++ b/diff_diff/had.py @@ -3725,12 +3725,30 @@ def _fit_event_study( on the period-F dose distribution, and then fits the chosen design path independently on each event-time horizon's first differences. - On the weighted path (``survey=`` or ``weights=``), per-horizon - variance is Binder-TSL via :func:`compute_survey_if_variance` and - the simultaneous confidence band (when ``cband=True``) is - constructed by a shared-PSU multiplier bootstrap over the stacked - per-horizon influence-function matrix (see - :func:`_sup_t_multiplier_bootstrap`). + Per-horizon variance regimes (matches the static-path contract): + + - **``survey=``**: Binder (1983) Taylor-series linearization + via :func:`compute_survey_if_variance` on the per-unit + β̂-scale influence function (continuous + mass-point both + route through the same helper). Inference is + t-distribution with ``df_survey``. + - **``weights=`` shortcut**: analytical SE — CCT-2014 + weighted-robust for continuous paths (``bc_fit.se_robust / + |den|``) and weighted 2SLS pweight sandwich for mass-point + (``_fit_mass_point_2sls`` HC1 / classical / CR1). Inference + is Normal (``df=None``). + + The simultaneous confidence band on the weighted path (when + ``cband=True``) is constructed by a shared-PSU multiplier + bootstrap over the stacked per-horizon β̂-scale IF matrix via + :func:`_sup_t_multiplier_bootstrap`. On the ``weights=`` + shortcut, sup-t calibration is routed through a synthetic + trivial ``ResolvedSurveyDesign`` so the centered + + sqrt(n/(n-1))-corrected survey-aware branch fires uniformly — + matches the analytical HC1 variance family at the + compute_survey_if_variance(IF, trivial) ≈ V_HC1 invariant. + Unweighted event-study skips the bootstrap (pre-Phase 4.5 B + numerical output preserved). """ # ---- Resolve effective fit-time state (local vars only, # feedback_fit_does_not_mutate_config). ---- diff --git a/docs/methodology/REGISTRY.md b/docs/methodology/REGISTRY.md index 19bb14a2..3ed941d6 100644 --- a/docs/methodology/REGISTRY.md +++ b/docs/methodology/REGISTRY.md @@ -2314,6 +2314,8 @@ Under `survey=SurveyDesign(weights, strata, psu, fpc)`, the variance composes vi **Scope**: sup-t bootstrap runs only when `aggregate="event_study"` AND `weights=` or `survey=` is supplied AND `cband=True` (default). Unweighted event-study skips the bootstrap entirely — pre-Phase 4.5 B numerical output bit-exactly preserved. Setting `cband=False` on the weighted path disables the bootstrap (useful for smoke-test bit-parity assertions against the unweighted path at uniform weights). +**`weights=` shortcut ↔ bootstrap routing**: on the `weights=` shortcut (no user-supplied `SurveyDesign`), per-horizon SE stays analytical (CCT-2014 robust for continuous, HC1/classical/CR1 sandwich for mass-point — NOT Binder-TSL), but sup-t calibration is routed through a synthetic trivial `ResolvedSurveyDesign` (pweight, no strata / PSU / FPC, `lonely_psu="remove"`) so the centered + sqrt(n/(n-1))-corrected bootstrap branch fires uniformly. This matches the analytical HC1 variance family — the `compute_survey_if_variance(IF, trivial) ≈ V_HC1` invariant from the IF scale convention — so the bootstrap variance target agrees with the per-horizon SE normalizer. Without this routing, the unit-level bootstrap branch would normalize against raw `sum(ψ²) = ((n-1)/n) · V_HC1` on the HC1-scaled IF and produce silently too-narrow simultaneous bands. Regression-locked by the `weights=`-shortcut / `survey=`-trivial equivalence test in `TestEventStudySurveyCband`. + - **Deviation from shared survey-bootstrap contract:** `_sup_t_multiplier_bootstrap` raises `NotImplementedError` on `SurveyDesign(lonely_psu="adjust")` with singleton strata. The shared `generate_survey_multiplier_weights_batch` helper pools singleton PSUs into a pseudo-stratum with NONZERO multipliers, but `compute_survey_if_variance` centers singleton PSU scores at the GLOBAL mean of PSU scores (rather than the pseudo-stratum mean). Matching the two would require a pooled-singleton pseudo-stratum centering transform in the HAD sup-t path that has not been derived. The HAD-specific limitation is scoped to: weighted event-study + `cband=True` + `lonely_psu="adjust"` + at least one singleton stratum. Practitioners can use `lonely_psu="remove"` or `"certainty"` (matches the analytical target bit-exactly on the HAD sup-t path), or pass `cband=False` to skip the simultaneous band. All other survey-bootstrap consumers (CallawaySantAnna, dCDH, SDID) retain full `lonely_psu="adjust"` support through the shared helper. - **Deviation: weighted mass-point `vcov_type="classical"` on survey/sup-t paths:** `vcov_type="classical"` raises `NotImplementedError` whenever the mass-point IF matrix is consumed downstream — specifically on `design="mass_point"` + `survey=` (static + event-study) and `design="mass_point"` + `weights=` + `aggregate="event_study"` + `cband=True`. The per-unit 2SLS IF returned by `_fit_mass_point_2sls` is scaled (`sqrt((n-1)/(n-k))`) to match V_HC1 via `compute_survey_if_variance`; mixing it with a classical analytical SE would silently return a V_HC1-targeted variance under a classical label. A classical-aligned IF derivation is queued for a follow-up PR. The allowed weighted-mass-point combinations are: `vcov_type="hc1"` on every path; `vcov_type="classical"` on `weights=` + `aggregate="overall"`, and `weights=` + `aggregate="event_study"` + `cband=False` (no IF consumption).