diff --git a/CHANGELOG.md b/CHANGELOG.md index d895c27b..3422dd64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **`RegressionDiscontinuity` - sharp regression discontinuity estimation with robust + bias-corrected inference (alias `RDD`).** Local-polynomial sharp RD per Calonico, + Cattaneo & Titiunik (2014), parity-targeting R `rdrobust` 4.0.0 end-to-end: all 10 + data-driven bandwidth selectors (`mserd` default, `msetwo`/`msesum`/comb and the + CER-optimal variants), triangular/epanechnikov/uniform kernels, `masspoints` + adjust/check/off, manual `h`/`b`/`rho` with R-exact resolution semantics (including + `rho`-without-`h` applying to selected bandwidths and the unconditional N<20 + full-range fallback), and the three-row Conventional / Bias-Corrected / Robust + output. **Canonical binding:** `att`/`se`/`t_stat`/`p_value`/`conf_int` are ONE + coherent row - the robust bias-corrected row (`att = tau_bc`, CI centered on it, + `t_stat == att/se`), preserving the library-wide field identities; rdrobust's + printed headline coefficient is exposed as `att_conventional` with a full inference + row, and `summary()` prints the familiar three-row table. Estimation-path port + (`rdrobust_fit_sharp`: Q_q bias-correction score matrix, conventional/robust NN + sandwiches) validated against a new estimates golden + (`benchmarks/data/rdrobust_estimates_golden.json`, 16 configurations incl. the + Senate anchors) at rtol=1e-9 in `tests/test_rdd_parity.py`; R-free methodology + anchors (CCT 2014 Remark 7 bias-corrected == local-quadratic equivalence at rel + 1e-10 across all kernels, invariances, joint-NaN degenerate contracts) in + `tests/test_rdd_methodology.py`; API/validation suite in `tests/test_rdd.py`. + Deviations from R (each labeled in the REGISTRY section): warn-instead-of-silent + NaN drops, warn-and-ignore `b`-without-`h`, fail-closed targeted errors on + degenerate designs, and the canonical-binding note above. Sharp designs only: + fuzzy RD, covariates (CCFT 2019 - review on file), cluster-robust variance, + weights, kink estimands, and rdplot/density diagnostics are documented follow-ups. - **Internal: mypy enforced at zero errors.** Triaged the 184 pre-existing `mypy diff_diff` errors to an enforceable zero and added a blocking Mypy job to the Lint CI workflow (pinned `mypy==2.1.0` + pinned numpy/pandas/scipy for stub @@ -24,7 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 already-stale ignores were removed. Tightening tracked in TODO.md. No public API or numerical behavior change. - **Internal: rdrobust sharp-RD bandwidth-selection port (`diff_diff/_rdrobust_port.py`).** - Step-1 machinery for the upcoming `RegressionDiscontinuity` estimator: a faithful + Step-1 machinery for the `RegressionDiscontinuity` estimator above: a faithful pure-Python NumPy/SciPy port of R `rdrobust` 4.0.0's `rdbwselect` sharp-RD path (all 10 data-driven selectors - mserd default, msetwo/msesum/comb and the CER-optimal variants - triangular/epanechnikov/uniform kernels, `masspoints` adjust/check/off, IK-style @@ -36,10 +61,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Cattaneo-Frandsen-Titiunik 2015) validated at rtol=1e-9 across 17 configurations x 10 selectors in `tests/test_rdrobust_port.py` (kernels, p/q orders, deriv, masspoints modes, stdvars, bwrestrict, scaleregul, nnmatch), including the 2017 Stata Journal - published Senate anchors under `masspoints="off"`. **No public API change** - the module is private; - the estimator, robust bias-corrected inference, and docs surfaces land in the follow-up PR. - Port-level deviations documented in `docs/methodology/REGISTRY.md` (new - RegressionDiscontinuity section stub). + published Senate anchors under `masspoints="off"`. The module is private; the public + estimator, robust bias-corrected inference, and docs surfaces are the + `RegressionDiscontinuity` entry above. Port-level deviations documented in + `docs/methodology/REGISTRY.md` (RegressionDiscontinuity section). - **Internal: ungated `Lint` CI workflow.** Check-only `ruff check` + `black --check` (at the versions pinned in the `dev` extra) run on every PR push and on pushes to main — no `ready-for-ci` label needed; the aggregate `Lint Gate` job is the single @@ -65,6 +90,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `benchmarks/data/qte_golden.json`. ### Changed +- `diff_diff/guides/llms-autonomous.txt` no longer lists regression discontinuity as + out of scope: sharp RD routes to `RegressionDiscontinuity`; only fuzzy RD is + referred to external tooling. - **Internal: repo-wide lint normalization + pinned tooling.** black/ruff/mypy are now pinned exactly in the `dev` extra (`black==26.3.1`, `ruff==0.15.13`, `mypy==2.1.0`; the tools require Python >= 3.10 — the library floor stays 3.9); full `black` + diff --git a/README.md b/README.md index d442de46..912e0bed 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`. - [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html) - triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility - [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html) - Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves - [HeterogeneousAdoptionDiD](https://diff-diff.readthedocs.io/en/stable/api/had.html) - de Chaisemartin, Ciccia, D'Haultfœuille & Knau (2026) for designs where **no unit remains untreated**; local-linear estimator at the dose support boundary returning Weighted Average Slope (WAS) on Design 1' (`d̲ = 0` / QUG) or `WAS_{d̲}` on Design 1 (`d̲ > 0`, continuous-near-d̲ or mass-point), with a multi-period event-study extension (last-treatment cohort, pointwise CIs). **Panel-only** in this release - repeated cross-sections rejected by the validator. Alias `HAD`. +- [RegressionDiscontinuity](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Calonico, Cattaneo & Titiunik (2014) sharp regression discontinuity with robust bias-corrected inference and rdrobust-parity bandwidth selection (all 10 selectors, mass-point handling). **Sharp designs only** in this release; canonical `att` is the bias-corrected estimate with a coherent robust CI (rdrobust's printed headline is `att_conventional`). Alias `RDD`. - [StackedDiD](https://diff-diff.readthedocs.io/en/stable/api/stacked_did.html) - Wing, Freedman & Hollingsworth (2024) stacked DiD with Q-weights and sub-experiments; optional covariate balancing (Ustyuzhanin 2026) - [EfficientDiD](https://diff-diff.readthedocs.io/en/stable/api/efficient_did.html) - Chen, Sant'Anna & Xie (2025) efficient DiD with optimal weighting for tighter SEs - [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html) - Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment diff --git a/benchmarks/R/generate_rdrobust_estimates_golden.R b/benchmarks/R/generate_rdrobust_estimates_golden.R new file mode 100644 index 00000000..ed629f10 --- /dev/null +++ b/benchmarks/R/generate_rdrobust_estimates_golden.R @@ -0,0 +1,136 @@ +# Golden-value generator for the diff-diff sharp-RD ESTIMATION port +# (diff_diff/_rdrobust_port.py::rdrobust_fit_sharp and the public +# RegressionDiscontinuity estimator). +# +# Deliberately a SEPARATE file/JSON from generate_rdrobust_golden.R so the +# bandwidth fixtures reviewed in the machinery PR are never regenerated. +# Same parity target: CRAN rdrobust 4.0.0 (tarball sha256 +# 78f0d6b4bdec4091cc8f42f6f1598704747f95926446d3aaee381ea1d613a36f); +# the GitHub 4.1.0-dev tree must NOT be installed when regenerating. +# +# Outputs benchmarks/data/rdrobust_estimates_golden.json: per config the +# embedded inputs (17 significant digits), selected/echoed bandwidths, the +# full three-row (Conventional / Bias-Corrected / Robust) coef/se/z/pv/ci +# block, effective counts, and per-side order-p coefficient vectors. +# +# Run from the repo root: Rscript benchmarks/R/generate_rdrobust_estimates_golden.R + +library(rdrobust) +library(jsonlite) + +stopifnot(packageVersion("rdrobust") == "4.0.0") + +TARBALL_SHA256 <- "78f0d6b4bdec4091cc8f42f6f1598704747f95926446d3aaee381ea1d613a36f" + +run_estimate <- function(y, x, c = 0, masspoints = "adjust", kernel = "tri", + p = 1, q = 2, h = NULL, b = NULL, rho = NULL, + level = 95, bwselect = "mserd") { + args <- list(y = y, x = x, c = c, masspoints = masspoints, kernel = kernel, + p = p, q = q, level = level, bwselect = bwselect) + if (!is.null(h)) args$h <- h + if (!is.null(b)) args$b <- b + if (!is.null(rho)) args$rho <- rho + r <- suppressWarnings(do.call(rdrobust, args)) + list( + c = c, masspoints = masspoints, kernel = kernel, p = p, q = q, + bwselect = bwselect, + h_in = if (is.null(h)) NA else h, + b_in = if (is.null(b)) NA else b, + rho_in = if (is.null(rho)) NA else rho, + level = level, + h_l = r$bws["h", "left"], h_r = r$bws["h", "right"], + b_l = r$bws["b", "left"], b_r = r$bws["b", "right"], + tau_cl = unname(r$coef[1, 1]), tau_bc = unname(r$coef[2, 1]), + se_cl = unname(r$se[1, 1]), se_rb = unname(r$se[3, 1]), + z = unname(r$z[, 1]), pv = unname(r$pv[, 1]), + ci_lower = unname(r$ci[, 1]), ci_upper = unname(r$ci[, 2]), + N = unname(r$N), N_h = unname(r$N_h), N_b = unname(r$N_b), + bias = unname(r$bias), + beta_p_l = unname(as.vector(r$beta_Y_p_l)), + beta_p_r = unname(as.vector(r$beta_Y_p_r)) + ) +} + +golden <- list() + +golden$metadata <- list( + rdrobust_version = as.character(packageVersion("rdrobust")), + rdrobust_tarball_sha256 = TARBALL_SHA256, + seeds = list(dgp_lee_smooth = 42L, dgp_ties_moderate = 123L, + dgp_asymmetric_scaled = 777L), + generator = "benchmarks/R/generate_rdrobust_estimates_golden.R", + algorithm = paste( + "rdrobust() sharp-RD estimation blocks (three-row coef/se/z/pv/ci,", + "counts, per-side beta_p) for the vce='nn' no-covariate path,", + "complementing the bandwidth fixtures in rdrobust_golden.json." + ), + r_version = R.version.string +) + +# Same seeded DGPs as generate_rdrobust_golden.R (kept in sync by seed). +set.seed(42) +n <- 1000 +x1 <- 2 * rbeta(n, 2, 4) - 1 +y1 <- 0.48 + 1.27 * x1 + 7.18 * x1^2 + 20.21 * x1^3 + 21.54 * x1^4 + + 7.33 * x1^5 + 0.04 * (x1 >= 0) + rnorm(n, sd = 0.1295) + +golden$dgp_lee_smooth <- list( + x = x1, y = y1, + configs = list( + default = run_estimate(y1, x1), + manual_h = run_estimate(y1, x1, h = 0.15), + h_rho2 = run_estimate(y1, x1, h = 0.15, rho = 2), + rho_only = run_estimate(y1, x1, rho = 2), + p2q3 = run_estimate(y1, x1, p = 2, q = 3), + p0q1 = run_estimate(y1, x1, p = 0, q = 1), + epa = run_estimate(y1, x1, kernel = "epa"), + uni = run_estimate(y1, x1, kernel = "uni"), + level90 = run_estimate(y1, x1, level = 90), + msetwo = run_estimate(y1, x1, bwselect = "msetwo"), + cercomb2 = run_estimate(y1, x1, bwselect = "cercomb2") + ) +) + +set.seed(123) +n2 <- 800 +x2 <- round(2 * rbeta(n2, 2, 4) - 1, 2) +y2 <- 0.5 + 0.8 * x2 + (x2 >= 0) * 1.0 + rnorm(n2, sd = 0.3) + +golden$dgp_ties_moderate <- list( + x = x2, y = y2, + configs = list( + adjust = run_estimate(y2, x2, masspoints = "adjust"), + off = run_estimate(y2, x2, masspoints = "off") + ) +) + +set.seed(777) +n3 <- 300 +x3 <- 40 * rbeta(n3, 5, 2) +y3 <- 2 + 0.05 * x3 - 0.001 * x3^2 + 0.8 * (x3 >= 28) + rnorm(n3, sd = 0.5) + +golden$dgp_asymmetric_scaled <- list( + x = x3, y = y3, + configs = list( + default = run_estimate(y3, x3, c = 28) + ) +) + +senate_path <- "benchmarks/data/rdrobust_senate.csv" +stopifnot(file.exists(senate_path)) +senate <- read.csv(senate_path) +ok <- complete.cases(senate$vote, senate$margin) +sv <- senate$vote[ok] +sm <- senate$margin[ok] + +golden$senate <- list( + csv = "benchmarks/data/rdrobust_senate.csv", + configs = list( + adjust = run_estimate(sv, sm, masspoints = "adjust"), + off = run_estimate(sv, sm, masspoints = "off") + ) +) + +out_path <- "benchmarks/data/rdrobust_estimates_golden.json" +write_json(golden, out_path, auto_unbox = TRUE, pretty = TRUE, digits = I(17)) +cat("Wrote", out_path, "\n") diff --git a/benchmarks/data/rdrobust_estimates_golden.json b/benchmarks/data/rdrobust_estimates_golden.json new file mode 100644 index 00000000..2da7c875 --- /dev/null +++ b/benchmarks/data/rdrobust_estimates_golden.json @@ -0,0 +1,517 @@ +{ + "metadata": { + "rdrobust_version": "4.0.0", + "rdrobust_tarball_sha256": "78f0d6b4bdec4091cc8f42f6f1598704747f95926446d3aaee381ea1d613a36f", + "seeds": { + "dgp_lee_smooth": 42, + "dgp_ties_moderate": 123, + "dgp_asymmetric_scaled": 777 + }, + "generator": "benchmarks/R/generate_rdrobust_estimates_golden.R", + "algorithm": "rdrobust() sharp-RD estimation blocks (three-row coef/se/z/pv/ci, counts, per-side beta_p) for the vce='nn' no-covariate path, complementing the bandwidth fixtures in rdrobust_golden.json.", + "r_version": "R version 4.5.2 (2025-10-31)" + }, + "dgp_lee_smooth": { + "x": [-0.56194513849098149, -0.16082078950903389, -0.021389840716477204, -0.14012300559554725, -0.38008098401860446, 0.45804831878152874, -0.3751133177545497, 0.69454681705497046, -0.36116625851726458, 0.34756860165104375, -0.80356028039014915, -0.45243334622869424, -0.39178377881145887, -0.019746789163162304, -0.45464722487793297, -0.95611512445921776, 0.35587723284210226, -0.46366823724017736, -0.40819582880814342, 0.29805979308505282, -0.5121444710175227, -0.44366901137064874, -0.87649372444815976, -0.11169067994404303, -0.58862020914094626, 0.017144402995256991, 0.19851534502589052, -0.57772398548558912, -0.088569367913942099, -0.86870791306319062, -0.6372402242462889, -0.65846664858787418, -0.95409071565618109, -0.31703206984329724, -0.23778056540524251, -0.48526960526644969, 0.045987737990112798, -0.61820796504072884, -0.79888999073493883, -0.12563200793662621, -0.64593080867885122, -0.51251733133546551, -0.0092435305780376176, -0.18132747817788153, -0.63703876567271611, -0.016045732967678195, -0.29246651289697401, -0.20364539341780719, 0.0034112192607986813, -0.29243313954299233, -0.98457286949664713, -0.19956822193861279, -0.48769220932941448, -0.2476946682433272, -0.048408107059237593, -0.6184042090244013, 0.34629852700156127, -0.17442343243935232, 0.20040488364937881, 0.13512315221576721, 0.025796021687382709, -0.71579690571957522, -0.37316997116977213, -0.026356390332396673, -0.68982874777791192, -0.55402726507755662, -0.048843783275267394, 0.051430207731289146, -0.10980799684788378, -0.66931689379720605, -0.73153600631837978, -0.21272647265992894, -0.2930763730735495, -0.3864515060611865, -0.75661589296675613, -0.032467796749657385, -0.4271319844064575, -0.4127635428492431, 0.1420984944766428, 0.080675552505649728, -0.52942364456451219, -0.012072969115929033, -0.73453212581264338, -0.66156278973559624, -0.74017877874309024, -0.81899960906894664, -0.29697662731244578, -0.010548539888331598, 0.29049259737407285, 0.20387309181290236, -0.70439275146571634, -0.59788724303677587, -0.28517070530263422, -0.50900423023663355, -0.38707658257664845, -0.24628423540397193, -0.27882072407697978, -0.34454118673399747, -0.28398213547670836, -0.53288452778907414, -0.53986125680939745, -0.85883015812312213, -0.49358835153158831, -0.54297553931630027, -0.7974773751778933, -0.27035895239086727, -0.82625571353921501, -0.82138715470665036, -0.27623451000393018, -0.70249037249020518, -0.33415063124979083, -0.51130046166359244, -0.5793982453185601, -0.91455274536780506, -0.76436996450079497, -0.25041159266502666, -0.055878133936614782, -0.53565570628260151, -0.33330512001849155, 0.24725996729975064, -0.73080706240143745, -0.38491199150918809, 0.2907869699685337, -0.52700739161145871, -0.35666047101440435, -0.37139495105908293, -0.66990325893553959, -0.51755408416191528, -0.34647549177463211, -0.50578651657789009, 0.24198634380412209, -0.53122613802661511, -0.51317441094783067, 0.012851478022069873, -0.71952475223893431, -0.35146361281453353, -0.83803319444411217, -0.45034528385807315, -0.526299658833012, -0.33681049838055077, -0.64236004227935584, -0.55062368298057951, -0.40971848448341897, -0.7238251214890572, -0.25814055076652898, -0.48592949860707702, -0.044090924294535316, 0.41637990818994775, -0.7013441066001026, -0.11448177413290261, -0.29471113878221955, 0.12123177761633475, -0.43236719986637673, 0.088810201724952575, -0.87347862559771539, 0.54790264248315257, -0.39923447234845544, -0.81681007846142795, -0.015987424248322779, -0.099815938624183809, -0.33618114591250514, -0.26368540370816984, -0.56890874198882146, -0.1579770783596387, -0.1947746662325015, -0.45262419796503794, -0.79147878021937035, 0.014942804357721995, -0.062918214885308021, -0.65386418219391162, -0.84643382609779894, -0.23827851363067154, -0.47897894131098051, -0.21530594572953854, -0.78145156704320795, -0.69150450384761797, 0.22665733327854931, -0.14037253802841043, -0.045950500691671503, -0.27430921296113076, -0.91150632078877225, -0.17545200734717514, 0.27410389782668898, -0.80084572847343172, -0.74526800364796575, -0.61119299072845124, -0.7188761692084451, -0.59061940036573524, -0.41386541205125282, -0.75838645835879381, -0.28412058028359077, -0.37052407346068628, -0.093400943871734698, -0.3164451395339487, -0.3912555548615031, 0.43986176386998954, -0.42708646288291152, -0.77893704198692038, -0.14950818106846275, -0.48151468317943469, -0.54634020686694917, -0.72281002610163692, -0.12789853303377485, -0.40908256081176531, -0.083012507662639234, -0.43426119702799904, -0.59503764764779676, -0.44958017048829113, 0.15017996124716504, -0.75847880342157525, -0.52564965648498552, -0.44859864881038813, -0.49694281819148456, -0.34350543777609577, -0.21937176017147897, -0.6865041344534224, -0.61904716601581322, 0.17795723877417924, 0.32217262738972119, 0.31688832051446147, -0.80170240854418995, 0.1883931333644584, -0.34526368352851444, 0.016482149164302795, -0.69511126646763666, -0.73054506091834537, -0.31115049645332404, -0.75768715923430574, -0.24541426664233779, 0.34365729252562782, -0.71317851987321412, -0.62211920406937948, -0.31228368422841613, -0.7085341736930979, -0.49395821083176228, 0.16213994490220829, -0.15042470544935826, -0.35733319806143315, 0.21499348091297965, 0.075981604542691095, -0.41614813908163739, -0.97251283558771195, -0.3057070522692622, -0.052656201744561204, -0.31263001837032767, -0.26087718264299697, -0.12626997125419337, -0.49910674626765694, 0.35035522226768911, -0.10640506839680164, -0.76886670731067475, 0.38915791876194672, 0.081935390860925272, -0.8591070907652254, -0.56360377575817289, -0.019979140369954851, -0.31357771882959407, -0.16837025508229253, 0.8403445256387605, -0.64816598991240837, -0.92522518422702216, -0.20504044479744499, -0.60501354402087859, -0.1188857553349485, -0.64624848621264264, -0.38765930100311541, -0.52985847126784158, -0.40580521127306868, 0.1030572337904605, -0.28896060430332893, -0.85230482641725724, -0.33540439520001319, 0.047116826821173463, -0.31572928694787872, 0.033469574907975863, -0.39740190548741616, -0.35904534017850631, -0.10024089334802122, -0.7697946934936617, -0.34826959455633733, -0.36478228787394229, -0.62889108724125475, -0.71331640931633933, -0.58538778324653751, -0.045781693493376796, -0.50672908248018933, -0.63737950501120522, -0.25804153902644256, -0.76737823487961487, 0.20449661640534833, -0.5325712547984145, -0.24583845483418809, -0.31025848371355924, -0.33344127985869354, -0.65038064648191285, 0.25978262020760767, -0.31909590997124182, -0.65351578809663846, -0.36381944767304708, 0.039806884222808758, -0.51025302737645095, -0.63320717608159027, -0.26306909343350293, 0.17270272458736269, -0.36587457075002061, -0.80314687505161708, -0.78449146792100732, -0.63200095978680815, -0.54828832224155222, -0.34290990123375387, -0.51372631348620557, -0.73536979346534415, -0.03505807330482924, -0.20417988865773784, -0.86266591315812169, -0.29359400525772905, 0.37392722360390462, 0.027576223179185622, -0.65267532357919233, 0.032093191313777858, -0.66503110745807037, -0.52085424896257682, -0.45702937062445115, 0.084385069824713499, -0.45746636278518726, -0.84291322926451961, -0.65780938554360358, -0.7284502579408747, -0.53245751548301334, -0.47550448392090572, -0.40033258505650549, -0.43791916810824227, -0.80291488340709738, -0.50615364415504327, -0.16783724690613566, -0.29303867029917274, 0.31154594499156407, -0.8558126705547886, 0.42378406508820698, -0.60602197752323916, -0.45147664738737359, -0.73416030669510635, -0.271946382833537, -0.72522452608792953, -0.098399213484543946, 0.39289463482558551, -0.41817593259487007, -0.4275616303387022, -0.46292142897627742, -0.039962497552362808, -0.18218712046702379, -0.46175313797367645, -0.091989588860734051, -0.55646409125172336, -0.4526230327785985, 0.23262018191377298, -0.12094307774627044, -0.2209252786924234, -0.44298921341201269, 0.1408191469354243, -0.66420588136232839, -0.30821285983427338, -0.31114298221360515, -0.56685106214630654, -0.70309912779712513, -0.33442412974725455, -0.0093161858962664823, 0.43989062756461905, -0.89897986538828611, -0.61005032875090104, -0.46558895320022031, -0.62612599216391662, -0.63492732000157615, -0.61682453243698165, -0.93351965556634853, -0.23422357979897446, -0.58580417434465293, 0.46271413899875302, 0.066562945641083893, -0.81778232454237121, -0.28847199536622337, -0.57235247149261448, 0.11721559998659026, -0.30931650510915565, -0.55917525187479744, -0.12278829787045598, -0.26314258496670817, -0.40510943586903325, -0.54912227582459217, -0.52740431012241484, -0.83691668294351107, -0.35138763248053062, 0.11835944133568743, -0.44071870490196741, -0.75333877008980099, 0.42143863971279649, -0.17509166409505672, 0.41473788666458389, -0.66359785440010333, 0.15167450970699559, -0.5692310597825232, -0.18180639618474326, -0.46075821942972839, -0.45233368705772892, -0.067146863622839725, -0.78380903119128342, -0.44106695194093348, -0.25444875075938134, -0.5017805296783413, -0.28556732591631351, -0.46856835523607021, 0.022185336081263607, -0.64068051747421806, 0.72540729664998116, -0.19166449317062295, -0.72695078271096714, -0.70529196838620767, -0.34912394279996062, 0.041774722870719039, -0.92806772926592351, -0.60934482100946918, -0.33738948650276068, -0.8187686307402029, 0.011596680290302475, -0.26752689640536109, -0.4970424046470181, -0.53501852331073074, -0.45440214296827597, -0.18485322527455805, -0.14413559134508702, -0.43064076818012009, -0.90122268285129092, -0.81469656465071361, -0.82122230324606926, 0.047997573459482412, -0.43807035548967588, -0.20700187735155773, -0.83949530224178426, -0.55138886341381965, -0.058591111032789578, -0.41089869056766459, -0.38397122387719218, -0.36526343633039648, 0.29598258005327072, -0.57982998750011605, 0.0017154310325417921, 0.12290526648340427, -0.75532383370268852, -0.27784214818990338, -0.33835738721649378, 0.1952042493940549, 0.1318339741646446, -0.33443464599435135, -0.27094345923866303, -0.61601491704593758, -0.41920825497977199, -0.71185198637082936, -0.69380622565782568, -0.76337557006595402, 0.088584624900273745, 0.24286642408383075, -0.50543134477418028, -0.73943868419700687, -0.37218899843849074, 0.7036192589661372, -0.86918825775380315, -0.48363844465814021, -0.35037736107562167, 0.50216986388442897, -0.47442294141708219, -0.036180115037299565, -0.22902135928603173, 0.13497064616401455, -0.7597467952047301, -0.57593991155811186, -0.15726099896408385, -0.89780819015158775, -0.73751513329477469, -0.48010126582576829, 0.31140916552860287, -0.45056954140445005, -0.25853103117062537, -0.36199700388064115, -0.61099570939491699, 0.37870580083733585, -0.73064274440352528, -0.43181140183083766, -0.48006118579175838, -0.074448085166346267, -0.083974754643566185, 0.38958732418784647, 0.048667851633931614, -0.58323494782622931, -0.29625745618160981, 0.38619740048746087, -0.83995654259351527, -0.17991862073187803, -0.40179084535492082, 0.11143250171311969, 0.041647835172653691, -0.19162838891700951, -0.83280339623491761, 0.12959328838073203, -0.84352933452207979, -0.045302643700739553, -0.29252118554277562, 0.16087459810891813, -0.2975154333676332, -0.84141710648621815, -0.91239880378002991, -0.069833448919824903, -0.3658388199344248, -0.56421912838821831, -0.46544407424831036, -0.59948179846768568, -0.40202440161613173, -0.68318466420190216, -0.90915516608624902, -0.42815293138148358, 0.20416455684173451, -0.33216379584475841, -0.36902493137720349, 0.15850353811806661, -0.3090879457132456, -0.51383351837938251, -0.87217117057296445, -0.77407416045823574, -0.13909089317048429, -0.81909361647266365, -0.79385480448493317, -0.41730237510964785, -0.33438073248364597, -0.3390757638695916, -0.26345391440756161, -0.039604018881128633, -0.38873580705092536, -0.44009128014796117, -0.25950112061485808, -0.51335469713610793, -0.93154957908158387, 0.19401817773153374, -0.44006967658280183, -0.13097226004170548, 0.088233941774004343, -0.59573415596873613, -0.85181115060672363, 0.10379023949016708, -0.54258933060846237, -0.90015031743624208, -0.89263848266178125, -0.21891010066226713, -0.20519631972517105, 0.25163970796465573, 0.089934702384864451, -0.59369409361047998, 0.15350683122208486, -0.16590679633380112, -0.23749289570517729, 0.17964639523250181, -0.58393262104766297, -0.14469218785279037, -0.67128425047964835, -0.022477065739431312, -0.51299754775520157, -0.11576752718167105, -0.15838182171291859, -0.44902032727311025, -0.10891230026473775, -0.052338288807053379, -0.43274809457657393, 0.27461164592109877, 0.39514117515539704, -0.59624079994551082, -0.63493301271469127, -0.084118007850370269, -0.67774477238107689, -0.51935073921852959, 0.070372337261186857, -0.56510195225812176, -0.90166901473294603, -0.0045696113415434336, -0.84795315952412631, -0.79574665433933267, 0.085860064358148414, -0.21604101675438303, -0.11056435932676045, -0.35202667459038828, -0.73311577963368435, -0.057896607137795875, 0.34674728427934953, -0.8276244684792371, -0.57703945730310235, -0.45787693587324707, -0.78040947788955783, 0.011368787736954822, -0.57427448663290082, -0.30911671752354741, 0.029694166962310531, 0.48367193090430183, -0.20897723981491256, -0.23552870496780343, -0.82998421126356736, -0.32324684022211192, 0.024315595538471735, -0.096584784353887065, -0.40902776037836186, -0.76258689864009499, -0.014180315528482756, -0.67869908334744866, -0.77114273256163579, -0.041497050677176128, -0.76639950873671392, -0.21930465220503015, -0.3320892742604411, -0.78959572929656496, 0.27873608100869629, -0.5720088482333503, 0.28994741898082776, -0.52899660092891121, -0.56566889772924078, -0.3472662599917955, -0.48409078370181813, -0.48351825070375998, -0.66681042714257244, -0.76136731975960914, -0.031784339990983623, -0.64426543894700083, -0.80075376528810227, -0.5412034644557645, -0.642570546653497, -0.93393277366753857, -0.45734907290521309, -0.25626404162726912, -0.19475598598909805, -0.14594442669457608, -0.70370873593541938, -0.47129843175404618, -0.70587626126416425, -0.58702947121347915, -0.76058913837212994, -0.73606888148154015, -0.022975410463552559, -0.44988834753145879, -0.44431161278591869, -0.35926734286644857, -0.55192728937319391, 0.040086092316734101, -0.53356401715277357, -0.4917719430441877, -0.58027219096202109, -0.74981336230071127, -0.17800085444983038, -0.48328553123109586, -0.21100154943584593, -0.17567460695436965, 0.41092887950831192, -0.0048447157608907387, -0.72434493674648248, -0.60817529548765181, -0.62466985389681939, -0.26833332366371798, -0.33510270417036103, -0.15841266875663729, -0.59463688151384908, -0.49125472829470973, 0.036874198725443064, -0.37725949552398286, -0.59358154103600103, 0.16303739613588641, -0.32726574166373179, -0.74980706847404199, -0.94000077390870951, -0.011658060657765756, -0.5503826633499761, -0.45112270605556803, -0.19724258473290401, -0.52578800405179005, -0.47724336383962906, -0.25268594132647471, -0.94235102403475612, -0.19680224444879313, -0.13097938774910056, 0.34203530963628714, -0.25742793939444719, -0.23828078871482894, -0.036382394423118969, -0.34189137990434026, -0.36860793936236558, -0.28030117760923012, 0.038398503897527192, -0.51245796821191136, -0.53812504694865404, -0.66292630188384694, -0.69108564434476716, -0.17891318344146456, -0.69436750794080226, -0.41515139803009404, -0.71207756390230759, 0.12814061270341459, -0.46796965906305832, -0.25065503660839616, -0.16836233860862437, -0.71893881556703365, -0.37948403507785822, -0.4317738155859282, 0.0013021839874358676, -0.76650845186643934, 0.05017310328282365, -0.57033227696459488, -0.5956561884981415, -0.28168448991614381, -0.46917431514544039, 0.63037202452624919, -0.21194355616699623, -0.25891881489319779, 0.1408423745612275, -0.35995069929220402, -0.3112921582924737, -0.72412857633530447, -0.94376389137536498, -0.75991731562206621, -0.28117436829801146, -0.7130147361385607, 0.0085211448382047639, 0.02806751949600006, -0.23232122604069316, -0.420684461249762, -0.79829701476155401, -0.46459025223422468, -0.0051910613130311534, 0.34961975491266895, -0.090596730888724331, -0.73925386070240418, 0.53887109837336689, 0.18477411350999007, -0.2293679716438547, -0.9353498687295726, -0.6901443665147049, -0.36373594392695185, 0.047694653388469455, -0.75288531622742361, -0.43490813868096434, -0.44460644639791669, -0.59671479793669358, -0.46861224165424098, 0.037434964654958325, -0.92168140497972173, -0.29061409635905344, -0.33414937910426989, -0.33919676133474286, -0.0026551757638212381, -0.33922669944786144, 0.055711123861833967, -0.91277379463152419, -0.73922485473543587, -0.16797780601292145, -0.48206069282424158, 0.14842126316269888, -0.70482935998904206, 0.027429765551886032, -0.59859510936155658, -0.73514241803462654, -0.47807642489440583, -0.31081814268377606, -0.76510975352174526, -0.63056203664285104, -0.76974159222566207, 0.18833826271441123, 0.0272730329366071, -0.7127959612606809, -0.061268324235327576, -0.44297847986965888, -0.73523763663221986, -0.1315427016515831, -0.47019202563796891, 0.11275202624476699, -0.41784387272521228, 0.28335855386664455, -0.54191113788892598, -0.23127932256063854, -0.15723134503516056, -0.20239488435781705, -0.23972171603177062, -0.33915025963974121, -0.37346502859156383, 0.38023692424982602, 0.12816498798590059, -0.57806362453471616, 0.39714614959924122, -0.26681795188216506, -0.58883324434510897, -0.25501967630286693, -0.40195946395878046, -0.28729294362623659, -0.69570211624309941, -0.64862723360288022, 0.37909577595004373, 0.15761087315356126, -0.66868392896331308, -0.67371400953219585, 0.56427776706359212, -0.63485867632061443, -0.78344611990413071, -0.22642544946420184, -0.24697146921016044, -0.29874666391749582, 0.40166222781061589, -0.41652786719972479, 0.43068701098279694, -0.4285907678425368, -0.088470525512549414, -0.71307152684130748, -0.25470171438193334, 0.29032843313806245, -0.40459632932515777, 0.33532853905893067, 0.14684385202001571, -0.44012347861368084, -0.28935956905930849, -0.51346484979376772, -0.98344943072146285, -0.29655948083134465, -0.28302223951143246, -0.23420928351199155, -0.83927645330429113, -0.23288263742629267, -0.24653629113906395, 0.32224977337070437, -0.078932084785923173, -0.6910100133104895, -0.3879598671797021, -0.24459490864077216, -0.20576210051422772, -0.6071278000014938, -0.57872668779359504, -0.8501833004640853, -0.14102215626335712, -0.0044815541217998467, 0.1480628944572604, -0.68766590277760964, -0.43998987355911401, -0.78964908201011585, 0.093278614271716576, -0.16852754000674408, 0.13127486563006308, -0.034439416875733597, -0.57723696482364018, -0.25034431193159257, -0.89887342408800897, -0.089609913605765645, 0.16178201410501214, -0.66621334161545387, -0.92780651963958771, -0.83226193618807576, -0.73516354242515103, -0.33177056883888689, -0.76764867024847305, -0.062131228973091868, -0.9329217695788522, -0.43947105448323331, 0.2416927095034731, -0.47762858488433002, -0.74973749722255756, -0.7456468016209894, -0.67809200155435634, -0.076219913878696088, 0.087965671480081387, 0.29500421100050689, 0.22108015899028666, -0.25806121733674847, -0.67004478413103197, -0.61802618200007586, -0.13843877316671016, 0.54501335594371514, -0.90139384094575614, -0.65313330398117975, 0.40929523606944218, -0.72168942512766221, -0.85004514660203756, -0.038916931494736295, 0.56888598575909577, -0.07408739741813497, 0.025141342904308361, -0.059828141793849987, -0.34142164308758582, -0.99322764151810106, 0.047367273442480817, -0.4970442422245589, 0.18452335219471161, -0.58407217004246503, -0.85397163479644278, 0.31489733514283325, -0.24952724027690354, 0.13228526441458666, 0.20595879928863248, 0.049063414524781246, -0.57576507467817106, -0.48894285644101809, 0.21224568002931932, 0.20962668556497177, -0.69956005440149616, 0.54554348200762326, -0.79856058811573505, -0.51319169663163988, -0.078617631901387042, 0.16279854651897807, -0.15387981346569368, -0.70228559057507844, -0.57367149099057824, -0.25717376749147003, -0.6575259333265715, -0.4603799199376799, -0.45438753158014233, 0.10688331908150972, -0.55458241224016158, -0.023548461006257515, -0.81100768307408688, -0.76506710070175632, -0.8611720346617906, -0.29494154508366255, -0.42254925051092196, 0.42140380999393812, -0.35194291561881019, 0.66448217576730539, -0.53493448966602175, -0.14811275043263883, -0.65657505239109226, 0.035049264674780867, -0.72603548418116115, -0.71107260682359841, -0.19765145882715351, -0.29198105224622806, -0.21043910195172033, -0.30187170129691143, -0.54885067514723873, -0.39044738053682571, -0.34133612769476951, -0.78730118216357048, -0.6564701178864083, -0.73209111844195218, -0.6737134156393203, -0.77343130039955743, -0.96845438479979484, -0.0020674863231070484, -0.0068956356184991519, -0.44093820382188165, -0.6172355328255601, -0.64618343180856375, -0.14291991293260764, -0.22790290303496452, -0.13131608842973685, -0.33116147347115688, -0.028763166610840774, -0.12648964631751625, -0.71915079524859182, 0.45638195744104348, -0.47467034512620232, -0.36216100611483548, -0.29647043500834769, -0.51889279788305842, -0.96404805924011572, -0.46619041484139279, -0.31464178879650961, -0.87224402023359193, -0.065146589217693673, -0.57359720670406167, -0.20934586254011889, -0.63864330254400326, -0.25599987937963442, -0.21289095055250906, 0.13032761125489545, -0.40182568619145043, -0.91564883462669855, -0.78281328041865483, -0.66045524077130913, -0.45488329937930805, -0.57964384159289439, -0.65657428583685862, -0.2626304125940041, -0.65863138244481723, -0.49963738563211157, -0.68578759743090334, -0.80342353900432817, 0.15418441178494158, -0.78706568153948919, -0.30017525374007281, -0.56320078613817726, -0.72010798028911149, -0.21204954998999315, -0.15580689353917598, -0.17969356207256448, -0.39398528389655862, -0.59219469941923086, -0.70438540176040265, -0.5512158277594289, -0.38193454641093583, -0.038589672463269276, -0.13425940965516558, -0.37991930834471388, -0.6602398210912922], + "y": [0.3305137255468299, 0.50967390638304055, 0.36211946971732262, 0.48426249811457056, 0.37452493511224499, 5.7817042284681976, 0.33858991979038028, 17.68636513097622, 0.43115389117684938, 2.9345117581300659, 0.20273244828040096, 0.48374751313176467, 0.12525908789030982, 0.65983640130850829, 0.44062519010788137, 0.24044236002763403, 3.1469933937976555, 0.16983215738765478, 0.22445689728529494, 2.3338653921677763, 0.10520388946937698, 0.24882280316445124, 0.17658094444521294, 0.39028147372518501, 0.18978309110117705, 0.75074388860374919, 1.1904368200770403, 0.070604846845176913, 0.51368541109897137, 0.23597788727114913, 0.16882187584419697, 0.20544328062317413, 0.12260739599376999, 0.48686188669280428, 0.43433756856221861, 0.36543565670160083, 0.51814460180242983, -0.090251885349571043, 0.069246267372009188, 0.28139763933010098, 0.42205581942898213, 0.1677896523751817, 0.44092862776256636, 0.52480342485483844, 0.21872053001733011, 0.54855058120589428, 0.70011331872865146, 0.40444315266629582, 0.77113683518808807, 0.39191677618411597, 0.17128395588099843, 0.095292828776292193, 0.3245189372286888, 0.24365668383322303, 0.43860611747436962, 0.37829657194828004, 3.05138780227976, 0.35125201741322143, 1.013422807146575, 0.77638792761407038, 0.48335277958929856, 0.20358108614549808, 0.3829674963585894, 0.45296358142762078, 0.14237514937247342, 0.15290311004443002, 0.43677779776300651, 0.64239236989591408, 0.46154062563524889, 0.36160086928281343, -0.029471566612295497, 0.57210478110549134, 0.44578326737613871, 0.11221693876969005, 0.081358917450060034, 0.36416679455037659, 0.16449924894251655, 0.096090696355365296, 0.84865172754802309, 0.62435956659685132, 0.16656627465844098, 0.19876453974320474, 0.17180840577494841, 0.22436664925153227, 0.21724063601116123, 0.24360577088351881, 0.38332212877530708, 0.65753015837687179, 2.0452823202276376, 1.1295547268197672, 0.16001562312809325, 0.21744662552279515, 0.26499578572176608, 0.22876597037611832, 0.35381752125921884, 0.42259442217406701, 0.47735288315187346, 0.45699270262792646, 0.04755300272678592, 0.42963191836538184, 0.25968651910580043, 0.10274484196206672, 0.091517908814001847, 0.31552645739606494, 0.1737786740159305, 0.50618385204516492, 0.1410196276251727, 0.11387012394613603, 0.65495706235644757, -0.1471040286600071, 0.39191632829675765, 0.27209886689377155, 0.22921916609747015, 0.31594157750415275, 0.083164981336785432, 0.52337569816710583, 0.11030438009398369, 0.0994553210598626, 0.32627160309625447, 1.6869219633548322, 0.1671596449321523, 0.39608333889797975, 1.9672400128367031, 0.2112860459380263, 0.39759209260484518, 0.49315944797564237, 0.11001715820476496, 0.42457235633607854, 0.29866519544113046, 0.22003222568267045, 1.6348320826989062, 0.14204705228632322, 0.31354453909210828, 0.36327126062933679, 0.11399827969689434, 0.26250780027653275, 0.087048226587570207, 0.41768684716340582, 0.34709129204326011, 0.54694119034601107, 0.10168361374281437, 0.19688844918679951, 0.096369893097141029, 0.064283744628515632, 0.59866964287040869, 0.18779500711465563, 0.28526957355436661, 4.497160848954092, 0.27816335401301467, 0.19759503863003627, 0.32534321775402902, 0.88092801270546939, 0.53770683037958689, 0.62523541458625875, 0.38862887225592835, 8.9423893806436947, 0.1707055753426249, -0.0045392225188694291, 0.43281777013407369, 0.4038339337931755, 0.29848941678744279, 0.50486611810690363, 0.20416016710134505, 0.37786883226247053, 0.30660473396545168, 0.099056690713860251, -0.12528852673946272, 0.56828589504273963, 0.42709487910492794, 0.30754245797259872, 0.2027325707898712, 0.47867402016933741, 0.25235599960491728, 0.27461271071945798, 0.16180633075430495, 0.16265567062641911, 1.3674277108964703, 0.39154540044675601, 0.6940379996415027, 0.21907777957076022, 0.43002082261612923, 0.35883396246780747, 2.14866937186624, 0.023834116016403728, 0.10690809723976459, 0.19260190219656109, 0.10018543971349883, 0.25029399705156441, 0.27289105703673971, 0.27414125878220769, 0.38410486964392021, 0.41157423561840989, 0.25560885483857998, 0.26415914042668892, 0.45042236628458776, 5.1806530624361873, 0.26775492464779277, 0.1092893187332014, 0.49105127378520796, 0.21612892161619468, 0.20554150559871076, -0.098567222442784846, 0.62798396414400903, 0.39691916519644738, 0.52126625307192964, 0.033508214851521445, 0.15945811172658625, 0.21618167602822336, 0.77439595551807217, 0.2118135370876674, 0.3711415248216472, 0.19821845671492946, 0.2036056569805392, 0.55828234088438433, 0.16608092230705596, 0.21621110164568497, 0.10714441797901852, 1.0952629739064397, 2.5286112038180537, 2.6692105863396702, -0.01250746882970899, 1.3027556342663451, 0.5178852657474452, 0.33038133582124851, -0.028045662802839211, 0.19112116664129089, 0.22783996775324922, 0.027895912459688116, 0.31597745633165375, 2.9156059132062677, 0.20633093344217435, 0.068025037590640464, 0.36277539550341958, 0.21121149557693672, 0.036098669490131741, 0.92722256598585262, 0.50444432724696497, 0.28058585273944309, 1.0468470015408249, 0.55646370216404462, 0.39861923205676408, 0.28263246004911752, 0.30152583872660282, 0.41136931539612104, 0.44422877172573993, 0.30583075859671083, 0.46030490252967904, 0.30584703727245199, 2.9467329469361681, 0.39645910928727646, 0.050446746689023034, 3.9061302446351323, 0.48905309219121007, 0.37620155104986602, 0.35765619817634714, 0.34818491485287084, 0.26390620604409737, 0.27191840800231215, 32.446752260746884, 0.068327097851894864, 0.27382025787829795, 0.13981717252761736, 0.1765044361196835, 0.40268109061677643, -0.073213042798167038, 0.34732484897071242, 0.21525334053329415, 0.27005146273976449, 0.75006427154937749, 0.41017618417902396, 0.21526313875715003, 0.28479053159050649, 0.74694780086632517, 0.34499992881068947, 0.55374827348213973, 0.42773631033150622, 0.45687461349883507, 0.31584790927784084, -0.0079193669110137199, 0.24281275147570405, 0.17293669007816342, -0.08816574245531919, 0.15541275418321238, 0.14961556630254103, 0.51827032630098591, 0.22555579344532531, 0.19120605665812557, 0.30600103217379504, -0.0045894214583369203, 1.3938614368754383, 0.30465748530874337, 0.14902575286620751, 0.46566502126795656, 0.11198366439419302, 0.12632944323424636, 1.7444420119682984, 0.3316758788570589, 0.27287966597850133, 0.16319296078407244, 0.54888156094601248, 0.55372081336875778, -0.051153513590546429, 0.28764248531792014, 1.0725599459097184, 0.3504735578941508, 0.06517136191864803, 0.091023571122416422, 0.28129512144343727, 0.17099594854044706, 0.3695272954637322, 0.2682639148785137, 0.167382906450461, 0.50724168497257593, 0.38990167370641521, 0.08866272470934311, 0.35108414254557546, 3.4053776346315972, 0.57301263514836964, 0.086215589222967931, 0.55352039293762012, -0.015903132641775147, 0.17830767092670158, 0.12249817148096714, 0.63903060014366753, 0.33152756793638338, 0.19310861339633836, 0.24550802542140188, 0.17768792197585068, 0.22068083570355687, 0.37897661663687404, 0.45783556047901824, 0.09641789479180915, 0.24964221766098349, 0.32695795571517305, 0.51164638058812073, 0.2411464237946721, 2.7600835971208348, -0.16227870849212661, 4.6816607136022057, 0.17065925522401454, 0.24835366112291493, 0.21146798047821486, 0.38213459667096011, 0.01376307935904493, 0.58984881946080447, 3.8968936062963495, 0.37063631233550576, 0.19870731487294363, 0.12195573273670521, 0.36102236870006077, 0.2501272152467634, 0.32912463857277285, 0.2086971812940881, 0.061738142630873738, 0.27041639334350132, 1.3042457646602745, 0.33470898004888999, 0.33991122760212195, 0.44828286750256741, 0.87318039612987253, 0.05896386953483658, 0.26701659948351913, 0.40960027773998159, 0.13968678580433264, 0.046042101489166473, 0.30494282117292643, 0.60672304799523757, 5.3186079642718296, 0.13919869009120561, 0.15562499782012715, 0.21522206674514482, 0.38448724004938306, 0.21593008087149779, -0.081532931171447887, 0.21569548030951433, 0.079494539397060526, 0.15609354647555518, 5.8811526168302724, 0.52690308761204241, -0.023093762819986569, 0.52312475372528655, 0.05230263008537879, 0.92184766080325886, 0.38016979288207692, 0.30059566933753379, 0.52941855869468379, 0.51638228711850487, 0.46043824346137863, 0.23204546066511503, 0.23223524277303106, 0.033264661132510448, 0.17580041657104575, 0.73092925918244922, 0.25802689307740206, -0.011214957236903605, 4.7279013154723648, 0.28534632333943466, 4.494801998498847, 0.23967372604393389, 0.80722063616684336, 0.31185439528541126, 0.11401655613515971, 0.29058725897161941, 0.46287230514772748, 0.4933345666931272, 0.24368131412073415, 0.20369771662122543, 0.40988525219005278, 0.37284604634209428, 0.1894058584454571, 0.3846748988705212, 0.36943038133658779, 0.072042117581797752, 20.710568909923357, 0.45135296011754478, 0.10273513863077258, 0.24288550130365374, 0.30216980253939912, 0.66764805927098014, 0.21889744839455827, -0.15161815477898177, 0.40088622285298897, 0.077404731851950515, 0.46144379673071367, 0.36793822797644737, 0.40706964030962811, 0.24251611639795295, 0.37458420498853162, 0.46795694556558465, 0.40759316295203069, 0.36975400220761212, 0.2075057042103059, -0.020780894051043997, 0.19031189124257761, 0.3048720096791348, 0.14981551072435259, 0.37455973304049156, 0.13827833931615405, 0.13769721188347706, 0.5544439559617963, 0.15799145811391094, -0.024599150624484811, 0.42599543795443789, 2.2367532609465055, 0.1580674463864023, 0.47153628007600101, 0.60872411917283986, -0.0063841729247747364, 0.42987407306192699, 0.34167065696689958, 1.0414311840561181, 0.9427879250993646, 0.44299159900157337, 0.3489531590234074, 0.14739510660100258, 0.47332632512647399, 0.16646408548285052, 0.27613045543667791, 0.11287703552040787, 0.89585457030868176, 1.61279250500565, 0.34561507321855139, 0.070275210493338305, 0.18146321764835446, 18.681767814112884, 0.48455028024316343, 0.20128768191150925, 0.52330013097502137, 7.1008202714088178, 0.37010982270783194, 0.31442885264644604, 0.41330614717068492, 0.78048558656617018, -0.057217985498593554, 0.25958272832492824, 0.60016461851862368, 0.11885173081954593, 0.00032907536094987921, 0.10730123889643409, 2.3868044161710533, 0.35461052743175236, 0.46233497542938473, 0.38412931593477312, 0.2507791336275938, 3.5677974040855438, 0.092151717935796809, 0.37033774563133326, 0.20975821103119618, 0.2930217691928132, 0.23465076686681277, 4.2109113976643435, 0.3811538350486679, 0.17363169571574555, 0.60812362534620346, 3.8960602454315612, 0.13547720627730275, 0.45647528979542606, 0.53370768667109914, 0.80664289086244068, 0.51317562407467321, 0.5732486849059637, 0.39576773612865834, 0.72799683159841866, 0.25558186287785162, 0.45216212844482895, 0.44989030229455251, 1.0777036537379978, 0.35449440016401118, 0.21745666159823276, 0.08385939708326387, 0.23807134890129975, 0.41793697520351214, 0.19405273048830241, 0.24026869218947591, -0.057683803958294783, 0.22865290496635099, 0.14558543337061411, 0.17313298751668338, 0.29512990962937202, 1.3404356149757868, 0.32444180906234382, 0.37814733742457529, 0.96501659914562199, 0.31131528867049346, 0.1874193886709859, 0.27316833210885888, 0.22805661447950959, 0.69800283497494542, 0.016519123997563862, 0.037922869555539473, 0.37557065350289354, 0.55325921026743985, 0.49772843073859074, 0.30810393896956589, 0.35193051876881454, 0.53059849113990398, 0.47586823947793788, 0.3976054006032993, 0.44093171126265224, 0.26782177374941113, 1.1109019236292412, 0.26953119432504213, 0.40782941861519817, 0.50121800517457293, 0.13950619935450309, 0.11569427718884551, 0.81796529351173819, 0.12981281660769634, 0.28645654904002699, 0.28384115357126921, 0.34365668708627645, 0.37699299076671983, 2.0184460204772541, 0.90891405519626456, 0.3643532379962855, 0.97736198926034568, 0.28128405663098266, 0.44958531381390487, 1.129400642534359, 0.6335846012991293, 0.14413484663021342, 0.097995782516389657, 0.64699866881471346, -0.050791934737012545, 0.36793806231783704, 0.43475537096174005, 0.24110774086663894, 0.28299432631649768, 0.41462965605496382, 0.45468474109225832, 1.7880726436111236, 4.1687120563261644, 0.27976440750480042, -0.020727771598637923, 0.21864466980069802, 0.12670609714659226, 0.25318756169668644, 0.80612823909714137, 0.17742462949568963, 0.28044469929147464, 0.64130202208341869, 0.31799211771390362, 0.23741994846077152, 0.73791699939337629, 0.4297658055585129, 0.55887636505591964, 0.47494429623264262, 0.10661375476180347, 0.47406827803178958, 3.2335292279859549, 0.34036919578993396, 0.047453674357706921, 0.45052897025523142, 0.11279206329526131, 0.33159323930125878, 0.34327362114995508, 0.36346863848096134, 0.68982103440925679, 6.7246586928492489, 0.52171982706775444, 0.33030585065997875, 0.13410785761428917, 0.21856764184796293, 0.44901290564722446, 0.4713146767340104, 0.25900659472682419, 0.20376923896532939, 0.5711750400499378, 0.16902054082045342, 0.069194978696148476, 0.24429961111801227, 0.13432632704546829, 0.24534807550783758, 0.42593820050884867, 0.22769103614399278, 2.0527847747442718, 0.043024196494029388, 2.0539545365606684, 0.14513811945828617, 0.20095315644411174, 0.63988016790999236, 0.12391623741698375, -0.035730951522284643, 0.11020098217058416, 0.0095591569626685413, 0.16492927765331028, 0.02174192736280707, 0.084582874395911667, 0.056901035333029815, 0.22842521113268027, 0.39009643842530639, 0.22131826066493396, 0.17887595153488325, 0.46158423598447912, 0.28333916379665414, 0.0088936100926575584, 0.27219838543481562, -0.010357620127780437, 0.12916481204147581, 0.018224452157487331, 0.16550812634402609, 0.34477556480425697, 0.15964464240788467, 0.14843727528144643, 0.32971374812466381, 0.34907744192890011, 0.46869526110281928, 0.23358995483387784, 0.15228885429492023, 0.030856097198415922, 0.25737744976606303, 0.3140503241293584, 0.45365374265767333, 0.29250336421969181, 0.47327899912168514, 4.5006218551790855, 0.7512367829863088, 0.0069288445009947491, 0.16397116352749241, 0.26592171576827872, 0.30223337832694802, 0.41786473222963438, 0.33571816189549247, 0.015643586437071977, -0.052060819845922895, 0.64429236358630471, 0.27365854486025143, 0.3855485500337491, 0.87698784995162171, 0.28129876421246114, 0.22062460674528958, 0.24836208382290331, 0.52934223337635156, 0.36805415715881562, 0.54989853883777351, 0.39082136502288078, 0.10268332502800992, 0.10791361553604706, 0.30095233794759962, 0.23071975750863757, 0.22742646126224686, 0.47489207434095582, 3.1165531760569385, 0.31300705152921965, 0.24071059075354845, 0.27739268820041835, 0.43720124690365147, 0.32709666537790472, 0.11666363684551834, 0.56451797912264179, 0.2214215509762206, 0.092659637028482789, -0.03096067291278104, 0.06008514409037443, 0.29664538013731401, 0.03070782705461067, -0.0046171222397209388, 0.095970914857730391, 0.97207366642944404, 0.40515684506282301, 0.64090494028754796, 0.42392408137069043, -0.021472287354359826, 0.38165274483694261, 0.42446894815047997, 0.76596836889509379, 0.22456819079932211, 0.788148326363309, 0.23549514893325563, -0.0036719456440022225, 0.2513255359263889, 0.49886200586755214, 13.458084186097556, 0.29480720622921125, 0.27860080337526338, 0.78740867030560391, 0.34713240758856861, 0.31924525366417728, -0.0074140662501528953, 0.44548843831479534, 0.12032194863061942, 0.46118607866065664, 0.065999419303187562, 0.55401166968680304, 0.69560368192083277, 0.46435757830826863, 0.41171873142190285, -0.10484804777297183, 0.28090392421813176, 0.44875890286944981, 2.9812610590712985, 0.3376420114941816, 0.33192185991514223, 8.656849076412156, 1.1352529538941092, 0.49913494070028103, 0.20413510535470358, 0.13694186849738338, 0.51315940939251248, 0.55613574398213783, 0.013069113247576217, 0.2087980473891356, 0.13670849052510806, 0.19381604122304252, 0.29738044585592277, 0.68951693150957949, 0.48729772919950975, 0.54170557538593544, 0.1793159732895156, 0.59770190635127918, 0.4947147323850653, 0.28872876378873941, 0.85704871384935499, 0.29668626429852796, 0.33904535785975898, 0.3840055057237996, 0.15001964827438208, 0.68912829817899302, 0.26213081786711739, 0.44566044897315121, 0.27275830656230948, 0.13018737050209497, 0.30891676681003466, 0.51585123536002042, 0.056298093207614364, 0.08186558028170321, 0.090394665747208475, 1.3150432417334574, 0.49891463035339118, 0.092509947222157857, 0.52791469475065633, 0.37482106839476137, 0.21785157355007137, 0.4036916319404395, 0.35248556880443049, 0.82409493577895565, 0.20983209160742713, 2.3020915000862092, -0.04214988801754524, 0.48658873304076244, 0.43596721461178123, 0.38612477952307794, 0.29590510718222368, 0.30510222671362186, 0.55402056536707445, 3.6479835469698054, 0.7995320831724827, 0.059563418931743911, 3.9668909075887382, 0.35674671728525797, -0.0096781343115327767, 0.5190671119655087, 0.31511403622173373, 0.55064169331753066, 0.24603165213973593, 0.31617169776811493, 3.463652274284545, 1.0667135525994957, 0.078283899063690904, 0.11740668173668309, 9.6795031777911422, 0.1673163062619886, 0.13718749965315377, 0.10367579134608246, 0.48363952025295542, 0.37666933297229149, 4.0095336628434586, 0.15004913308818757, 4.7737118799210547, 0.079213947938106599, 0.42896645385358595, 0.30976568076805011, 0.39615388260194229, 2.2249280689345432, 0.10863562475402727, 2.6193661564571666, 0.81973586800979792, 0.36942482480811556, 0.24563854291697812, 0.29294678532712481, 0.32978313276753241, 0.40719452926618194, 0.22784913021099784, 0.25351450362954958, 0.14542678422581704, 0.34969234955819978, 0.14172055536057973, 2.6784781711800121, 0.16287898228151104, 0.06499097177483748, 0.31853829053558752, 0.28295151014841829, 0.2007185345560917, 0.10305352433032899, 0.15856243157467195, 0.048737635602719792, 0.21935982929862688, 0.31974954081303986, 0.87347905248162161, 0.060932487686431558, 0.45996630808283367, -0.038864815818245579, 0.76838014653385778, 0.33547560016244177, 0.87488143467348611, 0.35804816558826108, 0.27783899766247799, 0.15181015873986192, 0.24227429885934137, 0.41403132898369044, 1.151267920286928, 0.096936094292352615, 0.32743518033016128, 0.20506731990919039, 0.030886942775235363, 0.3315888306087052, 0.10251439617884105, 0.56926585872648294, 0.30196113647027678, 0.31415127750625849, 1.9147521913456111, 0.3019883632933571, 0.16300618208814244, 0.18437748058669859, 0.32260190304231284, 0.54118465434493546, 0.74618721035265867, 2.1387718810475671, 1.4716525449224931, 0.38856035300550251, -0.11312329755218911, -0.051182210339495549, 0.16333451207971808, 9.0529104773792035, 0.30571808116373289, -0.17330734611539622, 4.3775104652453036, 0.12289197138739949, 0.13509390283550668, 0.34389176202918281, 9.8154540966747881, 0.52944391707456773, 0.48208469821604921, 0.40853217625071081, 0.4136977464680654, 0.44605744443778572, 0.54757733715556822, 0.16319896400050338, 1.1859299545986972, 0.016691435074499467, 0.21156989012896593, 2.5199142917933464, 0.40502668903196831, 0.83133421431837828, 1.0808241167581716, 0.34223990759748069, 0.0075873682808196841, 0.28680225234513185, 1.333390902676677, 1.2374036156406389, 0.015782408308436802, 8.9646624498861947, -0.076770318558496892, 0.31770617944335633, 0.47369760960343421, 0.90754403767554692, 0.37973484271655206, 0.2322461902617817, 0.35125605683593297, 0.56330757959852362, 0.17733595610881464, 0.23334197846186416, 0.32457019100234424, 0.73193168138731313, 0.059934065021714417, 0.5631342227874947, 0.22868756879664254, 0.28243566306843404, 0.39475000413935318, 0.5487321022476771, 0.3907452428320829, 4.614551295697181, 0.32043206018095532, 15.559012051662283, 0.10316945697999058, 0.3430673876657615, 0.20915543180855323, 0.62405157953651436, -0.041624025949129173, 0.27053589587668031, 0.55129150503338287, 0.55128451846687354, 0.22312657204413841, 0.3019526761386741, 0.14094929363519931, 0.25141467893463648, 0.264618054551784, 0.054103061977396755, 0.0040184626792335587, 0.041663590937675174, 0.080475543588107462, 0.14660156685012515, 0.40897974734658316, 0.60144943619103752, 0.36787138442303879, 0.28096600178336839, 0.45129939189556234, 0.074282512968092906, 0.49058776009862864, 0.28742741595418431, 0.53536031575121545, 0.4133705776604879, 0.33003650223175701, 0.53137366936141028, 0.26053311127120171, 5.71010197356118, -0.10813678132290294, 0.34530057442069961, 0.55437397499574514, 0.19427554065990332, 0.2374800755545774, 0.23676949780346165, 0.041595925772012099, 0.043102332452479686, 0.41122829580397319, 0.28453849205281356, 0.40927071278282795, 0.22066154492922646, 0.23715807830993765, 0.57349085889419937, 1.0128648914955625, 0.38246390482092979, 0.22282693542353291, 0.23639113882424329, 0.047835653116522991, 0.15148691732767577, -0.1163607417923449, 0.15469135604818038, 0.06942579210669636, 0.12569428253664369, 0.2883696408947567, 0.22226531747526812, 0.26671011348186424, 1.0530691946235478, 0.015210418914419918, 0.47147583412920629, 0.19182667276602972, -0.12677526477660014, 0.36660419911044972, 0.16105666159592255, 0.25102392273189023, 0.22739296134734382, 0.087963255481242841, 0.23631005438203867, 0.18379019671982808, 0.19375764241913032, 0.35848425427302394, 0.32219146760736267, 0.34071119962668389, 0.19810511219650084], + "configs": { + "default": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.12374156050324364, + "h_r": 0.12374156050324364, + "b_l": 0.19828485207529925, + "b_r": 0.19828485207529925, + "tau_cl": 0.0045867625674280155, + "tau_bc": 0.014457398760135964, + "se_cl": 0.050528070598693874, + "se_rb": 0.061727521277366693, + "z": [0.090776523090643824, 0.28612607979750726, 0.23421317527352234], + "pv": [0.92767016487618448, 0.77478155739641563, 0.81481948706848217], + "ci_lower": [-0.094446436014309146, -0.084575799821601197, -0.10652631979843258], + "ci_upper": [0.10361996114916518, 0.11349059734187313, 0.13544111731870451], + "N": [822, 178], + "N_h": [80, 61], + "N_b": [134, 96], + "bias": [-0.014269209334528077, -0.024139845527236026], + "beta_p_l": [0.49300242348215811, 1.4784272749921221], + "beta_p_r": [0.49758918604958613, 1.9762373628573231] + }, + "manual_h": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": 0.14999999999999999, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.14999999999999999, + "h_r": 0.14999999999999999, + "b_l": 0.14999999999999999, + "b_r": 0.14999999999999999, + "tau_cl": 0.0058688028024410088, + "tau_bc": 0.011184213112585439, + "se_cl": 0.043425618516998728, + "se_rb": 0.072373081111194629, + "z": [0.1351460958499347, 0.25754873492953101, 0.15453553919311405], + "pv": [0.89249636524506171, 0.79675519871087253, 0.87718749154526332], + "ci_lower": [-0.079243845497252136, -0.073928435187107705, -0.13066441931555206], + "ci_upper": [0.090981451102134153, 0.096296861412278584, 0.15303284554072294], + "N": [822, 178], + "N_h": [100, 76], + "N_b": [100, 76], + "bias": [-0.048428190216693479, -0.053743600526837909], + "beta_p_l": [0.47793264540837382, 0.98752065102483433], + "beta_p_r": [0.48380144821081483, 2.4134432596327442] + }, + "h_rho2": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": 0.14999999999999999, + "b_in": null, + "rho_in": 2, + "level": 95, + "h_l": 0.14999999999999999, + "h_r": 0.14999999999999999, + "b_l": 0.074999999999999997, + "b_r": 0.074999999999999997, + "tau_cl": 0.0058688028024410088, + "tau_bc": 0.12362513918779416, + "se_cl": 0.043425618516998728, + "se_rb": 0.26665015412090759, + "z": [0.1351460958499347, 2.8468250633989647, 0.46362298043802602], + "pv": [0.89249636524506171, 0.0044157620751211326, 0.64291788693480623], + "ci_lower": [-0.079243845497252136, 0.03851249088810102, -0.39899955936123921], + "ci_upper": [0.090981451102134153, 0.20873778748748731, 0.64624983773682754], + "N": [822, 178], + "N_h": [100, 76], + "N_b": [54, 41], + "bias": [-0.22772699019094583, -0.34548332657629899], + "beta_p_l": [0.47793264540837382, 0.98752065102483433], + "beta_p_r": [0.48380144821081483, 2.4134432596327442] + }, + "rho_only": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": 2, + "level": 95, + "h_l": 0.12374156050324364, + "h_r": 0.12374156050324364, + "b_l": 0.061870780251621821, + "b_r": 0.061870780251621821, + "tau_cl": 0.0045867625674285151, + "tau_bc": 0.17849333180019944, + "se_cl": 0.050528113941012746, + "se_rb": 0.25169142352186114, + "z": [0.090776445223804875, 3.5325548071826933, 0.70917526430810651], + "pv": [0.92767022674947786, 0.00041156490597466362, 0.47821572145698604], + "ci_lower": [-0.094446520963692646, 0.079460048269078279, -0.31481279352026564], + "ci_upper": [0.10362004609854968, 0.27752661533132061, 0.67179945712066447], + "N": [822, 178], + "N_h": [80, 61], + "N_b": [47, 39], + "bias": [-0.12413959126938806, -0.29804616050215899], + "beta_p_l": [0.49300242348215823, 1.4784272749921255], + "beta_p_r": [0.49758918604958674, 1.976237362857306] + }, + "p2q3": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 2, + "q": 3, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.2049530822916461, + "h_r": 0.2049530822916461, + "b_l": 0.30702988217791288, + "b_r": 0.30702988217791288, + "tau_cl": 0.02434045570962251, + "tau_bc": 0.0088626421325563864, + "se_cl": 0.058803587413759045, + "se_rb": 0.066007210822619358, + "z": [0.41392807446178453, 0.15071601108613109, 0.13426778714181534], + "pv": [0.67892678267421491, 0.88019974249410293, 0.89319082469381794], + "ci_lower": [-0.090912457783098005, -0.10639027136016413, -0.12050911379971999], + "ci_upper": [0.13959336920234303, 0.1241155556252769, 0.13823439806483276], + "N": [822, 178], + "N_h": [138, 101], + "N_b": [243, 125], + "bias": [-0.0055315047159327779, 0.0099463088611333461], + "beta_p_l": [0.50037383566227689, 2.1535540572777725, 9.3916289987697237], + "beta_p_r": [0.5247142913718994, 0.42996917847191218, 15.09527327918364] + }, + "p0q1": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 0, + "q": 1, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.026944497304939571, + "h_r": 0.026944497304939571, + "b_l": 0.078731009939489421, + "b_r": 0.078731009939489421, + "tau_cl": 0.04045940922839919, + "tau_bc": 0.012453587913814901, + "se_cl": 0.061873695952977287, + "se_rb": 0.077128224155717531, + "z": [0.65390322341738716, 0.20127434965707183, 0.1614660268680867], + "pv": [0.51317415709021863, 0.8404840581428612, 0.87172636686153893], + "ci_lower": [-0.080810806429817944, -0.10881662774440223, -0.13871495362292363], + "ci_upper": [0.16172962488661632, 0.13372380357203204, 0.16362212945055343], + "N": [822, 178], + "N_h": [22, 14], + "N_b": [56, 42], + "bias": [-0.022841368737924306, 0.0051644525766599836], + "beta_p_l": 0.50959037275686325, + "beta_p_r": 0.55004978198526244 + }, + "epa": { + "c": 0, + "masspoints": "adjust", + "kernel": "epa", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.11465123781673565, + "h_r": 0.11465123781673565, + "b_l": 0.18917029925767406, + "b_r": 0.18917029925767406, + "tau_cl": 0.0051370512478132735, + "tau_bc": 0.012413827262671373, + "se_cl": 0.049927762311371728, + "se_rb": 0.061217853423602836, + "z": [0.10288967520267256, 0.24863576270959684, 0.20278115890102677], + "pv": [0.9180505322041238, 0.80364254236637545, 0.83930608500534065], + "ci_lower": [-0.092719564711151559, -0.085442788696293459, -0.1075709606584422], + "ci_upper": [0.10299366720677811, 0.11027044322163621, 0.13239861518378493], + "N": [822, 178], + "N_h": [76, 57], + "N_b": [127, 94], + "bias": [-0.013642530100213768, -0.020919306115071867], + "beta_p_l": [0.48963577948154069, 1.4336491930868287], + "beta_p_r": [0.49477283072935396, 1.9939853981873976] + }, + "uni": { + "c": 0, + "masspoints": "adjust", + "kernel": "uni", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.094847532972169801, + "h_r": 0.094847532972169801, + "b_l": 0.16837305267701771, + "b_r": 0.16837305267701771, + "tau_cl": -0.0060971035489166825, + "tau_bc": -0.00060007960621022427, + "se_cl": 0.049700514768542071, + "se_rb": 0.060703036752554056, + "z": [-0.12267686918960934, -0.012073911286529461, -0.009885495657430618], + "pv": [0.9023629812027365, 0.99036664665384877, 0.99211264410197375], + "ci_lower": [-0.10350832250836017, -0.098011298565653715, -0.11957584539342737], + "ci_upper": [0.091314115410526808, 0.096811139353233266, 0.11837568618100693], + "N": [822, 178], + "N_h": [66, 52], + "N_b": [114, 87], + "bias": [-0.013719040200740107, -0.019216064143446565], + "beta_p_l": [0.49479153474093163, 1.6181880792361112], + "beta_p_r": [0.48869443119201494, 2.1779553141550232] + }, + "level90": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 90, + "h_l": 0.12374156050324364, + "h_r": 0.12374156050324364, + "b_l": 0.19828485207529925, + "b_r": 0.19828485207529925, + "tau_cl": 0.0045867625674280155, + "tau_bc": 0.014457398760135964, + "se_cl": 0.050528070598693874, + "se_rb": 0.061727521277366693, + "z": [0.090776523090643824, 0.28612607979750726, 0.23421317527352234], + "pv": [0.92767016487618448, 0.77478155739641563, 0.81481948706848217], + "ci_lower": [-0.078524517619693646, -0.068653881426985697, -0.087075338495664817], + "ci_upper": [0.087698042754549677, 0.097568678947257625, 0.11599013601593675], + "N": [822, 178], + "N_h": [80, 61], + "N_b": [134, 96], + "bias": [-0.014269209334528077, -0.024139845527236026], + "beta_p_l": [0.49300242348215811, 1.4784272749921221], + "beta_p_r": [0.49758918604958613, 1.9762373628573231] + }, + "msetwo": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "msetwo", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.1713946342456534, + "h_r": 0.1008626906036811, + "b_l": 0.26966566236717282, + "b_r": 0.19499668784156782, + "tau_cl": 0.044212263133197705, + "tau_bc": 0.052473332067347167, + "se_cl": 0.05166351825043184, + "se_rb": 0.060383983570265719, + "z": [0.85577337027038702, 1.0156747709861698, 0.86899420947090489], + "pv": [0.39212314766656337, 0.30978428588254164, 0.38485029798992226], + "ci_lower": [-0.057046371952276451, -0.048785303018126988, -0.065877100973431954], + "ci_upper": [0.14547089821867187, 0.15373196715282134, 0.1708237651081263], + "N": [822, 178], + "N_h": [115, 52], + "N_b": [205, 95], + "bias": [-0.0081916658773175666, -0.016452734811467029], + "beta_p_l": [0.47002174661489238, 0.76697669451501349], + "beta_p_r": [0.51423400974809008, 1.3716452788946818] + }, + "cercomb2": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "cercomb2", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.08760231611138608, + "h_r": 0.077552351314838466, + "b_l": 0.22896180010747066, + "b_r": 0.19828485207529925, + "tau_cl": 0.022968334395886303, + "tau_bc": 0.026897175618029068, + "se_cl": 0.069236154856099005, + "se_rb": 0.073097890303373214, + "z": [0.33173902339931904, 0.38848453779578573, 0.36796103836101896], + "pv": [0.74008633131514934, 0.69765749438558056, 0.71290228601651862], + "ci_lower": [-0.11273203555010566, -0.1088031943279629, -0.11637205672244202], + "ci_upper": [0.15866870434187827, 0.16259754556402103, 0.17016640795850016], + "N": [822, 178], + "N_h": [60, 42], + "N_b": [158, 96], + "bias": [-0.0039153639852980149, -0.0078442052074407798], + "beta_p_l": [0.51104889888935268, 2.2300898325119816], + "beta_p_r": [0.53401723328523898, 0.54446009209947699] + } + } + }, + "dgp_ties_moderate": { + "x": [-0.56000000000000005, -0.42999999999999999, 0.47999999999999998, -0.29999999999999999, -0.27000000000000002, -0.11, -0.77000000000000002, -0.59999999999999998, -0.52000000000000002, 0.29999999999999999, -0.14000000000000001, -0.28000000000000003, -0.56000000000000005, -0.089999999999999997, -0.91000000000000003, 0.02, -0.53000000000000003, -0.71999999999999997, -0.42999999999999999, -0.70999999999999996, -0.62, -0.57999999999999996, -0.85999999999999999, 0.089999999999999997, -0.26000000000000001, -0.73999999999999999, 0.32000000000000001, -0.13, -0.46000000000000002, 0.12, 0.11, 0.080000000000000002, 0.01, -0.059999999999999998, -0.35999999999999999, -0.46000000000000002, -0.48999999999999999, -0.60999999999999999, -0.41999999999999998, -0.77000000000000002, 0.28999999999999998, -0.73999999999999999, -0.5, -0.53000000000000003, 0.059999999999999998, -0.37, -0.20999999999999999, -0.34999999999999998, -0.34999999999999998, 0.38, -0.42999999999999999, 0.46000000000000002, -0.050000000000000003, -0.28000000000000003, -0.23000000000000001, -0.14999999999999999, -0.54000000000000004, -0.46999999999999997, -0.70999999999999996, -0.71999999999999997, -0.19, -0.12, -0.31, 0.14000000000000001, -0.53000000000000003, -0.94999999999999996, 0.17999999999999999, -0.60999999999999999, -0.60999999999999999, 0.19, -0.45000000000000001, -0.76000000000000001, -0.25, -0.39000000000000001, -0.33000000000000002, -0.14999999999999999, -0.48999999999999999, -0.02, -0.42999999999999999, -0.17999999999999999, 0.23000000000000001, -0.12, -0.46999999999999997, 0.26000000000000001, 0.17999999999999999, -0.070000000000000007, -0.22, -0.57999999999999996, -0.56999999999999995, 0.46999999999999997, -0.42999999999999999, -0.70999999999999996, -0.60999999999999999, -0.20999999999999999, -0.44, -0.47999999999999998, -0.68999999999999995, -0.34999999999999998, -0.64000000000000001, -0.85999999999999999, -0.48999999999999999, -0.56999999999999995, -0.029999999999999999, -0.84999999999999998, -0.35999999999999999, -0.080000000000000002, -0.19, -0.28999999999999998, -0.58999999999999997, -0.66000000000000003, -0.70999999999999996, -0.28000000000000003, -0.68999999999999995, -0.53000000000000003, -0.44, -0.58999999999999997, -0.22, -0.29999999999999999, -0.68999999999999995, -0.35999999999999999, -0.12, -0.31, -0.51000000000000001, -0.92000000000000004, 0.25, -0.81999999999999995, 0.040000000000000001, -0.81999999999999995, 0.02, -0.45000000000000001, -0.83999999999999997, -0.82999999999999996, -0.84999999999999998, -0.55000000000000004, -0.81999999999999995, 0.01, 0.72999999999999998, -0.78000000000000003, 0.059999999999999998, 0.050000000000000003, -0.17999999999999999, -0.70999999999999996, -0.39000000000000001, -0.45000000000000001, -0.059999999999999998, -0.48999999999999999, 0.17000000000000001, -0.48999999999999999, 0.20999999999999999, -0.71999999999999997, -0.77000000000000002, -0.51000000000000001, -0.19, -0.02, -0.53000000000000003, -0.080000000000000002, -0.16, -0.42999999999999999, -0.29999999999999999, -0.34999999999999998, -0.62, -0.72999999999999998, -0.32000000000000001, -0.19, -0.12, -0.52000000000000002, -0.71999999999999997, 0.32000000000000001, -0.47999999999999998, -0.66000000000000003, -0.44, -0.41999999999999998, 0.23999999999999999, -0.28999999999999998, 0.040000000000000001, -0.54000000000000004, -0.23000000000000001, -0.46999999999999997, -0.28999999999999998, -0.67000000000000004, -0.79000000000000004, -0.040000000000000001, -0.77000000000000002, -0.57999999999999996, 0.34999999999999998, -0.45000000000000001, -0.070000000000000007, -0.51000000000000001, -0.53000000000000003, -0.64000000000000001, -0.56999999999999995, 0.53000000000000003, -0.35999999999999999, -0.28000000000000003, -0.22, 0.31, -0.54000000000000004, -0.69999999999999996, -0.62, -0.77000000000000002, -0.78000000000000003, -0.56999999999999995, -0.029999999999999999, 0.23999999999999999, 0.02, -0.48999999999999999, -0.31, -0.60999999999999999, -0.62, 0.12, -0.70999999999999996, -0.37, -0.23000000000000001, -0.62, -0.56999999999999995, -0.79000000000000004, -0.40999999999999998, -0.13, -0.17999999999999999, -0.64000000000000001, -0.64000000000000001, -0.54000000000000004, 0.45000000000000001, -0.73999999999999999, -0.40999999999999998, 0.65000000000000002, -0.38, -0.59999999999999998, -0.10000000000000001, -0.48999999999999999, -0.56000000000000005, -0.47999999999999998, -0.28999999999999998, 0.5, -0.37, 0.22, -0.02, -0.38, -0.82999999999999996, -0.55000000000000004, -0.53000000000000003, -0.31, 0.34999999999999998, -0.39000000000000001, -0.88, -0.5, -0.28999999999999998, 0.089999999999999997, 0.16, -0.81000000000000005, 0.10000000000000001, -0.52000000000000002, -0.25, -0.29999999999999999, -0.13, -0.32000000000000001, 0.029999999999999999, -0.14999999999999999, -0.45000000000000001, -0.28000000000000003, -0.27000000000000002, -0.23000000000000001, -0.42999999999999999, -0.26000000000000001, 0.20999999999999999, 0.26000000000000001, -0.56999999999999995, -0.29999999999999999, 0.63, -0.79000000000000004, -0.32000000000000001, 0.32000000000000001, -0.60999999999999999, -0.63, -0.68999999999999995, -0.71999999999999997, -0.51000000000000001, -0.17999999999999999, -0.23000000000000001, 0.69999999999999996, 0.050000000000000003, -0.87, -0.57999999999999996, -0.47999999999999998, 0.02, -0.38, -0.77000000000000002, 0.13, -0.22, 0.29999999999999999, -0.79000000000000004, -0.01, -0.55000000000000004, 0.01, -0.35999999999999999, -0.02, 0.35999999999999999, -0.33000000000000002, 0.19, -0.76000000000000001, -0.62, 0.46000000000000002, -0.14999999999999999, -0.92000000000000004, -0.80000000000000004, -0.41999999999999998, 0.10000000000000001, -0.38, -0.029999999999999999, -0.31, -0.35999999999999999, -0.87, -0.28999999999999998, -0.56000000000000005, -0.69999999999999996, -0.40999999999999998, 0.19, -0.51000000000000001, -0.28000000000000003, -0.67000000000000004, -0.17999999999999999, -0.14000000000000001, -0.34999999999999998, -0.95999999999999996, -0.41999999999999998, -0.01, -0.20999999999999999, 0.19, 0.080000000000000002, -0.41999999999999998, -0.14999999999999999, -0.68999999999999995, -0.52000000000000002, -0.85999999999999999, -0.52000000000000002, 0.080000000000000002, -0.089999999999999997, -0.56999999999999995, -0.69999999999999996, -0.27000000000000002, -0.34000000000000002, -0.88, -0.32000000000000001, -0.25, -0.25, -0.71999999999999997, -0.10000000000000001, -0.11, -0.73999999999999999, -0.51000000000000001, -0.17000000000000001, -0.58999999999999997, -0.93000000000000005, 0.11, -0.65000000000000002, -0.56999999999999995, -0.63, 0.089999999999999997, -0.77000000000000002, -0.47999999999999998, -0.37, -0.75, -0.58999999999999997, -0.34999999999999998, -0, -0.85999999999999999, -0.47999999999999998, 0.050000000000000003, -0.55000000000000004, -0.23000000000000001, -0.10000000000000001, -0.20999999999999999, -0.01, -0.39000000000000001, -0.51000000000000001, -0.37, -0.13, -0.070000000000000007, -0.56000000000000005, -0.20000000000000001, -0.28000000000000003, 0.33000000000000002, -0.62, -0.52000000000000002, -0.33000000000000002, 0.52000000000000002, -0.81000000000000005, -0.41999999999999998, -0.14999999999999999, -0.19, -0.70999999999999996, -0.32000000000000001, -0.72999999999999998, 0.02, 0.23000000000000001, -0.93999999999999995, 0.31, -0.77000000000000002, -0.11, -0.01, -0.41999999999999998, -0.58999999999999997, -0.26000000000000001, -0.12, 0.11, -0.92000000000000004, -0.84999999999999998, 0.41999999999999998, 0.20000000000000001, -0.12, 0.02, 0.13, -0.96999999999999997, 0.23999999999999999, -0.53000000000000003, -0.23000000000000001, -0.46000000000000002, 0.11, -0.47999999999999998, -0.080000000000000002, -0.5, -0.72999999999999998, 0.29999999999999999, 0.040000000000000001, 0.56000000000000005, -0.29999999999999999, 0.25, -0.45000000000000001, -0.79000000000000004, -0.44, -0.42999999999999999, -0.26000000000000001, 0.56000000000000005, -0.46999999999999997, -0.16, -0.42999999999999999, -0.32000000000000001, -0.19, 0.35999999999999999, -0.28000000000000003, 0.02, 0.059999999999999998, 0.13, -0.56999999999999995, 0.52000000000000002, -0.48999999999999999, -0.38, -0.72999999999999998, -0.67000000000000004, -0.80000000000000004, -0.25, -0.26000000000000001, -0.16, -0.059999999999999998, -0.57999999999999996, -0.69999999999999996, 0.19, 0.040000000000000001, -0.82999999999999996, -0.38, -0.67000000000000004, -0.92000000000000004, -0.26000000000000001, -0.37, -0.38, -0.23000000000000001, 0.11, -0.23999999999999999, -0.57999999999999996, -0.62, -0.39000000000000001, -0.19, -0.70999999999999996, -0.40999999999999998, 0.16, -0.38, -0.60999999999999999, -0.45000000000000001, -0.070000000000000007, 0.31, -0.27000000000000002, -0.14000000000000001, -0.56000000000000005, -0.040000000000000001, -0.54000000000000004, -0.81000000000000005, -0.27000000000000002, 0.070000000000000007, -0.16, -0.57999999999999996, -0.41999999999999998, -0.45000000000000001, -0.53000000000000003, 0.02, -0.76000000000000001, 0.11, 0.10000000000000001, -0.76000000000000001, -0.02, -0.56000000000000005, 0.089999999999999997, -0.64000000000000001, 0.23999999999999999, -0.22, -0.81999999999999995, -0.35999999999999999, -0.55000000000000004, -0.41999999999999998, -0.57999999999999996, -0.65000000000000002, -0.050000000000000003, -0.72999999999999998, -0.75, -0.28999999999999998, -0.070000000000000007, -0.050000000000000003, -0.46000000000000002, -0.29999999999999999, 0.16, -0.64000000000000001, -0.17999999999999999, -0.52000000000000002, 0.38, 0, -0.29999999999999999, -0.82999999999999996, -0.32000000000000001, -0.46999999999999997, -0.38, -0.75, -0.089999999999999997, -0.70999999999999996, -0.42999999999999999, -0.14999999999999999, -0.64000000000000001, -0.050000000000000003, -0.79000000000000004, -0.97999999999999998, -0.11, 0.089999999999999997, -0.46000000000000002, -0.089999999999999997, -0.75, -0.39000000000000001, -0.90000000000000002, 0.28000000000000003, -0.34999999999999998, -0.34999999999999998, -0.82999999999999996, 0.059999999999999998, -0.58999999999999997, -0.81000000000000005, -0.46000000000000002, -0.66000000000000003, -0.5, -0.76000000000000001, 0.55000000000000004, -0.34000000000000002, 0.22, -0.96999999999999997, -0.52000000000000002, -0.59999999999999998, -0.76000000000000001, 0.47999999999999998, -0.81000000000000005, -0.17999999999999999, 0.089999999999999997, -0.25, -0.67000000000000004, 0.14999999999999999, -0.25, -0.71999999999999997, -0.80000000000000004, -0.59999999999999998, -0.89000000000000001, -0.070000000000000007, -0.19, -0.90000000000000002, -0.38, 0.26000000000000001, -0.02, -0.54000000000000004, -0.65000000000000002, -0.20999999999999999, -0.26000000000000001, -0.02, -0.5, 0.12, -0.65000000000000002, -0.46999999999999997, 0.040000000000000001, 0.17000000000000001, -0.90000000000000002, -0.28000000000000003, -0.029999999999999999, -0.81999999999999995, -0.10000000000000001, -0.65000000000000002, 0.19, -0.070000000000000007, 0.050000000000000003, -0.28000000000000003, 0.10000000000000001, 0.39000000000000001, -0.34999999999999998, -0.93999999999999995, -0.32000000000000001, -0.23999999999999999, -0.40000000000000002, -0.059999999999999998, 0.17999999999999999, -0.54000000000000004, -0.46000000000000002, -0.14000000000000001, -0.56000000000000005, -0.28999999999999998, -0.91000000000000003, -0.73999999999999999, -0.79000000000000004, -0.66000000000000003, -0.60999999999999999, -0.14999999999999999, 0.17000000000000001, -0.62, -0.69999999999999996, -0.71999999999999997, -0.51000000000000001, -0.44, -0.10000000000000001, -0.46999999999999997, -0.92000000000000004, -0.37, 0.28000000000000003, 0.28999999999999998, -0.64000000000000001, -0.83999999999999997, -0.40000000000000002, -0.38, -0.13, -0.84999999999999998, -0.62, -0.83999999999999997, -0.60999999999999999, -0.28000000000000003, -0.80000000000000004, -0.040000000000000001, -0.20000000000000001, -0.67000000000000004, -0.23000000000000001, 0.040000000000000001, 0.20999999999999999, -0.42999999999999999, -0.37, -0.37, 0.41999999999999998, 0.25, 0.089999999999999997, -0.46000000000000002, -0.16, -0.14000000000000001, -0.71999999999999997, -0.87, -0.02, -0.82999999999999996, -0.33000000000000002, -0.22, -0.059999999999999998, -0.25, -0.62, 0.17000000000000001, 0.11, 0.40000000000000002, -0.35999999999999999, -0.080000000000000002, -0.029999999999999999, -0.38, -0.37, 0.19, 0.02, 0.17000000000000001, 0.81999999999999995, -0, -0.23999999999999999, -0.93999999999999995, -0.53000000000000003, -0.16, -0.38, -0.089999999999999997, -0.23000000000000001, -0.40999999999999998, -0.39000000000000001, 0.19, -0.41999999999999998, -0.92000000000000004, -0.41999999999999998, -0.070000000000000007, -0.029999999999999999, -0.029999999999999999, -0.85999999999999999, -0.16, 0.16, 0.33000000000000002, -0.42999999999999999, -0.46999999999999997, 0.45000000000000001, -0.85999999999999999, -0.51000000000000001, -0.11, -0.84999999999999998, -0.20000000000000001, 0.64000000000000001, -0.34000000000000002, -0.45000000000000001, -0.10000000000000001, -0.45000000000000001, 0.080000000000000002, 0.13, -0.33000000000000002, -0.75, -0.79000000000000004, -0.56999999999999995, 0.070000000000000007, -0.91000000000000003, -0.90000000000000002, -0.58999999999999997, -0.14999999999999999, -0.68000000000000005, 0.12, -0.17999999999999999, -0.40999999999999998, 0.080000000000000002, -0.14999999999999999, -0.52000000000000002, -0.23000000000000001, -0.01, -0.16, -0.58999999999999997, 0.10000000000000001, 0.26000000000000001, -0.20000000000000001, -0.27000000000000002, -0.37, -0.20000000000000001, -0.40000000000000002, -0.95999999999999996, -0.83999999999999997, -0.37, -0.23999999999999999, -0.20000000000000001, 0.080000000000000002, -0.41999999999999998, -0.68000000000000005, -0.76000000000000001, -0.77000000000000002, -0.37, 0.059999999999999998, -0.45000000000000001, -0.56999999999999995, -0.48999999999999999, -0.89000000000000001, -0.75, -0.81999999999999995, 0.20999999999999999, -0.56999999999999995, 0.059999999999999998, 0.46000000000000002, -0.41999999999999998, -0.20000000000000001, -0.87, -0.65000000000000002, -0.31, -0.19, -0.63, -0.52000000000000002, -0.29999999999999999, -0.34000000000000002, -0.58999999999999997, -0.34999999999999998, -0.059999999999999998, -0.38, 0.19, -0.75, -0.57999999999999996, -0.81999999999999995, -0.47999999999999998, -0.27000000000000002, 0.52000000000000002, 0.13, -0.27000000000000002, -0.80000000000000004, -0.66000000000000003, -0.40000000000000002, 0.25, -0.55000000000000004, -0.73999999999999999, -0.27000000000000002, -0.72999999999999998, 0.13, 0.44, 0.070000000000000007], + "y": [0.60497987637334627, 0.52992717259849387, 1.8444375264527952, 0.40311117148540998, -0.0075982634427801399, 0.35643934917970183, 0.25028911250464636, 0.1823852430447051, 0.22120719948759895, 1.4285606880938384, 0.20664603089976497, 0.046618195873929347, 0.17058876100404965, 0.13084771086003233, -0.059387582970284486, 1.1810750764199374, 0.62455913744060931, 0.062177404843431233, -0.054301083675980938, 0.0043137797503670361, -0.10173595976838684, 0.14734438792789711, -0.11494018437190993, 1.2677657482015579, 0.054605836354159026, -0.0021218945781308896, 2.2477155726254683, 0.72138510260127664, -0.055370242085428234, 1.8437768704942483, 1.5734294941731135, 1.6543940955155214, 1.5861084472209674, 1.2246349291170173, -0.14358664317616726, 0.162275957703857, -0.42599318639028472, 0.18895077688762757, 0.49298254151642151, 0.31769867233034588, 1.1544564244488638, 0.031830849158209079, 0.57801098541509544, -0.048204758912690965, 1.4843548403754152, 0.19303883333857763, 0.44150562542991068, 0.41954796272379069, 0.6153462651134447, 1.7753537228665024, 0.21488341364174063, 2.6143993631664384, 0.58932967843449735, 0.33262593279894997, -0.086672937585034693, 0.38085681428666796, 0.0016021540499356335, 0.12068625105718786, -0.24062529238134991, -0.2820446957328101, 0.13176791045396341, 0.33964864538006778, 0.66243979440858802, 1.9267259879677985, -0.031992535382916978, -0.76577493636162186, 1.390624971439214, -0.12532815997215235, 0.043091401268491245, 1.4532178172897559, 0.74200420724956695, -0.18968026032550539, -0.064183341071852751, 0.14562147281524596, -0.065613274478024175, 0.42684671218259795, 0.17809008422696082, 0.59067628348851531, -0.33055747773726435, 0.42221338729119462, 1.7771350241624939, -0.022332534251346292, 0.41060969191912333, 1.9432512637283013, 2.3338858083463339, 0.49101089621271837, 0.33802005825540016, 0.064975750224730941, 0.064929869270090215, 1.3214581673987, -0.34533811759267302, -0.091261690037360954, -0.16232021416994949, 0.34842095751722757, -0.48536251197815933, -0.33360947635802735, -0.38244503168162264, 0.51581746631807424, -0.34154700212703482, -0.42785418607295927, 0.13196214560221578, -0.052823908547532619, 0.51992515360038871, 0.51151859461370752, -0.12538110137747094, 0.3443591080270586, 0.19297216507688011, 0.72171862804973619, -0.20284547686708324, -0.052626071132715523, 0.16814008429681232, -0.041577160784519951, 0.44455274472260303, 0.27872872464227372, -0.17426198285910643, 0.16437334271732143, 0.26000785703007945, 0.35396863159830039, -0.078992559104698962, 0.53315481103350304, -0.0013301157110002237, 0.095214990841225255, 0.017242796673380203, -0.53473961745540011, 1.3880134868701417, -0.16139407217299881, 1.4923474601429796, -0.92080283242512961, 1.828172036715813, 0.21491772079555702, 0.55286221194449858, 0.041559471415329008, -0.31408779268859777, 0.89921734408742904, 0.69366780717465237, 1.1423864551129461, 2.2247095867866271, -0.18737407558964511, 1.6041153439662037, 1.6082628185706442, -0.022570138860715738, 0.017676874072768509, 0.71277420894253873, 0.090772998759559145, 0.40312198679046385, 0.52757160190123997, 1.9055188722048846, -0.38654844551438539, 1.7365670919786562, 0.42006417040332011, 0.30858290497574264, 0.21798548114426891, 0.5643662421338681, 0.12491943634339497, 0.16603947006441194, 0.14966531944365341, 0.2345945795709167, 0.43668110513388075, -0.081067933507342926, 0.30007547526191752, 0.13249961184146303, -0.067526409048111052, 0.79065664655062451, 0.041295801427425061, 0.58583907843067984, 0.057320829764799727, -0.15424967315017721, 1.8952273701557143, -0.19012017720517904, -0.42203527495552157, -0.00034426264502684933, 0.68952714492663425, 1.7087294323837561, 0.36743031946836585, 1.4750460086967381, 0.20914781958614756, 0.030496137094856213, 0.47137313997059221, 0.44341157807240539, -0.27793584680751149, -0.11563402625640243, 0.68289948584973681, 0.05131929461638074, 0.48058020476306434, 1.596103673531545, 0.47484098464711794, 0.75496440298236567, 0.043255060429143594, -0.21677800784656553, -0.33874355726535488, 0.1813360873834913, 1.9026619798837316, 0.74573080005543502, 0.43654138814886134, 0.21241653745095138, 1.4403373254508658, -0.10672050238157579, 0.042866517869199386, -0.13128039411699516, 0.038269036069207008, -0.2243014156509347, 0.012332027361657408, 0.25684709815765172, 2.2635130754726109, 1.6157865194411183, 0.17719009214983517, -0.2555587244652201, 0.20993756986479814, -0.30308707666391355, 1.3285435276937108, 0.20750235130649469, 0.068189806047530971, -0.20851168400095421, 0.5349712329668066, -0.66922207761775621, 0.039843458957875388, 0.477174774725383, 0.2067096400018397, 0.48928611542346623, 0.11973911651656655, 0.30018694587435379, 0.21322981638575658, 1.7865348662722427, 0.18279761738206335, 0.41218670695292986, 1.7390292897592623, -0.2242362301987193, 0.068083261997953337, 0.33781128756744516, -0.18766173376868878, 0.077179203854509798, -0.27999895794607171, 0.31636790539785969, 1.7125214837374241, 0.49114928225774418, 2.40334674234846, 0.2092062268939624, 0.51329925128289244, 0.083544918330484941, 0.038941732708392338, -0.060093912217104334, 0.72459231205124541, 1.1783626545291259, -0.0049584374780990148, -0.63505303309733274, 0.51859403168382312, 0.21078897020654291, 1.4145986401590986, 2.5832133422198988, -0.16301118036628859, 1.446875206440043, 0.17395957504084342, -0.17052738622649061, 0.4070907928016203, 0.36715103967600998, 0.38455753675904369, 1.2292888092186438, 0.073104847355617064, -0.068024398982855477, 0.045603128072165322, 0.67371499000507673, 0.78974366854242706, 0.10893241408827983, 0.18431903182459586, 1.5692883508734996, 1.7287709433559049, 0.073071270110316472, 0.34701031632967116, 1.7799963176861233, -0.38606891664609577, 0.60312329912382534, 1.5914117916690032, 0.10291370856763522, -0.021091160140999606, -0.33935481764520065, 0.10131857282345227, 0.1439314620478864, 0.77593500686375227, 0.35123787538809637, 1.9605362725397442, 1.6234884739916093, -0.55167749471060179, -0.21476821601807899, 0.26908197534182915, 1.4160637296328153, 0.17621171609426092, -0.15056651282658501, 1.4088462143676415, -0.28160659772472518, 1.8446504910529777, 0.096491852294057434, 0.10538512939496253, 0.50472081553758186, 1.6235464471061705, 0.61449208849816161, 0.19684885862731349, 1.8380343865062754, 0.20599581231835748, 1.8825522295645329, -0.28075787037380806, 0.00097069841426167041, 1.3344022553768167, 0.14671356713884992, -0.19848983490046707, -0.35189644771646711, 0.15092915425655598, 1.4396222078224341, 0.37807904084844701, 0.8265464926248689, 0.0052495761577432132, 0.11988903242158148, 0.23592837751092255, -0.391676976255112, -0.043951337522220579, 0.55941128524300276, 0.83007702071927647, 1.6989785970050451, -0.16708268518409153, 0.3256372258471314, -0.23183231878410587, 0.79184518468744525, 0.14605520328908625, 0.3318734785626245, -0.082044977629392418, -0.06325304820875477, 0.74745740445185493, 0.1076210087251081, 1.8410719488961942, 1.8929984899310659, -0.13253287671496183, 0.71239851229387918, -0.19885986136372361, 0.1723060165310904, -0.12744875831336919, -0.044158915901460621, 1.6444308609993357, 0.058870722372848683, 0.0031589383445107047, 0.18773724949955123, -0.3682373947687943, -0.21837785620747646, -0.55258126727909185, -0.23272690824362885, 0.42587491178890768, 0.0021214950731968107, -0.72536412559869212, 0.22872937047586506, 0.29480942455755821, 0.16503564213343308, -0.23912564079964871, 0.71238677672160877, 0.14750881468858151, -0.13529435270933671, 1.3322422989056495, 0.56610036396941132, -0.0052812496291465352, -0.55146927525349987, 1.5108430585822512, -0.69633322121961527, 0.022846965263997973, 0.07733189904311441, 0.10454890712102294, 0.33084885572124728, 0.0021685105296537222, 1.7418326605125076, 0.23929693290062898, -0.11924320145279391, 1.3442786900502002, 0.25523350894610453, 0.37091439098318058, 0.58463248813328494, 0.75340528836426879, 0.6081249358729186, 0.50351038105261181, 0.27887163870115828, 0.33408611694265644, 0.51182533145532361, 0.83139699133742395, -0.24867796211319648, 0.0084451812527516767, 0.45358380101916268, 1.7280931007600659, 0.026201563505488138, 0.30638321315141964, 0.46198851435472288, 1.8371988489914666, -0.24176316069913084, 0.18607958270236077, 0.69890533678842304, 0.4758061460037013, 0.36190225324695835, 0.24170893887400841, 0.25370028156622587, 1.7809006933051741, 1.8676250382700212, -0.12758978796646067, 1.6640352808572445, -0.14871125275076968, 0.48081864871254004, 0.50666666720378584, 0.44696734051586617, -0.0047951369559293203, 0.27088692352365168, 0.25870427373030058, 1.5464991014370015, -0.25662969269115343, -0.87412073216982467, 1.4265504908357047, 1.6382539270144125, 0.32441486754065035, 1.1557392015377692, 1.0065385471870945, -0.38231076597402802, 1.8880487297082829, 0.60797158957826947, 0.3044629618871984, 0.5799554521883149, 1.6129066472907905, 0.15065963127786786, 0.53344759400462394, -0.16117317357827571, -0.099515463868464979, 2.0125343096376751, 1.3070822639443433, 1.8515181790336117, -0.0843312251377763, 1.8063056589348063, 0.26743993471670663, 0.062504205367224452, -0.21794300945507056, 0.18817051042642918, 0.0087826925050463744, 1.9478846053851928, 0.5267871760018239, 0.22094241391971739, 0.3710049962816474, 0.019099424774891488, 0.2044415368649084, 1.9196165251759849, 0.072266318862639595, 1.0051105494563695, 1.9275505305639991, 1.712107171363954, -0.13109183216802686, 1.3177763638008197, 0.67866293141979139, 1.2131112463814624, -0.021755877793363, 0.21894199425335664, 0.22736809362224814, 0.08945866993841739, 0.18664113113303793, -0.14613631099355884, 0.23102653031843831, 0.22272293486583436, -0.14721479677226107, 1.5877365397450771, 1.4982321345492453, -0.72310009474868142, 0.44728898026950109, -0.46904786668062631, -0.29857104872469459, 0.16043096138235083, 0.13842185494314055, 0.63398978342073575, 0.1413820246253891, 1.3530707212864312, -0.1478961984831535, -0.20570942448337473, -0.34585541221032151, 0.31038385887448872, 0.089098726197636979, 0.02321261050844084, 0.12807175364562384, 1.1979313460267589, -0.041182335716809404, 0.27753373651730329, 0.41092282578637024, 1.0456719822901883, 1.7469259074800658, -0.16474804419464195, 0.15747489189545222, 0.17454655145204445, 1.0380409004684166, 0.10100273703191677, 0.19411604753854067, 0.51442439140767826, 1.2055725133648001, 0.32066620432054244, 0.4275784609043049, 0.42682883287877404, 0.27913884247033927, 0.21913427361306373, 1.3685784099371794, -0.5038155940032607, 1.9766277372493923, 1.1539341524999214, -0.38966877591174903, 0.6726894977559521, -0.32665836481688049, 1.4064388760181625, -0.36683985203474329, 1.878199067318685, 0.45789390499142857, -0.029434592017893668, 0.34473943164369103, 0.22717372391368157, 0.35580694759115505, -0.55459846700226645, -0.064644908407960389, 0.49373914376557015, 0.13340286079432784, -0.45624582280158549, 0.11811994305522608, 0.12190710277465244, 0.77717206381290382, 0.51572177496280014, 0.49630301760776985, 1.2612789852269028, 0.12358563502099612, 0.7011347559142751, 0.1343822942209377, 1.6341672001312091, 1.1741645278165951, 0.060409161331953204, 0.050454506784801595, 0.11450166988542165, 0.19228448198202627, 0.58448373870109138, 0.073500482140836404, 0.83740183444206173, -0.57847394079812187, 0.071797116085862214, 0.39952040584765797, 0.16157678748255438, 0.10923801354130902, 0.10985545663418644, -0.19178297715130005, 0.491141804081415, 1.7245454374815139, 0.097092468036901636, 0.70566382954973428, 0.094468932119446791, 0.14293718774969016, 0.092113105802209716, 1.8117676054838545, 0.42062541983183938, 0.041746707512733627, 0.31012955109577611, 1.546803316695005, 0.28235283066804273, -0.17803495776780742, 0.048111027905820128, 0.20733147358400192, -0.37538499337735243, 0.035509844338209057, 2.0580699118984445, -0.58059881073735486, 1.7865131985599771, -0.92652532420410172, 0.28194131306781589, -0.11617412001435704, -0.31648104757046835, 1.8819461090482004, 0.26391561349519682, 0.16540307683463185, 1.739430988184633, 0.40234736052656311, -0.38985558874246717, 1.0976933947998093, -0.2977757313717429, 0.089382263440366705, -0.15042261845938965, 0.57517151108318665, -0.039897467521086638, 0.69890876732478258, 0.74831507560048593, -0.37021572938949221, 0.34902937846854665, 1.9686379810732462, 0.89480550640090817, 0.29679534389791051, 0.10634415191119984, 0.071532785818632982, 0.510868108287308, 0.63407976172548619, 0.29027507611207304, 1.723093513671677, -0.080551413413001288, 0.10094023046979575, 1.738209233984455, 1.6874894520773622, -0.46903257229809658, 0.18895226405829973, 0.080262282747047109, -0.44610957080067354, 0.37661667895934475, -0.55943976691247765, 1.1454372576225267, 0.77476955983082529, 1.3670143227365681, -0.27950751886544639, 1.5461410281713075, 2.2083207801664546, 0.41867628908039234, -0.11958504049390878, 0.59912377368395231, 0.076549567669731866, 0.39890675740561105, 0.27587431526634199, 1.6442292559087544, 0.73233959580203756, 0.4228303187127781, 0.61840231411516444, -0.28049837354544799, 0.032129224001586076, 0.45723494407848009, -0.41999022919199314, -0.067656187414478236, 0.23977131789112177, 0.3176273917026774, 0.70673360327577162, 1.5870613030344358, -0.24229600913599916, -0.15217716989659177, -0.34662940256183894, 0.28012062293449502, 0.48410650852783205, 1.0581640657480578, 0.23383431487951567, -0.49843441315810577, 0.51134245889796826, 1.995427668108593, 1.660525391229152, -0.47935647116169416, 0.056392968618174194, 0.51874331869248791, 0.10746765078821972, 0.55687284550740768, -0.26276714242515331, 0.20869457354125395, -0.20718721441445467, -0.091402759176018547, 0.30948614945916308, -0.22502159449468886, 0.29069485066371448, 0.24521892062728051, -0.038445645697660583, 0.37824854221554211, 1.9917270864877517, 1.2606006507139549, 0.096114284834117256, 0.39345693833424789, 0.73260627103286002, 1.9638043087846486, 1.6958739750573799, 1.4797329271210924, 0.25629244923248679, 0.66871737605891679, 0.33284250683618422, -0.026871577946032725, -0.13091909675668489, 0.70278329026289676, 0.16941412210740664, 0.31974824508188932, 0.30114879853584597, 0.87039893966257287, 0.34936023528167537, 0.4773555936380437, 1.6174232027141351, 1.7721768892581058, 1.3561734217264285, 0.17828241159877778, 0.42946163801173426, 0.24849637477503639, -0.11476786532668964, 0.4884477907655434, 1.9262476201783159, 1.1263804015460621, 1.7633136384803028, 1.8222364040134744, 1.1846780321455326, 0.46562373447990668, -0.45780720002791619, 0.37404399463666416, 0.38355707972260361, 0.35684469273571706, 0.27091199063415161, -0.029366400483077926, 0.4464256723008268, 0.25942144746881413, 1.5802796721635881, 0.18477059796372086, 0.16177250277977362, -0.045449990386021655, 0.21917746666104573, 0.29011548418779365, 0.0005026195370400055, 0.05788844125112258, 0.42971089402327384, 1.6901515922653787, 1.7509957936956939, 0.0029518675727670252, -0.1230255841328671, 2.1155569209878018, -0.61585540187604559, 0.22408968258962339, 0.17421650477269487, -0.095306935636359905, 0.11779284334295409, 1.854997495194791, 0.75880977510810332, 0.34048478568005369, -0.22346910728269781, 0.17792372483009503, 1.4284561191228327, 1.2630121436666994, 0.29893576712935821, -0.061010345194191386, -0.23055197177042519, 0.6358110702012354, 0.88139297995503285, 0.023465816183547372, -0.30670698113877204, 0.2249540231738148, 0.24380068980839534, -0.22215936873352951, 1.0828861000816121, 0.29316547151817851, 0.9156237404071268, 1.860910662438811, 0.88267164674804854, 0.35848959549421228, 0.65927881238855657, 0.76286292434973224, 0.51461772952953089, -0.1467586321991024, 1.4201195788029575, 1.2277480010874133, 0.33825468583600221, 0.55380670275701382, 0.51357676695782195, 0.3685398113366255, 0.015711714833426649, 0.71915523299555906, 0.049005659375127186, 0.63017259146374061, 0.20669580779980273, 0.32861271188362157, 1.6985821293120178, 0.66695669358448795, -0.13744236363683829, 0.14808470003492064, -0.74444439029606879, 0.051823669775510089, 1.160397276971006, 0.47400881520712967, -0.0053359264840771373, -0.0091122246020226144, 0.19872995385957237, 0.2348818574720074, -0.4254063610402492, 1.7963599462828941, -0.32453337055367487, 1.4053154927366465, 2.3529732911440853, 0.59903838539913279, 0.67270562664005118, -0.36727116589909725, -0.58442944108266615, -0.10070945520724744, 0.63376695738825584, -0.0911703658495263, -0.56478244367039387, 0.2059437536850614, 0.65107176633737285, 0.22104059215586025, -0.026377563058826575, -0.011774995619507234, -0.051964167691036511, 1.6623583013964569, 0.16642211027361709, -0.54598204643707682, 0.15096052658223674, 0.11763731797640657, 0.45493369088174851, 1.4200233310295387, 1.4040036859633456, 0.14952974322034285, 0.17316740449270834, 0.28045221421862482, 0.31052713777450103, 2.1812636544871653, -0.094623359923609845, 0.21176115813217716, 0.27321799106278427, -0.28420262885292258, 1.8810140114992513, 2.2663300992548501, 1.8194751248554548], + "configs": { + "adjust": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.20539818185805991, + "h_r": 0.20539818185805991, + "b_l": 0.31697265904653676, + "b_r": 0.31697265904653676, + "tau_cl": 0.88959328209279964, + "tau_bc": 0.90547493494602094, + "se_cl": 0.087436779969590234, + "se_rb": 0.10512765262083801, + "z": [10.174131325538207, 10.355767164126325, 8.6130995258857439], + "pv": [2.5869787506023715e-24, 3.9401503937231234e-25, 7.1111308050075396e-18], + "ci_lower": [0.71822034242824961, 0.73410199528147091, 0.69942852202994066], + "ci_upper": [1.0609662217573497, 1.076847874610571, 1.1115213478621011], + "N": [638, 162], + "N_h": [120, 94], + "N_b": [204, 124], + "bias": [0.012986538694649941, -0.0028951141585713547], + "beta_p_l": [0.51616090620987309, 0.8762932488650983], + "beta_p_r": [1.4057541883026727, 1.3112358885881596] + }, + "off": { + "c": 0, + "masspoints": "off", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 0.20231957840691825, + "h_r": 0.20231957840691825, + "b_l": 0.31568473130699781, + "b_r": 0.31568473130699781, + "tau_cl": 0.88962822550741083, + "tau_bc": 0.90555207238136426, + "se_cl": 0.08812140523839905, + "se_rb": 0.10548329398826797, + "z": [10.095483873647465, 10.276187379576289, 8.5847913744718785], + "pv": [5.7845086305000397e-24, 9.0226474656596352e-25, 9.1001420095608472e-18], + "ci_lower": [0.71691344497308951, 0.73283729184704294, 0.69880861519370874], + "ci_upper": [1.0623430060417323, 1.0782668529156856, 1.1122955295690198], + "N": [638, 162], + "N_h": [120, 94], + "N_b": [204, 124], + "bias": [0.012779412100947352, -0.0031444347730060773], + "beta_p_l": [0.51632908089263263, 0.87939476433990471], + "beta_p_r": [1.4059573064000435, 1.3071882888555832] + } + } + }, + "dgp_asymmetric_scaled": { + "x": [24.199383630079979, 23.992978947688535, 31.52616644316128, 11.54989378984564, 22.886146447652472, 26.862921082468102, 17.587469655940907, 30.165313617769968, 31.391502441572111, 30.873203321851182, 28.128824557310111, 38.229379570763776, 28.842191174009201, 19.127246265161435, 19.213381948208113, 32.473849366957147, 22.372700581252204, 24.091391162292105, 38.569744781265605, 26.641905165689654, 13.814639517677044, 24.806063824989934, 37.871083195654265, 28.774855246186757, 32.457756486284886, 22.475190405785437, 20.38366224237479, 15.952831082280053, 36.005848493513739, 36.096033773754669, 32.216161236547556, 32.688080051932971, 26.248215713969827, 30.852158209143845, 31.640956662455402, 33.597404434434537, 34.169198826367925, 22.81858529786216, 21.040288997581932, 33.588834697587224, 34.176384778985451, 31.145064542862059, 35.986189658564172, 29.127453883266867, 26.432253462002077, 25.624562057485555, 32.371233384935913, 25.692221168498506, 31.218841669613081, 27.099300495638715, 29.209601042237139, 37.453593315249599, 15.744673545810254, 29.997018244352482, 32.702874588811397, 27.909360802648351, 32.143207695240221, 29.860093870670553, 19.271115244759326, 33.702447224819913, 36.673346603584875, 24.330375104374685, 35.922337096359747, 23.015493811148996, 31.840907740181681, 27.315054582032815, 34.876242712349836, 24.230393700818652, 20.693013870437923, 28.509769972127255, 18.3777947771631, 28.787655240454164, 29.645550197059958, 28.086380819727971, 34.351172704977785, 34.677604978212827, 20.382527949790848, 36.817241180596952, 37.85220939483483, 32.632269270152491, 21.354129180975725, 34.513498224109512, 34.045943641099051, 31.810306068931432, 21.306497964654206, 31.861111964155384, 25.149239044110665, 20.379135258196541, 27.152431719943063, 24.365316675989067, 35.55391914284121, 17.898477263267708, 26.723144194702648, 24.542645740689373, 36.619104043693483, 29.668663851318826, 15.934752740824973, 26.447880151376477, 31.761498382977372, 25.929703551992262, 35.691122952616489, 32.00329383375562, 29.273931698678016, 24.455079050870832, 28.477649022981634, 25.671904668158728, 35.02506491105612, 35.717563176919448, 29.782961747881185, 33.535338129480181, 22.325646517327918, 24.000173292319747, 20.678100325237416, 12.403814388912265, 32.389217854074182, 34.70557490707418, 19.985520925488995, 22.533527996700133, 29.752910568593677, 36.096900942449899, 8.324128369794332, 36.147037051551315, 32.78965398675507, 24.927874845171818, 26.722840504629094, 15.391103750000205, 31.396291723877713, 16.785043852990032, 35.207027317876751, 29.699752628499148, 33.55300768603928, 23.928588281758643, 26.070270974365766, 23.462883280281623, 27.503401671665078, 18.116547875708672, 35.925777971806596, 36.389990711316337, 32.555020730461251, 28.611247698821952, 32.994429741979332, 28.043688345655966, 24.585037374127097, 37.068672300459525, 38.235819214737063, 19.054399705455939, 36.569814628943973, 34.702963177919003, 39.010438590359769, 33.955334707257677, 30.266511214041113, 27.425562671069457, 30.514996581010458, 30.763614846344908, 38.018446891153424, 12.655614469084915, 30.229227320375387, 35.308844614146921, 29.005973908119536, 33.386251146115107, 29.697723711432623, 21.735120519801661, 31.05191330445852, 36.325260816307519, 24.2524791381216, 27.099809627212611, 24.096245574791453, 19.62154802165028, 36.467062449552465, 24.446347259132679, 22.542870123698417, 22.42456591817767, 21.106287807650965, 29.889639251785617, 31.378111525238182, 33.834180100314406, 32.223812697038056, 28.936768230164887, 29.529768002780109, 30.666246325304286, 33.619016476771321, 35.36698145800267, 21.811972843602717, 33.415744037987913, 27.464540202103667, 34.970536729310886, 21.972491509925945, 36.328451887983519, 18.356071882294568, 32.233760368317583, 21.831756858838375, 13.540254490760779, 38.111575883332911, 33.810659992015637, 30.394453105352465, 20.120941935868288, 37.55431459569914, 22.661926593097746, 16.20534545422629, 27.422601680060971, 36.959176364826206, 13.556044065532598, 26.546913311303083, 15.99393342521893, 33.818873756421432, 29.819277432253717, 18.63921678057125, 24.506083464985213, 24.68120428173404, 27.842953787839853, 34.299150334101199, 26.289096178797205, 24.893958878369567, 29.524540625216407, 34.973895255142821, 28.831811703669334, 30.580444465109082, 33.127538968467235, 29.409753008768643, 30.571823276409695, 31.622609632079826, 15.147125713285945, 18.488602835725924, 31.227288324296456, 28.341422733918137, 20.729524685870313, 35.386859526009175, 28.751136669559155, 28.150571475141916, 15.525771091226153, 34.653027568448771, 32.602654161752788, 25.258090892621603, 23.988870678992082, 24.805171779153234, 32.029965721532719, 27.110861172110333, 35.383161796245915, 37.961111161368002, 13.569361075840918, 35.284287148295348, 31.287369242353449, 28.890219811213719, 38.761138524886157, 37.057069917699174, 26.756424819222026, 32.700913266910746, 33.079416476745337, 35.798924254329677, 32.968865182846386, 33.97196547408258, 33.177011351928861, 38.11477029033292, 24.473039927165733, 25.925194244085155, 9.816334201844958, 27.98111669583616, 35.998063843727508, 35.189664099581449, 36.13802594288876, 24.586417660228165, 21.5810382450113, 21.735949902950921, 36.146346813100344, 17.608449894125634, 31.336393122494567, 28.800720026022127, 33.958590133939367, 25.216706101347839, 34.189443620475217, 31.228225509478619, 23.776812548646372, 23.236620072818752, 33.049387755117621, 27.392028394578524, 24.624033284270276, 37.145087046088157, 17.309478993752371, 17.382063671307833, 23.956300058699004, 30.474175312499604, 28.260125765057673, 10.830231149103478, 30.394113553321233, 28.818692819793569, 29.701724565831917, 26.374194090539941, 17.395328912351165, 35.17839411416103, 33.575919751875134, 37.88555426907989, 35.853131161481933, 32.081757897389963, 25.285321114463443, 26.796623813866105, 19.389373354018261, 22.566114429963989, 32.15750637649986, 14.169019237971089, 37.672808942462169], + "y": [3.0210865026149851, 2.3743459065056745, 3.0096609842181001, 2.5251386318693014, 2.3893927546911344, 2.2380525452913997, 2.0915729025895184, 4.0969474368208205, 2.9454494707461887, 4.0597195391419527, 2.7607545139311362, 3.1540361370524694, 2.583202951865851, 3.2836607772431528, 2.5541378783540982, 3.2817022934834972, 1.4256196864839414, 2.2072656509518689, 2.681069422946643, 2.5593467294936478, 2.6200995187398899, 2.9006685561614889, 2.5846109053858033, 2.6455851255322251, 3.3250934830303862, 1.6271109533867176, 2.8232396087523877, 1.8212399567710891, 3.334277024315587, 3.3284254035371825, 3.0995979736869081, 2.5429997683727361, 2.6134824297209591, 3.6462614692122743, 4.1407311645758806, 3.6153398530574501, 2.8039531444899728, 3.0206939075441603, 2.1358001651875194, 2.8503701907839512, 3.5238821550287547, 3.5325961951862181, 3.8576458852809448, 3.536290415766262, 3.0730838594449748, 3.2050892854617583, 3.6768653735647532, 2.9647544476479033, 3.9171442010118351, 2.7385556749250353, 3.7043566915712534, 2.6007717631688259, 3.0984567233665423, 3.6902456646126893, 3.4888217000978687, 3.1678732826517071, 3.0462774474385146, 3.477218701834861, 2.0435054934042629, 2.7287731504767079, 3.7733173082815528, 2.6760775380990922, 2.9915278726678323, 1.7884387172873795, 3.2850989109488511, 2.7871885733365906, 3.3552752968507624, 2.7641683395082475, 2.8191851942821535, 4.6812322255675802, 2.8186871540611018, 2.5746891795398943, 3.3941069503329047, 3.7221721626591715, 2.8044117106364856, 3.5693208696182697, 2.4938165666306413, 3.5939502937964534, 3.1716391259716921, 3.2309789518783685, 3.1179224057194546, 2.292564087869994, 3.2122624087269158, 3.1320950268211107, 3.0407327674469955, 3.9540031082321212, 2.8599443935354762, 3.0363910233775022, 2.3227633485662333, 2.8623297696570149, 3.6703878477706438, 1.7528212664126581, 2.028046256780704, 2.6030729714579701, 2.9335253671739645, 2.6812642065976631, 2.1299108253388863, 3.1675015223792888, 3.199373824223533, 2.4980695613842543, 2.6824999464220909, 3.8469534666715957, 3.1577975267998348, 2.7896888586496207, 3.5376285432199928, 2.4544472531114145, 2.4955461099241623, 3.0605890679776495, 3.6027497580899106, 3.6478928796265024, 3.1153917391024701, 2.9308701951018965, 2.3824867773754659, 2.7593843908847706, 3.7461632161943328, 3.7050761584353769, 2.1425793597412452, 1.6503433561743668, 3.4322933232174058, 3.7137067923672586, 2.2727937689951387, 2.7329859684922386, 2.7252132102264706, 2.8632826502687978, 2.3531798804076351, 1.6704887372233621, 3.4492221623978834, 2.3242016921052264, 2.9334907989362886, 3.6137875604685226, 3.0350022319277388, 4.1738591182125981, 2.5332230681683843, 1.8807616656672606, 2.7349796331854903, 2.2503556371618947, 4.3128859322637325, 3.3681717211854609, 3.7837744450720154, 2.9086978448902183, 3.5308603802650795, 3.1672141467423902, 2.1517005782202814, 3.1671083882507314, 3.2227255910961463, 3.1762132542598405, 3.9841171597901126, 3.7582393424309739, 3.5176198309565772, 3.7917947407221111, 3.4898600698298723, 3.0201133506762914, 3.4200046083347342, 3.7258205302673231, 3.8187350109418059, 2.8518566784343817, 2.8941581114149884, 2.8708201189680285, 2.9349484458440598, 3.4303761766204222, 3.6646940016186047, 3.1698617209337723, 2.3858943469806588, 2.5711475374616692, 3.1863045741385077, 2.5261865111413142, 3.1669536147913715, 1.6186928642067961, 2.7671659283413361, 2.5928196271011803, 2.6989110168786681, 2.5394796678696419, 2.1754216441201013, 3.8018052274352074, 4.4505001091849392, 3.6992629895147591, 2.9391591843800704, 4.4741829676461551, 3.5503070708715443, 3.0511898002436717, 3.2270494485065764, 2.6945381197333922, 2.5033638198894765, 2.8979880524151214, 2.2301603595234565, 3.2446512389225668, 2.5103379707054883, 3.2628099039134084, 2.3660119200007497, 2.9762223775513901, 2.9347736512939315, 2.8175330582410738, 2.9418366578507458, 3.546345423102149, 3.716720388871285, 3.3355618958827344, 3.5329320545646881, 3.226962023997491, 3.3063121353652476, 2.7277562021298829, 4.0503212836836084, 1.814811241388286, 2.9808094348109018, 2.0856681460887923, 3.7147219940458291, 3.943279800891514, 3.0229236920942415, 2.9648059997237022, 2.7561500329366098, 1.9332233460330723, 4.0948221271472267, 3.3292293946723492, 2.5971621786423009, 3.7131904689463284, 3.7605097426343721, 3.5486298866259114, 3.5178406505954158, 3.8091016914957008, 3.6901369481045134, 3.2950106890583708, 2.17581612940418, 2.0351911875284645, 2.2535311970913452, 3.1222264980507748, 3.9535177824022583, 2.352715348903522, 3.9136434048837359, 3.8439464738756755, 4.2476366263347671, 2.4832756563047131, 4.277203270357969, 2.5141468780153868, 2.7792292685943663, 2.5591077081718039, 3.1714731435903438, 3.5725796933368574, 3.2725025763509716, 3.8987780452739802, 3.0971609746104405, 3.0380131565244239, 2.6010213222359697, 3.0078455654729015, 3.7252026388697121, 3.238457797640228, 3.2637597794534368, 2.3418393880321142, 3.8486251694953566, 4.2105382375424689, 3.9517422088586569, 3.1107687558790653, 3.3137542216528626, 3.8144766893949966, 3.4100192244545462, 1.7873709140257985, 2.3865013566004114, 1.8011484259781501, 2.3059270323249086, 2.7675166092340788, 3.1712564629198199, 2.6704344373664939, 3.0609834994137546, 3.2954059667368671, 2.8392941758633232, 3.3915081348109095, 2.1689545535482235, 4.3934745754025881, 3.6623266327800335, 3.2574608888003995, 3.1557692795275223, 2.9536534626384854, 3.2730061887942137, 1.9491802865844512, 1.9820326858054989, 3.3093238899356048, 2.4025744717134403, 3.0007757837619873, 3.2269928669663548, 2.1792340722031827, 2.8520245172806789, 2.4044561473781281, 3.8802791084508206, 2.5874439177905248, 2.1214903420598268, 3.9673628614633278, 3.7236057412767236, 3.3242238396410233, 3.0801696835324757, 3.0032486788241028, 2.9160622493751345, 3.1898932561446469, 2.781240496537738, 2.9663896439657309, 2.9932092893310847, 2.6457475738981131, 2.2411311211250768, 1.9373431720681666, 2.3971261780696396, 3.2931909469872971, 2.963697570508018, 3.5290747000557814], + "configs": { + "default": { + "c": 28, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 3.4779177488224566, + "h_r": 3.4779177488224566, + "b_l": 5.3205997883790817, + "b_r": 5.3205997883790817, + "tau_cl": 0.91784565995214118, + "tau_bc": 0.94318641739393883, + "se_cl": 0.25397671273253714, + "se_rb": 0.31225260343388661, + "z": [3.6138969202216757, 3.7136728294739698, 3.0205878414513849], + "pv": [0.00030162901980462201, 0.00020427284730730049, 0.0025228451736385481], + "ci_lower": [0.42006045008449316, 0.44540120752629081, 0.33118256058465323], + "ci_upper": [1.4156308698197893, 1.4409716272615869, 1.5551902742032244], + "N": [129, 171], + "N_h": [44, 59], + "N_b": [66, 91], + "bias": [0.068652793420998393, 0.043312035979200747], + "beta_p_l": [2.5061885179348509, -0.11895715775931892], + "beta_p_r": [3.424034177886992, 0.048092363533800142] + } + } + }, + "senate": { + "csv": "benchmarks/data/rdrobust_senate.csv", + "configs": { + "adjust": { + "c": 0, + "masspoints": "adjust", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 17.75439722117796, + "h_r": 17.75439722117796, + "b_l": 28.028087151168126, + "b_r": 28.028087151168126, + "tau_cl": 7.4141308019720285, + "tau_bc": 7.5065024705852963, + "se_cl": 1.4587160238253243, + "se_rb": 1.7412584146072718, + "z": [5.0826416388635236, 5.1459655943864311, 4.3109640749551428], + "pv": [3.7222155663538071e-07, 2.6614820346989611e-07, 1.625443145019196e-05], + "ci_lower": [4.555099931602923, 4.6474716002161909, 4.0936986901777317], + "ci_upper": [10.273161672341134, 10.365533340954402, 10.919306250992861], + "N": [595, 702], + "N_h": [360, 323], + "N_b": [465, 437], + "bias": [0.29435997097418465, 0.2019883023609168], + "beta_p_l": [45.173126078527019, 0.16391205248637886], + "beta_p_r": [52.587256880499048, 0.26094945342143439] + }, + "off": { + "c": 0, + "masspoints": "off", + "kernel": "tri", + "p": 1, + "q": 2, + "bwselect": "mserd", + "h_in": null, + "b_in": null, + "rho_in": null, + "level": 95, + "h_l": 17.70802871288921, + "h_r": 17.70802871288921, + "b_l": 27.984122002016232, + "b_r": 27.984122002016232, + "tau_cl": 7.4160377658529555, + "tau_bc": 7.5099397343930576, + "se_cl": 1.4603614368496678, + "se_rb": 1.7426468817274425, + "z": [5.0782207600955545, 5.142521258705453, 4.3095017201354304], + "pv": [3.8098586245343082e-07, 2.7107575701416745e-07, 1.6362278604050302e-05], + "ci_lower": [4.5537819452164428, 4.6476839137565449, 4.0944146084362405], + "ci_upper": [10.278293586489468, 10.37219555502957, 10.925464860349875], + "N": [595, 702], + "N_h": [359, 322], + "N_b": [465, 436], + "bias": [0.29498627786583143, 0.20108430932572929], + "beta_p_l": [45.171389081144049, 0.16349689300315043], + "beta_p_r": [52.587426846997005, 0.26090741254604422] + } + } + } +} diff --git a/diff_diff/__init__.py b/diff_diff/__init__.py index 6d6d824f..8b9ac0f1 100644 --- a/diff_diff/__init__.py +++ b/diff_diff/__init__.py @@ -206,6 +206,10 @@ TreatmentDoseShape, profile_panel, ) +from diff_diff.rdd import ( + RegressionDiscontinuity, + RegressionDiscontinuityResults, +) from diff_diff.results import ( DiDResults, MultiPeriodDiDResults, @@ -308,6 +312,7 @@ DCDH = ChaisemartinDHaultfoeuille HAD = HeterogeneousAdoptionDiD CiC = ChangesInChanges +RDD = RegressionDiscontinuity __version__ = "3.7.0" __all__ = [ @@ -508,6 +513,10 @@ "HeterogeneousAdoptionDiDResults", "HeterogeneousAdoptionDiDEventStudyResults", "HAD", + # RegressionDiscontinuity (sharp RD, rdrobust parity) + "RegressionDiscontinuity", + "RegressionDiscontinuityResults", + "RDD", # HeterogeneousAdoptionDiD pre-test diagnostics (Phase 3) "qug_test", "stute_test", diff --git a/diff_diff/_rdrobust_port.py b/diff_diff/_rdrobust_port.py index b7958f5b..fed721ae 100644 --- a/diff_diff/_rdrobust_port.py +++ b/diff_diff/_rdrobust_port.py @@ -43,6 +43,9 @@ ``ValueError`` rather than propagating R's opaque downstream errors. * Only ``vce="nn"`` is implemented; ``hc0``-``hc3`` and cluster modes raise ``NotImplementedError`` (documented v1 seam). +* ``deriv`` is machinery-supported for any ``0 <= deriv <= p`` but golden- + covered only for ``deriv in {0, 1}``; the public estimator does not expose + it (sharp levels only). * ``qrXXinv``'s Cholesky-failure fallback maps ``MASS::ginv(G)`` to ``numpy.linalg.pinv(G, rcond=sqrt(eps))`` - both are Moore-Penrose pseudo-inverses with the same default singular-value cutoff. Reachable @@ -56,6 +59,7 @@ from __future__ import annotations +import math import warnings from dataclasses import dataclass, field from typing import Dict, Optional, Tuple @@ -78,6 +82,8 @@ "rdrobust_bw", "rdbwselect_sharp", "quantile_type2", + "RdFitSharpResult", + "rdrobust_fit_sharp", ] # Upstream pin: CRAN source tarball of record. rdrobust has no git SHA on @@ -406,6 +412,7 @@ def rdbwselect_sharp( bwrestrict: bool = True, scaleregul: float = 1.0, stdvars: bool = False, + warn_masspoints: bool = True, ) -> RdBwselectResult: """Sharp-RD data-driven bandwidth selection, all 10 selectors (rdbwselect.R main flow at the anchors cited inline). @@ -531,17 +538,24 @@ def rdbwselect_sharp( mass_l = 1.0 - M_l / N_l mass_r = 1.0 - M_r / N_r if mass_l >= 0.2 or mass_r >= 0.2: - warnings.warn( - "Mass points detected in the running variable.", - UserWarning, - stacklevel=2, - ) - if masspoints == "check": + # warn_masspoints=False silences ONLY the warnings (the + # bwcheck floor below still applies): R's rdrobust() runs the + # detection itself before its inline selection (rdrobust.R: + # 365-380), so the estimator warns once at fit() level and + # passes False here to avoid a stacked duplicate. Direct + # callers keep rdbwselect.R:139-159's own warning behavior. + if warn_masspoints: warnings.warn( - "Try using option masspoints='adjust'.", + "Mass points detected in the running variable.", UserWarning, stacklevel=2, ) + if masspoints == "check": + warnings.warn( + "Try using option masspoints='adjust'.", + UserWarning, + stacklevel=2, + ) if bwcheck is None and masspoints == "adjust": bwcheck_effective = 10 # rdbwselect.R:157 @@ -613,6 +627,27 @@ def _bw(side: str, o: int, nu: int, o_B: int, h_B: float, scale: float) -> _BwPi vcache_r, ) + def _stage_bw(num, den, rate, clamp_max=None, floors=()): + """One MSE-stage bandwidth: (num/den)^rate with R division + semantics (0/0 -> NaN, x/0 -> inf), bwrestrict clamp, bwcheck + floor, and a fail-closed finiteness check. R propagates NaN into + the next pilot fit and fails opaquely; we raise a targeted error + (documented deviation).""" + with np.errstate(divide="ignore", invalid="ignore"): + val = float(np.float64(num) / np.float64(den)) ** rate + if not np.isnan(val): + if clamp_max is not None: + val = min(val, clamp_max) # absorbs +inf under bwrestrict + if floors: + val = max(val, *floors) + if not np.isfinite(val): + raise ValueError( + "Bandwidth selection produced a non-finite pilot bandwidth " + "(degenerate MSE objective, e.g. a constant outcome or no " + "curvature signal); supply manual bandwidths h=/b= instead." + ) + return val + # --- d-stage pilots (rdbwselect.R:386-387): o=q+1, nu=q+1, o_B=q+2, # h_B = full per-side range, UNregularized (scale=0). --- C_d_l = _bw("l", q + 1, q + 1, q + 2, range_l, 0.0) @@ -629,26 +664,33 @@ def _bw(side: str, o: int, nu: int, o_B: int, h_B: float, scale: float) -> _BwPi "C_d_r_B": C_d_r.B, } + _global_max = bw_max if bwrestrict else None + _d_floors = (bw_min_l, bw_min_r) if bwcheck_effective is not None else () + # ============ mserd chain (rdbwselect.R:444-462) ============ - d_bw_d = ((C_d_l.V + C_d_r.V) / (C_d_r.B - C_d_l.B) ** 2) ** rate_d - if bwrestrict: - d_bw_d = min(d_bw_d, bw_max) - if bwcheck_effective is not None: - d_bw_d = max(d_bw_d, bw_min_l, bw_min_r) # rdbwselect.R:449 + d_bw_d = _stage_bw( + C_d_l.V + C_d_r.V, + (C_d_r.B - C_d_l.B) ** 2, + rate_d, + clamp_max=_global_max, + floors=_d_floors, + ) C_b_l = _bw("l", q, p + 1, q + 1, d_bw_d, scaleregul) C_b_r = _bw("r", q, p + 1, q + 1, d_bw_d, scaleregul) - b_bw_d = ( - (C_b_l.V + C_b_r.V) / ((C_b_r.B - C_b_l.B) ** 2 + scaleregul * (C_b_r.R + C_b_l.R)) - ) ** C_b_l.rate - if bwrestrict: - b_bw_d = min(b_bw_d, bw_max) + b_bw_d = _stage_bw( + C_b_l.V + C_b_r.V, + (C_b_r.B - C_b_l.B) ** 2 + scaleregul * (C_b_r.R + C_b_l.R), + C_b_l.rate, + clamp_max=_global_max, + ) C_h_l = _bw("l", p, deriv, q, b_bw_d, scaleregul) C_h_r = _bw("r", p, deriv, q, b_bw_d, scaleregul) - h_bw_d = ( - (C_h_l.V + C_h_r.V) / ((C_h_r.B - C_h_l.B) ** 2 + scaleregul * (C_h_r.R + C_h_l.R)) - ) ** C_h_l.rate - if bwrestrict: - h_bw_d = min(h_bw_d, bw_max) + h_bw_d = _stage_bw( + C_h_l.V + C_h_r.V, + (C_h_r.B - C_h_l.B) ** 2 + scaleregul * (C_h_r.R + C_h_l.R), + C_h_l.rate, + clamp_max=_global_max, + ) diagnostics.update( d_bw_d=d_bw_d, b_bw_d=b_bw_d, @@ -670,50 +712,46 @@ def _bw(side: str, o: int, nu: int, o_B: int, h_B: float, scale: float) -> _BwPi # ============ msetwo chain (rdbwselect.R:389-420) ============ # Per-side clamps throughout; bwcheck floor applies at the d-stage only # (rdbwselect.R:396-399); b/h stages clamp with bwrestrict only. - d_bw_l = (C_d_l.V / C_d_l.B**2) ** rate_d - d_bw_r = (C_d_r.V / C_d_r.B**2) ** rate_d - if bwrestrict: - d_bw_l = min(d_bw_l, bw_max_l) - d_bw_r = min(d_bw_r, bw_max_r) - if bwcheck_effective is not None: - d_bw_l = max(d_bw_l, bw_min_l) - d_bw_r = max(d_bw_r, bw_min_r) + _max_l = bw_max_l if bwrestrict else None + _max_r = bw_max_r if bwrestrict else None + _fl_l = (bw_min_l,) if bwcheck_effective is not None else () + _fl_r = (bw_min_r,) if bwcheck_effective is not None else () + d_bw_l = _stage_bw(C_d_l.V, C_d_l.B**2, rate_d, clamp_max=_max_l, floors=_fl_l) + d_bw_r = _stage_bw(C_d_r.V, C_d_r.B**2, rate_d, clamp_max=_max_r, floors=_fl_r) C_b_l2 = _bw("l", q, p + 1, q + 1, d_bw_l, scaleregul) - b_bw_l = (C_b_l2.V / (C_b_l2.B**2 + scaleregul * C_b_l2.R)) ** C_b_l2.rate + b_bw_l = _stage_bw(C_b_l2.V, C_b_l2.B**2 + scaleregul * C_b_l2.R, C_b_l2.rate, clamp_max=_max_l) C_b_r2 = _bw("r", q, p + 1, q + 1, d_bw_r, scaleregul) - b_bw_r = (C_b_r2.V / (C_b_r2.B**2 + scaleregul * C_b_r2.R)) ** C_b_r2.rate - if bwrestrict: - b_bw_l = min(b_bw_l, bw_max_l) - b_bw_r = min(b_bw_r, bw_max_r) + b_bw_r = _stage_bw(C_b_r2.V, C_b_r2.B**2 + scaleregul * C_b_r2.R, C_b_r2.rate, clamp_max=_max_r) C_h_l2 = _bw("l", p, deriv, q, b_bw_l, scaleregul) - h_bw_l = (C_h_l2.V / (C_h_l2.B**2 + scaleregul * C_h_l2.R)) ** C_h_l2.rate + h_bw_l = _stage_bw(C_h_l2.V, C_h_l2.B**2 + scaleregul * C_h_l2.R, C_h_l2.rate, clamp_max=_max_l) C_h_r2 = _bw("r", p, deriv, q, b_bw_r, scaleregul) - h_bw_r = (C_h_r2.V / (C_h_r2.B**2 + scaleregul * C_h_r2.R)) ** C_h_r2.rate - if bwrestrict: - h_bw_l = min(h_bw_l, bw_max_l) - h_bw_r = min(h_bw_r, bw_max_r) + h_bw_r = _stage_bw(C_h_r2.V, C_h_r2.B**2 + scaleregul * C_h_r2.R, C_h_r2.rate, clamp_max=_max_r) # ============ msesum chain (rdbwselect.R:422-441) ============ # Sum expansion: (B_r + B_l)^2 in the denominator; global clamps. - d_bw_s = ((C_d_l.V + C_d_r.V) / (C_d_r.B + C_d_l.B) ** 2) ** rate_d - if bwrestrict: - d_bw_s = min(d_bw_s, bw_max) - if bwcheck_effective is not None: - d_bw_s = max(d_bw_s, bw_min_l, bw_min_r) + d_bw_s = _stage_bw( + C_d_l.V + C_d_r.V, + (C_d_r.B + C_d_l.B) ** 2, + rate_d, + clamp_max=_global_max, + floors=_d_floors, + ) C_b_ls = _bw("l", q, p + 1, q + 1, d_bw_s, scaleregul) C_b_rs = _bw("r", q, p + 1, q + 1, d_bw_s, scaleregul) - b_bw_s = ( - (C_b_ls.V + C_b_rs.V) / ((C_b_rs.B + C_b_ls.B) ** 2 + scaleregul * (C_b_rs.R + C_b_ls.R)) - ) ** C_b_ls.rate - if bwrestrict: - b_bw_s = min(b_bw_s, bw_max) + b_bw_s = _stage_bw( + C_b_ls.V + C_b_rs.V, + (C_b_rs.B + C_b_ls.B) ** 2 + scaleregul * (C_b_rs.R + C_b_ls.R), + C_b_ls.rate, + clamp_max=_global_max, + ) C_h_ls = _bw("l", p, deriv, q, b_bw_s, scaleregul) C_h_rs = _bw("r", p, deriv, q, b_bw_s, scaleregul) - h_bw_s = ( - (C_h_ls.V + C_h_rs.V) / ((C_h_rs.B + C_h_ls.B) ** 2 + scaleregul * (C_h_rs.R + C_h_ls.R)) - ) ** C_h_ls.rate - if bwrestrict: - h_bw_s = min(h_bw_s, bw_max) + h_bw_s = _stage_bw( + C_h_ls.V + C_h_rs.V, + (C_h_rs.B + C_h_ls.B) ** 2 + scaleregul * (C_h_rs.R + C_h_ls.R), + C_h_ls.rate, + clamp_max=_global_max, + ) # ============ Rescale + assemble (rdbwselect.R:464-546) ============ h_mserd, b_mserd = x_sd * h_bw_d, x_sd * b_bw_d @@ -770,3 +808,222 @@ def _bw(side: str, o: int, nu: int, o_B: int, h_B: float, scale: float) -> _BwPi bwcheck_effective=bwcheck_effective, diagnostics=diagnostics, ) + + +@dataclass +class RdFitSharpResult: + """Sharp-RD point estimates and variances (rdrobust.R estimation body). + + ``tau_cl`` is the conventional local-polynomial RD estimate, + ``tau_bc`` the bias-corrected estimate; ``se_cl``/``se_rb`` are the + conventional and robust bias-corrected standard errors. rdrobust's + three output rows map as Conventional = (tau_cl, se_cl), + Bias-Corrected = (tau_bc, se_cl), Robust = (tau_bc, se_rb) + (rdrobust.R:854-863). ``beta_p_l``/``beta_p_r`` are the per-side + order-p coefficient vectors (rdplot seam); ``bias_l``/``bias_r`` the + per-side estimated biases (rdrobust.R:629-630). + """ + + tau_cl: float + tau_bc: float + se_cl: float + se_rb: float + bias_l: float + bias_r: float + beta_p_l: np.ndarray + beta_p_r: np.ndarray + N_h_l: int + N_h_r: int + N_b_l: int + N_b_r: int + + +def rdrobust_fit_sharp( + y: np.ndarray, + x: np.ndarray, + c: float, + h_l: float, + h_r: float, + b_l: float, + b_r: float, + p: int = 1, + q: int = 2, + deriv: int = 0, + kernel: str = "triangular", + vce: str = "nn", + nnmatch: int = 3, +) -> RdFitSharpResult: + """Sharp-RD estimation at known bandwidths (rdrobust.R:533-800, sharp + no-covariate/no-cluster path with ``scalepar = 1``). + + Inputs must be complete-case 1-D arrays (same contract as + :func:`rdbwselect_sharp`); sorting, side-splitting, and NN tie blocks + are handled internally. Per-side bandwidths follow rdrobust's + ``bws = [[h_l, b_l], [h_r, b_r]]`` layout. Steps: + + 1. Effective window per side = observations with positive kernel weight + inside the WIDER of h and b (rdrobust.R:541-549); observations inside + the b-window but outside the h-window carry ``W_h = 0`` and drop out + of the p-regression through the weights. + 2. Point estimates: order-p WLS per side at h (``beta_p``); the + bias-corrected coefficient vector uses the ``Q_q`` score matrix + (rdrobust.R:577-578, 609-618). + 3. Variances: conventional sandwiches ``R_p * W_h`` with same-side NN + residuals; robust sandwiches ``Q_q`` with the SAME residuals + (``res_b = res_h`` for vce="nn", rdrobust.R:753-754; the h==b + special branches at rdrobust.R:773-786 are cluster-only and never + taken on this path). + """ + y = np.asarray(y, dtype=np.float64) + x = np.asarray(x, dtype=np.float64) + for name, arr in (("y", y), ("x", x)): + # Same input contract as rdbwselect_sharp: 1-D vectors or explicit + # (n, 1) columns only. + if not (arr.ndim == 1 or (arr.ndim == 2 and arr.shape[1] == 1)): + raise ValueError( + f"{name} must be a 1-D vector or an (n, 1) column; got shape {arr.shape}." + ) + y = y.reshape(-1) + x = x.reshape(-1) + if y.shape[0] != x.shape[0]: + raise ValueError(f"y and x must have equal length; got {y.shape[0]} vs {x.shape[0]}.") + if vce != "nn": + raise NotImplementedError( + "Only vce='nn' is ported in v1 (rdrobust default); hc0-hc3 and " + "cluster variance modes are a documented seam." + ) + kernel = _normalize_kernel(kernel) + for name, val in (("p", p), ("q", q), ("deriv", deriv)): + if not isinstance(val, (int, np.integer)): + raise ValueError(f"{name} must be an integer; got {val!r}.") + if not (0 <= deriv <= p < q): + raise ValueError( + f"Orders must satisfy 0 <= deriv <= p < q; got deriv={deriv}, " f"p={p}, q={q}." + ) + if not (isinstance(nnmatch, (int, np.integer)) and nnmatch >= 1): + raise ValueError(f"nnmatch must be an integer >= 1; got {nnmatch!r}.") + for name, val in (("h_l", h_l), ("h_r", h_r), ("b_l", b_l), ("b_r", b_r)): + # Fail-closed: a non-finite or non-positive bandwidth (e.g. from a + # degenerate selector run) must not reach the kernel weights. + if not (np.isfinite(val) and val > 0): + raise ValueError(f"{name} must be finite and positive; got {val!r}.") + if not (np.all(np.isfinite(y)) and np.all(np.isfinite(x))): + raise ValueError( + "y and x must be finite and complete-case; drop or impute " + "missing values before estimation." + ) + + # Sort + side split, mirroring the rdbwselect preamble (X == c treated). + order_x = np.argsort(x, kind="stable") + x = x[order_x] + y = y[order_x] + ind_l = x < c + ind_r = x >= c + X_l, X_r = x[ind_l], x[ind_r] + Y_l, Y_r = y[ind_l], y[ind_r] + if X_l.shape[0] == 0 or X_r.shape[0] == 0: + raise ValueError( + "All observations fall on one side of the cutoff; sharp RD " + "requires data on both sides." + ) + dups_l, dupsid_l = compute_dups_dupsid(X_l) + dups_r, dupsid_r = compute_dups_dupsid(X_r) + + def _side( + X: np.ndarray, + Y: np.ndarray, + h: float, + b: float, + dups: np.ndarray, + dupsid: np.ndarray, + side: str, + ): + # Weights + effective window (rdrobust.R:533-549) + w_h = rdrobust_kweight(X, c, h, kernel) + w_b = rdrobust_kweight(X, c, b, kernel) + ind_h = w_h > 0 + ind_b = w_b > 0 + N_h = int(np.sum(ind_h)) + N_b = int(np.sum(ind_b)) + ind = ind_b + if h > b: + ind = ind_h + eY = Y[ind] + eX = X[ind] + W_h = w_h[ind] + W_b = w_b[ind] + edups = dups[ind] + edupsid = dupsid[ind] + # Per-window identification guards (documented deviation: fail closed + # where 4.0.0's ginv fallback would silently return a degenerate fit, + # e.g. an empty b-window collapsing Q_q back to the conventional + # score). A weighted Vandermonde Gram matrix is nonsingular iff its + # positive-weight window holds at least order+1 distinct + # running-variable values, so the distinct count IS the exact rank + # condition - no numerical rank estimate is needed. + for w_label, w_vec, order_needed in ( + ("main", W_h, p), + ("bias", W_b, q), + ): + support = eX[w_vec > 0] + n_distinct = int(np.unique(support).size) + if n_distinct <= order_needed: + raise ValueError( + f"Too few observations inside the {side} {w_label} " + f"bandwidth window to identify the order-{order_needed} " + f"local polynomial fit ({support.size} observations, " + f"{n_distinct} distinct running-variable values; need " + f">= {order_needed + 1} distinct)." + ) + # Design matrices (rdrobust.R:569-578) + u = (eX - c) / h + R_q = rdrobust_vander(eX - c, q) + R_p = R_q[:, : p + 1] + L = (R_p * W_h[:, None]).T @ (u ** (p + 1)) # (p+1,) + invG_q = qrXXinv(np.sqrt(W_b)[:, None] * R_q) + invG_p = qrXXinv(np.sqrt(W_h)[:, None] * R_p) + # Q_q = R_p*W_h - h^(p+1) * outer(M[:, p+1], L) with + # M = (R_q @ invG_q) * W_b (rdrobust.R:577, algebraically identical + # to the nested-transpose R expression; e_p1 selects column p+2 + # 1-based = p+1 0-based). + M = (R_q @ invG_q) * W_b[:, None] + Q_q = R_p * W_h[:, None] - h ** (p + 1) * np.outer(M[:, p + 1], L) + # Point estimates (rdrobust.R:609-614) + beta_p = invG_p @ (R_p * W_h[:, None]).T @ eY + beta_bc = invG_p @ Q_q.T @ eY + # NN residuals shared by both variances (rdrobust.R:750-754) + res_h = rdrobust_res_nn(eX, eY, nnmatch, edups, edupsid) + # Conventional / robust meats (rdrobust.R:762-764, 789-798) + V_cl = invG_p @ rdrobust_vce_sharp(R_p * W_h[:, None], res_h) @ invG_p + V_rb = invG_p @ rdrobust_vce_sharp(Q_q, res_h) @ invG_p + return beta_p, beta_bc, V_cl, V_rb, N_h, N_b + + beta_p_l, beta_bc_l, V_cl_l, V_rb_l, N_h_l, N_b_l = _side( + X_l, Y_l, h_l, b_l, dups_l, dupsid_l, "left" + ) + beta_p_r, beta_bc_r, V_cl_r, V_rb_r, N_h_r, N_b_r = _side( + X_r, Y_r, h_r, b_r, dups_r, dupsid_r, "right" + ) + + # factorial(deriv) scaling per rdrobust.R:621-622 (deriv=0 -> 1). + fact = float(math.factorial(deriv)) + tau_cl = fact * float(beta_p_r[deriv] - beta_p_l[deriv]) + tau_bc = fact * float(beta_bc_r[deriv] - beta_bc_l[deriv]) + bias_l = fact * float(beta_p_l[deriv]) - fact * float(beta_bc_l[deriv]) + bias_r = fact * float(beta_p_r[deriv]) - fact * float(beta_bc_r[deriv]) + V_tau_cl = fact**2 * float((V_cl_l + V_cl_r)[deriv, deriv]) + V_tau_rb = fact**2 * float((V_rb_l + V_rb_r)[deriv, deriv]) + return RdFitSharpResult( + tau_cl=tau_cl, + tau_bc=tau_bc, + se_cl=float(np.sqrt(V_tau_cl)), + se_rb=float(np.sqrt(V_tau_rb)), + bias_l=bias_l, + bias_r=bias_r, + beta_p_l=beta_p_l, + beta_p_r=beta_p_r, + N_h_l=N_h_l, + N_h_r=N_h_r, + N_b_l=N_b_l, + N_b_r=N_b_r, + ) diff --git a/diff_diff/guides/llms-autonomous.txt b/diff_diff/guides/llms-autonomous.txt index a5584a2c..52359fa0 100644 --- a/diff_diff/guides/llms-autonomous.txt +++ b/diff_diff/guides/llms-autonomous.txt @@ -355,6 +355,7 @@ supported / out of scope; `warn` supported but with documented caveats; | `StaggeredTripleDifference` | ✓ | ✓ | ✗ | ✓ | ✗ | ✓ | ✗ | ✗ | ✓ | | `ContinuousDiD` | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✓ | | `HeterogeneousAdoptionDiD` | ✗ | partial | partial | ✗ | ✗ | ✗ | ✗ | ✓ | warn | +| `RegressionDiscontinuity` | ✗ (cross-sectional; treatment = running >= cutoff) | ✗ | ✗ | ✗ | ✗ | ✗ (follow-up) | ✗ | ✗ | ✗ (follow-up) | **Footnotes.** - `TwoWayFixedEffects` + staggered: fits but mixes positive and negative @@ -1296,9 +1297,13 @@ This guide does **not**: estimator-native diagnostics. Post-fit validation is mandatory, not optional, and belongs in the final write-up. - Cover methods outside diff-diff's estimator suite (e.g., instrumental - variables, regression discontinuity, synthetic control for a single - treated unit). When those apply, point the user at dedicated - libraries. + variables, fuzzy regression discontinuity, synthetic control for a + single treated unit). When those apply, point the user at dedicated + libraries. SHARP regression discontinuity IS in scope as of this + release: route running-variable/threshold designs to + `RegressionDiscontinuity` (alias `RDD`; sharp designs only - fuzzy RD, + covariate adjustment, and cluster-robust RD variance are documented + follow-ups, so point users needing those at R rdrobust). **If in doubt, consult the primary references in §8 and use `get_llm_guide("practitioner")` for the Baker et al. workflow.** diff --git a/diff_diff/guides/llms-full.txt b/diff_diff/guides/llms-full.txt index 36fde499..235ee2ff 100644 --- a/diff_diff/guides/llms-full.txt +++ b/diff_diff/guides/llms-full.txt @@ -824,6 +824,62 @@ es = est.fit(data_mp, outcome_col='y', unit_col='unit', **Mass-point + survey constraint.** When fitting `design="mass_point"` with `survey_design=SurveyDesign(...)`, `vcov_type="hc1"` (or `robust=True`) is required: the survey path composes the standard error via Binder-TSL on the HC1-scale influence function, so the default classical sandwich path raises `NotImplementedError` — on both the static path and the event-study path (the event-study rejection fires regardless of `cband`, since the Binder-TSL analytical SE consumes the HC1-scaled IF either way). The one exception is `cluster=`: a clustered mass-point fit resolves to the CR1 sandwich regardless of `vcov_type` (and uses the clustered sup-t band), so there is no classical-vs-HC1 mismatch. Passing `vcov_type="hc1"` is a safe default on weighted survey + sup-t examples since `vcov_type` is unused on the continuous designs (CCT-2014 robust SE is the only formula there). +### RegressionDiscontinuity + +Sharp regression discontinuity estimator (Calonico, Cattaneo & Titiunik 2014), parity-targeting R rdrobust 4.0.0. Treatment is assigned by a known threshold of an observed running variable (`running >= cutoff`; units exactly at the cutoff are treated). Point estimation via kernel-weighted local polynomials on each side; data-driven MSE/CER-optimal bandwidths (all 10 rdrobust selectors); robust bias-corrected inference. Cross-sectional - no panel/time dimension and no treatment column (sharp design derives treatment from the running variable). + +```python +RegressionDiscontinuity( + cutoff: float = 0.0, # Known threshold c of the running variable + p: int = 1, # Local-polynomial order, 0..20 (1 = local linear, 0 = local constant) + q: int | None = None, # Bias-regression order; explicit q needs p < q <= 20; None -> p + 1 unvalidated (R-exact: p=20 yields q=21) + kernel: str = "triangular", # triangular / epanechnikov / uniform ("tri"/"epa"/"uni" accepted) + bwselect: str = "mserd", # 10-option rdrobust menu: mserd/msetwo/msesum/msecomb1/msecomb2 + cer* variants + h: float | None = None, # Manual main bandwidth (both sides); h alone -> b = h + b: float | None = None, # Manual bias bandwidth (requires h; ignored with a warning otherwise) + rho: float | None = None, # h/b ratio; with h -> b = h/rho; without h applies to SELECTED bandwidths + vcov_type: str = "nn", # Only "nn" in this release (same-side nearest-neighbor variance) + nnmatch: int = 3, # Minimum NN matches for the variance + masspoints: str = "adjust", # "adjust" (rdrobust default) / "check" / "off" + bwcheck: int | None = None, # Force >= this many unique support points into the window + bwrestrict: bool = True, # Clamp bandwidths to the observed running-variable range + scaleregul: float = 1.0, # IK-style regularization scale (0 removes) + alpha: float = 0.05, # rdrobust level = 100*(1-alpha) +) +``` + +**Alias:** `RDD` + +**fit() parameters:** + +```python +rd.fit( + data: pd.DataFrame, + outcome_col: str, + running_col: str, +) -> RegressionDiscontinuityResults +``` + +```python +from diff_diff import RegressionDiscontinuity + +rd = RegressionDiscontinuity(cutoff=0.0) +results = rd.fit(df, outcome_col="y", running_col="score") +results.att # ROBUST bias-corrected estimate (canonical binding: t_stat == att/se, conf_int centered on att) +results.att_conventional # rdrobust's printed headline coefficient (conventional local-polynomial estimate) +results.conf_int # robust bias-corrected CI +results.h_left, results.b_left # selected bandwidths +print(results.summary()) # three-row Conventional / Bias-Corrected / Robust table, as in rdrobust +``` + +Canonical fields are ONE coherent row (the robust row): att = bias-corrected +estimate, se = robust SE. rdrobust prints the conventional estimate as its +headline - that is att_conventional here, with a full inference row of its own. +Sharp designs only in this release: fuzzy RD, covariates, cluster-robust +variance, weights, and kink estimands are documented follow-ups; missing +rows are dropped WITH a warning (R drops silently); N < 20 falls back to +full-range bandwidths exactly as rdrobust does (overriding manual h). + ### StackedDiD Stacked DiD estimator (Wing, Freedman & Hollingsworth 2024). Addresses TWFE bias with corrective Q-weights. @@ -2228,7 +2284,7 @@ sd_female, data_female = sd.subpopulation(data, mask=lambda df: df['sex'] == 'F' **Key features:** - Taylor Series Linearization (TSL) variance with strata + PSU + FPC -- Replicate weight variance: BRR, Fay's BRR, JK1, JKn, SDR (13 of 22 estimators, including dCDH) +- Replicate weight variance: BRR, Fay's BRR, JK1, JKn, SDR (13 of 23 estimators, including dCDH) - Survey-aware bootstrap: multiplier at PSU (Hall-Mammen wild; dCDH, staggered) or Rao-Wu rescaled (SunAbraham, SyntheticDiD, TROP). SyntheticDiD bootstrap composes Rao-Wu rescaled per-draw weights with the weighted Frank-Wolfe variant of `_sc_weight_fw` (PR #355): each draw solves `min ||A·diag(rw)·ω - b||² + ζ²·Σ rw_i ω_i²` and composes `ω_eff = rw·ω/Σ(rw·ω)` for the SDID estimator. Pweight-only fits use constant `rw = w_control`; full designs use Rao-Wu. SDID's placebo (stratified permutation + weighted FW) and jackknife (PSU-level LOO with stratum aggregation, Rust & Rao 1996) paths also support pweight-only and full strata/PSU/FPC designs - DEFF diagnostics, subpopulation analysis, weight trimming (`trim_weights`) - Repeated cross-sections: `CallawaySantAnna(panel=False)` diff --git a/diff_diff/guides/llms.txt b/diff_diff/guides/llms.txt index f547c076..c34a2b52 100644 --- a/diff_diff/guides/llms.txt +++ b/diff_diff/guides/llms.txt @@ -2,7 +2,7 @@ > A Python library for Difference-in-Differences (DiD) causal inference analysis. Provides sklearn-like estimators with statsmodels-style summary output for econometric analysis. -diff-diff offers 22 estimators covering basic 2x2 DiD, modern staggered adoption methods, reversible (non-absorbing) treatments, advanced panel estimators, nonlinear models, and diagnostic tools. It supports robust and cluster-robust standard errors, wild cluster bootstrap, formula and column-name interfaces, fixed effects (dummy and absorbed), complex survey designs (strata/PSU/FPC, replicate weights, design-based variance), and publication-ready output. The optional Rust backend accelerates compute-intensive estimators like Synthetic DiD and TROP. +diff-diff offers 23 estimators covering basic 2x2 DiD, modern staggered adoption methods, reversible (non-absorbing) treatments, advanced panel estimators, nonlinear models, sharp regression discontinuity designs, and diagnostic tools. It supports robust and cluster-robust standard errors, wild cluster bootstrap, formula and column-name interfaces, fixed effects (dummy and absorbed), complex survey designs (strata/PSU/FPC, replicate weights, design-based variance), and publication-ready output. The optional Rust backend accelerates compute-intensive estimators like Synthetic DiD and TROP. - Install: `pip install diff-diff` - License: MIT @@ -64,6 +64,7 @@ Full practitioner guide: call `diff_diff.get_llm_guide("practitioner")` - [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html): Triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility - [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html): Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves - [HeterogeneousAdoptionDiD](https://diff-diff.readthedocs.io/en/stable/api/had.html): de Chaisemartin, Ciccia, D'Haultfœuille & Knau (2026) for designs where **no unit remains untreated**; local-linear estimator at the dose support boundary returning Weighted Average Slope (WAS) on Design 1' (`d̲=0` / QUG) or `WAS_{d̲}` on Design 1 (`d̲>0`, continuous-near-d̲ or mass-point), with multi-period event-study extension (last-treatment cohort, pointwise CIs). **Panel-only** in this release (repeated cross-sections rejected by the validator). Alias `HAD`. +- [RegressionDiscontinuity](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html): Calonico, Cattaneo & Titiunik (2014) sharp regression discontinuity with robust bias-corrected inference, parity-targeting R rdrobust 4.0.0 (all 10 data-driven bandwidth selectors, mass-point handling, three-row conventional/bias-corrected/robust output; canonical `att` = the bias-corrected estimate with a coherent robust CI - rdrobust's printed headline is `att_conventional`). **Sharp designs only** in this release (fuzzy/covariates/cluster documented follow-ups). Alias `RDD`. - [StackedDiD](https://diff-diff.readthedocs.io/en/stable/api/stacked_did.html): Wing, Freedman & Hollingsworth (2024) stacked DiD with Q-weights and sub-experiments; optional covariate balancing (`balance="entropy"`, Ustyuzhanin 2026) - [EfficientDiD](https://diff-diff.readthedocs.io/en/stable/api/efficient_did.html): Chen, Sant'Anna & Xie (2025) efficient DiD with optimal weighting for tighter SEs - [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html): Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment (absorbing by default; `non_absorbing=True` for on/off treatment, method='local') diff --git a/diff_diff/rdd.py b/diff_diff/rdd.py new file mode 100644 index 00000000..1f9a85ff --- /dev/null +++ b/diff_diff/rdd.py @@ -0,0 +1,715 @@ +""" +Sharp regression discontinuity design (RDD) estimation with robust +bias-corrected inference, parity-targeting R ``rdrobust`` 4.0.0. + +Implements the local-polynomial sharp-RD estimator of Calonico, Cattaneo & +Titiunik (2014): treatment is assigned by ``running >= cutoff``; the effect +is the jump in the conditional expectation of the outcome at the cutoff, +estimated by kernel-weighted polynomial regressions on each side with +data-driven MSE/CER-optimal bandwidths, and reported with robust +bias-corrected (RBC) inference. + +Canonical inference binding +--------------------------- +``RegressionDiscontinuityResults`` binds the library-canonical fields to ONE +internally coherent inference row - the ROBUST row of rdrobust's output: +``att`` is the bias-corrected point estimate ``tau_bc``, ``se`` its robust +standard error, and ``t_stat``/``p_value``/``conf_int`` are computed from +that same pair, so the library-wide identities hold (``t_stat == att/se``, +``conf_int`` centered on ``att``). This deliberately differs from rdrobust's +PRINTED headline, which reports the conventional estimate ``tau_cl`` in the +coefficient column while taking inference from the robust row; ``tau_cl`` is +first-class here as ``att_conventional`` (with its own full inference row), +and ``summary()`` prints the familiar three-row rdrobust table. + +rdrobust equivalents +-------------------- +===================== ========================================== +diff-diff R rdrobust +===================== ========================================== +``cutoff`` ``c`` +``vcov_type`` ``vce`` +``alpha`` ``1 - level/100`` +``h``, ``b``, ``rho`` ``h``, ``b``, ``rho`` (same semantics) +``p``, ``q`` ``p``, ``q`` +``bwselect`` ``bwselect`` (same 10-option menu) +``kernel`` ``kernel`` (accepts "tri"/"epa"/"uni" too) +``masspoints`` ``masspoints`` ("adjust"/"check"/"off") +``nnmatch`` ``nnmatch`` +===================== ========================================== + +Not in v1 (documented seams, see REGISTRY.md): fuzzy designs, covariate +adjustment, cluster-robust variance, weights, ``deriv``/kink estimands, +``scalepar``, ``stdvars``, hc0-hc3 variance modes. + +References +---------- +- Calonico, S., Cattaneo, M. D., & Titiunik, R. (2014). Robust Nonparametric + Confidence Intervals for Regression-Discontinuity Designs. *Econometrica*, + 82(6), 2295-2326. +- Calonico, S., Cattaneo, M. D., Farrell, M. H., & Titiunik, R. (2017). + rdrobust: Software for regression-discontinuity designs. *Stata Journal*, + 17(2), 372-404. +- Calonico, S., Cattaneo, M. D., & Farrell, M. H. (2018). On the Effect of + Bias Estimation on Coverage Accuracy in Nonparametric Inference. *JASA*, + 113(522), 767-779. +""" + +from __future__ import annotations + +import warnings +from dataclasses import dataclass, field +from typing import Any, Dict, Optional, Tuple + +import numpy as np +import pandas as pd + +from diff_diff._rdrobust_port import ( + BWSELECT_OPTIONS, + _normalize_kernel, + rdbwselect_sharp, + rdrobust_fit_sharp, +) +from diff_diff.utils import safe_inference + +__all__ = [ + "RegressionDiscontinuity", + "RegressionDiscontinuityResults", +] + + +def _json_safe(value: Any) -> Any: + if isinstance(value, (np.floating, np.integer)): + return value.item() + return value + + +@dataclass +class RegressionDiscontinuityResults: + """Results of a sharp regression discontinuity fit. + + Canonical inference fields (``att``, ``se``, ``t_stat``, ``p_value``, + ``conf_int``) all describe the ROBUST bias-corrected row: ``att`` is the + bias-corrected point estimate and ``conf_int`` is centered on it (see + module docstring for the binding rationale and the deviation from + rdrobust's printed headline). The conventional row is exposed as + explicit ``*_conventional`` fields. The (rarely used) middle + "Bias-Corrected" row shares its coefficient with ``att`` (both are + ``tau_bc``) and its standard error with ``se_conventional`` - only its + inference triple carries the ``*_bias_corrected`` suffix + (``t_stat_bias_corrected``, ``p_value_bias_corrected``, + ``conf_int_bias_corrected``); there are deliberately no redundant + ``att_bias_corrected`` / ``se_bias_corrected`` fields. Together the + three rows mirror rdrobust's output exactly. + """ + + # Canonical (robust row; internally coherent) + att: float + se: float + t_stat: float + p_value: float + conf_int: Tuple[float, float] + alpha: float + + # Conventional row (rdrobust's printed headline coefficient) + att_conventional: float + se_conventional: float + t_stat_conventional: float + p_value_conventional: float + conf_int_conventional: Tuple[float, float] + + # Bias-corrected middle row (tau_bc with the CONVENTIONAL SE; exposed + # for rdrobust parity - prefer the robust row for inference) + t_stat_bias_corrected: float + p_value_bias_corrected: float + conf_int_bias_corrected: Tuple[float, float] + + # Explicit duplicates for clarity + se_robust: float + + # Bandwidths (rdrobust bws layout) + h_left: float + h_right: float + b_left: float + b_right: float + + # Sample composition + n_obs: int + n_left: int + n_right: int + n_h_left: int + n_h_right: int + n_b_left: int + n_b_right: int + n_unique_left: int + n_unique_right: int + n_dropped: int + + # Config echoes. ``bwselect`` is the RESOLVED selector label ("Manual" + # when bandwidths were user-supplied or N<20 forced the full-range + # fallback, matching rdrobust's printed "BW type"); ``h_input`` / + # ``b_input`` / ``rho_input`` echo the constructor arguments as supplied + # (None when data-driven; a warned-and-ignored ``b``-without-``h`` still + # echoes here) - together with the other echoes they reconstruct the + # full fit configuration from a saved result. Resolved per-side + # bandwidths live in ``h_left``/``h_right``/``b_left``/``b_right``. + cutoff: float + p: int + q: int + kernel: str + bwselect: str + vcov_type: str + nnmatch: int + masspoints: str + bwcheck: Optional[int] + bwrestrict: bool + scaleregul: float + h_input: Optional[float] + b_input: Optional[float] + rho_input: Optional[float] + + # Per-side order-p coefficient vectors (rdplot seam); always populated + # by fit(), so typed non-Optional despite the dataclass default. + beta_p_left: np.ndarray = field(repr=False, default=None) + beta_p_right: np.ndarray = field(repr=False, default=None) + + def summary(self) -> str: + """Human-readable summary with the three-row rdrobust table.""" + width = 72 + conf_level = 100 * (1 - self.alpha) + lines = [] + lines.append("=" * width) + lines.append("Sharp Regression Discontinuity (rdrobust parity)".center(width)) + lines.append("=" * width) + lines.append(f"Cutoff: {self.cutoff:g}") + lines.append(f"Kernel: {self.kernel:<14} Bandwidth selector: {self.bwselect}") + lines.append( + f"Order (p, q): ({self.p}, {self.q}) VCE: {self.vcov_type} " + f"(nnmatch={self.nnmatch}) Masspoints: {self.masspoints}" + ) + lines.append( + f"N = {self.n_obs} ({self.n_left} left / {self.n_right} right); " + f"effective N_h = {self.n_h_left}/{self.n_h_right}, " + f"N_b = {self.n_b_left}/{self.n_b_right}" + ) + lines.append( + f"h = [{self.h_left:.4f}, {self.h_right:.4f}] " + f"b = [{self.b_left:.4f}, {self.b_right:.4f}]" + ) + lines.append("-" * width) + header = ( + f"{'Method':<16}{'Coef.':>11}{'Std. Err.':>11}{'z':>9}" + f"{'P>|z|':>9}{'[' + f'{conf_level:g}% Conf. Int.]':>16}" + ) + lines.append(header) + lines.append("-" * width) + rows = [ + ( + "Conventional", + self.att_conventional, + self.se_conventional, + self.t_stat_conventional, + self.p_value_conventional, + self.conf_int_conventional, + ), + ( + "Bias-Corrected", + self.att, + self.se_conventional, + self.t_stat_bias_corrected, + self.p_value_bias_corrected, + self.conf_int_bias_corrected, + ), + ( + "Robust", + self.att, + self.se_robust, + self.t_stat, + self.p_value, + self.conf_int, + ), + ] + for name, coef, se, z, pv, ci in rows: + lines.append( + f"{name:<16}{coef:>11.4f}{se:>11.4f}{z:>9.3f}{pv:>9.3f}" + f" [{ci[0]:>7.4f}, {ci[1]:>7.4f}]" + ) + lines.append("-" * width) + lines.append("Note: canonical att/se/t_stat/p_value/conf_int are the ROBUST row") + lines.append("(att = bias-corrected estimate; rdrobust prints the conventional") + lines.append("estimate as its headline coefficient - see att_conventional).") + lines.append("=" * width) + return "\n".join(lines) + + def print_summary(self) -> None: + print(self.summary()) + + def to_dict(self) -> Dict[str, Any]: + """Flat scalar dict; confidence intervals split into lower/upper.""" + out: Dict[str, Any] = { + "att": self.att, + "se": self.se, + "t_stat": self.t_stat, + "p_value": self.p_value, + "conf_int_lower": self.conf_int[0], + "conf_int_upper": self.conf_int[1], + "alpha": self.alpha, + "att_conventional": self.att_conventional, + "se_conventional": self.se_conventional, + "t_stat_conventional": self.t_stat_conventional, + "p_value_conventional": self.p_value_conventional, + "conf_int_conventional_lower": self.conf_int_conventional[0], + "conf_int_conventional_upper": self.conf_int_conventional[1], + "t_stat_bias_corrected": self.t_stat_bias_corrected, + "p_value_bias_corrected": self.p_value_bias_corrected, + "conf_int_bias_corrected_lower": self.conf_int_bias_corrected[0], + "conf_int_bias_corrected_upper": self.conf_int_bias_corrected[1], + "se_robust": self.se_robust, + "h_left": self.h_left, + "h_right": self.h_right, + "b_left": self.b_left, + "b_right": self.b_right, + "n_obs": self.n_obs, + "n_left": self.n_left, + "n_right": self.n_right, + "n_h_left": self.n_h_left, + "n_h_right": self.n_h_right, + "n_b_left": self.n_b_left, + "n_b_right": self.n_b_right, + "n_unique_left": self.n_unique_left, + "n_unique_right": self.n_unique_right, + "n_dropped": self.n_dropped, + "cutoff": self.cutoff, + "p": self.p, + "q": self.q, + "kernel": self.kernel, + "bwselect": self.bwselect, + "vcov_type": self.vcov_type, + "nnmatch": self.nnmatch, + "masspoints": self.masspoints, + "bwcheck": self.bwcheck, + "bwrestrict": self.bwrestrict, + "scaleregul": self.scaleregul, + "h_input": self.h_input, + "b_input": self.b_input, + "rho_input": self.rho_input, + } + return {k: _json_safe(v) for k, v in out.items()} + + def to_dataframe(self) -> pd.DataFrame: + return pd.DataFrame([self.to_dict()]) + + +class RegressionDiscontinuity: + """Sharp regression discontinuity estimator (rdrobust 4.0.0 parity). + + Treatment is defined by the running variable crossing a known cutoff + (``running >= cutoff`` treated, matching rdrobust: units exactly at the + cutoff are treated). Point estimation uses kernel-weighted local + polynomials of order ``p`` on each side; inference is robust + bias-corrected per Calonico, Cattaneo & Titiunik (2014). Defaults + reproduce ``rdrobust(y, x)``: ``p=1``, ``q=2``, triangular kernel, + ``bwselect="mserd"``, nearest-neighbor variance with 3 matches, + ``masspoints="adjust"``. + + Parameters + ---------- + cutoff : float, default 0.0 + The known threshold ``c`` of the running variable. + p : int, default 1 + Local-polynomial order for point estimation; integer in 0..20 + (mirroring rdrobust's accepted surface; ``p=0`` is the + local-constant fit). + q : int or None, default None + Order for the bias regression; an explicit ``q`` must satisfy + ``p < q <= 20``. ``None`` resolves to ``p + 1`` WITHOUT + re-validation, exactly as R does (rdrobust.R:53-57) - so ``p=20`` + with the default ``q`` yields ``q=21`` in both implementations + while an explicit ``q=21`` is rejected. + kernel : str, default "triangular" + "triangular", "epanechnikov", or "uniform" (R spellings + "tri"/"epa"/"uni" accepted). + bwselect : str, default "mserd" + Data-driven bandwidth selector; one of the 10 rdrobust options + (mserd, msetwo, msesum, msecomb1, msecomb2, cerrd, certwo, cersum, + cercomb1, cercomb2). Ignored when ``h`` is supplied. + h, b : float or None + Manual main / bias bandwidths (both sides). ``h`` alone implies + ``b = h``; ``h`` with ``rho`` implies ``b = h/rho`` (overriding a + supplied ``b``, as in R); ``b`` without ``h`` is ignored with a + warning (R ignores it silently - documented deviation). + rho : float or None + Bandwidth ratio ``h/b``. Without ``h``, applies to the SELECTED + bandwidths (``b = h_selected/rho``), mirroring rdrobust. + vcov_type : str, default "nn" + Variance estimator. Only "nn" (same-side nearest-neighbor, + rdrobust's default) is implemented in this release; "hc0"-"hc3" + and cluster modes raise ``NotImplementedError``. + nnmatch : int, default 3 + Minimum number of nearest neighbors for the NN variance. + masspoints : str, default "adjust" + Mass-point handling: "adjust" (rdrobust default), "check", "off". + bwcheck : int or None, default None + Minimum unique support points forced inside the bandwidth window. + bwrestrict : bool, default True + Clamp bandwidths to the running variable's observed range. + scaleregul : float, default 1.0 + Scale of the IK-style regularization in bandwidth selection + (0 removes it). + alpha : float, default 0.05 + Significance level (rdrobust ``level = 100*(1-alpha)``). + + Examples + -------- + >>> rd = RegressionDiscontinuity(cutoff=0.0) + >>> results = rd.fit(df, outcome_col="y", running_col="x") + >>> results.att, results.conf_int # robust bias-corrected inference + """ + + def __init__( + self, + cutoff: float = 0.0, + p: int = 1, + q: Optional[int] = None, + kernel: str = "triangular", + bwselect: str = "mserd", + h: Optional[float] = None, + b: Optional[float] = None, + rho: Optional[float] = None, + vcov_type: str = "nn", + nnmatch: int = 3, + masspoints: str = "adjust", + bwcheck: Optional[int] = None, + bwrestrict: bool = True, + scaleregul: float = 1.0, + alpha: float = 0.05, + ): + self.cutoff = cutoff + self.p = p + self.q = q + self.kernel = kernel + self.bwselect = bwselect + self.h = h + self.b = b + self.rho = rho + self.vcov_type = vcov_type + self.nnmatch = nnmatch + self.masspoints = masspoints + self.bwcheck = bwcheck + self.bwrestrict = bwrestrict + self.scaleregul = scaleregul + self.alpha = alpha + self._validate_constructor_args() + + # ------------------------------------------------------------------ + # Configuration plumbing (sklearn-like) + # ------------------------------------------------------------------ + @staticmethod + def _is_real_scalar(val: Any) -> bool: + # Reject non-numeric types up front so every scalar knob fails with + # the estimator's ValueError, not NumPy's TypeError (bool excluded: + # True is not a bandwidth). + return isinstance(val, (int, float, np.integer, np.floating)) and not isinstance( + val, (bool, np.bool_) + ) + + @staticmethod + def _is_int_scalar(val: Any) -> bool: + # bool is an int subclass; p=True must not silently become p=1. + return isinstance(val, (int, np.integer)) and not isinstance(val, (bool, np.bool_)) + + def _validate_constructor_args(self) -> None: + if not (self._is_real_scalar(self.cutoff) and np.isfinite(self.cutoff)): + raise ValueError(f"cutoff must be finite; got {self.cutoff!r}.") + # p/q bounds mirror rdrobust.R:47-57 exactly: integers in 0:20 with + # q > p (p=0 is R's local-constant fit; q caps at 20 like p). + if not (self._is_int_scalar(self.p) and 0 <= self.p <= 20): + raise ValueError(f"p must be an integer in 0..20; got {self.p!r}.") + if self.q is not None and not (self._is_int_scalar(self.q) and self.p < self.q <= 20): + raise ValueError( + f"q must be None (-> p+1) or an integer > p and <= 20; got {self.q!r}." + ) + _normalize_kernel(self.kernel) # raises on unknown kernel + if self.bwselect not in BWSELECT_OPTIONS: + raise ValueError(f"bwselect must be one of {BWSELECT_OPTIONS}; got {self.bwselect!r}.") + for name, val in (("h", self.h), ("b", self.b), ("rho", self.rho)): + if val is not None and not (self._is_real_scalar(val) and np.isfinite(val) and val > 0): + raise ValueError(f"{name} must be None or finite and > 0; got {val!r}.") + if self.vcov_type != "nn": + raise NotImplementedError( + "Only vcov_type='nn' (rdrobust's default nearest-neighbor " + "variance) is implemented in this release; 'hc0'-'hc3' and " + "cluster-robust modes are a documented seam." + ) + if not (self._is_int_scalar(self.nnmatch) and self.nnmatch >= 1): + raise ValueError(f"nnmatch must be an integer >= 1; got {self.nnmatch!r}.") + if self.masspoints not in ("adjust", "check", "off"): + raise ValueError( + f"masspoints must be 'adjust', 'check', or 'off'; got {self.masspoints!r}." + ) + if self.bwcheck is not None and not ( + self._is_int_scalar(self.bwcheck) and self.bwcheck >= 1 + ): + raise ValueError(f"bwcheck must be None or an integer >= 1; got {self.bwcheck!r}.") + if not isinstance(self.bwrestrict, (bool, np.bool_)): + # No silent truthiness: a string like "False" must not coerce + # to bandwidth-restriction ON. + raise ValueError(f"bwrestrict must be a bool; got {self.bwrestrict!r}.") + if not ( + self._is_real_scalar(self.scaleregul) + and np.isfinite(self.scaleregul) + and self.scaleregul >= 0 + ): + raise ValueError(f"scaleregul must be finite and >= 0; got {self.scaleregul!r}.") + if not (self._is_real_scalar(self.alpha) and 0 < self.alpha < 1): + raise ValueError(f"alpha must be in (0, 1); got {self.alpha!r}.") + + def get_params(self, deep: bool = True) -> Dict[str, Any]: + """Return raw constructor parameters (sklearn-compatible).""" + del deep + return { + "cutoff": self.cutoff, + "p": self.p, + "q": self.q, + "kernel": self.kernel, + "bwselect": self.bwselect, + "h": self.h, + "b": self.b, + "rho": self.rho, + "vcov_type": self.vcov_type, + "nnmatch": self.nnmatch, + "masspoints": self.masspoints, + "bwcheck": self.bwcheck, + "bwrestrict": self.bwrestrict, + "scaleregul": self.scaleregul, + "alpha": self.alpha, + } + + def set_params(self, **params: Any) -> "RegressionDiscontinuity": + """Transactionally update parameters (validate before mutating).""" + valid = set(self.get_params().keys()) + unknown = set(params) - valid + if unknown: + raise ValueError(f"Unknown parameter(s): {sorted(unknown)}. Valid: {sorted(valid)}.") + merged = self.get_params() + merged.update(params) + type(self)(**merged) # dry-run: raises before any mutation + for key, value in params.items(): + setattr(self, key, value) + return self + + # ------------------------------------------------------------------ + # Fitting + # ------------------------------------------------------------------ + def fit( + self, + data: pd.DataFrame, + outcome_col: str, + running_col: str, + ) -> RegressionDiscontinuityResults: + """Estimate the sharp RD effect at the cutoff. + + Parameters + ---------- + data : pd.DataFrame + Cross-sectional data. Treatment is derived as + ``running >= cutoff`` (no treatment column - sharp design). + outcome_col, running_col : str + Column names of the outcome and the running variable. + """ + for col in (outcome_col, running_col): + if col not in data.columns: + raise ValueError(f"Column {col!r} not found in data.") + y_raw = np.asarray(pd.to_numeric(data[outcome_col], errors="coerce"), dtype=np.float64) + x_raw = np.asarray(pd.to_numeric(data[running_col], errors="coerce"), dtype=np.float64) + ok = np.isfinite(y_raw) & np.isfinite(x_raw) + n_dropped = int(y_raw.shape[0] - np.sum(ok)) + if n_dropped > 0: + # Deviation from R (which drops silently via complete.cases): + warnings.warn( + f"Dropping {n_dropped} row(s) with missing or non-numeric " + f"values in {outcome_col!r}/{running_col!r}.", + UserWarning, + stacklevel=2, + ) + y = y_raw[ok] + x = x_raw[ok] + N = int(y.shape[0]) + if N == 0: + raise ValueError("No complete-case observations to fit on.") + c = float(self.cutoff) + if not (np.min(x) <= c <= np.max(x)): + raise ValueError( + f"cutoff={c:g} lies outside the observed running-variable " + f"range [{np.min(x):g}, {np.max(x):g}]." + ) + p = int(self.p) + q = int(self.q) if self.q is not None else p + 1 + kernel = _normalize_kernel(self.kernel) + + # --- Mass points (rdrobust.R:365-380) --- + # R's rdrobust() runs this detection ITSELF, before the manual-vs- + # data-driven bandwidth branch, so the warning fires on manual-h + # fits too (verified against installed 4.0.0). The port's + # rdbwselect-level copy is silenced below (warn_masspoints=False) + # to mirror R's single warning from the estimation call. + n_left_pre = int(np.sum(x < c)) + n_right_pre = int(np.sum(x >= c)) + n_unique_left = int(np.unique(x[x < c]).shape[0]) + n_unique_right = int(np.unique(x[x >= c]).shape[0]) + if self.masspoints in ("check", "adjust") and n_left_pre > 0 and n_right_pre > 0: + mass_l = 1.0 - n_unique_left / n_left_pre + mass_r = 1.0 - n_unique_right / n_right_pre + if mass_l >= 0.2 or mass_r >= 0.2: + warnings.warn( + "Mass points detected in the running variable.", + UserWarning, + stacklevel=2, + ) + if self.masspoints == "check": + warnings.warn( + "Try using option masspoints='adjust'.", + UserWarning, + stacklevel=2, + ) + + # --- Bandwidth resolution (rdrobust.R:295-307, 501-504) --- + h_user, b_user, rho = self.h, self.b, self.rho + bwselect_label = self.bwselect + if b_user is not None and h_user is None: + # R silently ignores b without h; we warn (documented deviation). + warnings.warn( + "b= was supplied without h= and is ignored (matching " + "rdrobust's behavior); supply h= to use a manual bias " + "bandwidth.", + UserWarning, + stacklevel=2, + ) + b_user = None + if N < 20: + # rdrobust.R:303-307: unconditional override, INCLUDING a + # user-supplied h (the block runs after manual-h resolution). + warnings.warn( + "Not enough observations to perform bandwidth calculations. " + "Estimates computed using entire sample.", + UserWarning, + stacklevel=2, + ) + x_min, x_max = float(np.min(x)), float(np.max(x)) + full = max(abs(c - x_min), abs(c - x_max)) + h_l = h_r = b_l = b_r = full + bwselect_label = "Manual" + elif h_user is not None: + bwselect_label = "Manual" + if rho is None and b_user is None: + b_resolved = h_user # rho = 1 (rdrobust.R:296-299) + elif rho is not None: + if b_user is not None: + warnings.warn( + "Both b= and rho= supplied with h=; rho takes " + "precedence (b = h/rho), matching rdrobust.", + UserWarning, + stacklevel=2, + ) + b_resolved = h_user / rho # rdrobust.R:300 + else: + # rho is None and b_user is not None (first branch handled + # the both-None case). + assert b_user is not None + b_resolved = b_user + h_l = h_r = float(h_user) + b_l = b_r = float(b_resolved) + else: + bw = rdbwselect_sharp( + y, + x, + c=c, + p=p, + q=q, + kernel=kernel, + vce=self.vcov_type, + nnmatch=int(self.nnmatch), + masspoints=self.masspoints, + bwcheck=None if self.bwcheck is None else int(self.bwcheck), + bwrestrict=bool(self.bwrestrict), + scaleregul=float(self.scaleregul), + warn_masspoints=False, # fit() already warned (rdrobust.R:365-380) + ) + h_l, h_r, b_l, b_r = bw.bws[self.bwselect] + n_unique_left = bw.M_l if self.masspoints != "off" else n_unique_left + n_unique_right = bw.M_r if self.masspoints != "off" else n_unique_right + if rho is not None: + # rdrobust.R:501-504: rho applies to the SELECTED bandwidths. + b_l = h_l / rho + b_r = h_r / rho + + # --- Estimation (port validates h/b finite and positive) --- + fit = rdrobust_fit_sharp( + y, + x, + c, + h_l, + h_r, + b_l, + b_r, + p=p, + q=q, + kernel=kernel, + vce=self.vcov_type, + nnmatch=int(self.nnmatch), + ) + + alpha = float(self.alpha) + # Three inference rows (rdrobust.R:854-863), each through the + # library's joint-NaN gate: + t_rb, p_rb, ci_rb = safe_inference(fit.tau_bc, fit.se_rb, alpha=alpha) + t_cl, p_cl, ci_cl = safe_inference(fit.tau_cl, fit.se_cl, alpha=alpha) + t_bcm, p_bcm, ci_bcm = safe_inference(fit.tau_bc, fit.se_cl, alpha=alpha) + + return RegressionDiscontinuityResults( + att=fit.tau_bc, + se=fit.se_rb, + t_stat=t_rb, + p_value=p_rb, + conf_int=ci_rb, + alpha=alpha, + att_conventional=fit.tau_cl, + se_conventional=fit.se_cl, + t_stat_conventional=t_cl, + p_value_conventional=p_cl, + conf_int_conventional=ci_cl, + t_stat_bias_corrected=t_bcm, + p_value_bias_corrected=p_bcm, + conf_int_bias_corrected=ci_bcm, + se_robust=fit.se_rb, + h_left=float(h_l), + h_right=float(h_r), + b_left=float(b_l), + b_right=float(b_r), + n_obs=N, + n_left=int(np.sum(x < c)), + n_right=int(np.sum(x >= c)), + n_h_left=fit.N_h_l, + n_h_right=fit.N_h_r, + n_b_left=fit.N_b_l, + n_b_right=fit.N_b_r, + n_unique_left=n_unique_left, + n_unique_right=n_unique_right, + n_dropped=n_dropped, + cutoff=c, + p=p, + q=q, + kernel=kernel, + bwselect=bwselect_label, + vcov_type=self.vcov_type, + nnmatch=int(self.nnmatch), + masspoints=self.masspoints, + bwcheck=None if self.bwcheck is None else int(self.bwcheck), + bwrestrict=bool(self.bwrestrict), + scaleregul=float(self.scaleregul), + h_input=None if self.h is None else float(self.h), + b_input=None if self.b is None else float(self.b), + rho_input=None if self.rho is None else float(self.rho), + beta_p_left=fit.beta_p_l, + beta_p_right=fit.beta_p_r, + ) diff --git a/docs/api/index.rst b/docs/api/index.rst index 44a2bd0e..7d00504e 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -319,6 +319,7 @@ Estimators synthetic_control continuous_did had + regression_discontinuity efficient_did two_stage spillover diff --git a/docs/api/regression_discontinuity.rst b/docs/api/regression_discontinuity.rst new file mode 100644 index 00000000..91cdf4a3 --- /dev/null +++ b/docs/api/regression_discontinuity.rst @@ -0,0 +1,50 @@ +Regression Discontinuity (Sharp) +================================ + +Sharp regression discontinuity estimation with robust bias-corrected +inference, parity-targeting R ``rdrobust`` 4.0.0. + +Treatment is assigned by a known threshold of an observed running variable +(``running >= cutoff``; units exactly at the cutoff are treated, matching +rdrobust). The effect is the jump in the conditional expectation of the +outcome at the cutoff, estimated by kernel-weighted local polynomials on +each side with data-driven MSE/CER-optimal bandwidths (all 10 rdrobust +selectors), and reported with robust bias-corrected inference per Calonico, +Cattaneo & Titiunik (2014). + +.. note:: + + **Canonical inference binding.** The result's ``att``, ``se``, + ``t_stat``, ``p_value``, and ``conf_int`` are one internally coherent + row - the ROBUST bias-corrected row (``att`` is the bias-corrected + estimate; ``conf_int`` is centered on it; ``t_stat == att/se``). R's + rdrobust prints the *conventional* estimate as its headline coefficient + while taking inference from the robust row; that estimate is available + as ``att_conventional`` with its own full inference row, and + ``summary()`` prints the familiar three-row rdrobust table. + +.. note:: + + **Scope of this release.** Sharp designs only, with the nearest-neighbor + variance estimator (rdrobust's default). Fuzzy designs, covariate + adjustment, cluster-robust variance, weights, kink estimands, and the + rdplot/density-test diagnostics are documented follow-ups - see the + methodology registry for the full deviations and seams list. + +RegressionDiscontinuity +----------------------- + +.. autoclass:: diff_diff.RegressionDiscontinuity + :no-index: + :members: + :undoc-members: + :show-inheritance: + +RegressionDiscontinuityResults +------------------------------ + +.. autoclass:: diff_diff.RegressionDiscontinuityResults + :no-index: + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/choosing_estimator.rst b/docs/choosing_estimator.rst index fe6376da..6eaa06c4 100644 --- a/docs/choosing_estimator.rst +++ b/docs/choosing_estimator.rst @@ -704,6 +704,9 @@ differences helps interpret results and choose appropriate inference. * - ``HeterogeneousAdoptionDiD`` - Path-dependent (CCT-2014 / 2SLS / Binder TSL) - Two SE regimes per :doc:`api/had`. **Unweighted**: continuous-dose paths use the CCT-2014 robust SE from the in-house ``lprobust`` port; mass-point uses a 2SLS sandwich. **``survey_design=SurveyDesign(weights="col", ...)``** (the sole weighting entry as of the 3.7.0 ``survey=`` / ``weights=`` removal): both paths compose Binder (1983) Taylor-series linearization (``variance_formula="survey_binder_tsl"`` / ``"survey_binder_tsl_2sls"``); the mass-point survey path rejects ``vcov_type="classical"`` (requires ``hc1`` / ``robust=True``), and ``survey_design=`` + ``cluster=`` is rejected outright (route weighted clustering via ``SurveyDesign(weights=, psu=)``; a bare ``cluster=`` gives unweighted CR1). Per-horizon CIs are pointwise; sup-t bands available on the event-study path via ``cband=True`` whenever ``survey_design=`` or ``cluster=`` is supplied. + * - ``RegressionDiscontinuity`` + - Robust bias-corrected (CCT 2014, NN variance) + - Sharp RD with rdrobust-4.0.0-parity inference. Canonical ``att``/``se``/``conf_int`` are the ROBUST bias-corrected row (``att`` = bias-corrected estimate, CI centered on it); the conventional estimate rdrobust prints as its headline is ``att_conventional`` with its own inference row. Only ``vcov_type="nn"`` in this release; cluster-robust RD variance is a documented follow-up. * - ``SunAbraham`` - Cluster-robust (unit level) - Clusters at unit level by default. Specify ``cluster`` to override. Use ``n_bootstrap`` for pairs bootstrap inference. @@ -872,6 +875,11 @@ estimation. The depth of support varies by estimator and variance method: - Full (Binder TSL) - -- - Multiplier (event-study, ``cband=True`` only) + * - ``RegressionDiscontinuity`` + - N/A (no survey support) + - N/A + - -- + - -- * - ``EfficientDiD`` - Full - Full diff --git a/docs/doc-deps.yaml b/docs/doc-deps.yaml index adc7a72f..10d2b5c7 100644 --- a/docs/doc-deps.yaml +++ b/docs/doc-deps.yaml @@ -397,6 +397,39 @@ sources: # ── HeterogeneousAdoptionDiD (HAD) ────────────────────────────────── + diff_diff/rdd.py: + drift_risk: medium + docs: + - path: docs/methodology/REGISTRY.md + section: "RegressionDiscontinuity" + type: methodology + - path: docs/api/regression_discontinuity.rst + type: api_reference + - path: README.md + section: "Estimators (one-line catalog entry)" + type: user_guide + - path: docs/references.rst + type: user_guide + - path: diff_diff/guides/llms.txt + section: "Estimators" + type: user_guide + - path: diff_diff/guides/llms-full.txt + section: "RegressionDiscontinuity" + type: user_guide + - path: diff_diff/guides/llms-autonomous.txt + section: "Estimator-support matrix + out-of-scope list" + type: user_guide + note: "Sharp RD is IN scope as of the RegressionDiscontinuity release; the out-of-scope bullet routes only fuzzy RD elsewhere. Keep both in sync with the estimator's v1 seams." + - path: docs/choosing_estimator.rst + section: "SE methodology + Survey Design Support tables" + type: user_guide + diff_diff/_rdrobust_port.py: + drift_risk: high + docs: + - path: docs/methodology/REGISTRY.md + section: "RegressionDiscontinuity" + type: methodology + note: "Parity pin (CRAN 4.0.0 tarball sha256), deviation labels, and float-tolerance rationale live in the registry section; any port change must re-run both golden suites (tests/test_rdrobust_port.py, tests/test_rdd_parity.py) and re-verify against the installed 4.0.0 namespace, NOT the GitHub 4.1.0-dev tree." diff_diff/had.py: drift_risk: medium docs: diff --git a/docs/methodology/REGISTRY.md b/docs/methodology/REGISTRY.md index 681c7548..a56da0a4 100644 --- a/docs/methodology/REGISTRY.md +++ b/docs/methodology/REGISTRY.md @@ -3505,13 +3505,164 @@ Shipped in `diff_diff/had_pretests.py` as `stute_joint_pretest()` (residuals-in ## RegressionDiscontinuity -**Status: in progress.** This PR ships the private sharp-RD bandwidth-selection -machinery (`diff_diff/_rdrobust_port.py`: `rdbwselect_sharp` and its -primitives) with R golden-file parity; the public `RegressionDiscontinuity` -estimator, robust bias-corrected inference, and the full methodology section -land in the follow-up PR. No public API is exported yet. +**Primary source:** Calonico, S., Cattaneo, M. D., & Titiunik, R. (2014). +Robust Nonparametric Confidence Intervals for Regression-Discontinuity +Designs. *Econometrica*, 82(6), 2295-2326. https://doi.org/10.3982/ECTA11757. +Software/parity reference: Calonico, Cattaneo, Farrell & Titiunik (2017), +*Stata Journal* 17(2), 372-404 (rdrobust); CER-optimal bandwidth theory: +Calonico, Cattaneo & Farrell (2018), *JASA* 113(522), 767-779. + +**Estimand and estimator (sharp RD):** with running variable `X`, known +cutoff `c`, and treatment `T = 1(X >= c)` (units exactly at the cutoff are +TREATED, matching rdrobust), the estimand is +`tau_SRD = E[Y(1) - Y(0) | X = c] = mu_+ - mu_-` (Hahn-Todd-van der Klaauw +2001 identification). Point estimation: order-`p` kernel-weighted local +polynomials on each side at bandwidth `h` (`tau_cl` = difference in +intercepts); bias correction via order-`q > p` fits at pilot bandwidth `b` +(`tau_bc = tau_cl - h^{p+1} B_hat`, computed through the `Q_q` score-matrix +formulation of rdrobust); robust bias-corrected variance per CCT 2014 +Theorem 1 / Theorem A.1(V) (the `Q_q` sandwich with same-side +nearest-neighbor residuals shared with the conventional sandwich). +Defaults reproduce `rdrobust(y, x)`: `p=1`, `q=2`, triangular kernel, +`bwselect="mserd"`, `vcov_type="nn"` (J=3), `masspoints="adjust"`, +normal-quantile CIs. + +**Bandwidth selection:** all 10 rdrobust data-driven selectors (`mserd` +default; `msetwo`/`msesum`/`msecomb1`/`msecomb2`; CER-optimal `cer*` +variants = the matching MSE selector's `h` shrunk by +`N^(-p/((3+p)(3+2p)))` with `b` inherited unchanged), via the faithful +4.0.0 port in `diff_diff/_rdrobust_port.py` (three-stage d->b->h pilot +chain, per-kernel pilot constants, IK-style regularization, +bwrestrict/bwcheck clamps, masspoints unique-count adjustment). Manual +`h`/`b`/`rho` semantics mirror rdrobust exactly: `h` alone -> `b = h`; +`h`+`rho` -> `b = h/rho` (overriding a supplied `b`, with a warning); +`rho` without `h` applies to the SELECTED bandwidths. + +- **Note (canonical inference binding; deviation from R's printed + output):** the result's canonical `att`/`se`/`t_stat`/`p_value`/ + `conf_int` are ONE coherent row - the ROBUST row: `att = tau_bc` + (bias-corrected), `se = se_robust`, with `t_stat == att/se` and + `conf_int` centered on `att`, preserving the library-wide field + identities. rdrobust instead PRINTS the conventional `tau_cl` as its + headline coefficient while taking inference from the robust row (whose + CI is centered at `tau_bc`, not `tau_cl`). `tau_cl` is first-class as + `att_conventional` with a full inference row, the middle + (bias-corrected, conventional-SE) row is exposed as + `*_bias_corrected` fields, and `summary()` prints the three-row + rdrobust table verbatim. Golden tests pin every row against R. +- **Note (parity pin):** the port targets the installed CRAN release + `rdrobust` 4.0.0, pinned by source-tarball sha256 + (`78f0d6b4...d613a36f` in `diff_diff/_rdrobust_port.py`). The GitHub + development tree (4.1.0-dev) is NOT the parity source: it adds an + `nn_tol` nearest-neighbor tie tolerance, flips the `stdvars` default + to TRUE, and drops 4.0.0's `+1e-8` bwcheck floor. The port implements + the 4.0.0 behaviors: EXACT nearest-neighbor distance comparison, + standardization machinery present but fixed to the 4.0.0 default + (off), and the `+1e-8` floor (rdbwselect.R:374-375). +- **Deviation from R:** missing/non-numeric rows are dropped WITH a + `UserWarning` naming the count (`fit()`), where R's `complete.cases` + filter drops silently (no-silent-failures policy). The private port + layer rejects non-finite inputs outright. +- **Deviation from R:** `b=` without `h=` is ignored WITH a warning + (R ignores it silently). `h`+`b`+`rho` warns that rho takes precedence + (R applies `b = h/rho` silently). +- **Note (N < 20, R-exact):** fewer than 20 complete-case observations + triggers rdrobust.R:303-307's behavior exactly - warn and estimate on + the entire sample (`h = b = max` distance to the cutoff), OVERRIDING + even a user-supplied `h` (the R block runs after manual-bandwidth + resolution); `bwselect` reports "Manual". +- **Deviation from R:** degenerate designs fail closed with targeted + `ValueError`s where R propagates NaN, errors opaquely, or silently + returns a pseudo-inverse fit: a non-finite pilot bandwidth from a + degenerate MSE objective (e.g. constant outcome under data-driven + selection), non-finite/non-positive bandwidths reaching estimation, + one-sided data, a cutoff outside the observed support, zero-variance + running variable, and - per side, per window - a main (`h`) or bias + (`b`) window whose positive-weight support has fewer than `p+1` + (resp. `q+1`) DISTINCT running-variable values. The distinct-support + count is the exact rank condition for a weighted Vandermonde design, + so this guard catches empty windows (e.g. manual `b` far smaller than + `h`, or a huge `rho` collapsing `b = h/rho`) and mass-point-only + support, where R's `ginv` fallback would silently collapse the + bias-correction score `Q_q` to the conventional one or report a + zeroed fit. A constant outcome under MANUAL bandwidths yields + `se = 0` and the standard joint-NaN inference gate via + `safe_inference()` (att = 0 is well defined; all three rows' triples + gate independently). +- **Note:** `qrXXinv`'s Cholesky-failure fallback maps `MASS::ginv(G)` + to `numpy.linalg.pinv(G, rcond=sqrt(eps))` - the same Moore-Penrose + pseudo-inverse with the same default cutoff. The fallback IS the 4.0.0 + behavior (verified against both the pinned tarball source, + functions.R:128-132, and `deparse` of the installed namespace); + rank/support protection for identification is provided by the + per-window distinct-support guards above (in the estimation port, + ahead of any Gram inversion), not by changing `qrXXinv`'s parity + behavior. +- **Note (v1 scope seams):** only `vcov_type="nn"` (rdrobust's default) + ships; `hc0`-`hc3` and cluster modes raise `NotImplementedError`. + Fuzzy designs, covariate adjustment (CCFT 2019 - review on file), + weights, kink estimands (`deriv`), `scalepar`, `stdvars`, per-side + manual bandwidths, and the rdplot/density diagnostics are documented + follow-ups; fuzzy/covariates/cluster/weights are not constructor + parameters at all. The port's `deriv` machinery is golden-covered for + `deriv in {0, 1}` only. +- **Note (p/q surface, R-exact):** public `p`/`q` validation mirrors + rdrobust.R:47-57 exactly - integers in 0:20 with `q > p`; `p=0` is R's + local-constant fit and is accepted. R resolves a NULL `q` to `p + 1` + BEFORE its `q` validation and never re-checks the default + (rdrobust.R:53-57), so `p=20` with `q=None` yields `q=21` in both R + and diff-diff while an EXPLICIT `q=21` is rejected - the asymmetry is + mirrored deliberately (locked by + `test_p20_default_q_resolution_matches_r_quirk`). Golden-anchored at + `p in {0, 1, 2}` (`p0q1`, default `p1q2`, `p2q3` configs); the + Remark-7 identity additionally anchors the `p=0 -> p=1` and + `p=1 -> p=2` steps R-free. All 10 public `bwselect` routings are + locked end-to-end against the bandwidth goldens + (`test_rdd_parity.py::TestPublicSelectorRouting`). +- **Note (cross-implementation float tolerance):** R's sample SD + accumulates sequentially while numpy sums pairwise, seeding a ~2-ULP + difference in the pilot constant `BWp` that the chained d->b->h + bandwidth stages amplify to an observed worst-case ~5e-11 relative + disagreement (p=2/q=3 config, darwin-arm64). Golden parity asserts + rtol=1e-9 on bandwidths and estimates; pre-linalg intermediates + (type-2 quantile IQR, unique counts) agree at float64 identity. + +**Edge cases:** +- Mass points (CCT 2014 Remark 1): `masspoints="adjust"` (default) + substitutes unique-value counts into the pilot bandwidth and floors + the window at 10 unique support points when >= 20% mass is detected; + "check" warns and suggests adjust; "off" reproduces the + pre-masspoints package (and the published 2017 Stata Journal Senate + numbers exactly). Adjust == off on tie-free data (tested). Detection + runs in `fit()` BEFORE the manual-vs-data-driven bandwidth branch, + exactly where R's rdrobust() runs it (rdrobust.R:365-380), so the + warning fires on manual-`h` fits too; the port's rdbwselect-level + copy of the warning is suppressed on the estimator path + (`warn_masspoints=False`) to mirror R's single warning per fit + (R's estimation-path selection is inline, so its rdbwselect warning + never stacks; verified against installed 4.0.0 on both paths). +- CCT 2014 Remark 7 (internal consistency, tested): with `b = h` and + the same kernel, `tau_bc(p, q=p+1)` is numerically identical to the + order-`(p+1)` conventional estimate, and the robust variance equals + the order-`(p+1)` conventional variance (verified at rel 1e-10 across + all three kernels). -**Primary sources (paper reviews on file):** +**Reference implementation(s):** +- R: `rdrobust::rdrobust()`, `rdrobust::rdbwselect()` 4.0.0 (CRAN). +- Stata: `rdrobust` (Calonico-Cattaneo-Farrell-Titiunik 2017). + +**Validation:** bandwidth goldens `benchmarks/data/rdrobust_golden.json` +(17 configs x 10 selectors, `tests/test_rdrobust_port.py`); estimation +goldens `benchmarks/data/rdrobust_estimates_golden.json` (16 configs, +full three-row blocks + counts + per-side coefficients, +`tests/test_rdd_parity.py`); vendored Senate data +(`benchmarks/data/rdrobust_senate.csv`, Cattaneo-Frandsen-Titiunik 2015) +anchoring the published 2017 Stata Journal numbers under +`masspoints="off"`; R-free methodology anchors in +`tests/test_rdd_methodology.py` (Remark 7 equivalence, invariances, +NaN/degenerate contracts). + +**Paper reviews on file:** `docs/methodology/papers/calonico-cattaneo-titiunik-2014-review.md` (CCT 2014, Econometrica - robust bias-corrected RD inference), `calonico-cattaneo-farrell-titiunik-2017-review.md` (Stata Journal - the @@ -3520,62 +3671,7 @@ rdrobust software reference this port parity-targets), `calonico-cattaneo-farrell-titiunik-2019-review.md` (REStat - covariate adjustment, deferred). -**Parity target and port-level notes:** -- **Note (parity pin):** the port targets the installed CRAN release - `rdrobust` 4.0.0, pinned by source-tarball sha256 - (`78f0d6b4...d613a36f` in `diff_diff/_rdrobust_port.py`). The GitHub - development tree (4.1.0-dev) is NOT the parity source: it adds an - `nn_tol` nearest-neighbor tie tolerance, flips the `stdvars` default to - TRUE, and drops 4.0.0's `+1e-8` bwcheck floor. The port implements the - 4.0.0 behaviors: EXACT nearest-neighbor distance comparison, `stdvars` - standardization ported but fixed to the 4.0.0 default (off), and the - `+1e-8` floor (rdbwselect.R:374-375). -- **Deviation from R:** the port rejects non-finite `y`/`x` with a targeted - `ValueError` instead of R's silent `complete.cases` row drop - (rdbwselect.R:72-95); the public estimator will warn-and-drop - (no-silent-failures policy). Golden files are generated on complete-case - data, so parity is unaffected. -- **Deviation from R:** `N < 20` raises `ValueError` in the port where R's - `rdbwselect` warns and aborts without output (rdbwselect.R:237-239); the - estimator-level warn-plus-full-range-bandwidth fallback (rdrobust.R - behavior) ships with the estimator PR. Empty-side and zero-variance - inputs likewise raise targeted `ValueError`s where R would fail with - opaque downstream errors. -- **Note:** `qrXXinv`'s Cholesky-failure fallback maps `MASS::ginv(G)` to - `numpy.linalg.pinv(G, rcond=sqrt(eps))` - the same Moore-Penrose - pseudo-inverse with the same default singular-value cutoff; reachable - only on rank-deficient kernel windows. The fallback IS the 4.0.0 - behavior - verified against both the sha256-pinned CRAN tarball source - (functions.R:128-132: `try(chol(G))` -> `ginv(G)` on failure) and - `deparse(getFromNamespace("qrXXinv", "rdrobust"))` of the installed - package - so the port reproduces R's output on degenerate windows - rather than erroring. Identification/rank-support guards (enough - distinct support points per side for the requested polynomial order, - per the CCT 2014 review's assumption checklist) are an ESTIMATOR-level - `fit()` responsibility and ship with the public class in the follow-up - PR, not inside the parity port. -- **Note:** only `vce="nn"` (rdrobust's default) is implemented; - `hc0`-`hc3` and cluster variance modes raise `NotImplementedError` - (documented seam, mirroring the HAD Phase 1c surface-restriction - pattern). -- **Note (cross-implementation float tolerance):** R's sample SD - accumulates sequentially while numpy sums pairwise, seeding a ~2-ULP - difference in the pilot constant `BWp` that the chained d->b->h - bandwidth stages amplify to an observed worst-case ~6e-12 relative - disagreement (darwin-arm64). Golden parity therefore asserts - rtol=1e-9 on bandwidths - far below methodological materiality - with - bit-level agreement asserted on the pre-linalg intermediates (type-2 - IQR, unique counts). - -**Reference implementation:** R `rdrobust::rdbwselect()` 4.0.0. Golden -fixtures: `benchmarks/data/rdrobust_golden.json` (generator -`benchmarks/R/generate_rdrobust_golden.R`; 17 configurations over three -seeded synthetic DGPs and the vendored Senate data -`benchmarks/data/rdrobust_senate.csv`, spanning kernels, p/q orders, -deriv, masspoints modes, stdvars, bwrestrict, scaleregul, and nnmatch), -validated in `tests/test_rdrobust_port.py` including the 2017 Stata -Journal published Senate anchors under `masspoints="off"`. --- @@ -4168,6 +4264,7 @@ should be a deliberate user choice. | QDiD | qte | `QDiD()` | | BaconDecomposition | bacondecomp | `bacon()` | | HonestDiD | HonestDiD | `createSensitivityResults()` | +| RegressionDiscontinuity | rdrobust | `rdrobust()` + `rdbwselect()` (4.0.0; sharp/nn path) | | PreTrendsPower | pretrends | `pretrends()` | | PowerAnalysis | pwr / DeclareDesign / pcpanel | `pwr::pwr.norm.test` (analytical, normal-based — D1) + `pcpanel` (Burlig 2020 panel, equicorrelated case) + simulation. The analytical multiplier is normal (z), so `pwr.t.test` is **not** the faithful parity target. | diff --git a/docs/practitioner_decision_tree.rst b/docs/practitioner_decision_tree.rst index 5dc7765a..b7055522 100644 --- a/docs/practitioner_decision_tree.rst +++ b/docs/practitioner_decision_tree.rst @@ -476,7 +476,7 @@ At a Glance What About the Other Estimators? -------------------------------- -diff-diff has 22 estimators covering advanced scenarios: Sun-Abraham for +diff-diff has 23 estimators covering advanced scenarios: Sun-Abraham for interaction-weighted estimation, Imputation DiD and Two-Stage DiD for alternative staggered approaches, Local Projections DiD, Stacked DiD, Efficient DiD, Triple Difference, TROP, Changes-in-Changes for distributional/quantile effects, and more. diff --git a/docs/references.rst b/docs/references.rst index 6fffabfb..5419b701 100644 --- a/docs/references.rst +++ b/docs/references.rst @@ -83,6 +83,14 @@ Nonparametric Bias-Corrected Inference CCF (2018, 2019) is the underlying ``nprobust`` machinery (MSE-optimal bandwidth selection and robust bias-corrected CIs) that ``HeterogeneousAdoptionDiD`` ports in-house for the continuous-dose paths. +- **Calonico, S., Cattaneo, M. D., Farrell, M. H., & Titiunik, R. (2017).** "rdrobust: Software for regression-discontinuity designs." *The Stata Journal*, 17(2), 372-404. https://doi.org/10.1177/1536867X1701700208 + + The rdrobust software reference that :class:`diff_diff.RegressionDiscontinuity` parity-targets (CRAN 4.0.0): the 10-selector bandwidth menu, mass-point handling, and the three-row conventional / bias-corrected / robust output. + +- **Calonico, S., Cattaneo, M. D., Farrell, M. H., & Titiunik, R. (2019).** "Regression Discontinuity Designs Using Covariates." *The Review of Economics and Statistics*, 101(3), 442-451. https://doi.org/10.1162/rest_a_00760 + + Covariate-adjusted RD (additive common-coefficient specification and its consistency conditions) - the documented fast-follow for :class:`diff_diff.RegressionDiscontinuity`; review on file. + Survey-Design Inference (Taylor-Series Linearization) ----------------------------------------------------- diff --git a/paper.bib b/paper.bib index a9dffc07..23e07fc0 100644 --- a/paper.bib +++ b/paper.bib @@ -148,6 +148,17 @@ @misc{Chen2025 doi = {10.48550/arXiv.2506.17729} } +@article{Calonico2014, + author = {Calonico, Sebastian and Cattaneo, Matias D. and Titiunik, Rocio}, + title = {Robust Nonparametric Confidence Intervals for Regression-Discontinuity Designs}, + journal = {Econometrica}, + volume = {82}, + number = {6}, + pages = {2295--2326}, + year = {2014}, + doi = {10.3982/ECTA11757} +} + @article{Athey2006, author = {Athey, Susan and Imbens, Guido W.}, title = {Identification and Inference in Nonlinear Difference-in-Differences Models}, diff --git a/paper.md b/paper.md index d987a4cb..3355c6f1 100644 --- a/paper.md +++ b/paper.md @@ -21,7 +21,7 @@ bibliography: paper.bib # Summary `diff-diff` is a Python library for Difference-in-Differences (DiD) causal inference -analysis. It provides 22 estimators covering the full modern DiD toolkit - from classic +analysis. It provides 23 estimators covering the full modern DiD toolkit - from classic two-group/two-period designs through heterogeneity-robust staggered adoption methods, synthetic control hybrids, and sensitivity analysis - under a consistent scikit-learn-style API. Most estimators accept an optional `SurveyDesign` object for design-based variance @@ -47,7 +47,7 @@ industry settings for marketing measurement, product experimentation, and policy evaluation - must either context-switch to another language, reimplement methods from scratch, or rely on partial implementations scattered across unrelated packages. -`diff-diff` fills this gap by providing a single-import library that covers 22 estimators +`diff-diff` fills this gap by providing a single-import library that covers 23 estimators with a consistent API, survey-weighted inference, and numerical validation against R. It is also the companion software for the design-based variance framework of @Gerber2026, which establishes design-consistent standard errors for modern DiD estimators under @@ -79,7 +79,7 @@ both, validated against the R reference implementations where they exist. # Key Features -**Breadth of methods.** `diff-diff` implements 22 estimators organized across the modern +**Breadth of methods.** `diff-diff` implements 23 estimators organized across the modern DiD taxonomy. Classic designs include two-group/two-period DiD, two-way fixed effects, and event-study estimation with period-specific effects. Heterogeneity-robust staggered-adoption estimators include Callaway-Sant'Anna [@Callaway2021], Sun-Abraham [@Sun2021], imputation @@ -92,9 +92,10 @@ designs include triple-difference and staggered triple-difference estimators [@OrtizVillavicencio2025], continuous-treatment DiD with dose-response curves [@Callaway2024], heterogeneous-adoption designs where no unit remains untreated [@deChaisemartin2026], nonlinear ETWFE [@Wooldridge2025; @Wooldridge2023], distributional effects via -changes-in-changes and quantile DiD [@Athey2006], and triply -robust panel estimation [@Athey2025]. Separate diagnostic and sensitivity tools - outside -the 22 estimators - include Goodman-Bacon decomposition [@GoodmanBacon2021], Honest DiD +changes-in-changes and quantile DiD [@Athey2006], triply robust +panel estimation [@Athey2025], and sharp regression-discontinuity estimation with +robust bias-corrected inference [@Calonico2014]. Separate diagnostic and sensitivity tools - outside +the 23 estimators - include Goodman-Bacon decomposition [@GoodmanBacon2021], Honest DiD sensitivity analysis [@Rambachan2023], placebo tests, and pre-trends power analysis [@Roth2022]. diff --git a/tests/test_rdd.py b/tests/test_rdd.py new file mode 100644 index 00000000..b68404a2 --- /dev/null +++ b/tests/test_rdd.py @@ -0,0 +1,359 @@ +"""API, validation, and behavioral tests for RegressionDiscontinuity.""" + +import warnings + +import numpy as np +import pandas as pd +import pytest + +import diff_diff +from diff_diff import RDD, RegressionDiscontinuity, RegressionDiscontinuityResults + + +def _df(n=200, seed=0, jump=1.0): + rng = np.random.default_rng(seed) + x = rng.uniform(-1, 1, n) + y = 0.5 * x + jump * (x >= 0) + rng.standard_normal(n) * 0.2 + return pd.DataFrame({"x": x, "y": y}) + + +class TestConstructor: + def test_constructor_defaults_match_rdrobust(self): + rd = RegressionDiscontinuity() + params = rd.get_params() + assert params == { + "cutoff": 0.0, + "p": 1, + "q": None, + "kernel": "triangular", + "bwselect": "mserd", + "h": None, + "b": None, + "rho": None, + "vcov_type": "nn", + "nnmatch": 3, + "masspoints": "adjust", + "bwcheck": None, + "bwrestrict": True, + "scaleregul": 1.0, + "alpha": 0.05, + } + + def test_invalid_kernel_raises(self): + with pytest.raises(ValueError, match="kernel must be one of"): + RegressionDiscontinuity(kernel="gaussian") + + def test_invalid_bwselect_raises(self): + with pytest.raises(ValueError, match="bwselect"): + RegressionDiscontinuity(bwselect="ik") + + def test_vcov_type_hc_raises_not_implemented(self): + with pytest.raises(NotImplementedError, match="vcov_type='nn'"): + RegressionDiscontinuity(vcov_type="hc1") + + def test_invalid_masspoints_raises(self): + with pytest.raises(ValueError, match="masspoints"): + RegressionDiscontinuity(masspoints="on") + + def test_q_le_p_raises(self): + with pytest.raises(ValueError, match="q must be None"): + RegressionDiscontinuity(p=2, q=2) + + def test_p_q_bounds_match_rdrobust(self): + # rdrobust.R:47-57: p and q are single integers in 0:20 with q > p; + # p=0 (local-constant RD) is inside R's accepted surface. + RegressionDiscontinuity(p=0) # q resolves to 1 at fit time + RegressionDiscontinuity(p=0, q=1) + with pytest.raises(ValueError, match="p must be an integer in 0..20"): + RegressionDiscontinuity(p=-1) + with pytest.raises(ValueError, match="p must be an integer in 0..20"): + RegressionDiscontinuity(p=21) + with pytest.raises(ValueError, match="q must be None"): + RegressionDiscontinuity(p=1, q=21) + + def test_p20_default_q_resolution_matches_r_quirk(self): + # rdrobust.R:53-57 resolves a NULL q to p+1 BEFORE its validation + # and never re-checks the default: R accepts p=20 (q -> 21) while + # rejecting an explicit q=21. Deliberately mirrored (REGISTRY p/q + # surface note). + RegressionDiscontinuity(p=20) # accepted; q resolves to 21 at fit + with pytest.raises(ValueError, match="q must be None"): + RegressionDiscontinuity(p=20, q=21) + + def test_bool_rejected_for_integer_knobs(self): + # bool is an int subclass; p=True must not silently fit p=1. + for kwargs in ( + {"p": True}, + {"p": 0, "q": True}, + {"nnmatch": True}, + {"bwcheck": True}, + ): + with pytest.raises(ValueError): + RegressionDiscontinuity(**kwargs) + + def test_non_numeric_scalars_raise_value_error(self): + # Strings (even numeric-looking ones) and other non-real types must + # fail with the estimator's ValueError, not NumPy's TypeError. + for kwargs in ( + {"cutoff": "0"}, + {"h": "0.5"}, + {"b": "0.5", "h": 0.5}, + {"rho": "2"}, + {"scaleregul": "1"}, + {"alpha": "0.05"}, + {"cutoff": True}, + ): + with pytest.raises(ValueError): + RegressionDiscontinuity(**kwargs) + + def test_non_bool_bwrestrict_raises(self): + # A string like "False" must not silently coerce to truthy and + # flip bandwidth restriction ON (no-silent-failures policy). + with pytest.raises(ValueError, match="bwrestrict must be a bool"): + RegressionDiscontinuity(bwrestrict="False") + with pytest.raises(ValueError, match="bwrestrict must be a bool"): + RegressionDiscontinuity(bwrestrict=1) + + def test_nonpositive_h_raises(self): + with pytest.raises(ValueError, match="h must be None or finite"): + RegressionDiscontinuity(h=0.0) + + def test_alpha_bounds(self): + with pytest.raises(ValueError, match="alpha"): + RegressionDiscontinuity(alpha=1.5) + + def test_kernel_r_spellings_accepted(self): + df = _df() + a = RegressionDiscontinuity(kernel="tri").fit(df, "y", "x") + b = RegressionDiscontinuity(kernel="triangular").fit(df, "y", "x") + assert a.att == b.att + assert a.kernel == "triangular" + + +class TestParamsPlumbing: + def test_set_params_atomic_on_invalid(self): + rd = RegressionDiscontinuity() + with pytest.raises(ValueError): + rd.set_params(kernel="uniform", alpha=7.0) + # Dry-run failed -> NOTHING mutated, including the valid kernel. + assert rd.kernel == "triangular" + assert rd.alpha == 0.05 + + def test_set_params_unknown_key_raises(self): + with pytest.raises(ValueError, match="Unknown parameter"): + RegressionDiscontinuity().set_params(bandwidth=1.0) + + def test_get_params_roundtrip_q_none(self): + rd = RegressionDiscontinuity(q=None) + clone = RegressionDiscontinuity(**rd.get_params()) + assert clone.get_params() == rd.get_params() + assert clone.q is None + + def test_sklearn_clone(self): + pytest.importorskip("sklearn") + from sklearn.base import clone + + rd = RegressionDiscontinuity(cutoff=2.0, kernel="uniform", rho=2.0) + cloned = clone(rd) + assert cloned.get_params() == rd.get_params() + + def test_fit_leaves_estimator_stateless(self): + rd = RegressionDiscontinuity() + before = rd.get_params() + rd.fit(_df(), "y", "x") + assert rd.get_params() == before + assert set(vars(rd).keys()) == set(before.keys()) + + def test_repeat_fit_identical(self): + rd = RegressionDiscontinuity() + df = _df() + r1 = rd.fit(df, "y", "x") + r2 = rd.fit(df, "y", "x") + assert r1.att == r2.att and r1.se == r2.se + + +class TestFitValidation: + def test_missing_column_raises(self): + with pytest.raises(ValueError, match="not found"): + RegressionDiscontinuity().fit(_df(), "y", "score") + + def test_nan_rows_warn_and_drop(self): + df = _df(100) + df.loc[3, "y"] = np.nan + df.loc[7, "x"] = np.nan + with pytest.warns(UserWarning, match="Dropping 2 row"): + r = RegressionDiscontinuity().fit(df, "y", "x") + assert r.n_obs == 98 + assert r.n_dropped == 2 + + def test_all_obs_one_side_raises(self): + df = _df(100) + df["x"] = np.abs(df["x"]) + 1.0 + with pytest.raises(ValueError, match="outside the observed"): + RegressionDiscontinuity().fit(df, "y", "x") + + def test_cutoff_outside_support_raises(self): + with pytest.raises(ValueError, match="outside the observed"): + RegressionDiscontinuity(cutoff=5.0).fit(_df(), "y", "x") + + def test_b_without_h_warns_and_ignored(self): + df = _df() + with pytest.warns(UserWarning, match="b= was supplied without h="): + r = RegressionDiscontinuity(b=0.5).fit(df, "y", "x") + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + ref = RegressionDiscontinuity().fit(df, "y", "x") + assert r.att == ref.att + assert r.b_left == ref.b_left + + def test_h_with_b_and_rho_warns_rho_wins(self): + df = _df() + with pytest.warns(UserWarning, match="rho takes"): + r = RegressionDiscontinuity(h=0.5, b=0.9, rho=2.0).fit(df, "y", "x") + assert r.b_left == pytest.approx(0.25) + + def test_rho_without_h_applies_to_selected_bandwidths(self): + df = _df(500, seed=3) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + base = RegressionDiscontinuity().fit(df, "y", "x") + r = RegressionDiscontinuity(rho=2.0).fit(df, "y", "x") + assert r.h_left == base.h_left + assert r.b_left == pytest.approx(base.h_left / 2.0) + + def test_manual_h_alone_sets_b_equal_h(self): + r = RegressionDiscontinuity(h=0.4).fit(_df(), "y", "x") + assert r.h_left == 0.4 and r.b_left == 0.4 + assert r.bwselect == "Manual" + + def test_n_below_20_warns_and_overrides_manual_h(self): + # rdrobust.R:303-307: the full-range override runs AFTER manual-h + # resolution and therefore overrides even a user-supplied h. + df = _df(15, seed=1) + with pytest.warns(UserWarning, match="entire sample"): + r = RegressionDiscontinuity(h=0.1).fit(df, "y", "x") + full = max(abs(df.x.min() - 0.0), abs(df.x.max() - 0.0)) + assert r.h_left == pytest.approx(full) + assert r.b_left == pytest.approx(full) + assert r.bwselect == "Manual" + + def test_x_equal_cutoff_assigned_treated_side(self): + df = _df(200, seed=5) + df.loc[0, "x"] = 0.0 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = RegressionDiscontinuity().fit(df, "y", "x") + assert r.n_left + r.n_right == 200 + assert r.n_right == int(np.sum(df.x.values >= 0)) + + def test_unsorted_input_matches_sorted(self): + df = _df(300, seed=7) + shuffled = df.sample(frac=1.0, random_state=11).reset_index(drop=True) + a = RegressionDiscontinuity().fit(df, "y", "x") + b = RegressionDiscontinuity().fit(shuffled, "y", "x") + assert a.att == pytest.approx(b.att, rel=1e-12) + assert a.se == pytest.approx(b.se, rel=1e-12) + + def test_int_dtype_running_var(self): + rng = np.random.default_rng(9) + x = rng.integers(-50, 50, 400) + y = 0.02 * x + (x >= 0) + rng.standard_normal(400) * 0.3 + df = pd.DataFrame({"x": x, "y": y}) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = RegressionDiscontinuity().fit(df, "y", "x") + assert np.isfinite(r.att) + + +class TestResults: + def test_canonical_identities_hold(self): + r = RegressionDiscontinuity().fit(_df(500, seed=2), "y", "x") + assert r.t_stat == pytest.approx(r.att / r.se, rel=1e-14) + lo, hi = r.conf_int + assert (lo + hi) / 2 == pytest.approx(r.att, rel=1e-12) + assert r.se == r.se_robust + + def test_att_is_bias_corrected_not_conventional(self): + r = RegressionDiscontinuity().fit(_df(500, seed=2), "y", "x") + # The two estimates differ by the (nonzero) estimated bias. + assert r.att != r.att_conventional + + def test_summary_contains_three_rows(self): + r = RegressionDiscontinuity().fit(_df(), "y", "x") + text = r.summary() + for token in ( + "Conventional", + "Bias-Corrected", + "Robust", + "att_conventional", + ): + assert token in text + + def test_to_dict_ci_split(self): + r = RegressionDiscontinuity().fit(_df(), "y", "x") + d = r.to_dict() + assert d["conf_int_lower"] == r.conf_int[0] + assert d["conf_int_upper"] == r.conf_int[1] + assert d["conf_int_conventional_lower"] == r.conf_int_conventional[0] + assert all(not isinstance(v, (np.floating, np.integer)) for v in d.values()) + + def test_to_dataframe_single_row(self): + r = RegressionDiscontinuity().fit(_df(), "y", "x") + frame = r.to_dataframe() + assert frame.shape[0] == 1 + assert frame.loc[0, "att"] == r.att + + def test_results_type(self): + assert isinstance( + RegressionDiscontinuity().fit(_df(), "y", "x"), + RegressionDiscontinuityResults, + ) + + def test_config_echoes_reconstruct_fit(self): + # Every constructor knob that affects fitting is echoed on the + # results object (and in to_dict), so a saved result suffices to + # reconstruct the fit configuration. h/b/rho echo RAW inputs + # (resolved per-side bandwidths live in h_left/right, b_left/right). + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = RegressionDiscontinuity( + h=0.3, rho=1.5, bwcheck=5, bwrestrict=False, scaleregul=0.5 + ).fit(_df(), "y", "x") + assert r.h_input == 0.3 + assert r.b_input is None + assert r.rho_input == 1.5 + assert r.bwcheck == 5 + assert r.bwrestrict is False + assert r.scaleregul == 0.5 + assert r.bwselect == "Manual" # resolved label (manual h supplied) + assert r.b_left == pytest.approx(0.3 / 1.5) # rho applied + d = r.to_dict() + for key in ( + "h_input", + "b_input", + "rho_input", + "bwcheck", + "bwrestrict", + "scaleregul", + ): + assert key in d + assert d["b_input"] is None + assert d["rho_input"] == 1.5 + + def test_config_echoes_data_driven_defaults(self): + r = RegressionDiscontinuity().fit(_df(), "y", "x") + assert r.h_input is None and r.b_input is None and r.rho_input is None + assert r.bwcheck is None + assert r.bwrestrict is True + assert r.scaleregul == 1.0 + assert r.bwselect == "mserd" + + +class TestAliasAndExports: + def test_alias_rdd_identity(self): + assert RDD is RegressionDiscontinuity + + def test_all_exports(self): + for name in ("RegressionDiscontinuity", "RegressionDiscontinuityResults", "RDD"): + assert name in diff_diff.__all__ + assert hasattr(diff_diff, name) diff --git a/tests/test_rdd_methodology.py b/tests/test_rdd_methodology.py new file mode 100644 index 00000000..87b7544e --- /dev/null +++ b/tests/test_rdd_methodology.py @@ -0,0 +1,213 @@ +"""Methodology tests for RegressionDiscontinuity - internal-consistency +anchors that require no R, plus NaN/degenerate behavioral contracts.""" + +import warnings + +import numpy as np +import pandas as pd +import pytest + +from diff_diff import RegressionDiscontinuity + +from .conftest import assert_nan_inference + + +def _df(n=800, seed=42, jump=0.5): + rng = np.random.default_rng(seed) + x = 2 * rng.beta(2, 4, n) - 1 + y = 0.5 * x + 0.8 * x**2 + jump * (x >= 0) + rng.standard_normal(n) * 0.2 + return pd.DataFrame({"x": x, "y": y}) + + +class TestCCT2014Remark7: + """CCT 2014 Remark 7: with b = h and the same kernel, the bias-corrected + order-p estimator is NUMERICALLY IDENTICAL to the (not bias-corrected) + order-(p+1) estimator, and the robust variance equals the order-(p+1) + conventional variance. The strongest R-free correctness anchor.""" + + @pytest.mark.parametrize("kernel", ["triangular", "epanechnikov", "uniform"]) + def test_bc_equals_local_quadratic(self, kernel): + df = _df() + h0 = 0.35 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + fit_p1 = RegressionDiscontinuity(p=1, q=2, h=h0, kernel=kernel).fit( + df, "y", "x" + ) # h alone -> b = h (rho = 1) + fit_p2 = RegressionDiscontinuity(p=2, q=3, h=h0, kernel=kernel).fit(df, "y", "x") + assert fit_p1.att == pytest.approx(fit_p2.att_conventional, rel=1e-10) + assert fit_p1.se_robust == pytest.approx(fit_p2.se_conventional, rel=1e-10) + + def test_bc_at_p0_equals_local_linear(self): + # Same identity one polynomial order down: the bias-corrected + # local-CONSTANT fit (p=0, q=1, b=h) is numerically identical to the + # conventional local-linear fit (p=1) - R-free anchor for the p=0 + # end of the public 0..20 order surface (golden-covered in + # test_rdd_parity.py::p0q1). + df = _df() + h0 = 0.35 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + fit_p0 = RegressionDiscontinuity(p=0, q=1, h=h0).fit(df, "y", "x") + fit_p1 = RegressionDiscontinuity(p=1, q=2, h=h0).fit(df, "y", "x") + assert fit_p0.att == pytest.approx(fit_p1.att_conventional, rel=1e-10) + assert fit_p0.se_robust == pytest.approx(fit_p1.se_conventional, rel=1e-10) + + +class TestMasspoints: + def test_adjust_equals_off_without_ties(self): + df = _df(600, seed=1) # continuous draws: no ties + a = RegressionDiscontinuity(masspoints="adjust").fit(df, "y", "x") + o = RegressionDiscontinuity(masspoints="off").fit(df, "y", "x") + assert a.att == o.att + assert a.se == o.se + assert a.h_left == o.h_left + + def test_check_warns_on_ties(self): + df = _df(600, seed=1) + df["x"] = df["x"].round(2) + with pytest.warns(UserWarning) as record: + RegressionDiscontinuity(masspoints="check").fit(df, "y", "x") + messages = [str(w.message) for w in record] + assert any("Mass points detected" in m for m in messages) + # R warns ONCE from rdrobust() itself (rdrobust.R:365-380; its + # selection is inline, so rdbwselect's copy never stacks) - the + # estimator must not emit a duplicate through the port. + assert sum("Mass points detected" in m for m in messages) == 1 + + def test_check_warns_on_ties_with_manual_h(self): + # rdrobust.R:365-380 runs BEFORE the manual-bandwidth branch, so + # the mass-point warning fires even when h= is supplied (verified + # against installed R 4.0.0: manual h + masspoints="check" emits + # both warning lines). + df = _df(600, seed=1) + df["x"] = df["x"].round(2) + with pytest.warns(UserWarning) as record: + RegressionDiscontinuity(h=0.5, masspoints="check").fit(df, "y", "x") + messages = [str(w.message) for w in record] + assert any("Mass points detected" in m for m in messages) + assert any("masspoints='adjust'" in m for m in messages) + + def test_adjust_warns_on_ties_with_manual_h(self): + # adjust warns too (without the "try adjust" hint); the default + # masspoints="adjust" must not silently swallow the detection on + # manual-bandwidth fits. + df = _df(600, seed=1) + df["x"] = df["x"].round(2) + with pytest.warns(UserWarning, match="Mass points detected"): + RegressionDiscontinuity(h=0.5, masspoints="adjust").fit(df, "y", "x") + + +class TestInvariances: + def test_outcome_scale_equivariance(self): + df = _df(500, seed=3) + scaled = df.assign(y=df.y * 7.0) + a = RegressionDiscontinuity().fit(df, "y", "x") + b = RegressionDiscontinuity().fit(scaled, "y", "x") + # Outcome scaling multiplies estimates and SEs; bandwidths are + # selected on the standardized objective so h may shift slightly - + # pin the manual-bandwidth case for exact equivariance. + am = RegressionDiscontinuity(h=0.3).fit(df, "y", "x") + bm = RegressionDiscontinuity(h=0.3).fit(scaled, "y", "x") + assert bm.att == pytest.approx(7.0 * am.att, rel=1e-12) + assert bm.se == pytest.approx(7.0 * am.se, rel=1e-12) + assert np.isfinite(a.att) and np.isfinite(b.att) + + def test_running_var_translation_invariance(self): + df = _df(500, seed=4) + shifted = df.assign(x=df.x + 10.0) + a = RegressionDiscontinuity(cutoff=0.0, h=0.3).fit(df, "y", "x") + b = RegressionDiscontinuity(cutoff=10.0, h=0.3).fit(shifted, "y", "x") + assert b.att == pytest.approx(a.att, rel=1e-9) + assert b.se == pytest.approx(a.se, rel=1e-9) + + +class TestInferenceStructure: + def test_robust_ci_wider_than_conventional_dgp_lock(self): + # FIXTURE-SPECIFIC behavior lock, not a theorem: on this DGP the + # robust CI is wider because se_rb adds bias-estimation + # variability. The exact widths are golden-locked at rtol=1e-9 in + # test_rdd_parity.py (se_cl/se_rb pins); this test only guards the + # qualitative ordering against accidental row swaps. + r = RegressionDiscontinuity().fit(_df(), "y", "x") + width_rb = r.conf_int[1] - r.conf_int[0] + width_cl = r.conf_int_conventional[1] - r.conf_int_conventional[0] + assert width_rb > width_cl + assert r.se_robust > r.se_conventional + + +class TestDegenerates: + def test_constant_outcome_manual_h_nan_gated(self): + # Constant y in the window: zero NN residuals -> se = 0 -> the + # downstream inference triple gates to NaN (library contract); + # the point estimate itself is a well-defined 0. + rng = np.random.default_rng(0) + df = pd.DataFrame({"x": rng.uniform(-1, 1, 100), "y": np.ones(100)}) + r = RegressionDiscontinuity(h=0.5).fit(df, "y", "x") + assert r.att == pytest.approx(0.0, abs=1e-12) + assert_nan_inference( + { + "se": r.se, + "t_stat": r.t_stat, + "p_value": r.p_value, + "conf_int": r.conf_int, + } + ) + # All three rows gate independently: + assert np.isnan(r.t_stat_conventional) + assert np.isnan(r.p_value_bias_corrected) + + def test_constant_outcome_auto_bandwidth_fails_closed(self): + # With data-driven selection, a constant outcome degenerates the + # pilot MSE objective (0/0 -> NaN bandwidths); the estimation port + # rejects non-finite bandwidths with a targeted error instead of + # emitting NaN everywhere as R does (documented deviation). + rng = np.random.default_rng(0) + df = pd.DataFrame({"x": rng.uniform(-1, 1, 100), "y": np.ones(100)}) + with pytest.raises(ValueError, match="non-finite pilot bandwidth"): + RegressionDiscontinuity().fit(df, "y", "x") + + def test_tiny_window_too_few_points_fails_closed(self): + df = _df(100, seed=6) + with pytest.raises(ValueError, match="Too few observations inside"): + RegressionDiscontinuity(h=1e-6).fit(df, "y", "x") + + def test_valid_h_empty_b_window_fails_closed(self): + # Valid main window but (near-)empty bias window: without the + # per-window guard the zero W_b Gram would pinv-collapse Q_q back + # to the conventional score and report finite "robust" inference. + df = _df(200, seed=7) + with pytest.raises(ValueError, match="bias bandwidth window"): + RegressionDiscontinuity(h=0.5, b=1e-9).fit(df, "y", "x") + + def test_empty_h_window_valid_b_fails_closed(self): + # Empty main window inside a valid bias window: the order-p fit is + # unidentified even though the effective (wider) window is populated. + df = _df(200, seed=7) + with pytest.raises(ValueError, match="main bandwidth window"): + RegressionDiscontinuity(h=1e-9, b=0.5).fit(df, "y", "x") + + def test_single_support_point_per_side_fails_closed(self): + # Many rows but ONE distinct running-variable value per side: raw + # counts pass any N-based check, yet the order-1 local fit is + # unidentified (rank 1 < p+1 = 2). + rng = np.random.default_rng(8) + df = pd.DataFrame( + { + "x": np.repeat([-0.5, 0.5], 50).astype(float), + "y": rng.normal(0.0, 1.0, 100), + } + ) + # The fit-level mass-point detection (rdrobust.R:365-380) fires + # first - one distinct value per side is extreme mass - then the + # identification guard raises. + with pytest.warns(UserWarning, match="Mass points detected"): + with pytest.raises(ValueError, match="distinct running-variable"): + RegressionDiscontinuity(h=1.0).fit(df, "y", "x") + + def test_huge_rho_empty_selected_b_fails_closed(self): + # rho without h applies b = h_selected/rho (rdrobust.R:501-504); a + # huge rho collapses the selected bias window to empty support. + df = _df(500, seed=9) + with pytest.raises(ValueError, match="bias bandwidth window"): + RegressionDiscontinuity(rho=1e12).fit(df, "y", "x") diff --git a/tests/test_rdd_parity.py b/tests/test_rdd_parity.py new file mode 100644 index 00000000..c63fb6ca --- /dev/null +++ b/tests/test_rdd_parity.py @@ -0,0 +1,204 @@ +"""Golden-file parity for the public RegressionDiscontinuity estimator +against R rdrobust 4.0.0 (estimation blocks; bandwidth-selection parity +lives in tests/test_rdrobust_port.py). + +Tolerance: rtol=1e-9 on estimates/SEs/CIs (same policy and root cause as +the bandwidth suite - see tests/test_rdrobust_port.py's module docstring; +observed worst ~5e-11 on the p=2/q=3 config whose higher-order pilots +amplify the summation-order seed most). +""" + +import json +import warnings +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from diff_diff import RegressionDiscontinuity + +GOLDEN_PATH = ( + Path(__file__).resolve().parents[1] / "benchmarks" / "data" / "rdrobust_estimates_golden.json" +) + +RTOL = 1e-9 + + +@pytest.fixture(scope="module") +def golden(): + if not GOLDEN_PATH.exists(): + pytest.skip( + "Golden values file not found; run: " + "Rscript benchmarks/R/generate_rdrobust_estimates_golden.R" + ) + with open(GOLDEN_PATH) as f: + return json.load(f) + + +def _frame(golden, dgp_name): + entry = golden[dgp_name] + if dgp_name == "senate": + csv_path = Path(__file__).resolve().parents[1] / entry["csv"] + if not csv_path.exists(): + pytest.skip(f"Vendored Senate CSV not found at {csv_path}") + df = pd.read_csv(csv_path)[["vote", "margin"]].dropna() + return df.rename(columns={"vote": "y", "margin": "x"}) + return pd.DataFrame({"x": entry["x"], "y": entry["y"]}) + + +def _kwargs_from_config(cfg): + kwargs = dict( + cutoff=float(cfg["c"]), + p=int(cfg["p"]), + q=int(cfg["q"]), + kernel=cfg["kernel"], + bwselect=cfg["bwselect"], + masspoints=cfg["masspoints"], + alpha=1 - cfg["level"] / 100.0, + ) + if cfg["h_in"] is not None: + kwargs["h"] = float(cfg["h_in"]) + if cfg["b_in"] is not None: + kwargs["b"] = float(cfg["b_in"]) + if cfg["rho_in"] is not None: + kwargs["rho"] = float(cfg["rho_in"]) + return kwargs + + +def _fit(golden, dgp_name, cfg): + df = _frame(golden, dgp_name) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + return RegressionDiscontinuity(**_kwargs_from_config(cfg)).fit(df, "y", "x") + + +class TestEstimateGoldenParity: + def test_all_configs(self, golden): + n_checked = 0 + for dgp_name, entry in golden.items(): + if dgp_name == "metadata": + continue + for cfg_name, cfg in entry["configs"].items(): + label = f"{dgp_name}/{cfg_name}" + r = _fit(golden, dgp_name, cfg) + pairs = [ + ("tau_cl", r.att_conventional, cfg["tau_cl"]), + ("tau_bc", r.att, cfg["tau_bc"]), + ("se_cl", r.se_conventional, cfg["se_cl"]), + ("se_rb", r.se, cfg["se_rb"]), + ("h_l", r.h_left, cfg["h_l"]), + ("h_r", r.h_right, cfg["h_r"]), + ("b_l", r.b_left, cfg["b_l"]), + ("b_r", r.b_right, cfg["b_r"]), + ("z_cl", r.t_stat_conventional, cfg["z"][0]), + ("z_bc", r.t_stat_bias_corrected, cfg["z"][1]), + ("z_rb", r.t_stat, cfg["z"][2]), + ("pv_cl", r.p_value_conventional, cfg["pv"][0]), + ("pv_bc", r.p_value_bias_corrected, cfg["pv"][1]), + ("pv_rb", r.p_value, cfg["pv"][2]), + ("ci_cl_lo", r.conf_int_conventional[0], cfg["ci_lower"][0]), + ("ci_cl_hi", r.conf_int_conventional[1], cfg["ci_upper"][0]), + ("ci_bc_lo", r.conf_int_bias_corrected[0], cfg["ci_lower"][1]), + ("ci_bc_hi", r.conf_int_bias_corrected[1], cfg["ci_upper"][1]), + ("ci_rb_lo", r.conf_int[0], cfg["ci_lower"][2]), + ("ci_rb_hi", r.conf_int[1], cfg["ci_upper"][2]), + ] + for name, got, want in pairs: + assert got == pytest.approx( + want, rel=RTOL, abs=1e-12 + ), f"{label}:{name}: {got} vs {want}" + assert (r.n_h_left, r.n_h_right) == tuple(cfg["N_h"]), label + assert (r.n_b_left, r.n_b_right) == tuple(cfg["N_b"]), label + np.testing.assert_allclose(r.beta_p_left, cfg["beta_p_l"], rtol=RTOL, err_msg=label) + np.testing.assert_allclose( + r.beta_p_right, cfg["beta_p_r"], rtol=RTOL, err_msg=label + ) + n_checked += 1 + # 16 configurations; fail loudly if the golden shrinks. + assert n_checked == 16 + + +class TestSenatePublished2017: + """Published 2017 Stata Journal numbers - an anchor independent of our + own R invocation. masspoints='off' reproduces the pre-masspoints-era + package the paper documents.""" + + def test_senate_masspoints_off_matches_stata_journal_2017(self, golden): + df = _frame(golden, "senate") + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + r = RegressionDiscontinuity(masspoints="off").fit(df, "y", "x") + # Stata Journal 2017 p. 392 default run (printed precision) + assert r.att_conventional == pytest.approx(7.416, abs=1e-3) + assert r.se_conventional == pytest.approx(1.4604, abs=1e-4) + assert r.h_left == pytest.approx(17.708, abs=1e-3) + assert r.b_left == pytest.approx(27.984, abs=1e-3) + assert r.conf_int[0] == pytest.approx(4.09441, abs=1e-4) + assert r.conf_int[1] == pytest.approx(10.9255, abs=1e-3) + assert r.t_stat_conventional == pytest.approx(5.0782, abs=1e-3) + assert r.t_stat == pytest.approx(4.3095, abs=1e-3) + assert (r.n_h_left, r.n_h_right) == (359, 322) + + def test_senate_default_adjust_matches_golden(self, golden): + cfg = golden["senate"]["configs"]["adjust"] + r = _fit(golden, "senate", cfg) + assert r.att_conventional == pytest.approx(cfg["tau_cl"], rel=RTOL) + assert r.h_left == pytest.approx(17.754, abs=1e-3) + + +BW_GOLDEN_PATH = ( + Path(__file__).resolve().parents[1] / "benchmarks" / "data" / "rdrobust_golden.json" +) + +BWSELECT_ALL = ( + "mserd", + "msetwo", + "msesum", + "msecomb1", + "msecomb2", + "cerrd", + "certwo", + "cersum", + "cercomb1", + "cercomb2", +) + + +@pytest.fixture(scope="module") +def bw_golden(): + if not BW_GOLDEN_PATH.exists(): + pytest.skip( + "Bandwidth golden file not found; run: " + "Rscript benchmarks/R/generate_rdrobust_golden.R" + ) + with open(BW_GOLDEN_PATH) as f: + return json.load(f) + + +class TestPublicSelectorRouting: + """End-to-end lock for the public ``bwselect`` surface: each of the 10 + selector options must route the estimator to the matching column of the + rdbwselect output (bandwidths equal to the PR-1 bandwidth goldens, which + the ``default`` config generated under the estimator's default settings) + and complete the fit with a coherent robust row.""" + + @pytest.mark.parametrize("selector", BWSELECT_ALL) + def test_selector_routes_to_golden_bandwidths(self, bw_golden, selector): + entry = bw_golden["dgp_lee_smooth"] + cfg = entry["configs"]["default"] + # The golden 'default' config was generated at the estimator's + # defaults; guard the premise so a golden regeneration can't + # silently invalidate this routing lock. + assert (cfg["p"], cfg["q"], cfg["kernel"]) == (1, 2, "tri") + assert (cfg["masspoints"], cfg["bwrestrict"]) == ("adjust", True) + df = pd.DataFrame({"x": entry["x"], "y": entry["y"]}) + r = RegressionDiscontinuity(bwselect=selector).fit(df, "y", "x") + h_l, h_r, b_l, b_r = cfg["bws"][selector] + assert r.h_left == pytest.approx(h_l, rel=RTOL) + assert r.h_right == pytest.approx(h_r, rel=RTOL) + assert r.b_left == pytest.approx(b_l, rel=RTOL) + assert r.b_right == pytest.approx(b_r, rel=RTOL) + assert r.bwselect == selector + assert np.isfinite(r.att) and np.isfinite(r.se) + assert r.t_stat == pytest.approx(r.att / r.se, rel=1e-14) diff --git a/tests/test_rdrobust_port.py b/tests/test_rdrobust_port.py index f4328d9b..041410a8 100644 --- a/tests/test_rdrobust_port.py +++ b/tests/test_rdrobust_port.py @@ -28,6 +28,7 @@ qrXXinv, quantile_type2, rdbwselect_sharp, + rdrobust_fit_sharp, rdrobust_kweight, rdrobust_res_nn, rdrobust_vander, @@ -473,3 +474,47 @@ def test_unsorted_input_matches_sorted(self, golden): b = rdbwselect_sharp(y[perm], x[perm]) for sel in BWSELECT_OPTIONS: np.testing.assert_allclose(a.bws[sel], b.bws[sel], rtol=1e-12) + + +class TestRdrobustFitSharpValidation: + """rdrobust_fit_sharp shares rdbwselect_sharp's input contract; direct + (non-estimator) callers get targeted errors, not opaque NumPy ones.""" + + def _yx(self, n=200, seed=11): + rng = np.random.default_rng(seed) + x = rng.uniform(-1, 1, n) + y = 0.3 * x + 0.5 * (x >= 0) + rng.normal(0, 0.1, n) + return y, x + + def test_two_dim_input_rejected(self): + y, x = self._yx() + with pytest.raises(ValueError, match="1-D vector"): + rdrobust_fit_sharp(y.reshape(50, 4), x, 0.0, 0.5, 0.5, 0.5, 0.5) + + def test_unequal_lengths_rejected(self): + y, x = self._yx() + with pytest.raises(ValueError, match="equal length"): + rdrobust_fit_sharp(y[:-5], x, 0.0, 0.5, 0.5, 0.5, 0.5) + + def test_non_integer_orders_rejected(self): + y, x = self._yx() + with pytest.raises(ValueError, match="p must be an integer"): + rdrobust_fit_sharp(y, x, 0.0, 0.5, 0.5, 0.5, 0.5, p=1.0) + + def test_order_inequality_enforced(self): + y, x = self._yx() + with pytest.raises(ValueError, match="0 <= deriv <= p < q"): + rdrobust_fit_sharp(y, x, 0.0, 0.5, 0.5, 0.5, 0.5, p=2, q=2) + with pytest.raises(ValueError, match="0 <= deriv <= p < q"): + rdrobust_fit_sharp(y, x, 0.0, 0.5, 0.5, 0.5, 0.5, deriv=2, p=1, q=2) + + def test_nnmatch_validated(self): + y, x = self._yx() + with pytest.raises(ValueError, match="nnmatch must be an integer >= 1"): + rdrobust_fit_sharp(y, x, 0.0, 0.5, 0.5, 0.5, 0.5, nnmatch=0) + + def test_column_vector_accepted(self): + y, x = self._yx() + a = rdrobust_fit_sharp(y, x, 0.0, 0.5, 0.5, 0.5, 0.5) + b = rdrobust_fit_sharp(y.reshape(-1, 1), x.reshape(-1, 1), 0.0, 0.5, 0.5, 0.5, 0.5) + assert a.tau_bc == b.tau_bc and a.se_rb == b.se_rb