Pure-MoonBit port of doubleml-for-py: Double / Debiased Machine Learning for partial linear, IRM, IV, DID, SSM, APO(S), PQ, QTE, LPQ, CVaR, RDD, BLP, and policy-tree models
Dependencies
| Item | Value |
|---|---|
| Repository | https://github.com/riantr/moonbit_doubleML |
| Author | riantr |
| License | MIT (port of upstream doubleml-for-py, BSD-3-Clause) |
| moon.mod version | 0.163.0 |
| Source layout | flat, moonbit_doubleML/ (the library) |
| .mbt file count | 220 .mbt files (65 production + 129 blackbox test + 26 whitebox test) |
| Estimators | 22 DoubleML* estimator structs (PLR / IRM / PLIV / IIVM / DID family / SSM / APO(S) / PQ / QTE / LPQ / LPLR / CVAR / RDD / BLP / PLPR / PolicyTree) |
| Backends | native, wasm, wasm-gc, js -- all pass moon test --deny-warn |
| Tests (native / wasm / js) | 1115 / 1115 / 1115 |
| Tests (wasm-gc) | 1121 / 1121 (lib + 6 doc tutorials) |
| Sandwich-variance coverage | 15 / 22 expose the scalar sandwich_se / cluster_sandwich_se / bias_corrected_coef; 4 more (DoubleMLAPOS, DoubleMLDIDCS, DoubleMLDIDMulti, DoubleMLQTE) expose per-cell variants sandwich_se_at / ..._at_idx; 3 expose none (DoubleMLBLP, DoubleMLPolicyTree, DoubleMLRDD). Introduced v0.89.0 |
| Python cross-checks | 24 / 24 PASS (validate_*_with_python.py); 25 of the 25 bit-exact items and 6 / 6 RNG keys are IEEE-754 identical to CPython |
| HTTP service | examples/api_server/ -- hand-rolled on moonbitlang/async, no third-party framework |
| ml_m | coef | rv |
|---|---|---|
| linear_regression | -225.86 | 0.043 |
| logistic_regression | 1.44 | 0.842 |
ml_m = LearnerDispatch::logistic_regression(LogisticRegression::new())| Estimator | OLS default | Evidence |
|---|---|---|
| DoubleMLIRM | hazardous | coef -225.86 vs 1.44 |
| DoubleMLAPO | hazardous | bias 5.246 vs 0.531; SE 86.28 vs 0.270 |
| DoubleMLAPOS | hazardous | per-level bias to 436.11 |
| DoubleMLDIDBinary | not demonstrated | 4 designs, ~14 configs, ~1e-4 |
| DoubleMLSSM | hazardous -- fixed in v0.138.0 | coef 242.116 vs 1.177 |
| DoubleMLCVAR | not demonstrated | 2.300 vs 2.373 |
| DoubleMLQTE | hazardous -- fixed in v0.139.0 | frozen at -1.583 for tau 0.50-0.90 |
| DoubleMLIIVM | not demonstrated | 0.987 vs 1.031 (16x smaller SE) |
| DoubleMLPLR | not demonstrated | 1.015 vs 1.103 -- OLS better |
| DoubleMLLPLR | not demonstrated -- root-caused in v0.140.0-v0.142.0 | not an ml_m hazard; two failure modes; see below |
| DoubleMLPQ | hazardous -- fixed in v0.139.0 | ignored its quantile entirely |
DoubleMLPQ IGNORED its quantile argument on the default path -- fixed in v0.139.0. An ordinary caller who never mentioned ml_m got the identical coefficient at every tau:
tau old default (OLS ml_m) shipped now 0.10 -0.38571498836568785 -0.38571498836568785 0.25 -0.38571498836568785 0.5268291897597495 0.50 -0.38571498836568785 0.9570929867084059 0.75 -0.38571498836568785 1.2753280162498020 0.90 -0.38571498836568785 1.2753280162498020 Cause: score[i] = treated[i] / m[i] * 1{y[i] <= theta} - q (quantile.mbt:127) with m clipped to [propensity_clip, 1 - propensity_clip] (quantile.mbt:175-177). An unbounded regressor propensity predicts outside [0,1], so treated rows saturate at m = 1e-6 and carry weight 1e6; the IPW distribution is then dominated by whichever rows saturated and its tau-quantile is the same for every q in (0,1). DoubleMLQTE shares solve_pq, and did freeze too -- at -1.5829876410256796 for tau 0.50-0.90.Every PQ test that existed passed quantile=0.5, so nothing could catch this. expand_v139_test.mbt now sweeps five taus on the shipped path across two fixtures, counting changed steps with exact inequality -- the defect is an exact tie, and a tolerance would hide it.The residual tie at tau 0.75 / 0.90 is a PLATEAU COLLISION, not a freeze (root-caused in v0.143.0). The score is a step function of theta: iy = if y[i] <= theta { 1.0 } else { 0.0 } (quantile.mbt:126), so mean(psi) is constant between observed outcomes. Two taus whose crossings land on the same step get the same root. Measured in a 0.05-wide window around the tied value: 25 distinct plateaus, longest flat run 62 of 400 grid steps.Two explanations were tested and both refuted:
- A gap in the weighted distribution -- no. At seed 17 the treated outcomes are 1033 of 1033 distinct, 0.75-quantile 3.2723 vs 0.90-quantile 3.8917, about 155 observations apart.
- QTE ties where PQ does not -- no. Across four seeds, both tie at seed 17 (1.275328016249802 each) and neither ties at seeds 23, 29 or 41. The v0.139.0 contrast was reading one fixture as an estimator difference.
And it resolves as the sample grows, which is what a plateau collision must do:
n coef @ 0.75 coef @ 0.9 tied 1000 1.4491898789269633 1.4491898789269633 yes 2000 1.275328016249802 1.275328016249802 yes 4000 0.9570929867084059 1.3732758716025382 no 8000 1.4356301994178935 1.7559108756022739 no More distinct outcomes means more steps, so two taus are less likely to share one. A solver defect would not behave this way. Nothing is patched: removing it would mean smoothing the indicator, which changes results in the healthy region to suppress correct behaviour.This is NOT the v0.137.0 defect, which froze the coefficient at every tau (0 of 4 distinct steps) and is fixed.
DoubleMLPQ reported the IPW root while publishing the orthogonal score -- fixed in v0.159.0. A different defect from the two above, and the only one that made the estimator silently self-inconsistent.solve_pq bisected the IPW score and returned that root as the coefficient, then built psi from the g cross-fit. Those are two different functionals of the data, so the number reported did not satisfy the estimating equation the estimator itself publishes. Measured on a continuous-confounding DGP: the reported coefficient's own orthogonal score averaged -0.224968.Upstream (doubleml 0.11.3) is explicit that the IPW root is PRELIMINARY. pq.py:378 solves it only to build g's target (pq.py:390 fits ml_g(x_treat, y_treat <= ipw_est)); pq.py:423 then sets _coef_start_val = mean(ipw_vec), and double_ml_score_mixins.py:149 runs Newton from there against the orthogonal score pq.py:268. The reported coefficient is that root. DoubleMLQTE shares solve_pq and inherits it.Both halves matter, and the second half is the one that bites. The score contains 1{y <= theta} -- the same step function the plateau collision above is built from -- so mean(score) is piecewise constant and a bisection converges onto a jump, not a zero; the midpoint of a jump is not a root. That is what upstream's Newton avoids. The deriv likewise must not be a central difference of a step: it is identically zero wherever no observation lies within +/- h, fit scales psi by 1/deriv unguarded (quantile.mbt:2427-2429), and 0/0 = NaN reaches ses. Measured ses = [NaN, 0.3019, 0.1758, 0.1994, 1.6937]. Upstream's _compute_score_deriv is a Silverman-bandwidth weighted Gaussian KDE, which is positive everywhere, so this is structurally unreachable rather than merely unlikely.
before after reported root IPW orthogonal (Newton from IPW root) mean(psi) at reported theta -0.224968 -0.0032 deriv central difference, can be 0 Silverman KDE, strictly positive On a DGP whose Q_0.6(Y(1)) = 2.2 holds by construction (confounding enters Y(0) only), theta = 2.230920809568266 with se = 0.044753173108603286, so the error 0.0309 is inside one SE. Newton moves theta only 0.0021 from the IPW root, which is the point: upstream treats that root as a start value, not the answer.
DoubleMLLPLR can return a degenerate standard error -- and it is NOT an ml_m default problem. Root-caused in v0.140.0. On the reference Liu-Zhang-Zhou 2021 design (make_lplr_LZZ2020, dgp_lplr.mbt:37) with a large true coefficient, both learners collapse to coef ~ -10 with SE ~ 1e-6 and a five-decimal confidence interval that EXCLUDES the truth:
true alpha ml_m = linear_regression ml_m = logistic_regression 0.50 0.6636 / SE 0.1954 0.6293 / SE 0.1684 0.75 1.3573 / SE 0.3861 1.2161 / SE 0.2993 1.00 -10.1863 / 1.39e-6 2.9981 / SE 1.8142 1.25 -10.0567 / 1.62e-6 -10.4075 / 1.03e-6 2.00 -9.0852 / 5.22e-6 -9.9926 / 1.93e-6 Across three seeds per cell, the regressor arm is degenerate at 3/3 for alpha >= 1.0 and the classifier arm at 3/3 for alpha >= 1.25. The classifier delays the failure by roughly one step and then fails identically, so passing it explicitly is not a workaround.Cause, read from the source and confirmed by measurement:
- prelim_beta_per_fold (lplr.mbt:71-95) returns beta built from w = ln(m / (1 - m)) -- the logit of the fitted propensity. As the effect grows, w grows and exp(r_hat) in the score (lplr.mbt:198-204) reaches O(1e3). Measured at alpha = 1.25: mean(psi) = 1097.07 and mean(psi_deriv) = -1097.07 -- the same order on BOTH arms, cancelling.
- The Newton convergence flag WAS computed and discarded -- let _ = converged, now only documented at lplr.mbt:885; v0.145.0 replaced the bare flag with a stop_reason string (lplr.mbt:887). With the step clamped to 0.25 (:853-854), 50 iterations cannot span an arbitrary distance, so the loop can stop away from the root, and fit computes a coefficient and a variance there with no error raised.
- se = sqrt(gamma / (j*j*n)) with j = mean(psi_deriv) (var_est.mbt:52). A huge j from step 1 makes the variance collapse.
So the default keeps its regressor -- changing it would be the v0.136.0 mistake again, trading a working default for a rationale that measurement does not support while leaving the cause untouched.v0.141.0 / v0.142.0: in the degenerate region the objective is ROOTLESS. Sweeping mean(psi) over theta in [-30, 30] and counting sign changes:
true alpha sign changes result 0.50 1 healthy -- a root exists 1.00 1 degenerate anyway (the solve misses the root it has) 1.25 0 degenerate -- no root anywhere in range v0.141.0 said "the objective has no root" without qualifying the region; that was too strong and is corrected here. The accurate statement is that it is rootless for alpha >= 1.25, which is exactly the degenerate region. At alpha = 1.0 a root does exist and the solver still fails to reach it -- so there are two distinct failure modes, not one.Every degenerate cell lands at -8.3 to -10.5 (mean -9.85) while the true effect ranges 0.75 to 2.0 -- an output that does not move when the estimand moves by 2.7x.Three candidate fixes were measured and all three were refuted:
- Bound t_pred -- degeneracy is uncorrelated with its range. A cell with t_pred in [-2.86, 4.16] is degenerate; a cell reaching +6.93 is healthy.
- Rewrite the score in bounded form -- psi_hat * exp(r_hat) == expit(r_hat) looks like free numerical hygiene and is not an identity here: measured max_abs_diff = 0.546 and relative difference up to 0.92 on healthy fixtures. Shipping it would silently change every LPLR result.
- Use a classifier for the t stage -- aborts at logistic.mbt:206, because that stage regresses on w_inner = ln(m/(1-m)), a continuous unbounded logit propensity, not a 0/1 label.
Where the objective is rootless (alpha >= 1.25), iteration budget, damping, start value and clipping are all inert; that case needs reparameterising the objective (a bounded link such as expit), which moves every LPLR result and is a model rewrite rather than a bug fix. Where a root exists but is missed (alpha = 1.0), solver quality genuinely is the issue.The diagnostic that settles which case you are in is cheap and general: count sign changes of the objective over a wide parameter range before touching the solver.Port-vs-upstream (v0.142.0), recorded because fidelity alone would mislead: the score formula is byte-identical to upstream (lplr.mbt:194-204 vs lplr.py:401-418, including exp(-coef*d)), but two things differ. Upstream keeps beta row-wise (lplr.py:315-346) while this port folds it to one scalar (lplr.mbt:866). That is a real divergence -- but adopting upstream's per-row shape makes the root worse (1 -> 0 sign changes at alpha = 1.0), so it is not the cause and must not be changed on fidelity grounds. Upstream also handles solve failure in double_ml_score_mixins.py:143-162 (brentq fallback on bounds; a warning or ValueError on non-convergence). This port omits that path entirely -- let _ = converged (lplr.mbt:885) -- which was the one clear, low-risk divergence available to fix, and which v0.145.0 took.
Two PSProcessor defects fixed in v0.159.0. Both were silent.Isotonic calibration collapsed on tied x. fit_isotonic ran PAVA over the raw sorted sequence without first pooling observations that share an x. An isotonic regression is a function of x, so ties must receive one shared fitted value; instead predict_isotonic resolved each tie to the LAST index of the run, and the [0.01, 0.99] clip turned the stray 1.0 into 0.99. The reported case -- predictions [0.5, 0.5], labels [0, 1] -- returned [0.99, 0.99] where sklearn 1.9.0's IsotonicRegression gives [0.5, 0.5] with y_thresholds_ = [0.5]. The fix adds pool_tied_x and passes the group size to pava as a weight, because the L2 objective is over observations and the group statistic is therefore a size-weighted mean. pava itself is unchanged. A tie-free control is unchanged, which is what shows the pooling is what moved the tied case.cv was never forwarded, so cv_calibration=true calibrated on the wrong split. adjust_ps declares a cv parameter and upstream passes cv=smpls at every call site (pq.py:410, did_binary.py:537, iivm.py:371, cvar.py:343), but in this port no estimator ever passed it. With cv_calibration=true the processor silently calibrated on its own default_5fold(n) -- five contiguous blocks -- instead of the estimator's n_folds partition. Now wired at irm.mbt:430 and iivm.mbt:525 via folds_to_cv_partition, which is sound because kfold's Fold guarantees disjoint test indices covering [0, n_obs) (kfold.mbt:199).Note this is narrower than "IRM/IIVM has no hookup", which was measured and found too strong: the isotonic config did already reach both fits. The five remaining adjust_ps call sites (apo.mbt:320, did.mbt:628, did_cross_section.mbt:615, did_cs_binary.mbt:374, cvar.mbt:736 and :1005) still do not forward cv.
max |att - 1.0| 14.41 <- the band FAILS on seeds 7, 77, 314
max |att - 1.0| / se 2.41 <- in SE units the error IS bounded
se range 0.589 .. 8.837var(y) 206.75 (SD 14.38)
corr(id, y) 0.975
r^2 X_0 / X_1 / X_2 vs y 1.0e-5 / 2.6e-3 / 9.2e-3
share of var(y) that is time-invariant 99.65 %
var(within unit) 1.11coef_at(0,1) 0.0 (the sentinel) -> 2.727522673844541
se_at(0,1) 0.0 -> 1.9428873717680928let model = DoubleMLLPLR::new(data, alpha=1.5).fit()
for w in model.warnings() { println(w) }| alpha | warns | se | coef |
|---|---|---|---|
| 0.25 / 0.50 / 0.75 | no | 0.285 / 0.465 / 1.507 | 0.703 / 1.204 / 2.351 |
| 1.00 | yes | 1.7e-06 | -10.367 |
| 1.25 / 1.50 / 2.00 | yes | 2.6e-06 / 3.8e-06 / 2.1e-05 | -10.001 / -9.702 / -8.370 |
DoubleMLBLP::confint_joint ignored level -- fixed in v0.159.0. It hardcoded let z = 1.959963984540054 (blp_policy.mbt:746) while level was accepted, range-checked at line 727, and then never used -- so level=0.50 and level=0.95 returned byte-identical intervals, which removes the entire point of the argument. Now level-aware, following the convention already used in this package (did_cross_section.mbt:485): the exact constant at level == 0.95, so the default stays bit-exact, and norm_ppf otherwise. The multiplier is sqrt(chi2(1, level)) = z_{(1+level)/2}, the same value statsmodels.OLS.conf_int(joint=True) applies. Because the interval is symmetric, the width is exactly 2 * z * se, so width ratios are data-independent and can be pinned without hard-coding a fitted number:
level width (n=400, contrast on (intercept, x)) 0.50 0.03691929183204845 0.95 0.10728184750635172 0.99 0.1409923584966286 width(0.95)/width(0.50) measured 2.9058479234757097 against 1.959963984540054 / norm_ppf(0.75) = 2.905847923475711, agreeing to 1.3e-15. Note the numerator is the hardcoded 95% constant, not norm_ppf(0.975): this package's norm_ppf returns 1.9599627869916687 there (accurate to ~6e-7 relative), which is exactly why the special case exists. Note the convention: the per-observation score is psi[i] = psi_at(coef, psi_a, psi_b)[i] = coef * psi_a[i] + psi_b[i] -- the estimating function f(theta) = E[theta * psi_a + psi_b] that var_est inverts, whose root is theta_hat = -mean(psi_b) / mean(psi_a) -- and M_inv = [[1 / mean(psi_a)]] = 1 / (d f / d theta). Only M_inv[0, 0]^2 enters the variance. The HC2 / HC3 leverage is the constant mean-regression diagonal h_ii = 1 / n_obs (fixed in v0.88.0), so HC2 == HC1 and HC3 == HC1^2 / HC0 and the ordering is HC1 == HC2 > HC0 and HC3 > HC2 -- a monotonic widening. See expand_v090_test.mbt for the score-order evidence and for the still-open factor between se() and sandwich_se(HC0).
| learner | kind | sample_weight | note |
|---|---|---|---|
| LinearRegression | regression | WLS | OLS, the default |
| Ridge | regression | weighted | v0.120.0+, L2 penalty, unpenalised intercept |
| Lasso | regression | weighted | v0.120.0+, L1 penalty, coordinate descent |
| ElasticNet | regression | weighted | v0.120.0+, the convex L1/L2 blend |
| LogisticRegression | classification | weighted IRLS | binary, IRLS Newton-Raphson |
| RFClassifier | classification | weighted | v0.117.0+, Breiman 2001, Gini splits |
| GBClassifier | classification | weighted Newton leaf | v0.117.0+, Friedman 2001, binary log loss |
| RFLearner | regression | weighted splits/leaf | Breiman 2001, MSE splits |
| GBLearner | regression | weighted leaf | Friedman 2001, squared-error loss |
| ConstantLearner, NoopLearner | either | ignored | constant / zero predictors |
$ moon test --deny-warn
Total tests: 593, passed: 593, failed: 0.
$ moon run examples/main
=== MoonBit DML PLR (partialling out) ===
true theta_0 = 1
estimated theta = 0.9763281577675552
standard error = 0.08740490230937094
...
$ python validate_irm_with_python.py
======================================================================
PASS |mb - handrolled_nrep5| (theta) = 5.24e-02 < max(MODEL_TOL=0.1, 2.0*handrolled_n5_se) = 1.91e-01let data = @moonbit_doubleML.DoubleMLData::new(x, y, d) // x : Matrix, y / d : Array[Double]
let fitted = @moonbit_doubleML.DoubleMLPLR::new(data, n_folds=2, n_rep=1, seed=3141).fit()
let coef = fitted.coef() // Double
let se = fitted.se() // Double
let (lo, hi) = fitted.confint()| Driver | Model | DGP | True theta |
|---|---|---|---|
| examples/main | DoubleMLPLR + 11 others (IRM / PLIV / IIVM / DID / SSM / BLP / RDD / PQ / QTE / LPQ / PolicyTree) | Simple partially linear, n=500, p=5 | 1.0 |
| examples/datasets | DoubleMLPLR + DoubleMLIRM | Synthetic 401(k)-style, n=5000, 9 controls | 1.5 |
| examples/did_binary | DoubleMLDIDBinary | 2-period panel DID, 400 units | 1.0 |
| examples/did_cs | DoubleMLDIDCS | Staggered CS-DID, 4 cohorts x 4 periods | 1.0 |
| examples/did_cs_binary | DoubleMLDIDCSBinary | Staggered CS-DID with binary outcome | 1.0 |
| examples/did_multi | DoubleMLDIDMulti | Top-level multi-period DID + aggregation | 1.0 |
| examples/did_cross_section | DoubleMLDIDCrossSection | Sant'Anna-Zhao 2020 cross-section DID, 500 units | 1.0 |
| examples/plpr, examples/lplr | DoubleMLPLPR / DoubleMLLPLR | Static-panel PLR / partially logistic regression | 1.0 |
| examples/apo, examples/apos | DoubleMLAPO / DoubleMLAPOS | 3-level discrete treatment: the LEVEL E[Y(t)] vs the CONTRAST | 1.0 / 2.5 |
| examples/cvar | DoubleMLCVAR | CVaR | 1.0 |
| examples/ssm | DoubleMLSSM | Sharp synthetic treatment | 1.0 |
| examples/cluster | DoubleMLPLIV / DoubleMLPLR + cluster_* | Clustered data, cluster-robust SEs | 1.0 |
| examples/splitting | DoubleMLPLR + set_sample_splitting | External sample splitting | 1.0 |
| examples/pava | pava core | Isotonic regression, exact (no MC error) | 1.0 |
| examples/fuzz | wrappers | Random-property fuzz harness across 12 surfaces | n/a |
| examples/consumer_demo | library-user pattern | Minimal end-to-end PLR (no estimator-specific extras) | 1.0 |
| examples/api_server | DoubleMLPLR + DoubleMLIRM | HTTP service -- see below | n/a |
$ moon run examples/main
$ moon run examples/datasets
$ moon run examples/did_binary
$ moon run examples/did_cs
$ moon run examples/did_multi
$ moon run examples/did_cross_section$ moon build --target native examples/api_server
$ _build/native/debug/build/api_server/api_server.exe # listens on 127.0.0.1:4000
$ curl -s http://127.0.0.1:4000/healthz
{"status":"ok"}
$ curl -s -X POST http://127.0.0.1:4000/fit/plr \
-H 'Content-Type: application/json' \
-d '{"x":[[1.0,0.5],[2.0,1.5],[3.0,2.5],[4.0,3.5],[5.0,4.5],[6.0,5.5]],"y":[2.0,4.0,6.0,8.0,10.0,12.0],"d":[1.0,2.0,3.0,4.0,5.0,6.0],"n_folds":2,"seed":3141}'
{"estimator":"plr","coef":2,"se":0,"ci_lo":2,"ci_hi":2,"n_obs":6,"n_features":2}| Model | Score | Description |
|---|---|---|
| DoubleMLPLR | partialling-out | partially linear regression (upstream) |
| DoubleMLIRM | ATE | interactive regression model (upstream) |
| DoubleMLPLIV | partialling-out | partially linear IV regression (upstream) |
| DoubleMLIIVM | LATE | interactive IV model (upstream) |
| DoubleMLDID | observational | difference-in-differences (upstream) |
| DoubleMLDIDBinary | observational | DID with binary outcome (upstream) |
| DoubleMLDIDCS | observational | DID with staggered adoption (Callaway-Sant'Anna) (upstream) |
| DoubleMLDIDCSBinary | observational | CS-DID with binary outcome (upstream) |
| DoubleMLDIDMulti | observational | multi-period DID with group-time ATT aggregation (upstream) |
| DoubleMLDIDCrossSection | observational | Sant'Anna-Zhao 2020 cross-section DID (no upstream counterpart) -- port-only |
| DoubleMLSSM | MAR | sample selection (missing-at-random) (upstream) |
| DoubleMLAPO | policy score | average prescriptive effect (upstream) |
| DoubleMLAPOS | policy score | APO with stratified treatment (upstream) |
| DoubleMLPQ | quantile | potential quantile (upstream) |
| DoubleMLQTE | quantile | quantile treatment effect (upstream) |
| DoubleMLLPQ | local polynomial | local potential quantile (upstream) |
| DoubleMLLPLR | partialling-out | partially logistic regression, Liu-Zhang-Zhou 2021 (upstream) |
| DoubleMLCVAR | CVaR | conditional value-at-risk (upstream) |
| DoubleMLRDD | observational | regression discontinuity (upstream, renamed) -- upstream RDFlex |
| DoubleMLBLP | IV | best linear predictor of treatment effect (upstream) |
| DoubleMLPLPR | partialling-out | partially linear panel regression, Clarke-Polselli 2025 (upstream) |
| DoubleMLPolicyTree | policy | policy tree (upstream), + v0.98.0 honest split (honest = true) with leaf_se / policy_value_se |
moonbit_doubleML/ <- the library (moon.mod v0.81.0, 144 .mbt files)
moonbit_doubleML.mbt <- main re-export file (the import surface)
... <- one file per estimator + DGPs + score / nuisance kernels
vectorized.mbt <- v0.81.0+ vectorised cross-fit helpers
doc/ <- wasm-gc-targeted numbered tutorials
001_introduction/
...
006_python_check/
examples/ <- 19 driver binaries (all listed in moon.work)
main/ <- 12 estimators, one perfect-DGP run each
datasets/ <- 401(k)-style ATE / ATT recovery
did_binary/ <- 2-period panel DID
did_cs/ <- staggered CS-DID
did_cs_binary/ <- staggered CS-DID (binary outcome)
did_multi/ <- multi-period DID aggregation
did_cross_section/ <- Sant'Anna-Zhao 2020
plpr/, lplr/, apos/, <- single-estimator focused demos
cvar/, fuzz/
consumer_demo/ <- library-user pattern (`example/consumer_demo`)
api_server/ <- HTTP service built on `moonbitlang/async`
moon.work <- 15-member workspace aggregator
AGENTS.md <- this file (per moonbit agent convention)
skills/moonbit_doubleML.md <- agent skill: API surface + anti-patterns| declared | imported by the library? | note |
|---|---|---|
| moonbitlang/async@0.20.3 | no | used only by examples/api_server/main.mbt, which declares it in its own moon.mod |
| moonbit-community/sqlite3@0.2.3 | no | not moonbitlang/* -- added in v0.85.0 for DoubleMLPolicyTree memoisation, which now uses an in-memory FitCache |
| Gate | Where | Scope |
|---|---|---|
| Shipped README == edited README | check.yml readme-sync | moonbit_doubleML/README.mbt.md byte-identity |
| moon build + moon test --deny-warn | check.yml x4 matrix | native, wasm-gc, wasm, js |
| moon fmt --check | check.yml "Format check" x4, publish.yml | see caveat below |
| moon check --deny-warn x4 | publish.yml | warnings are errors |
| Generated-interface drift | publish.yml | public surface |
| typos (pinned 1.19.0) | publish.yml | prose |
| 24 validate_*_with_python.py | check.yml | estimator parity; see the two-tier note below |
| 21-activity QA LOOP (fast scope) | check.yml qa-loop, publish.yml | 22 registered checks, 12 of them fast |
| operation | bit-exact | why |
|---|---|---|
| + - * / | yes | IEEE-754 requires correct rounding, so same operands in the same order MUST agree |
| sqrt | yes | same guarantee -- 6 / 6 measured |
| cos, sin | observed 6 / 6 | agreement today, not guaranteed: neither is correctly rounded |
| ln | 5 / 6 | ln(3.0) differs by exactly 1 ULP: MoonBit 1.0986122886681096, CPython 1.0986122886681098 |
python .github/scripts/ci_local.py list # which jobs exist, and their steps
python .github/scripts/ci_local.py provision # build the WSL guest toolchain
python .github/scripts/ci_local.py run --jobs "qa-loop"
python .github/scripts/ci_local.py run --all --release-tag v0.163.0
python .github/scripts/ci_local.py selftest # prove the simulator can fail| Verdict | Means |
|---|---|
| PASS | the step ran and succeeded |
| FAIL | the step ran and failed -- a real defect |
| SKIP | a uses: step; deliberately not simulated, never a silent PASS |
| BLOCKED | this machine lacks a tool the step needs; nothing was proven |
pub suberror BootstrapMethodError {
UnknownMethod(String)
}pub suberror BracketSignError {
UpperSignFailed
}pub suberror CalibrationFittingError {
IncompleteCVPartition
}pub suberror ClusterDataError {
MissingUnit(Int)
}pub suberror DIDDataError {
NonBinaryTreatment(Int)
}pub suberror EmptyArrayErrorpub suberror InvalidCalibrationError {
UnknownMethod(String)
}pub suberror PSConfigError {
InconsistentCVCalibration
}pub suberror VarEstClusterError {
JTooSmall(Double, Double, Int)
}type DidCsDatatype DidMultiDatapub struct DoubleMLAPO {
data : DoubleMLData
treatment_level : Double
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ps_processor_config : PSProcessorConfig
normalize_ipw : Bool
ml_g : LearnerDispatch
ml_m : LearnerDispatch
g_hat : Array[Double]
m_hat : Array[Double]
coef : Double
se : Double
fitted : Bool
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLAPO::bootstrap(self : DoubleMLAPO, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLAPOfn DoubleMLAPO::fit(self : DoubleMLAPO, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLAPOfn DoubleMLAPO::new(data : DoubleMLData, treatment_level? : Double, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, normalize_ipw? : Bool, ps_processor_config? : PSProcessorConfig?, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLAPOfn DoubleMLAPO::sensitivity_analysis(self : DoubleMLAPO, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLAPO::sensitivity_analysis_cluster(self : DoubleMLAPO, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLAPO::sensitivity_nu2_elements(self : DoubleMLAPO) -> (Array[Double], Array[Double]) raise m_alpha = 1 / m_hat_adj
rr = treated / m_hat_adj
nu2 = mean( 2*m_alpha - rr^2 )fn DoubleMLAPO::tune(self : DoubleMLAPO, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLAPOpub struct DoubleMLAPOS {
data : DoubleMLData
treatment_levels : Array[Double]
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ps_processor_config : PSProcessorConfig
normalize_ipw : Bool
ml_g : LearnerDispatch
ml_m : LearnerDispatch
coefs : Array[Double]
ses : Array[Double]
fitted : Bool
boot_t_stat : Array[Array[Double]]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
psi_b_flat : Array[Double]
} derive(Debug)fn DoubleMLAPOS::bootstrap(self : DoubleMLAPOS, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLAPOSfn DoubleMLAPOS::causal_contrast(self : DoubleMLAPOS, reference_levels : Array[Double]) -> Array[Array[Double]]fn DoubleMLAPOS::cluster_sandwich_se_at(self : DoubleMLAPOS, treatment_index : Int, cluster_ids : Array[Int]) -> Doublefn DoubleMLAPOS::confint(self : DoubleMLAPOS, level? : Double, joint? : Bool) -> Array[(Double, Double)]fn DoubleMLAPOS::fit(self : DoubleMLAPOS, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLAPOSfn DoubleMLAPOS::new(data : DoubleMLData, treatment_levels : Array[Double], n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, normalize_ipw? : Bool, ps_processor_config? : PSProcessorConfig?, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLAPOSfn DoubleMLAPOS::sandwich_se_at(self : DoubleMLAPOS, treatment_index : Int, kind : SandwichKind) -> Doublefn DoubleMLAPOS::sensitivity_analysis(self : DoubleMLAPOS, cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisefn DoubleMLAPOS::sensitivity_analysis_cluster(self : DoubleMLAPOS, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisefn DoubleMLAPOS::set_sample_splitting(self : DoubleMLAPOS, smpls : Array[Array[Fold]]) -> DoubleMLAPOSfn DoubleMLAPOS::tune(self : DoubleMLAPOS, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLAPOSpub struct DoubleMLBLP {
basis : Matrix
orth_signal : Array[Double]
cov_type : String
ml_g : LearnerDispatch
coef : Array[Double]
se : Array[Double]
fitted : Bool
n_obs : Int
rss : Double
var_y : Double
residuals : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLBLP::bootstrap(self : DoubleMLBLP, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLBLP S_cj = sum_{i in c} s_ij
var_j = sum_c S_cj^2 * n_c / (n_c - 1)fn DoubleMLBLP::confint_joint(self : DoubleMLBLP, contrast : Matrix, level? : Double) -> Array[(Double, Double)]fn DoubleMLBLP::new(basis : Matrix, orth_signal : Array[Double], cov_type? : String, ml_g? : LearnerDispatch) -> DoubleMLBLPfn DoubleMLBLP::sensitivity_analysis(self : DoubleMLBLP, cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisefn DoubleMLBLP::sensitivity_analysis_cluster(self : DoubleMLBLP, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raise Y = expit(D * theta_0 + r_0(X)), Y in {0, 1}pub struct DoubleMLCVAR {
data : DoubleMLData
treatment : Double
quantile : Double
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ps_processor_config : PSProcessorConfig
normalize_ipw : Bool
ml_g : LearnerDispatch
ml_m : LearnerDispatch
g_hat : Array[Double]
m_hat : Array[Double]
coef : Double
se : Double
fitted : Bool
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLCVAR::bootstrap(self : DoubleMLCVAR, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLCVARfn DoubleMLCVAR::fit(self : DoubleMLCVAR, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLCVARfn DoubleMLCVAR::new(data : DoubleMLData, treatment? : Double, quantile? : Double, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, ps_processor_config? : PSProcessorConfig?, normalize_ipw? : Bool, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLCVARfn DoubleMLCVAR::sensitivity_analysis(self : DoubleMLCVAR, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLCVAR::sensitivity_analysis_cluster(self : DoubleMLCVAR, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLCVAR::set_sample_splitting(self : DoubleMLCVAR, smpls : Array[Array[Fold]]) -> DoubleMLCVARfn DoubleMLCVAR::tune(self : DoubleMLCVAR, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLCVARpub struct DoubleMLDID {
data : DoubleMLDIDData
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ps_processor_config : PSProcessorConfig
score : String
in_sample_normalization : Bool
ml_g : LearnerDispatch
ml_m : LearnerDispatch
strata : Array[Int]
g0_hat : Array[Double]
g1_hat : Array[Double]
m_hat : Array[Double]
coef : Double
se : Double
psi_a : Array[Double]
psi_b : Array[Double]
fitted : Bool
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug) Y_post = g_0(0, X) + D * theta + U_post, E[U | D, X] = 0
Y_pre = g_0(0, X) + U_pre, E[U_pre | X] = 0 dY = Y_post - Y_pre = g_1(1, X) - g_0(0, X) + D * theta + (U_post - U_pre) g0(X) = E[Y | D = 0, X] (trained on D = 0)
g1(X) = E[Y | D = 1, X] (trained on D = 1)
m(X) = P(D = 1 | X) (trained on all obs; observational
only, clipped to [eps, 1 - eps]) resid_d0 = Y - g0
p_hat = mean(D)
weight_psi_a = D / p_hat
weight_resid = (D - m) / (p_hat * (1 - m))
psi_b = (D - m)/(p_hat (1 - m)) * (Y - g0) [the g1 term cancels in ATT]
psi_a = -D / p_hat
psi(theta) = theta * psi_a + psi_b theta_hat = -mean(psi_b) / mean(psi_a)
J = mean(psi_a)
gamma = mean(psi(theta_hat)^2)
sigma2 = gamma / (J^2 * n)
se = sqrt(sigma2).fn DoubleMLDID::bootstrap(self : DoubleMLDID, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLDIDfn DoubleMLDID::fit(self : DoubleMLDID, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDfn DoubleMLDID::new(data : DoubleMLDIDData, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, ps_processor? : PSProcessor, ps_processor_config? : PSProcessorConfig?, score? : String, in_sample_normalization? : Bool, strata? : Array[Int], ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDfn DoubleMLDID::sensitivity_analysis(self : DoubleMLDID, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLDID::sensitivity_analysis_cluster(self : DoubleMLDID, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLDID::sensitivity_nu2_elements(self : DoubleMLDID) -> (Array[Double], Array[Double]) raisep_hat = mean(d)
propensity_weight_d0 = m_hat / (1 - m_hat)
score == "observational", in_sample_normalization:
weight_d0 = (1 - d) * propensity_weight_d0
m_alpha = (d / p_hat) * (1/p_hat + propensity_weight_d0 / mean(weight_d0))
rr = d / p_hat - weight_d0 / mean(weight_d0)
score == "observational", not in_sample_normalization:
m_alpha = (d / p_hat^2) * (1 + propensity_weight_d0)
rr = d / p_hat - ((1 - d) / p_hat) * propensity_weight_d0
score == "experimental": (identical with and without the flag)
m_alpha = 1/p_hat + 1/(1 - p_hat) -- a CONSTANT array
rr = d / p_hat - (1 - d)/(1 - p_hat)
nu2_score_element = 2 * m_alpha - rr^2
nu2 = mean(nu2_score_element)fn DoubleMLDID::tune(self : DoubleMLDID, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLDIDpub struct DoubleMLDIDBinary {
data : DoubleMLDIDBinaryData
g_value : Int
t_value_pre : Int
t_value_eval : Int
control_group : String
anticipation_periods : Int
n_folds : Int
n_rep : Int
seed : Int
propensity_clip : Double
ps_processor_config : PSProcessorConfig
score : String
in_sample_normalization : Bool
ml_g : LearnerDispatch
ml_m : LearnerDispatch
eval_idx : Array[Int]
inner : DoubleMLDID
fitted : Bool
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
} derive(Debug)fn DoubleMLDIDBinary::bootstrap(self : DoubleMLDIDBinary, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLDIDBinaryfn DoubleMLDIDBinary::cluster_sandwich_se(self : DoubleMLDIDBinary, cluster_ids : Array[Int]) -> Doublefn DoubleMLDIDBinary::confint(self : DoubleMLDIDBinary, joint? : Bool, level? : Double) -> (Double, Double)fn DoubleMLDIDBinary::fit(self : DoubleMLDIDBinary, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDBinaryfn DoubleMLDIDBinary::new(data : DoubleMLDIDBinaryData, g_value : Int, t_value_pre : Int, t_value_eval : Int, control_group? : String, anticipation_periods? : Int, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, ps_processor? : PSProcessor, ps_processor_config? : PSProcessorConfig?, score? : String, in_sample_normalization? : Bool, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDBinaryfn DoubleMLDIDBinary::sensitivity_analysis(self : DoubleMLDIDBinary, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLDIDBinary::sensitivity_analysis_cluster(self : DoubleMLDIDBinary, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLDIDBinary::tune(self : DoubleMLDIDBinary, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLDIDBinaryfn DoubleMLDIDBinaryData::new(x : Matrix, y : Array[Double], d : Array[Double], t : Array[Int], g : Array[Int], id : Array[Int]) -> DoubleMLDIDBinaryDatapub struct DoubleMLDIDCS {
data : DoubleMLDIDCSData
control_group : String
anticipation_periods : Int
n_folds : Int
n_rep : Int
seed : Int
propensity_clip : Double
ps_processor_config : PSProcessorConfig
in_sample_normalization : Bool
coef_matrix : Array[Double]
se_matrix : Array[Double]
psi_matrix : Array[Double]
psi_a_matrix : Array[Double]
residuals_matrix : Array[Double]
cell_psi_a : Array[Array[Double]]
cell_psi_b : Array[Array[Double]]
m_hat_long : Array[Double]
panel_folds : Array[Fold]
g_hat_d0t0_long : Array[Double]
g_hat_d0t1_long : Array[Double]
g_hat_d1t0_long : Array[Double]
g_hat_d1t1_long : Array[Double]
sigma2_panel : Double
nu2_panel : Double
psi_sigma2_long : Array[Double]
psi_nu2_long : Array[Double]
riesz_rep_long : Array[Double]
n_groups : Int
n_periods : Int
fitted : Bool
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
ml_g : LearnerDispatch
ml_m : LearnerDispatch
} derive(Debug)fn DoubleMLDIDCS::bias_corrected_coef_at(self : DoubleMLDIDCS, group_idx : Int, period_idx : Int) -> Doublefn DoubleMLDIDCS::bootstrap(self : DoubleMLDIDCS, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLDIDCSfn DoubleMLDIDCS::cluster_sandwich_se_at(self : DoubleMLDIDCS, group_idx : Int, period_idx : Int, cluster_ids : Array[Int]) -> Doublefn DoubleMLDIDCS::confint(self : DoubleMLDIDCS, joint? : Bool, level? : Double) -> Array[(Double, Double)]fn DoubleMLDIDCS::fit(self : DoubleMLDIDCS, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDCSfn DoubleMLDIDCS::new(data : DoubleMLDIDCSData, control_group? : String, anticipation_periods? : Int, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, ps_processor? : PSProcessor, ps_processor_config? : PSProcessorConfig?, in_sample_normalization? : Bool, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDCSfn DoubleMLDIDCS::sandwich_se_at(self : DoubleMLDIDCS, group_idx : Int, period_idx : Int, kind : SandwichKind) -> Double sandwich_se_at(g, t, HC0) == se_at(g, t)fn DoubleMLDIDCS::sensitivity_analysis(self : DoubleMLDIDCS, cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisefn DoubleMLDIDCS::sensitivity_analysis_cluster(self : DoubleMLDIDCS, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raise nu2 = mean(2*m_alpha - rr^2) sigma2 = mean((y - g_hat)^2)fn DoubleMLDIDCS::tune(self : DoubleMLDIDCS, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLDIDCSpub struct DoubleMLDIDCSBinary {
data : DoubleMLDIDCSData
g_value : Int
t_value_pre : Int
t_value_eval : Int
control_group : String
anticipation_periods : Int
n_folds : Int
n_rep : Int
seed : Int
propensity_clip : Double
ps_processor_config : PSProcessorConfig
score : String
in_sample_normalization : Bool
coef : Double
se : Double
psi_a : Array[Double]
psi_b : Array[Double]
g_d0_t0 : Array[Double]
g_d0_t1 : Array[Double]
g_d1_t0 : Array[Double]
g_d1_t1 : Array[Double]
m_hat : Array[Double]
subset_y : Array[Double]
subset_g : Array[Double]
subset_t : Array[Double]
n_obs_subset : Int
n_g_subset : Int
n_c_subset : Int
fitted : Bool
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
ml_g : LearnerDispatch
ml_m : LearnerDispatch
} derive(Debug)fn DoubleMLDIDCSBinary::bootstrap(self : DoubleMLDIDCSBinary, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLDIDCSBinaryfn DoubleMLDIDCSBinary::cluster_sandwich_se(self : DoubleMLDIDCSBinary, cluster_ids : Array[Int]) -> Doublefn DoubleMLDIDCSBinary::confint(self : DoubleMLDIDCSBinary, joint? : Bool, level? : Double) -> (Double, Double)fn DoubleMLDIDCSBinary::fit(self : DoubleMLDIDCSBinary, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDCSBinaryfn DoubleMLDIDCSBinary::new(data : DoubleMLDIDCSData, g_value : Int, t_value_pre : Int, t_value_eval : Int, control_group? : String, anticipation_periods? : Int, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, ps_processor? : PSProcessor, ps_processor_config? : PSProcessorConfig?, score? : String, in_sample_normalization? : Bool, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDCSBinaryfn DoubleMLDIDCSBinary::sensitivity_analysis(self : DoubleMLDIDCSBinary, cf_y? : Double, cf_d? : Double) -> SensitivityResult raise let d = self.data.d
residuals[i] = self.data.y[i] - g00[i] - (g10[i] - g00[i]) * d[i]fn DoubleMLDIDCSBinary::sensitivity_analysis_cluster(self : DoubleMLDIDCSBinary, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLDIDCSBinary::tune(self : DoubleMLDIDCSBinary, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLDIDCSBinaryfn DoubleMLDIDCSData::new(x : Matrix, y : Array[Double], d : Array[Double], t : Array[Int], id : Array[Int], g : Array[Int]) -> DoubleMLDIDCSDatapub struct DoubleMLDIDCrossSection {
data : DoubleMLDIDCrossSectionData
n_folds : Int
n_rep : Int
seed : Int
score : String
in_sample_normalization : Bool
propensity_clip : Double
ps_processor_config : PSProcessorConfig
ml_g : LearnerDispatch
ml_m : LearnerDispatch
coef : Double
se : Double
psi_a : Array[Double]
psi_b : Array[Double]
g_d0_t0 : Array[Double]
g_d0_t1 : Array[Double]
g_d1_t0 : Array[Double]
g_d1_t1 : Array[Double]
m_hat : Array[Double]
fitted : Bool
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
} derive(Debug)fn DoubleMLDIDCrossSection::bootstrap(self : DoubleMLDIDCrossSection, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLDIDCrossSectionfn DoubleMLDIDCrossSection::cluster_sandwich_se(self : DoubleMLDIDCrossSection, cluster_ids : Array[Int]) -> Doublefn DoubleMLDIDCrossSection::confint(self : DoubleMLDIDCrossSection, joint? : Bool, level? : Double) -> (Double, Double)fn DoubleMLDIDCrossSection::disable_memoize(self : DoubleMLDIDCrossSection) -> DoubleMLDIDCrossSectionfn DoubleMLDIDCrossSection::enable_memoize(self : DoubleMLDIDCrossSection) -> DoubleMLDIDCrossSectionfn DoubleMLDIDCrossSection::fit(self : DoubleMLDIDCrossSection, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDCrossSectionfn DoubleMLDIDCrossSection::new(data : DoubleMLDIDCrossSectionData, n_folds? : Int, n_rep? : Int, seed? : Int, score? : String, in_sample_normalization? : Bool, propensity_clip? : Double, ps_processor? : PSProcessor, ps_processor_config? : PSProcessorConfig?, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDCrossSectionfn DoubleMLDIDCrossSection::sandwich_se(self : DoubleMLDIDCrossSection, kind : SandwichKind) -> Doublefn DoubleMLDIDCrossSection::sensitivity_analysis(self : DoubleMLDIDCrossSection, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLDIDCrossSection::sensitivity_analysis_cluster(self : DoubleMLDIDCrossSection, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLDIDCrossSection::tune(self : DoubleMLDIDCrossSection, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLDIDCrossSectionfn DoubleMLDIDCrossSectionData::new(x : Matrix, y : Array[Double], d : Array[Double], t : Array[Int], name? : String) -> DoubleMLDIDCrossSectionDatafn DoubleMLDIDData::new(x : Matrix, y : Array[Double], d : Array[Double]) -> DoubleMLDIDData raise DIDDataErrorpub struct DoubleMLDIDMulti {
data : DoubleMLDIDCSData
gt_combinations : Array[(Int, Int, Int)]
control_group : String
anticipation_periods : Int
n_folds : Int
n_rep : Int
seed : Int
ps_processor : PSProcessor
in_sample_normalization : Bool
ml_g : LearnerDispatch
ml_m : LearnerDispatch
group_sizes : Array[Int]
inner : DoubleMLDIDCS
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
fitted : Bool
memoize_enabled : Bool
} derive(Debug)fn DoubleMLDIDMulti::bootstrap(self : DoubleMLDIDMulti, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLDIDMultifn DoubleMLDIDMulti::cluster_sandwich_se_at_idx(self : DoubleMLDIDMulti, idx : Int, cluster_ids : Array[Int]) -> Doublefn DoubleMLDIDMulti::confint(self : DoubleMLDIDMulti, joint? : Bool, level? : Double) -> Array[(Double, Double)]fn DoubleMLDIDMulti::fit(self : DoubleMLDIDMulti, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDMultifn DoubleMLDIDMulti::new(data : DoubleMLDIDCSData, gt_combinations? : Array[(Int, Int, Int)], gt_combinations_keyword? : String, control_group? : String, anticipation_periods? : Int, n_folds? : Int, n_rep? : Int, seed? : Int, ps_processor? : PSProcessor, in_sample_normalization? : Bool, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLDIDMultifn DoubleMLDIDMulti::sandwich_se_at_idx(self : DoubleMLDIDMulti, idx : Int, kind : SandwichKind) -> Double sandwich_se_at_idx(idx, kind) ==
inner.sandwich_se_at(gi, pi, kind) sandwich_se_at_idx(idx, HC0) == se_at_idx(idx)fn DoubleMLDIDMulti::sensitivity_analysis(self : DoubleMLDIDMulti, cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisefn DoubleMLDIDMulti::sensitivity_analysis_cluster(self : DoubleMLDIDMulti, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisefn DoubleMLDIDMulti::tune(self : DoubleMLDIDMulti, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLDIDMultifn DoubleMLData::new(x : Matrix, y : Array[Double], d : Array[Double], cluster_vars? : Array[Int], z? : Array[Double]) -> DoubleMLDatapub struct DoubleMLIIVM {
data : DoubleMLIIVMData
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ps_processor_config : PSProcessorConfig
ml_g : LearnerDispatch
ml_m : LearnerDispatch
ml_r : LearnerDispatch
g0_hat : Array[Double]
g1_hat : Array[Double]
m_hat : Array[Double]
r0_hat : Array[Double]
r1_hat : Array[Double]
coef : Double
se : Double
fitted : Bool
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug) Y = theta * D + g_0(D, X) + U, E[U | D, X] = 0
D = m_0(X, Z) + V, E[V | X, Z] = 0 g0(X) = E[Y | Z = 0, X] (trained only on Z = 0)
g1(X) = E[Y | Z = 1, X] (trained only on Z = 1)
m(X) = E[Z | X] (trained on all obs, then
clipped to [eps, 1 - eps])
r0(X) = E[D | Z = 0, X] (trained only on Z = 0)
r1(X) = E[D | Z = 1, X] (trained only on Z = 1) u_hat0 = Y - g0, u_hat1 = Y - g1
w_hat0 = D - r0, w_hat1 = D - r1 psi_b = (g1 - g0) + Z u_hat1 / m - (1 - Z) u_hat0 / (1 - m)
psi_a = -(r1 - r0) - Z w_hat1 / m + (1 - Z) w_hat0 / (1 - m)
psi(theta) = theta * psi_a + psi_b theta_hat = -mean(psi_b) / mean(psi_a)
J = mean(psi_a)
gamma = mean(psi(theta_hat)^2)
sigma2 = gamma / (J^2 * n)
se = sqrt(sigma2).fn DoubleMLIIVM::bootstrap(self : DoubleMLIIVM, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLIIVMfn DoubleMLIIVM::fit(self : DoubleMLIIVM, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch, ml_r? : LearnerDispatch, max_attempts? : Int) -> DoubleMLIIVMfn DoubleMLIIVM::new(data : DoubleMLIIVMData, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, ps_processor_config? : PSProcessorConfig?, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch, ml_r? : LearnerDispatch) -> DoubleMLIIVMfn DoubleMLIIVM::sensitivity_analysis(self : DoubleMLIIVM, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLIIVM::sensitivity_analysis_cluster(self : DoubleMLIIVM, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLIIVM::set_sample_splitting(self : DoubleMLIIVM, smpls : Array[Array[Fold]]) -> DoubleMLIIVMfn DoubleMLIIVM::tune(self : DoubleMLIIVM, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLIIVMfn DoubleMLIIVMData::new(x : Matrix, y : Array[Double], d : Array[Double], z : Array[Double], cluster_vars? : Array[Int]) -> DoubleMLIIVMDatapub struct DoubleMLIRM {
data : DoubleMLData
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ps_processor_config : PSProcessorConfig
ml_g : LearnerDispatch
ml_m : LearnerDispatch
g0_hat : Array[Double]
g1_hat : Array[Double]
m_hat : Array[Double]
m_raw : Array[Double]
coef : Double
se : Double
fitted : Bool
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug) Y = g_0(D, X) + U, E[U | D, X] = 0
D = m_0(X) + V, E[V | X] = 0 g0(X) = E[Y | D=0, X]
g1(X) = E[Y | D=1, X]
m(X) = P(D=1 | X) (the propensity score)
u0 = Y - g0(X)
u1 = Y - g1(X)
psi_b = (g1 - g0) + (D u1 / m - (1 - D) u0 / (1 - m))
psi_a = -1
psi(theta) = theta * psi_a + psi_b theta_hat = -mean(psi_b) / mean(psi_a) = mean(psi_b) J = mean(psi_a) = -1
gamma = mean(psi(theta_hat)^2)
sigma2 = gamma / (J^2 * n)
se = sqrt(sigma2)fn DoubleMLIRM::bootstrap(self : DoubleMLIRM, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLIRMfn DoubleMLIRM::fit(self : DoubleMLIRM, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch, max_attempts? : Int) -> DoubleMLIRMfn DoubleMLIRM::new(data : DoubleMLData, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, ps_processor_config? : PSProcessorConfig?, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLIRMfn DoubleMLIRM::sensitivity_analysis(self : DoubleMLIRM, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLIRM::sensitivity_analysis_cluster(self : DoubleMLIRM, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLIRM::sensitivity_nu2_elements(self : DoubleMLIRM) -> (Array[Double], Array[Double]) raise m_alpha = 1/m_hat + 1/(1 - m_hat)
rr = d/m_hat - (1 - d)/(1 - m_hat)
nu2 = mean( 2*m_alpha - rr^2 )fn DoubleMLIRM::tune(self : DoubleMLIRM, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLIRMpub struct DoubleMLLPLR {
data : DoubleMLBinaryData
score : String
n_folds : Int
n_folds_inner : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
ml_g : LearnerDispatch
ml_m : LearnerDispatch
coef_ : Double
se_ : Double
r_hat : Array[Double]
m_hat : Array[Double]
a_hat : Array[Double]
fitted : Bool
fit_warnings : Array[String]
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLLPLR::bootstrap(self : DoubleMLLPLR, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLLPLRfn DoubleMLLPLR::fit(self : DoubleMLLPLR, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLLPLRfn DoubleMLLPLR::new(data : DoubleMLBinaryData, score? : String, n_folds? : Int, n_folds_inner? : Int, n_rep? : Int, seed? : Int, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLLPLR self.psi_a = sc.psi - theta_hat * sc.psi_deriv (OFFSET)
self.psi_b = sc.psi_deriv (DERIVATIVE) sandwich_se(HC0) == se()fn DoubleMLLPLR::sensitivity_analysis(self : DoubleMLLPLR, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLLPLR::sensitivity_analysis_cluster(self : DoubleMLLPLR, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLLPLR::set_sample_splitting(self : DoubleMLLPLR, smpls : Array[Array[Fold]]) -> DoubleMLLPLRfn DoubleMLLPLR::tune(self : DoubleMLLPLR, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLLPLRlet model = DoubleMLLPLR::new(data, alpha=1.5).fit()
for w in model.warnings() {
println(w) // the CALLER decides whether to print
}| stop_reason | meaning |
|---|---|
| degenerate_derivative | |d psi / d theta| < 1e-300 -- the Jacobian vanished, so the Newton direction is undefined |
| diverged | the iterate passed 1e6 and was abandoned |
| exhausted_step_budget | all 50 damped-Newton steps were used without meeting the 1e-9 step tolerance |
pub struct DoubleMLLPQ {
data : DoubleMLLPQData
treatment : Double
quantile : Double
n_folds : Int
seed : Int
propensity_clip : Double
ps_processor_config : PSProcessorConfig
coef : Double
se : Double
fitted : Bool
predictions_g0 : Array[Double]
predictions_g1 : Array[Double]
predictions_m : Array[Double]
psi : Array[Double]
psi_a : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLLPQ::bootstrap(self : DoubleMLLPQ, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLLPQfn DoubleMLLPQ::new(data : DoubleMLLPQData, treatment? : Double, quantile? : Double, n_folds? : Int, seed? : Int, propensity_clip? : Double, ps_processor_config? : PSProcessorConfig?) -> DoubleMLLPQfn DoubleMLLPQ::sensitivity_analysis(self : DoubleMLLPQ, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLLPQ::sensitivity_analysis_cluster(self : DoubleMLLPQ, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLLPQData::new(x : Matrix, y : Array[Double], d : Array[Double], z : Array[Double]) -> DoubleMLLPQDatapub struct DoubleMLPLIV {
data : DoubleMLPLIVData
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
learner : LearnerDispatch
l_hat : Array[Double]
r_hat : Array[Double]
m_hat : Array[Double]
coef : Double
se : Double
fitted : Bool
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug) Y = D * theta_0 + g_0(X) + zeta, E[zeta | D, X] = 0
D = m_0(X) + V, E[V | X] = 0
Z = ell_0(X) + xi, E[xi | X] = 0,
Cov(Z, V) != 0 (relevance) l_hat = E_hat[Y | X]
r_hat = E_hat[D | X]
m_hat = E_hat[Z | X]
u_hat = Y - l_hat
w_hat = D - r_hat
v_hat = Z - m_hat
psi_a = -w_hat * v_hat
psi_b = v_hat * u_hat
psi(theta) = theta * psi_a + psi_b theta_hat = -mean(psi_b) / mean(psi_a)
= mean(v_hat * u_hat) / mean(w_hat * v_hat)
J = mean(psi_a)
gamma = mean(psi(theta_hat)^2)
sigma2 = gamma / (J^2 * n)
se = sqrt(sigma2).fn DoubleMLPLIV::bootstrap(self : DoubleMLPLIV, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLPLIVfn DoubleMLPLIV::fit(self : DoubleMLPLIV, learner? : LearnerDispatch, max_attempts? : Int) -> DoubleMLPLIVfn DoubleMLPLIV::new(data : DoubleMLPLIVData, n_folds? : Int, n_rep? : Int, seed? : Int, learner? : LearnerDispatch) -> DoubleMLPLIVfn DoubleMLPLIV::sensitivity_analysis(self : DoubleMLPLIV, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLPLIV::sensitivity_analysis_cluster(self : DoubleMLPLIV, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLPLIV::set_sample_splitting(self : DoubleMLPLIV, smpls : Array[Array[Fold]]) -> DoubleMLPLIVfn DoubleMLPLIV::tune(self : DoubleMLPLIV, param_set~ : Array[LearnerDispatch], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLPLIVfn DoubleMLPLIVData::new(x : Matrix, y : Array[Double], d : Array[Double], z : Array[Double], cluster_vars? : Array[Int]) -> DoubleMLPLIVDatapub struct DoubleMLPLPR {
panel : DoubleMLPanelData
approach : String
score : String
n_folds : Int
n_rep : Int
seed : Int
learner : LearnerDispatch
coef_ : Double
se_ : Double
l_hat : Array[Double]
m_hat : Array[Double]
fitted : Bool
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLPLPR::bootstrap(self : DoubleMLPLPR, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLPLPRfn DoubleMLPLPR::fit(self : DoubleMLPLPR, learner? : LearnerDispatch, max_attempts? : Int) -> DoubleMLPLPRfn DoubleMLPLPR::new(panel : DoubleMLPanelData, approach? : String, score? : String, n_folds? : Int, n_rep? : Int, seed? : Int, learner? : LearnerDispatch) -> DoubleMLPLPRfn DoubleMLPLPR::sensitivity_analysis(self : DoubleMLPLPR, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLPLPR::sensitivity_analysis_cluster(self : DoubleMLPLPR, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLPLPR::tune(self : DoubleMLPLPR, param_set~ : Array[LearnerDispatch], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLPLPRpub struct DoubleMLPLR {
data : DoubleMLData
learner_l : LearnerDispatch
learner_m : LearnerDispatch
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
l_hat : Array[Double]
m_hat : Array[Double]
coef : Double
se : Double
fitted : Bool
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
tune_result : TuneResult?
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug) Y = D * theta_0 + g_0(X) + zeta, E[zeta | D, X] = 0
D = m_0(X) + V, E[V | X] = 0 psi_a(theta) = -(D - m_hat)^2,
psi_b(theta) = (D - m_hat) * (Y - l_hat),
psi(theta) = theta * psi_a + psi_b theta_hat = -mean(psi_b) / mean(psi_a)
= mean((D - m_hat)(Y - l_hat)) / mean((D - m_hat)^2). J = mean(psi_a) # expected derivative of psi w.r.t. theta
gamma = mean(psi(theta_hat)^2)
sigma2 = gamma / (J^2 * n)
se = sqrt(sigma2).fn DoubleMLPLR::bootstrap(self : DoubleMLPLR, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLPLRfn DoubleMLPLR::fit(self : DoubleMLPLR, learner_l? : LearnerDispatch, learner_m? : LearnerDispatch, max_attempts? : Int, score? : String, tune_result? : TuneResult?) -> DoubleMLPLRfn DoubleMLPLR::new(data : DoubleMLData, learner_l? : LearnerDispatch, learner_m? : LearnerDispatch, n_folds? : Int, n_rep? : Int, seed? : Int) -> DoubleMLPLRfn DoubleMLPLR::sensitivity_analysis(self : DoubleMLPLR, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLPLR::sensitivity_analysis_cluster(self : DoubleMLPLR, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLPLR::tune(self : DoubleMLPLR, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int, score? : String) -> DoubleMLPLRpub struct DoubleMLPQ {
data : DoubleMLData
treatment : Double
quantile : Double
n_folds : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ps_processor_config : PSProcessorConfig
ml_l : LearnerDispatch
ml_m : LearnerDispatch
coef : Double
se : Double
fitted : Bool
psi : Array[Double]
psi_a : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLPQ::bootstrap(self : DoubleMLPQ, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLPQfn DoubleMLPQ::fit(self : DoubleMLPQ, ml_l? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLPQfn DoubleMLPQ::new(data : DoubleMLData, treatment? : Double, quantile? : Double, n_folds? : Int, seed? : Int, propensity_clip? : Double, ps_processor_config? : PSProcessorConfig?, ml_l? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLPQfn DoubleMLPQ::sensitivity_analysis(self : DoubleMLPQ, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLPQ::sensitivity_analysis_cluster(self : DoubleMLPQ, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLPQ::tune(self : DoubleMLPQ, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLPQ Y_it = D_it * theta_0 + g_0(X_it) + alpha_i + zeta_itfn DoubleMLPanelData::new(x : Matrix, y : Array[Double], d : Array[Double], t : Array[Int], id : Array[Int]) -> DoubleMLPanelDatapub struct DoubleMLPolicyTree {
features : Matrix
orth_signal : Array[Double]
depth : Int
root : PolicyTreeNode
split_feature : Int
split_value : Double
left_treatment : Int
right_treatment : Int
leaf_assignment : Array[Int]
leaf_signal_mean : Array[Double]
leaf_count : Array[Int]
honest : Bool
split_seed : Int
min_leaf_n : Int
est_indices : Array[Int]
split_indices : Array[Int]
leaf_se : Array[Double]
fitted : Bool
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLPolicyTree::enable_split_seed(self : DoubleMLPolicyTree, seed : Int) -> DoubleMLPolicyTree f_k(theta) = (1/n_k) * sum_{i in k} (y_i - theta)
= (1/n_k) * sum_{i in k} y_i - theta
f_k'(theta) = -n_k * (1/n_k) = -1
f_k(theta_hat_k) = 0 at theta_hat_k = mean(y in k) IF_k(i) = -[(y_i - theta_hat_k) / n_k] / (-1)
= (y_i - theta_hat_k) / n_kfn DoubleMLPolicyTree::new(features : Matrix, orth_signal : Array[Double], depth? : Int, honest? : Bool, split_seed? : Int, min_leaf_n? : Int) -> DoubleMLPolicyTree policy_value = sum_k (n_k / n) * theta_k sum_k (n_k / n) * theta_k = (1/n) * sum_k sum_{i in k} y_i
= (1/n) * sum_j y_j
= mean(y) policy_value_se = sqrt( sum_{j=1..n} (y_j - ybar)^2
/ (n * (n - 1)) )
= sd(y) / sqrt(n)fn DoubleMLPolicyTree::sensitivity_analysis(self : DoubleMLPolicyTree, cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisepub struct DoubleMLQTE {
data : DoubleMLData
quantiles : Array[Double]
n_folds : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ml_l : LearnerDispatch
ml_m : LearnerDispatch
coefs : Array[Double]
ses : Array[Double]
psi_flat : Array[Double]
derivs1 : Array[Double]
derivs0 : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLQTE::bootstrap(self : DoubleMLQTE, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLQTE Sigma[j, k] = M_inv^2 * sum_c scale_c * S_c[j] * S_c[k] / n^2
S_c[j] = sum_{i in c} psi_flat[j, i]
scale_c = n_c / (n_c - 1) for n_c > 1, else 1fn DoubleMLQTE::cluster_sandwich_se_at(self : DoubleMLQTE, quantile_index : Int, cluster_ids : Array[Int]) -> Doublefn DoubleMLQTE::confint(self : DoubleMLQTE, level? : Double, joint? : Bool) -> Array[(Double, Double)]fn DoubleMLQTE::fit(self : DoubleMLQTE, ml_l? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLQTE Sigma[j, k] = M_inv^2 * sum_i psi_flat[j, i] * psi_flat[k, i] / n^2fn DoubleMLQTE::new(data : DoubleMLData, quantiles? : Array[Double], n_folds? : Int, seed? : Int, propensity_clip? : Double, ml_l? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLQTEfn DoubleMLQTE::sandwich_se_at(self : DoubleMLQTE, quantile_index : Int, kind : SandwichKind) -> Doublefn DoubleMLQTE::sensitivity_analysis(self : DoubleMLQTE, cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisefn DoubleMLQTE::sensitivity_analysis_cluster(self : DoubleMLQTE, cluster_ids? : Array[Int], cf_y? : Double, cf_d? : Double) -> Array[SensitivityResult] raisefn DoubleMLQTE::tune(self : DoubleMLQTE, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLQTEpub struct DoubleMLRDD {
data : DoubleMLRDDData
cutoff : Double
bandwidth : Double
kernel : RDDKernel
fuzzy : Bool
cov_type : String
ml_g : LearnerDispatch
coef : Double
se : Double
n_local : Int
residuals : Array[Double]
psi_a : Array[Double]
hc0_m : Array[Double]
hc0_e : Array[Double]
leverage : Array[Double]
fitted : Bool
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug)fn DoubleMLRDD::bandwidth_mse(self : DoubleMLRDD, h : Double, nnmatch? : Int, order? : Int) -> Double sigma4 = (1 / N_left) * SUM_i mean_j (eps_right[j] - eps_left[i])^2fn DoubleMLRDD::bandwidth_variance_term(self : DoubleMLRDD, h : Double, nnmatch? : Int, order? : Int) -> Doublevariance_term(h) = (h / h_pilot)^(2 * (order + 1)) * sigma4 / nfn DoubleMLRDD::bias(self : DoubleMLRDD, b? : Double, rho? : Double, p? : Int, q? : Int) -> (Double, Double)| quantity | value |
|---|---|
| xi_p(h) | 0.1327887 |
| xi_bc (rdrobust) | 0.0561884 |
| xi_q(b) (was shipped) | 0.0681351 |
fn DoubleMLRDD::bootstrap(self : DoubleMLRDD, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLRDD S_c = sum_{k in c} s_k
var = sum_c S_c^2 * n_c / (n_c - 1)fn DoubleMLRDD::new(data : DoubleMLRDDData, cutoff? : Double, bandwidth? : Double, kernel? : RDDKernel, fuzzy? : Bool, cov_type? : String, ml_g? : LearnerDispatch) -> DoubleMLRDDfn DoubleMLRDD::sensitivity_analysis(self : DoubleMLRDD, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLRDD::sensitivity_analysis_cluster(self : DoubleMLRDD, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLRDD::tau_bc(self : DoubleMLRDD, b? : Double, rho? : Double, p? : Int, q? : Int) -> Double tau_bc_s = xi_p(h)_s - [xi_p(h)_s - xi_q(b)_s] = xi_q(b)_s| quantity | upstream rdrobust | this port (v0.115.0) |
|---|---|---|
| xi_bc, left | 0.0561884317 | 0.0681350859 |
| tau_bc | 0.7092313868 | 0.6910832334 |
fn DoubleMLRDD::tau_bc_collapsed(self : DoubleMLRDD, b? : Double, rho? : Double, q? : Int) -> Doublefn DoubleMLRDD::tau_bc_se_rb(self : DoubleMLRDD, b? : Double, rho? : Double, q? : Int, vce? : String, nnmatch? : Int) -> Doublefn DoubleMLRDD::tune(self : DoubleMLRDD, param_set~ : Array[LearnerDispatch], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLRDDfn DoubleMLRDDData::new(x : Matrix, y : Array[Double], d : Array[Double], score : Array[Double]) -> DoubleMLRDDDatapub struct DoubleMLSSM {
data : DoubleMLSSMData
n_folds : Int
n_rep : Int
seed : Int
smpls : Array[Array[Fold]]
propensity_clip : Double
ps_processor_config : PSProcessorConfig
ml_g : LearnerDispatch
ml_m : LearnerDispatch
pi_hat : Array[Double]
m_hat : Array[Double]
g_d1 : Array[Double]
g_d0 : Array[Double]
coef : Double
se : Double
fitted : Bool
psi_a : Array[Double]
psi_b : Array[Double]
boot_t_stat : Array[Double]
boot_method : String
n_rep_boot : Int
boot_seed : Int
memoize_enabled : Bool
fit_cache : FitCache
} derive(Debug) Y = theta * D + X @ beta * D + U, E[U | D, X] = 0
S = 1{D + gamma Z + X @ beta + V > 0}, E[V | X, D] = 0 g_d1(X) = E[Y | D = 1, S = 1, X] (trained on D=1 ∧ S=1,
features = X only —
Bug #1 fix: previously
`pi_hat` was appended as
an extra feature, which
leaked the test-fold pi
into the training fold)
g_d0(X) = E[Y | D = 0, S = 1, X] (trained on D=0 ∧ S=1,
features = X only)
m(X) = P(D = 1 | X) (trained on all obs,
clipped to [eps, 1 - eps])
pi(X, D) = P(S = 1 | D, X) (trained on (X, D),
clipped to [eps, 1 - eps]) psi_a = -1
psi_b1 = (D == 1) * S * (Y - g_d1) / (m * pi) + g_d1
psi_b0 = (D == 0) * S * (Y - g_d0) / ((1 - m) * pi) + g_d0
psi_b = psi_b1 - psi_b0 theta_hat = -mean(psi_b) / mean(psi_a) = mean(psi_b)
J = mean(psi_a) = -1
gamma = mean(psi(theta_hat)^2)
sigma2 = gamma / (J^2 * n)
se = sqrt(sigma2).fn DoubleMLSSM::bootstrap(self : DoubleMLSSM, method_name? : String, n_rep_boot? : Int, seed? : Int) -> DoubleMLSSMfn DoubleMLSSM::fit(self : DoubleMLSSM, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch, ml_pi? : LearnerDispatch) -> DoubleMLSSMfn DoubleMLSSM::new(data : DoubleMLSSMData, n_folds? : Int, n_rep? : Int, seed? : Int, propensity_clip? : Double, ps_processor_config? : PSProcessorConfig?, ml_g? : LearnerDispatch, ml_m? : LearnerDispatch) -> DoubleMLSSMfn DoubleMLSSM::sensitivity_analysis(self : DoubleMLSSM, cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLSSM::sensitivity_analysis_cluster(self : DoubleMLSSM, cluster_ids : Array[Int], cf_y? : Double, cf_d? : Double) -> SensitivityResult raisefn DoubleMLSSM::tune(self : DoubleMLSSM, param_set~ : Array[TuneParam], scoring_method? : String, n_folds_tune? : Int, seed? : Int) -> DoubleMLSSMfn DoubleMLSSMData::new(x : Matrix, y : Array[Double], d : Array[Double], s : Array[Double]) -> DoubleMLSSMData (1 / 2n) ||y - Xw||^2fn ElasticNet::fit_weighted(self : ElasticNet, x : Matrix, y : Array[Double], w : Array[Double]) -> ElasticNetfn ElasticNet::new(alpha? : Double, l1_ratio? : Double, tol? : Double, max_iter? : Int) -> ElasticNetfn GBClassifier::fit_weighted(self : GBClassifier, x : Matrix, y : Array[Double], w : Array[Double]) -> GBClassifierfn GBClassifier::new(n_trees? : Int, learning_rate? : Double, max_depth? : Int, min_samples_leaf? : Int, mtry? : Int, subsample? : Double, bootstrap_seed? : Int) -> GBClassifierfn GainStatsSource::from_blp_cv_repeated(blp : DoubleMLBLP, n_folds? : Int, n_repeats? : Int, seed? : Int) -> GainStatsSourcefn GainStatsSource::new(var_y_residuals : Array[Double], nu2 : Array[Double], all_coef : Array[Double], n_rep : Int, var_y : Double) -> GainStatsSourcetype IivmDatatype IrmConfoundedDatatype IrmDatatype IrmDiscreteDatatype IrmHeterogeneousDatapub struct JointSensitivityInput {
name : String
coef : Double
se : Double
sigma_sq : Double
rho : Double
cf_y : Double
cf_d : Double
} derive(Debug)fn JointSensitivityInput::new(name : String, coef : Double, se : Double, sigma_sq : Double, rho : Double, cf_y : Double, cf_d : Double) -> JointSensitivityInputpub struct JointSensitivityResult {
joint_lower : Double
joint_upper : Double
joint_rv : Double
joint_rv_q : Double
per_estimator_lower : Array[Double]
per_estimator_upper : Array[Double]
per_estimator_rv : Array[Double]
per_estimator_rv_q : Array[Double]
bonferroni_alpha : Double
num_estimators : Int
} derive(Debug)pub enum LearnerDispatch {
LinearRegression(LinearRegression)
Constant(ConstantLearner)
Noop(NoopLearner)
RandomForest(RFLearner)
GradientBoosting(GBLearner)
LogisticRegression(LogisticRegression)
RandomForestClassifier(RFClassifier)
GradientBoostingClassifier(GBClassifier)
Ridge(Ridge)
Lasso(Lasso)
ElasticNet(ElasticNet)
} derive(Debug)fn LinearRegression::fit(self : LinearRegression, x : Matrix, y : Array[Double]) -> LinearRegressionfn LinearRegression::fit_weighted(self : LinearRegression, x : Matrix, y : Array[Double], w : Array[Double]) -> LinearRegressionfn LinearRegression::sandwich_se(self : LinearRegression, x : Matrix, y : Array[Double]) -> Array[Double]fn LinearRegression::sandwich_se_weighted(self : LinearRegression, x : Matrix, y : Array[Double], w : Array[Double]) -> Array[Double] cov(beta_hat) = (X'WX)^{-1} (X' diag(w · e^2) X) (X'WX)^{-1}fn LogisticRegression::fit(self : LogisticRegression, x : Matrix, y : Array[Double], max_iter? : Int, tol? : Double, ridge? : Double, sample_weight? : Array[Double]) -> LogisticRegressionfn LogisticRegression::predict_class(self : LogisticRegression, x : Matrix, threshold? : Double) -> Array[Double]type LplrDatapub enum LplrScoreKind {
NuisanceSpace
Instrument
}fn PSProcessor::adjust_ps(self : PSProcessor, ps : Array[Double], treatment : Array[Double], cv? : Array[(Array[Int], Array[Int])]?) -> Array[Double]pub(all) struct PSProcessorConfig {
clipping_threshold : Double
extreme_threshold : Double
calibration_method : String
cv_calibration : Bool
} derive(Debug)fn PSProcessorConfig::new(clipping_threshold? : Double, extreme_threshold? : Double, calibration_method? : String, cv_calibration? : Bool) -> PSProcessorConfig raise PSConfigErrortype PlivClusterDatatype PlivDatatype PlprDatatype PlrCcddhnr2018type PlrConfoundedDatapub enum PolicyTreeNode {
Leaf(Int)
Split(Int, Double, PolicyTreeNode, PolicyTreeNode)
} derive(Debug)| variant | K(u/h) | in rdrobust? |
|---|---|---|
| Triangular | 1 - t | yes (default) |
| Uniform | 1 | yes |
| Epanechnikov | 0.75 * (1 - t^2) | yes |
| Normal | exp(-t^2 / 2) | no |
| Quadratic | 1 - t^2 | no |
fn RFClassifier::fit_weighted(self : RFClassifier, x : Matrix, y : Array[Double], w : Array[Double]) -> RFClassifierfn RFClassifier::new(n_trees? : Int, max_depth? : Int, min_samples_leaf? : Int, mtry? : Int, bootstrap_seed? : Int) -> RFClassifiertype RddSimpleDatapub struct SensitivityResult {
rv : Double
sigma2 : Double
nu2 : Double
cf_y : Double
cf_d : Double
max_bias : Double
} derive(Debug)type SsmData| family | first slot (learner_l) actually holds | second slot (learner_m) |
|---|---|---|
| PLR PLIV LPLR PLPR | outcome learner E[y\|x] | nuisance E[m\|x] |
| APO APOS IRM IIVM CVAR PQ QTE SSM DID* | treatment / propensity learner E[g\|d,x] | outcome learner E[m0+d·m1\|x] |
| RDD | discontinuity-side learner E[y\|x] | unused (RDD has a single slot) |
theta_hat = median(theta_1, ..., theta_R)
ub_r = theta_r + 1.96 * se_r for each rep r
ub_hat = median(ub_1, ..., ub_R)
se_hat = (ub_hat - theta_hat) / 1.96 theta_hat = median([c]) = c
ub_hat = median([c + 1.96 * s]) = c + 1.96 * s
se_hat = (c + 1.96 * s - c) / 1.96 = s theta_corrected = theta_hat + mean(bias_per_obs)#callsite(autofill(loc))
fn check(condition : Bool, loc~ : SourceLoc) -> Unit raise PreconditionErrorfn check_sample_splitting(smpls : Array[Array[Fold]], n_obs : Int) -> SampleSplitting raise PreconditionError S_c = (sum_{i in c} psi[i])^2
var = M_inv[0,0]^2 *
(sum_c S_c * n_c / (n_c - 1)) / n^2 train_index_cond = np.intersect1d(train_cond, train_index)
y_hat.append(ml_model.fit(x[train_index_cond],
y[train_index_cond]).predict(x[test_index]))fn draw_bootstrap_weights(method_name : String, n_rep_boot : Int, n_obs : Int, seed : Int) -> Array[Double] raise BootstrapMethodError F = 0.5 / n * sum_i wt_i r_i^2
+ alpha * l1_ratio * ||coef||_1
+ 0.5 * alpha * (1 - l1_ratio) * ||coef||^2 theta = - sum_k w_k * sum_{i in k} psi_b
/ sum_k w_k * sum_{i in k} psi_a, w_k = 1/|I_k|.fn expit(x : Double) -> Doublefn fit_predict_one_dispatch(learner : LearnerDispatch, x_train : Matrix, y_train : Array[Double], x_test : Matrix) -> Array[Double]fn g_cross_fit_calls() -> Int f_hat(x) = (1 / (n * h * sqrt(2*pi))) * sum_i exp(-(x - y_i)^2 / (2 * h^2)) f_hat(x) = (1 / (h * sqrt(2*pi))) * sum_i w[i] * exp(-(x - y[i])^2 / (2*h^2)) d/dtheta mean(psi_ipw) = (1/n) * sum_i w_i * delta(y_i - theta)
≈ (1/n) * f_hat_weighted(theta)fn hash_hyperparams(estimator_kind : String, ml_g : LearnerDispatch, ml_m : LearnerDispatch, propensity_clip : Double, ps_processor_config? : PSProcessorConfig?) -> UInt64fn irm_style_sensitivity(theta : Double, residuals : Array[Double], psi_a : Array[Double], cf_y : Double, cf_d : Double) -> SensitivityResult raisefn irm_style_sensitivity_cluster(theta : Double, residuals : Array[Double], psi_a : Array[Double], cluster_ids : Array[Int], cf_y : Double, cf_d : Double, kernel_weights? : Array[Double]) -> SensitivityResult raisefn irm_style_sensitivity_from_elements(theta : Double, residuals : Array[Double], nu2_score_element : Array[Double], riesz_rep : Array[Double], cf_y : Double, cf_d : Double) -> SensitivityResult raise nu2_score_element = 2 * m_alpha - rr^2
nu2 = mean(nu2_score_element)fn joint_sensitivity(inputs : Array[JointSensitivityInput], alpha : Double) -> JointSensitivityResult raise s = 0.0
for x in arr: s = s + x sum = 0.0
c = 0.0 // compensation for low-order bits lost in the next add
for x in arr:
y = x - c // align x with the running sum's precision
t = sum + y // primary add
c = (t - sum) - y // low-order bits of `t` that did not fit
sum = t
return sumfn logit(p : Double, eps? : Double) -> Doublefn lpq_score_ipw(data : DoubleMLLPQData, treated : Array[Double], m : Array[Double], comp : Double, theta : Double, q : Double, sign : Double) -> Array[Double]fn make_confounded_plr_data(n_obs : Int, dim_x : Int, theta : Double, seed : Int) -> PlrConfoundedDatafn make_irm_confounded_data(n_obs : Int, dim_x : Int, theta : Double, seed : Int) -> IrmConfoundedDatafn make_irm_discrete_treatments(n_obs : Int, dim_x : Int, theta : Double, seed : Int) -> IrmDiscreteDatafn make_pliv_multiway_cluster(n_obs : Int, dim_x : Int, theta : Double, seed : Int) -> PlivClusterDatares[pos] = sqrt(Ji / (Ji + 1)) * (y[pos] - mean(y over those neighbours))fn norm_cdf(x : Double) -> Doublefn norm_ppf(p : Double) -> Doublefn norm_sf(x : Double) -> Double mean_treat1 = mean(treatment / propensity)
mean_treat0 = mean((1 - treatment) / (1 - propensity))
normalized = treatment * propensity * mean_treat1
+ (1 - treatment) * (1 - (1 - propensity) * mean_treat0)score[i] = ind_d[i] * ((y[i] <= theta) - g[i]) / m[i] + g[i] - quantile psi[i] = coef * psi_a[i] + psi_b[i]#callsite(autofill(loc))
fn require(condition : Bool, loc~ : SourceLoc) -> Unit raise PreconditionErrorfn reset_g_cross_fit_count() -> Unitfn resolve_ps_processor(ps_processor_config? : PSProcessorConfig?, trimming_threshold? : Double) -> PSProcessor if ps_processor_config is not None:
config = ps_processor_config # config wins outright
else:
config = PSProcessorConfig(clipping_threshold=trimming_threshold)fn resolve_ps_processor_config(ps_processor_config? : PSProcessorConfig?, trimming_threshold? : Double) -> PSProcessorConfigfn sandwich_variance(kind : SandwichKind, psi_a : Array[Double], psi : Array[Double], m_inv : Matrix, n_obs : Int, n_params : Int) -> Double var = M_inv[0,0]^2 * sum_i psi[i]^2 / n^2
= M_inv[0,0]^2 * mean_i psi[i]^2 / n var = M_inv[0,0]^2 * sum_i (psi[i]^2 / (1 - h_ii)) / n^2fn sensitivity_from_variance_baselines(theta : Double, sigma2 : Double, nu2 : Double, n : Int, cf_y : Double, cf_d : Double) -> SensitivityResult raisefn single_psi_sensitivity(theta : Double, psi : Array[Double], y : Array[Double], cf_y : Double, cf_d : Double) -> SensitivityResult raisefn solve_pq(ml_l : LearnerDispatch, ml_m : LearnerDispatch, data : DoubleMLData, treatment : Double, q : Double, n_folds : Int, seed : Int, clip : Double, folds? : Array[Fold], ps_processor_config? : PSProcessorConfig?) -> (Double, Array[Double], Double) raise BracketSignErrorfn tune_score_grid(x : Matrix, y : Array[Double], learners : Array[LearnerDispatch], n_folds_tune : Int, seed : Int, scoring : TuneScoring) -> TuneGridScore theta_hat = -mean(psi_b) / mean(psi_a)
J = mean(psi_a)
gamma = mean(psi(theta_hat)^2) where psi(theta) = theta * psi_a + psi_b
sigma2 = gamma / (J^2 * n)
se = sqrt(sigma2) gamma += S_g^2 / |I_k|, S_g = sum of psi over unit g,
J += (sum of psi_deriv over the fold's rows) / |I_k|,
sigma2 = (gamma / npc) / (N_units * (J / npc)^2).Install
Download zipPure-MoonBit port of doubleml-for-py: Double / Debiased Machine Learning for partial linear, IRM, IV, DID, SSM, APO(S), PQ, QTE, LPQ, CVaR, RDD, BLP, and policy-tree models
Dependencies