Skip to main content

pragdml

Causal inference when your controls are AI-learned representations.

PyPI Python License: MIT Tests

A Python implementation of Pragmatic DML with AI-Learned Representations (Aradillas Fernández, Chernozhukov, Cinelli, Klaassen, Newey, Spindler, Teichert-Kluge & Vijaykumar, 2026), together with the omitted-variable-bias theory it builds on (Chernozhukov, Cinelli, Newey, Sharma & Syrgkanis, Review of Economics and Statistics, 2026).


The problem it solves

You have a rich covariate — a product description, a product image, a clinical note, a panel of attributes. You compress it into an embedding with a foundation model and use that embedding as a control. Then you report an ATE.

That ATE is not the ATE you think it is. Compressing the covariate changes two things at once: the regression you adjust with, and the population parameter your procedure is centred on. The paper's central result says exactly how much:

$$\theta^* - \theta_\lambda = E\big[(g^* - g_\lambda)(\alpha^* - \alpha_\lambda)\big]$$

The gap between the true causal parameter and the one you estimate is the product of two representation errors — one in the outcome regression g, one in the balancing weight (Riesz representer) α. Not their sum. Their product.

Three consequences, and pragdml implements all three:

1. You can compress a lot, safely. The representation need not reconstruct the covariate. It only has to preserve one of the two objects — regression or balancing weight. If either error is small, the target barely moves.
2. You can fine-tune inside the folds. Learn the representation fold-wise and the usual DML inference still holds, with no stability or differentiability condition on the tuning algorithm. Only the fitted functions' prediction rates matter.
3. When loss is material, you get a region. Not a point you cannot defend, but an identified interval for the true parameter with root-n inference for its endpoints — plus a robustness value that says, in one number, how strong the discarded information would have to be to overturn your sign.

Install

pip install pragdml

Python 3.10+. Hard dependencies are only numpy, scipy, pandas, scikit-learn, matplotlib and joblib.

Never used Python before? Jump to Starting from absolutely nothing.

Sixty seconds

import numpy as np
from pragdml import PragmaticDML

rng = np.random.default_rng(0)
X = rng.normal(size=(2000, 5))                      # your embedding goes here
e = 1 / (1 + np.exp(-X[:, 0]))                      # confounded assignment
D = (rng.uniform(size=2000) < e).astype(float)
Y = 2.0 * D + X[:, 0] - 0.5 * X[:, 1] + rng.normal(size=2000)

res = PragmaticDML(functional="ate", seed=0).fit(Y, D, X)
print(res.summary())

print("robustness value:", res.robustness_value())
                  Pragmatic DML -- ATE

  Observations      2000
  Folds (K)         5
  Variance          cross-fitted score variance (Eq. 12)
  Interval refers to: representation-based target (theta_lambda, Eq. 3)
  Representation dim 5
==============================================================================
quantity estimate std_error       t p_value [95% CI lower  upper]
------------------------------------------------------------------------------
     ATE   2.0183    0.0479 42.1392  0.0000        1.9244  2.1122  ***
==============================================================================
Signif.: *** p<0.01, ** p<0.05, * p<0.1
Score: psi = m(O, g) + alpha(W){Y - g(W)}  (Eq. 8), Neyman-orthogonal.
Under target adaptivity (Def. 1) the same interval covers theta*;
otherwise report a sensitivity region (Sec. 4): res.sensitivity().

Note what the table refuses to be vague about: which parameter the interval covers. That is the whole point of the paper.


The complete worked example (real data)

pragdml ships the 1991 SIPP 401(k) dataset — 9,915 households, the canonical dataset of the DML literature and the empirical application of the parent paper.

python examples/pension_401k.py

The question

401(k) eligibility is not randomised: it depends on your employer. The naive gap in net financial assets between eligible and ineligible households is $19,559 — and it is confounded. How much survives adjustment, and how strong would unobserved firm characteristics have to be to overturn what is left?

Step 1 — load and estimate

import numpy as np
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import SplineTransformer

from pragdml import (AdditiveFeatures, LinearOutcomeLearner,
                     LinearRieszLearner, PragmaticDML)
from pragdml.datasets import load_pension, PENSION_COVARIATES

Y, D, X = load_pension(return_X_y=True)

# Flexible in the covariates, linear in the treatment: the "partially linear
# model" of the literature. This matters -- see "Practical guidance" below.
basis = ColumnTransformer(
    [("spline", SplineTransformer(n_knots=6, degree=3), [0, 1, 2, 3])],
    remainder="passthrough",
)

res = PragmaticDML(
    functional="ate",
    outcome_learner=LinearOutcomeLearner(AdditiveFeatures(basis=basis), ridge=1e-8),
    riesz_learner=LinearRieszLearner(AdditiveFeatures(basis=basis), ridge=1e-8),
    n_folds=5, n_rep=5, seed=20261017,
).fit(Y, D, X)

print(res.summary())

Result: $7,855 (se $1,392), 95% CI [$5,126, $10,583]. The published figures are about $9,000 and about $8,000 — both inside this interval. Adjustment removed 60% of the naive gap; a large positive effect remains.

Step 2 — ask how fragile that is

sa = res.sensitivity()
print(sa.summary())
print("RV(0, 0.05) =", round(sa.robustness_value(), 4))

Robustness value = 4.2%. Published values: 5.4% and 4.5%.

Read it as: unobserved firm characteristics explaining less than 4.2% of the residual variation — both of the outcome and of the treatment, in the worst-case alignment — cannot bring the lower confidence limit to zero.

Step 3 — see the whole surface

from pragdml._plots import plot_sensitivity_contour
plot_sensitivity_contour(sa, cy2_max=0.12, cd_share_max=0.12, rho=1.0)

Sensitivity contour

Every point is a confounding scenario. The colour is the lower 95% bound on the true effect. The red dashed line is where that bound hits zero — beyond it, the sign is no longer safe. The star sits where the 45° line crosses it: that is the robustness value, and it is why one number can stand in for the whole picture.

Step 4 — calibrate from observed covariates

Instead of asserting strengths out of thin air, anchor them: "the omitted information is no stronger than observed income."

from pragdml.sensitivity import benchmark_covariate

j = PENSION_COVARIATES.index("inc")
short = PragmaticDML(...).fit(Y, D, np.delete(X, j, axis=1))
bm = benchmark_covariate(res.crossfit_, short.crossfit_, name="inc", k_y=0.25, k_d=0.25)
print(bm.cy2, bm.cd_share)      # the implied scenario for "1/4 as strong as income"

Step 5 — compare information sets

from pragdml import compare_information_sets
cmp = compare_information_sets(Y, D, {"demographics": ..., "economic": ..., "all": X},
                               functional="ate", seed=0)
print(cmp.summary())
information set estimate std error CI lower CI upper
demographics 18382 1434 15572 21192
economic 7828 1432 5021 10636
assets 12159 1335 9542 14777
all 7805 1350 5159 10452
Star aggregate 8019 1315 5442 10597

By information set

Each row is a different population target θ_λ, not a competing estimate of one number. Reading them: demographics alone barely adjusts anything; the economic block (income, education) reproduces almost the whole adjustment. Income is doing the work — which is exactly why the parent paper benchmarks against it.


Interpreting the output

The summary table, line by line

Line What it means
estimate θ̂, the cross-fitted orthogonal-score estimate (Eq. 11).
std_error From the empirical variance of the cross-fitted scores (Eq. 12), or cluster-robust if you passed cluster=.
[95% CI lower upper] The Wald interval of Eq. (16).
Interval refers to: The most important line. θ_λ for a single information set; θ₀ for a pipeline. It is θ* only under target adaptivity.
Folds (K) Cross-fitting folds, and repetitions if n_rep > 1.

The sensitivity components

Symbol Meaning Identified?
σ₀² = E[(Y−g₀)²] Residual outcome variance yes
ν₀² = E[α₀²] Second moment of the balancing weight yes
S₀ = (σ₀²ν₀²)^½ The scale of any possible bias yes
C_Y² Share of residual outcome variation the omitted information could explain no — you assume it
C_D² The same for the balancing weight no — you assume it
ρ Alignment of the two omitted components, in [−1, 1] no — you assume it

Bias = ρ · C_Y · C_D · S₀. The library estimates S₀; you supply the rest, and the robustness value tells you which values would matter.

ρ is not a strength. Confounding can be overwhelming in both equations and still produce no bias if its functional form differs between them. The parent paper's example: D = U², Y = D + U. Here U explains 100% of the residual variation in both, yet U² is uncorrelated with U, so the bias is zero. That is why ρ multiplies the other two rather than being implied by them.

A warning you may see

RuntimeWarning: the cross-fitted representer has nu_hat^2 = -1.48 <= 0 ...

ν̂² estimates E[α₀²] > 0. A negative value means the Riesz fit has failed — almost always an over-wide basis making the normal equations ill-conditioned. Do not report that estimate. Add ridge=, narrow the basis, or switch from VaryingCoefficientFeatures to AdditiveFeatures.


Practical guidance

Choosing a feature map. VaryingCoefficientFeatures is the paper's own architecture (Eqs. 68–69) and the default: φ(d,x) = [d·b(x), b(x)]. It doubles the basis width, so with a wide basis prefer AdditiveFeatures ([d, b(x)]) and a small ridge. If you see the ν̂² warning, this is the knob.

Flexible in X is not optional. On the 401(k) data a specification linear in the covariates returns about $4,900 — essentially plain OLS — against about $7,900 with splines on the continuous covariates. The treatment can be linear; the covariates usually cannot.

Binary vs continuous treatment. ATE needs D ∈ {0,1}. For a continuous treatment use AverageDerivative (Eq. 67) — and note that trees, forests and boosting are inadmissible for it, because a piecewise-constant function has zero derivative almost everywhere. pragdml warns you.

Clustered data. Pass cluster=. Whole clusters stay inside a fold and the variance becomes cluster-robust. Section 5.1 of the paper requires this for repeated observations on a unit but supplies no estimator; this is an addition.

Star or convex aggregation? Star costs log M and tolerates a menu that grows faster than any power of n; convex costs √(M log n) and needs M log n = o(√n). Call pragdml.diagnostics.pooling_conditions to decide on your numbers rather than by feel.

Learn the two heads separately. The outcome regression and the Riesz representer solve different prediction problems and may legitimately select different candidates — the paper's own application does exactly that. Prediction quality alone does not summarise target preservation.


Syntax reference

PragmaticDML(...) — one information set

Argument Type Default What it does
functional str or Functional "ate" "ate", "att", "apo", "average_derivative" (aliases "derivative", "elasticity"). Eq. (2)–(3).
basis sklearn transformer None b(X) inside the feature map. PCA, SplineTransformer, a Pipeline.
outcome_learner OutcomeLearner linear over the map The g head.
riesz_learner RieszLearner linear over the map The α head.
ridge float 0.0 Penalty for both default learners, in the rescaled space.
n_folds int 5 K ≥ 2.
n_rep int 1 Repeated splits, median-aggregated.
seed int None Seeds every split.

.fit(Y, D, X, cluster=None) → DMLResult.

DMLResult

Member Returns
.theta, .se, .tvalue, .pvalue, .n floats
.conf_int(level=0.95) Wald interval, Eq. (16)
.bootstrap_conf_int(n_boot=999, seed=None) multiplier-bootstrap interval
.sensitivity() SensitivityAnalysis
.robustness_value(target=0.0, level=0.95) float
.summary(), .to_frame(), .to_latex(), .to_markdown() report
.plot_influence(), .plot_sensitivity_contour() figures
.crossfit_ CrossFitOutput with every fitted quantity

SensitivityAnalysis

Member Returns
.theta, .sigma2, .nu2, .S the Eq. (55)–(57) components
.interval(cy2=, cd2=, rho=1.0) or .interval(rv=) the Eq. (51) region
.evaluate(..., level=, method=) SensitivityResult with endpoints, SEs and envelope
.envelope(c, method=) "bonferroni" (Eq. 65), "one_sided" ([9] Thm 4), "joint"
.grid(rhos=, r2s=, parameterization=, tail=) the Table 3 layout
.contour_data(...) the contour-plot grid
.robustness_value(target=0.0, level=0.95) [9] Definition 2

Other entry points

from pragdml import (PipelineDML,              # a menu, aggregated fold-wise
                     compare_information_sets,  # the Table 1 layout
                     pragmatic_dml)             # one-call wrapper
from pragdml.sensitivity import benchmark_covariate, calibrate_deletion
from pragdml.diagnostics import (omitted_information_gap, target_adaptivity,
                                 product_rate, pooling_conditions)

Methods: step → equation map

Every block of code is named against the paper. The full map is in docs/methods.md; the essentials:

Paper Code
Eqs. (2)–(3), the target class functionals.Functional
Eqs. (4)–(5), Riesz representation riesz.LinearRieszLearner
Prop. 1, Eqs. (6)–(7) — the omitted-information identity diagnostics.omitted_information_gap
Eq. (8), the orthogonal score _core.debiased_score
Eqs. (11)–(12), (16) — estimator, variance, interval _core.CrossFitOutput
Def. 1, Eq. (17) — target adaptivity diagnostics.target_adaptivity
Def. 2, Eq. (19) — adaptive efficiency diagnostics.adaptive_efficiency
Def. 3 — the pipeline targets on H features.FeatureMap span
Prop. 2, Eqs. (23)–(24) diagnostics.mixed_bias_drift
Lemma 2, Eqs. (28)–(29) — excess risk = squared error diagnostics.excess_risk_identity
Eq. (32), the product-rate condition diagnostics.product_rate
Eq. (35), the critical radius diagnostics.critical_radius_l1_hull
Def. 4, Eqs. (36)–(37) — star aggregation aggregation.star_aggregate
Defs. 5–6, Eqs. (40)–(49) — pooled aggregation aggregation.pooled_*_aggregate
Eq. (50) — the bias factorization sensitivity.bias_factor
Eq. (51) — the identified region SensitivityAnalysis.interval
Eqs. (52)–(54) — the auxiliary scores SensitivityAnalysis.scores
Eqs. (55)–(57) — the estimated components CrossFitOutput.sigma2_outcome, .nu2_representer, .S
Eq. (64) — the endpoint influence function SensitivityAnalysis.scores
Eq. (65) — the confidence envelope SensitivityAnalysis.envelope
Eqs. (67)–(70) — the application's specification AverageDerivative, VaryingCoefficientFeatures
[9] Def. 2 — robustness values sensitivity.robustness_value
[9] App. E — covariate benchmarking sensitivity.benchmark_covariate

How it is verified

170 tests, run on every push. The ones that matter:

  • Exact identities. Proposition 1, Proposition 2 and Lemma 2 are algebra, not approximations, and are asserted to machine precision. Linearity of m is checked for all six functionals.
  • The paper's Table 3, reproduced. All eighteen endpoints, to the last printed digit, from the published centre and scale.
  • Published numbers. The naive 401(k) gap to the dollar ($19,559.3); the ATE interval containing both published figures; the robustness value at 4.2% against published 4.5% and 5.4%.
  • Cross-implementation. For the ATE with an analytic representer, the Eq. (8) score is DoubleML's IRM score; given identical folds the two agree to within 2% of a standard error. The average derivative with a partially linear map equals the OLS slope to 1e-9.
  • Known-truth recovery with an asserted root-n rate, reproducibility (bit-identical under a seed), and input contracts.

Two inconsistencies found in the source paper

Documented, not papered over (both pinned by tests):

  1. Table 3's note says C_Y² = C_D² = r², but its printed endpoints are only reproducible under C_D² = r²/(1−r²) — the parent paper's convention. Max residual 0.00044 under that reading against 0.00147 under the note's. This is why pragdml keeps both parameterizations distinct ("cy_cd" vs "rv").
  2. Table 2's printed C_Y² = 0.0017 is the raw numerator R²_full − R²_text, not the ratio (R²_full − R²_text)/(1 − R²_text) = 0.001752 that Section 5.4 specifies. pragdml implements the stated formula.

Starting from absolutely nothing

For researchers who have never typed a command.

1. Install Python

Go to python.org/downloads and install the latest version. On Windows, tick "Add Python to PATH" on the first screen.

Alternatively install Anaconda, which bundles Python with the scientific libraries.

2. Open a terminal

  • Windows: press Win, type cmd, press Enter.
  • macOS: press Cmd+Space, type Terminal, press Enter.

Check it worked:

python --version

3. Install the package

pip install pragdml

4. Write your first script

Open Notepad (or TextEdit, or IDLE, which came with Python) and save this as my_analysis.py:

from pragdml import PragmaticDML
from pragdml.datasets import load_pension

Y, D, X = load_pension(return_X_y=True)
res = PragmaticDML(functional="ate", seed=0).fit(Y, D, X)
print(res.summary())
print("robustness value:", res.robustness_value())

Run it:

python my_analysis.py

You will get about $3,980. That is the default specification, which is linear in the covariates — and as Practical guidance explains, that is too restrictive for this dataset. The headline $7,855 comes from the spline specification shown in the worked example. Seeing the two differ is the lesson, not a bug: how you model the covariates matters more here than anything else you will choose.

5. Use your own data

If your data is in Excel or CSV:

import pandas as pd
from pragdml import PragmaticDML

df = pd.read_csv("my_data.csv")        # or pd.read_excel("my_data.xlsx")

Y = df["outcome"]
D = df["treatment"]
X = df[["control1", "control2", "control3"]]

res = PragmaticDML(functional="ate", seed=0).fit(Y, D, X)
print(res.summary())

Your treatment must be 0/1 for "ate". For a continuous treatment use functional="average_derivative".

6. Get the table into your paper

print(res.to_latex())        # paste into LaTeX
print(res.to_markdown())     # paste into Word, Google Docs or GitHub
res.to_frame().to_csv("results.csv")

7. Working more comfortably

Install Jupyter (pip install notebook, then jupyter notebook) or Spyder, which gives you an editor, a console and a variable explorer in one window.


Limitations

  • The paper's own application is not reproducible here. Its product data is scraped from Keepa.com and is neither public nor redistributable. pragdml reproduces the paper's arithmetic (Table 3's endpoints, Table 2's calibration) but cannot reproduce its data. If the authors release it, this will be added.
  • No reference implementation exists for the target paper, so the compatibility evidence is the ATE special case against DoubleML plus the published numbers of the parent paper — not a line-by-line match against the authors' own code.
  • The sensitivity parameters are assumptions. pragdml estimates the scale S₀; the strengths and the alignment are yours to defend. The robustness value and the benchmarking tools exist to make that defensible, not automatic.
  • Oracle bounds are rate shapes, not guarantees. The universal constants in Propositions 5–8 are unspecified in the paper; the reported numbers use C = 1 and are useful for comparing designs, not as finite-sample bounds.
  • RieszBoost is not implemented. PartitionRieszLearner covers the nonlinear case.
  • 0.x versioning: the API may change.

References

All DOIs verified 17 October 2026.

  1. A. Aradillas Fernández, V. Chernozhukov, C. Cinelli, S. Klaassen, W. Newey, M. Spindler, J. Teichert-Kluge & S. Vijaykumar (2026). Pragmatic DML with AI-Learned Representations. arXiv:2610.01935
  2. V. Chernozhukov, C. Cinelli, W. K. Newey, A. Sharma & V. Syrgkanis (2026). Long Story Short: Omitted Variable Bias in Causal Machine Learning. Review of Economics and Statistics. doi:10.1162/REST.a.1705
  3. V. Chernozhukov, W. K. Newey & R. Singh (2022). Automatic Debiased Machine Learning of Causal and Structural Effects. Econometrica 90(3):967–1027. doi:10.3982/ECTA18515
  4. V. Chernozhukov, D. Chetverikov, M. Demirer, E. Duflo, C. Hansen, W. Newey & J. Robins (2018). Double/Debiased Machine Learning for Treatment and Structural Parameters. The Econometrics Journal 21(1):C1–C68. doi:10.1111/ectj.12097
  5. S. Vijaykumar (2021). Localization, Convexity, and Star Aggregation. NeurIPS 34:4570–4581. arXiv:2105.08866
  6. T. Liang, A. Rakhlin & K. Sridharan (2015). Learning with Square Loss: Localization through Offset Rademacher Complexity. COLT. arXiv:1502.06134
  7. J.-Y. Audibert (2007). Progressive Mixture Rules are Deviation Suboptimal. NeurIPS 20.
  8. P. Bach, V. Chernozhukov, M. S. Kurz & M. Spindler (2022). DoubleML — An Object-Oriented Implementation of Double Machine Learning in Python. JMLR 23:1–6.
  9. C. Cinelli & C. Hazlett (2020). Making Sense of Sensitivity: Extending Omitted Variable Bias. JRSS-B 82(1):39–67. doi:10.1111/rssb.12348

Citing

@software{roudane2026pragdml,
  author  = {Roudane, Merwan},
  title   = {pragdml: Pragmatic double machine learning with AI-learned representations},
  year    = {2026},
  version = {0.1.0},
  url     = {https://github.com/merwanroudane/PragDML}
}

Please also cite the papers the package implements (references 1 and 2 above).

Changelog

See CHANGELOG.md.

License

MIT. See LICENSE.

Author

Dr Merwan Roudane merwanroudane920@gmail.com github.com/merwanroudane

Related packages: QuantileOnQuantile, mqqr, qqkrls, mqqcause (CRAN).

Metadata

Release files for pragdml 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pragdml 0.1.0
File Size Uploaded Provenance
pragdml-0.1.0.tar.gz 271.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pragdml 0.1.0
File Interpreter ABI Platform
pragdml-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 517.7 kB

Release files / pragdml-0.1.0.tar.gz

Download URL pragdml-0.1.0.tar.gz
Size 271.5 kB
Tags Source
SHA-256 checksum
How to use checksums
302870d31c9d4399e3b8abc80888fd3c679f596df2b5ac7cb60633fc4f0c8917
BLAKE2b-256 checksum
How to use checksums
69532fcb9f181d67060ea489c3234c931da4f3af5c9827a4e3e9fe362eaf619b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.0

Release files / pragdml-0.1.0-py3-none-any.whl

Download URL pragdml-0.1.0-py3-none-any.whl
Size 246.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0d03be47595f18a900d74d289767c365cc0ac83599c9e685da5252422eb925cc
BLAKE2b-256 checksum
How to use checksums
5333d3fb86d590511df99866599ef12a8deb1d3c5312851a82db200461672837
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.0

Security for pragdml 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page