Validation¶
dml-rs is tested against the reference implementation, DoubleML 0.11.4, on identical data and identical sample splits.
Parity tests¶
parity/generate_fixtures.py
runs DoubleML on fixed data and fixed folds and stores data, folds and results as JSON. The Rust
test suite refits every case on the same folds and requires agreement to 1e-9 for closed-form
learners (OLS, ridge) and 1e-7 for iterative ones (Lasso, logistic regression, cross-validated
learners). Compared are the nuisance predictions, coefficients, standard errors, t-statistics,
p-values (raw and multiplicity-adjusted), confidence intervals, bootstrap bands, BLP and GATE
inference, staggered-DiD aggregations, sensitivity bounds and robustness values, and learner
losses.
| Fixtures | Coverage |
|---|---|
plr_ols, plr_ridge, plr_lasso, plr_ivtype_rep3 |
PLR with partialling-out and IV-type scores, several learners, repeated splits |
plr_lasso_cv, irm_ate_lasso_cv_logit_cv |
scikit-learn's LassoCV and LogisticRegressionCV as nuisance learners |
plr_boot_bayes, plr_boot_wild, plr_multi_rep3_boot |
Bootstrap methods, multiple treatments, joint intervals, Romano–Wolf |
rd_*, rdflex_* |
The RD back end against the rdrobust package (bandwidths and estimates, sharp, fuzzy and with mass points); RDFlex in sharp and fuzzy designs with every first-stage specification and kernel, a non-zero cutoff, one to three bandwidth rounds and repeated splits |
rd_options, rdflex_*_rd_options, rdflex_fuzzy_covs_cluster, rdflex_*binary* |
54 rdrobust option sets (polynomial orders, kernels, all ten bandwidth selectors, HC variances, mass points, regularisation, rho, covariates with a collinear column, clusters with the cr1, cr2 and cr3 variances for h = b and h ≠ b, manual bandwidths) and RDFlex with rdrobust options, RD covariates, clusters and a classifier for a binary outcome |
*_isotonic* |
Isotonic propensity calibration, on all observations and cross-fitted: IRM (ATE, ATTE with sensitivity), APO, IIVM, SSM and staggered DiD |
plpr_* |
Panel regression: all four approaches, unbalanced panels with gaps, IV-type score, DoubleML's unit-clustered splits |
lplr_* |
Logistic partially linear regression: both scores, continuous and binary treatments, replaying DoubleML's outer and inner splits |
irm_ate, irm_atte, irm_ate_normalized, irm_cate_gate |
IRM (ATE / ATTE, normalised IPW), BLP and GATE |
apo_*, apos_rep2 |
Average potential outcomes, causal contrasts and their sensitivity bounds |
pq_*, lpq_t1, cvar_*, qte_rep2 |
Potential, local potential and CVaR quantiles for both treatment levels, with and without normalised weights, and quantile treatment effects |
ssm_* |
Sample selection, missing at random and nonignorable, with and without normalised weights |
pliv_ols, pliv_ivtype, pliv_*_instruments*, pliv_partial_z, pliv_partial_xz, iivm_late_rep2, iivm_no_always_takers |
PLIV and IIVM, including one-sided non-compliance |
did_panel_*, did_cs_* |
Two-period DiD for panels and cross-sections, all score and normalisation variants |
staggered_*, staggered_cs_* |
Staggered DiD: every ATT(g, t) and all aggregations, with their sensitivity bounds and (panels) benchmarks |
framework_arithmetic_rep2 |
Framework arithmetic: sums, differences, multiples and concat of a PLR and an IRM fit, with sensitivity bounds |
plr_cluster_*, irm_cluster_one_way, pliv_cluster_two_way |
One-way and two-way clustering |
| all PLR, IRM and DiD fixtures | Sensitivity analysis in two scenarios, and benchmarks |
The Python tests also compare DoubleML's tune (grid and randomized search, on the whole sample
and on the folds) and tune_ml_models (Optuna) with the same random state: the chosen parameters
and the resulting estimates agree for PLR (both scores), IRM, APO, IIVM, DID, DIDCS, SSM and PLIV.
So do set_ml_nuisance_params (including fold- and treatment-specific parameters), the learners
stored with store_models, and callable scores for PLR, IRM, PLIV and IIVM.
The simulated data sets (dml_rs.datasets) draw from another random generator than DoubleML, so
their tests compare output formats and, on samples of 20,000, the means and standard deviations of
the outcomes, treatments, instruments and covariates with DoubleML's generators, as well as the
oracle confounding strengths; the deterministic effect functions agree exactly. The Rust tests
check that every design's effect is recovered by the matching estimator.
fetch_401K and fetch_bonus return the same DataFrames as DoubleML's (the bonus preparation is
compared on a synthetic raw file, the 401(k) data with a download).
The Python test suite checks the bindings against DoubleML and scikit-learn (including the
learners themselves), and the type stubs are checked against the compiled extension with
mypy.stubtest.
Statistical tests¶
Clustered staggered DiD has no DoubleML counterpart: its tests check that singleton clusters reproduce the iid standard errors exactly and that cluster-robust intervals reach their coverage when shocks are shared within clusters.
AutoDML has no DoubleML counterpart. Its tests compare it with independent computations of
the same estimator: with an unpenalised Riesz representer against the closed form
\(\hat\rho = \hat G^{-1}\hat M\), and with a penalty against scikit-learn's Lasso (the Riesz
loss equals a least-squares loss on a transformed outcome), both to 1e-8; a saturated dictionary
reproduces the inverse propensity weights exactly. Simulations check that it recovers average
derivatives, shift and policy effects and the ATE (where it agrees with IRM), that the Riesz
correction removes the bias of a misspecified regression, and that its confidence intervals cover.
Monte-Carlo tests simulate data with a known effect and check that confidence intervals reach their nominal coverage: PLR, LPLR (both scores), PLPR (three approaches, with correlated unit effects), RDFlex (sharp and fuzzy), IRM (ATE and ATTE), PLIV, IIVM, DiD, sample selection (both scores), quantile and CVaR effects, joint bootstrap bands, and clustered data (where iid intervals cover about 75% of the time and cluster-robust ones about 95%).
Known differences¶
These are deliberate, documented, and covered by tests:
- IIVM with
normalize_ipw=True. dml-rs normalises the inverse-probability weights with the instrument; DoubleML 0.11 used the treatment. The DoubleML maintainers confirmed the instrument is correct (issue #411). - Contrasts of clustered fits (
APOS.causal_contrastand staggered-DiD aggregations) use the cluster-robust variance of the combined score. DoubleML's framework arithmetic scales the score by the number of clusters but ignores the correlation within clusters. In a simulation with treatment assigned per cluster, cluster-robust contrast intervals cover the true effect 95% of the time and iid intervals 62%. - Robustness values are solved exactly; DoubleML uses a bounded numerical minimiser, so the two
agree to about
1e-5. - Gradient boosting breaks ties between equally good splits deterministically, scikit-learn by a random feature order. Training-set predictions are identical; held-out predictions and estimates agree up to scikit-learn's own seed-to-seed variation.
- Lasso with more covariates than observations. At scikit-learn's default tolerance
(
tol=1e-4) the coordinate descent may stop at slightly different points, depending on floating-point rounding (as scikit-learn does across platforms); both are within the tolerance. With a tighter tolerance the two agree to numerical precision. - Quantile models need bit-identical propensities for exact agreement. The preliminary
estimate minimises the absolute value of a step function with Brent's method (as DoubleML), and
that search reacts to differences in the last bit of the propensity scores. With identical
learners (for example the same scikit-learn estimator in Python) dml-rs and DoubleML agree to
1e-9. With learners that differ by rounding, such as the built-inLogisticRegressionagainst scikit-learn's, the search occasionally ends at a neighbouring point: in 57 potential-quantile fits on simulated data, 54 agreed to1e-9and the other three differed by at most 0.08 standard errors. The Rust parity fixtures therefore use class-share propensities, which are exact on both sides. - LPLR with
n_folds_innerdifferent fromn_folds. The outer predictions ofml_Mandml_aaverage the inner models. DoubleML 0.11 sums the inner models' predictions and divides by the number of outer folds, which scales them byn_folds_inner / n_foldswhen the two differ (with 4 outer and 2 inner folds itsml_Mpredictions average half the outcome mean) and changes the estimate throughr̂ = t̂ − β·â. This was reported in DoubleML #401 and is fixed on its main branch (#402), not yet in a release; dml-rs follows the fixed behaviour. With the default, equal fold counts the two agree to1e-9. - Sensitivity contour plots.
sensitivity_contoursgives the same grid of bounds as DoubleML'ssensitivity_plot(to1e-9), with rows indexed bycf_d. DoubleML 0.11 hands that grid to plotly aszwithx = cf_dandy = cf_y, but plotly reads the rows ofzalongy, so its contours are drawn transposed;sensitivity_plotdraws them withcf_don the x axis as labelled. The bound is not symmetric incf_yandcf_d, so the two pictures differ slightly (reported as DoubleML #414). - Negative multiples of a framework. dml-rs reports
|c|·seforc · framework. DoubleML 0.11 multiplies the standard errors bycitself, so a negative multiple gets a negative standard error, a t-statistic with the wrong sign and a confidence interval with its bounds swapped. The estimates and sensitivity bounds agree. - Sensitivity benchmarks of a staggered DiD. dml-rs refits every ATT(g, t) without the
benchmarking covariates. DoubleML 0.11's
DoubleMLDIDMulti.sensitivity_benchmarkchanges the covariate list of a copy whose group-time models have already read their covariates, so its short model equals the long one and every benchmark is 0. The fixtures compare with DoubleML's benchmark statistics computed from a short model fitted on data without the covariates, on the same splits; they agree to1e-7. - PQ with external propensities only. A supplied
ml_malso serves the preliminary estimate, on the rows of the first half of each training fold. DoubleML 0.11 indexes the external propensities by position within that half instead of by row, so it pairs them with the wrong observations. With externalml_g(with or withoutml_m) the two agree to1e-9. - σ² and ν² of combinations. For a multiple of a single estimate dml-rs keeps the
sensitivity elements (with
ν²scaled by the squared weight), so benchmarks remain possible; DoubleML drops them once frameworks are added, even when all but one weight is zero. The bounds are the same. - Staggered DiD data with anticipation.
make_did_CS2021(anticipation_periods > 0, time_type="float")in DoubleML 0.11 also drops the never-treated units (its first filter testspd.isna, which is false for theinfcohort; the datetime version keeps them). dml-rs keeps them in both versions. - Fold-specific tuning with repeated splits. DoubleML 0.11's
tune(tune_on_folds=True)stores its results in a list indexed[repetition][treatment]that has one entry per treatment, so it fails whenever there are more repetitions than treatments. dml-rs tunes every repetition; the tests compare with DoubleML for one repetition. - Optuna tuning of IV-type PLIV. DoubleML 0.11's
tune_ml_modelsforDoubleMLPLIVwithscore="IV-type"callscross_val_predictforml_rwithout the treatment as target and fails; dml-rs uses the treatment, as its grid search does. - Initial estimate for tuning
ml_g(PLR and PLIV, IV-type). With a classifier forml_m, dml-rs uses predicted probabilities in the partialling-out estimate that definesml_g's target; DoubleML's grid search uses predicted classes. With regressors the two agree. LogisticRegressionCVsolves each fit to the exact optimum with Newton's method; scikit-learn's default L-BFGS solver stops earlier, so its probabilities can differ by about1e-3at default settings.