Changelog

All notable changes to XCausal are documented here. Dates are release dates; “Unreleased” entries describe changes on main not yet tagged.

Unreleased

0.9.0 — 2026-10-28

  • Seed and limit workers inside explicit X-Learner effect models. EconMLAdapter now includes cate_models when it propagates random_state and, inside an XCausal worker, pins n_jobs to 1. The slot may be one estimator, a list, or a tuple. A scikit-learn Pipeline is walked through to the step that owns those settings, so a nested random forest is seeded and limited to one job with the rest of the component. Outside a worker the user’s n_jobs is left unchanged. Default X-Learners keep cate_models=None; their seeded predictions are unchanged.

  • Require CausalML 0.18.0 and drop the R-learner model_p workaround. The causalml and all extras are now causalml>=0.18.0,<0.18.1. CausalML 0.18.0 assigns model_p from propensity_learner before fitting (uber/causalml#937), so the propensity model that is trained is the one configured on BaseRRegressor or BaseRClassifier, including a replacement on a component returned by default_methods(). The extra model_p estimator set at construction was only needed for the 0.16 and 0.17 initialization order, where that separate instance was what fit() actually trained.

    On CausalML 0.18.0, removing the extra instance does not change seeded default R-learner predictions: fit() was already training propensity_learner. On CausalML 0.17 the workaround’s separate model_p was the propensity model that fit() trained, and it received a different adapter seed than propensity_learner. Holding every other seed fixed, that swap moves Lalonde CATE by up to about 4770. Those 0.17 fits are not interchangeable with 0.18 fits.

  • Corrected subsample confidence intervals with the finite-population scale. method="subsample" now uses sqrt(m/(n-m)), with the actual integer subsample size m and training size n, for ensemble ATE/CATE intervals and reconstructed component intervals, summaries, and forest plots. The default fraction remains 0.5; at exact half-sampling interval widths increase by sqrt(2). Nonparametric bootstrap intervals, point estimates, fitted weights, seeds, resampling, and raw replicate arrays are unchanged. The correction is motivated by regular asymptotic linearity; it does not establish general CATE coverage or validity for nonregular aggregation or every inferential target under treatment stratification.

    Subsample interval reconstruction now requires integer subsample_m and n_train metadata satisfying 1 <= subsample_m < n_train, rather than silently falling back to scale 1 for missing metadata. Nonparametric results still need no size metadata.

    Saved results: older pickled BootstrapResult objects retain their stored ATE/CATE endpoints, while ci_at(), component summaries, and component forest-plot intervals use the installed code. Recompute stored endpoints consistently before using these objects with the new code. With valid size metadata, an old stored endpoint can be transformed as theta + sqrt(n/(n-m)) * (endpoint - theta), using the corresponding original-fit ATE or CATE as theta. No model refits are needed solely for this correction; retain provenance for the original results.

  • Clarified the ate() docstring. Agreement and supervised strategies return the mean of fitted ensemble CATE predictions over the evaluation covariates; AIPW pseudo-outcomes, when used, serve as training targets for supervised weights. Pointwise strategies aggregate component ATE estimates directly. This is a documentation-only change with no change to returned estimates.

  • Warn when binary ensemble point estimates leave [-1, 1]. The new public RiskDifferenceWarning, a direct subclass of XCausalWarning, applies to every aggregation strategy. cate() reports the count of out-of-range entries; ate() checks the ensemble ATE and, for agreement/supervised strategies, the ensemble CATE used to compute it. bootstrap() checks the original-fit ATE and CATE at most once each, without warnings for individual replicates or CI endpoints. Warnings point to the caller. Point estimates, fitted weights, bootstrap distributions, and confidence intervals remain unchanged; no clipping or constraints are added. Existing warning-as-error filters on XCausalWarning also catch this new category.

0.8.0 — 2026-10-21

  • Renamed the project to XCausal. The PyPI distribution and import package are now xcausal. MetaCausalWarning is now XCausalWarning, and the parallelism sentinel is XCAUSAL_INNER_WORKER (xcausal.adapters.INNER_WORKER_ENV). This is the first xcausal release; it continues the code previously published as metacausal 0.7.4. Entries below keep the wording from those releases.

0.7.4 — 2026-10-17

  • Widened the doubleml, econml, and causalml caps to admit each library’s current latest patch/minor (doubleml<0.11.5, econml<0.17.1, causalml<0.18.1), validating CI (including pytest -m integration) against doubleml==0.11.4, econml==0.17.0, and causalml==0.18.0. Floors are unchanged; per the versioning policy in pyproject.toml, only the cap moves until the floor itself is deliberately re-validated and bumped.

0.7.3 — 2026-07-24

  • README bootstrap recipes now use n_boot=10, not 200/500. A code comment at each call site states the recommended 200+ (500+ for the last recipe) for real use. This keeps the examples fast to actually run, and — paired with the new CI gate below — lets every README recipe execute in about a minute instead of the 10+ minutes the documented values would take across the full multi-framework default pool.

  • Added a mechanical README example gate to CI. A new docs job step (phmdoctest) extracts every fenced Python block from README.md and executes it, unmodified, on every push — closing the same class of gap the docstring doctest gate (0.7.0) closed for docstrings: a plain code block, however carefully checked by hand once, can silently drift as the package evolves without something actually running it.

  • Fixed two more non-executable README snippets, found while building that gate. The CausalForestDML-workaround recipe (under the “Known issue” callout) and the closing “Reproducibility and parallelism” recipe were each missing an import (CausalEnsemble; CausalEnsemble and CausalStacking respectively) that a reader copy-pasting just that snippet would have hit as a NameError — the same bug class as the two README fixes in 0.7.0 and 0.7.2, just not yet mechanically caught until this release’s gate existed.

  • Removed a stray reference to “the bundled replication runner” from the “Reproducibility and parallelism” section. No such runner is bundled here.

0.7.2 — 2026-07-20

  • Fixed a data-contamination bug in the README’s “CATE estimation with a supervised strategy” recipe. It relied on X, T, Y without redefining them, per the “Usage recipes” convention — but the preceding “Binary outcome on real data” recipe rebinds X, T, Y to a binary outcome, so running the recipes in order silently fit CausalStacking against binary data instead of the continuous Lalonde outcome the recipe’s own comments describe. It now reloads X, T, Y = load_lalonde() explicitly.

  • Documented a known CausalForestDML segfault risk under bootstrap. EconML’s generalized-random-forest tree builder can intermittently crash (SIGSEGV/SIGBUS) under the repeated component refits bootstrap()/estimate(..., n_boot=...) perform on the default pool — the same latent upstream bug (py-why/EconML#470, open, unfixed as of EconML 0.16.0). Added a “Known issue” callout to README’s “Reproducibility and parallelism” section with a copy-pasteable default_methods()-filtering workaround.

  • summary() methods now display correctly when typed bare at a REPL/notebook prompt. AteEstimate.summary(), CateEstimate.summary(), CausalEnsemble.summary(), and BootstrapResult.summary() build multi-line, human-formatted text, but as plain str returns, bare-expression evaluation (which uses repr(), not str()) showed embedded newlines as literal \n rather than rendering them – forcing print(...) to be remembered every time. They now return SummaryStr, a str subclass overriding only __repr__ to match __str__; every other str behavior (isinstance, equality, concatenation, formatting, etc.) is unchanged.

  • Added BootstrapResult.ci_at(), a confidence interval at an arbitrary level from an already-computed bootstrap run, without re-fitting. Previously, bootstrap() only returned a CI at the single alpha it was called with, for the ensemble’s own aggregated ATE; getting a CI for another level, for a specific component (component_boot_ates), or for a derived statistic (e.g. a per-replicate mean across components) required hand-rolling the same Politis-Romano scaled-percentile formula against the raw distributions. ci_at() exposes that formula directly, and defaults to reproducing the ensemble’s own CI. bootstrap()’s internal CI construction now calls the same shared implementation, so there’s a single source of truth for the formula.

  • Fixed BootstrapResult.component_ate_summary() (and thus summary()’s printed per-component rows) ignoring the method="subsample" scale correction. It computed per-component CIs with plain percentiles regardless of resampling scheme, while the ensemble-level ate_ci_lower/ate_ci_upper already correctly applied the Politis–Romano sqrt(m/n) scale/center correction for method="subsample" — an inconsistency between the two, not a difference in scheme handling. Component CIs now also center on the component’s own full-sample fit estimate (component_ate_estimates) rather than the bootstrap mean, matching the class’s documented point-estimate convention (falls back to the bootstrap mean when unavailable, e.g. on a hand-built BootstrapResult). Under method="nonparametric" (the default), this recentering is a no-op — the scale factor is 1.0, so the formula reduces algebraically to the previous plain-percentile result; only method="subsample" users are affected. Also added BootstrapResult.n_train and .scale, needed to reconstruct the correction after the fact.

0.7.1 — 2026-07-18

  • Fixed the default R-Learner pool components silently ignoring their HGB propensity model. CausalML’s BaseRLearner.fit() assigns self.model_p = self.propensity_learner after calling _set_propensity_models(), which reads self.model_p (not propensity_learner) to decide whether to use a custom propensity model. On a fresh BaseRRegressor/BaseRClassifier instance, model_p doesn’t exist yet at that point, so every first fit() silently fell back to CausalML’s own ElasticNetPropensityModel() default instead of the HistGradientBoostingClassifier the default pools intended — and that default’s saga+elasticnet grid search routinely fails to converge on Lalonde-sized data, flooding ConvergenceWarnings in interactive use. The shipped default pools now set model_p directly at construction, alongside the existing cv_n_jobs=1 workaround. This is a known upstream bug (fixed on CausalML’s master via uber/causalml#937, not yet in a PyPI release as of causalml==0.17.0); the workaround is harmless once CausalML ships the fix.

  • Widened the causalml, stochtree, and doubleml caps to admit each library’s current latest patch/minor (causalml<0.17.1, stochtree<0.4.6, doubleml<0.11.4), validating CI (including pytest -m integration) against causalml==0.17.0, stochtree==0.4.5, and doubleml==0.11.3. Floors are unchanged; per the versioning policy in pyproject.toml, only the cap moves until the floor itself is deliberately re-validated and bumped.

0.7.0 — 2026-07-17

  • New aggregation strategy: Select. Chooses the single component model minimizing a pseudo-outcome risk (loss="dr" for DR/AIPW plug-in risk, loss="r" for R-risk on the Robinson residuals) instead of combining components. Implemented as a SupervisedStrategy so it shares cross-fitted nuisance estimation, EnsembleWeights introspection, and bootstrap with the true aggregators — it returns a one-hot weight vector rather than a blend. Select(loss="dr") is the closed-form vertex of QAggregation’s nu=1 limit, computed directly by argmin rather than via the general solver. Serves as the feasible-selection comparator for ensemble-vs-selection studies.

  • Fixed core/process oversubscription from CausalML’s R-Learner default pool component. CausalML’s BaseRLearner family (BaseRRegressor/BaseRClassifier, in the default continuous/binary pools) defaults cv_n_jobs=-1 for its internal cross_val_predict call — unlike EconML’s n_jobs, which most wrapped estimators default to serial, this knob is on by default, so left alone it spawned a full-core joblib pool on every single fit, nesting inside MetaCausal’s own outer parallelism (fit()/bootstrap(), or an external harness’s own outer parallel loop) and oversubscribing cores. The shipped default pools now pin cv_n_jobs=1 at construction, and CausalMLAdapter gates the wrapped model’s cv_n_jobs on METACAUSAL_INNER_WORKER (mirroring EconMLAdapter’s existing n_jobs suppression) so user-supplied R-Learner components get the same protection. The two adapters’ pinning logic is now shared via metacausal._parallel.force_serial.

  • Plotting functions are now also methods. forest, weights, and cate_profile (from metacausal.plots) each stay the primary, documented implementation, but are now also reachable as thin delegating methods on the class they act on: BootstrapResult.forest(), BootstrapResult.cate_profile(x, ...), CateEstimate.cate_profile(x, ...), EnsembleWeights.plot(), CausalEnsemble.weights(), and CausalEnsemble.disagreement(X). Both surfaces stay in sync — the methods just call the functions — and matplotlib remains an optional dependency imported only when one of these six methods (or functions) is actually called.

  • Added a Sphinx API reference, published at asmahani.github.io/metacausal. Full public-API coverage via autodoc/napoleon/autosummary — CausalEnsemble, every result/estimate class, metacausal.aggregation, metacausal.adapters, metacausal.plots, metacausal.datasets — built with the furo theme, and checked in CI with sphinx-build -W (warnings-as-errors) plus the doctest builder. Deployed to GitHub Pages automatically on every push to main. Build locally with pip install -e ".[all,docs]" then python -m sphinx -b html docs docs/_build/html.

  • Fixed three non-executable docstring examples. CausalEnsemble, DoubleMLAdapter, and StochtreeAdapter each had an Example:: code block that referenced undefined placeholder data (X_train/T_train/Y_train or X/T/Y), and CausalEnsemble’s additionally passed DoubleMLIRM the wrong kwarg (ml_l=, which is PLR-specific; IRM takes ml_g=). All three are now self-contained, fast, and written as real >>> doctests that the new docs CI job actually executes.

  • Fixed two non-executable README snippets. The Visualisation helpers recipe referenced an undefined grid, and the CATE-with-a-supervised-strategy recipe referenced an undefined X_eval; both are now defined inline. Every Python code block in this README (except the intentionally-skeletal GenericCATEAdapter template) has been run end to end against this release. Added a note at the top of “Usage recipes” clarifying that snippets reusing X, T, Y without redefining them assume the Quick start data.

  • Published the custom-component parallelism cooperation contract. metacausal.adapters.INNER_WORKER_ENV (the sentinel env var custom components can check to cooperate with MetaCausal’s own-worker parallelism guard) is now a public, documented export, not just an underscore-prefixed internal in metacausal._parallel. Documented the cooperation contract in GenericCATEAdapter’s docstring and README’s “Extending MetaCausal” section (with a worked example), and added a sentence to “Reproducibility and parallelism” stating known-adapter n_jobs/cv_n_jobs suppression (EconML, CausalML, DoubleML, stochtree) as an ongoing guarantee rather than a changelog-only artifact.

0.6.1 — 2026-07-01

  • Documentation: the README quick start and recipes now use the result objects’ summary() methods and the EnsembleWeights repr (from 0.6.0) in place of hand-formatted loops; corrected the stated default continuous-pool size (nine, not ten) and aligned the bootstrap wording with the rest of the docs (“full-pipeline”, not “honest”).

0.6.0 — 2026-06-30

  • Human-readable result summaries: CausalEnsemble and the main result objects now expose compact __repr__ output, and AteEstimate, CateEstimate, and BootstrapResult now provide explicit summary() methods for terminal-friendly multi-line reports without dumping full NumPy arrays. summary() accepts digits= and signed= for column formatting; AteEstimate.summary() additionally accepts show_ci=False to omit the per-component native confidence intervals (cleaner point-estimate table when CIs are unavailable for some methods or reported separately).

  • EconML/sklearn deprecation suppression: EconMLAdapter now suppresses sklearn’s FutureWarning about force_all_finite being renamed to ensure_all_finite around wrapped EconML prediction and analytical-interval calls (ate, cate, and their fallback/effect paths). The filter is adapter-local, narrowed by category + message + module, and scoped to the single call so unrelated FutureWarnings still surface.

0.5.0 — 2026-06-15

  • bootstrap() flags a point estimate outside its CI under both resampling schemes. The BootstrapWarning for an ATE confidence interval that does not contain the point estimate previously fired only under the subsample scheme; it now fires under nonparametric too, with a scheme-specific explanation. Containment is governed by the same percentile condition for both schemes — the √(m/n) scaling changes only the interval width — and with-replacement resampling (~63% distinct units) or per-replicate weight re-optimization can shift the replicate distribution off the point estimate. Pre-1.0 behavior change: nonparametric bootstraps that previously ran silently may now emit a warning.

  • Single-valued outcomes are rejected with an actionable error. infer_outcome_type previously classified a constant Y (all zeros, all ones, or any single repeated value) as binary, which surfaced later as an opaque scikit-learn “needs samples of at least 2 classes” failure. It now raises a clear ValueError up front. Pre-1.0 behavior change.

  • Docstring corrections. The supervised aggregation strategies (CausalStacking, RStacking, QAggregation) documented a stale LGBMRegressor/LGBMClassifier default and omitted the regressor-vs-classifier requirement for binary outcomes; they now describe the HistGradientBoosting* defaults and the outcome-type-dependent model contract. GenericCATEAdapter no longer references a non-existent extensibility guide, and its fn_ate example now performs a genuine doubly robust (AIPW) computation instead of silently reproducing the default mean(cate(X)).

  • Correction to the 0.4.0 parallel-safety note. The intermittent segmentation faults in EconML’s Cython tree builder (CausalForestDML) are a latent out-of-bounds bug in EconML’s generalized random forest — not merely loky oversubscription. They reproduce single-threaded, surfacing under the repeated component refits a bootstrap performs (EconML #470, unresolved as of econml 0.16.0). The 0.4.0 n_jobs=1 pin is kept as defensive hardening (it removes genuine oversubscription and lowers crash probability) but does not fully eliminate the crash: bootstrap() with CausalForestDML in the pool can still segfault intermittently under heavy parallelism.

0.4.0 — 2026-06-07

  • Parallel-safety fix: the default CausalForestDML now builds its forest with n_jobs=1, and EconMLAdapter pins any wrapped joblib-parallel estimator to a single job when it runs inside one of MetaCausal’s own parallel workers (fit, bootstrap, or supervised cross-fitting). EconML defaults CausalForestDML to n_jobs=-1, whose inner loky pool would otherwise nest inside the outer worker and intermittently segfault EconML’s Cython tree builder under oversubscription. Outputs are unchanged — the forest is seeded deterministically — only the nested parallelism is removed.

  • Default pool update: S-Learner and its classifier sibling (BaseSClassifier) are now excluded from the default pools for continuous and binary outcomes, respectively; the default pools are now nine and seven components.

0.3.1 — 2026-06-05

  • Dependency declaration: joblib>=1.2 is now a direct core dependency instead of arriving only transitively through scikit-learn.

  • Top-level adapter imports: DoubleMLAdapter, EconMLAdapter, and StochtreeAdapter are now exported from metacausal, so users no longer need submodule import paths for those adapters.

  • Custom CATE shorthand: CausalEnsemble now auto-wraps 2- and 3-callable lists/tuples as GenericCATEAdapter, allowing (fn_fit, fn_cate) and (fn_fit, fn_cate, fn_ate) directly in methods=[...].

0.3.0 — 2026-05-30

  • Analytical CI controls: DoubleMLAdapter(alpha=...) now forwards to DoubleML.confint(level=1 - alpha), and EconMLAdapter(alpha=..., inference=...) now forwards both the interval level and the fit-time inference backend.

  • CausalML clarification: analytical ATE CI level was already configurable upstream via the wrapped estimator (ate_alpha / estimator-specific alpha); the docs now describe that accurately instead of implying a missing adapter feature.

  • API cleanup: removed the unused alpha argument from CausalEnsemble.cate(). Analytical component-level CI configuration now lives on the relevant adapter or wrapped upstream estimator, while ensemble CI level remains on bootstrap(alpha=...).

0.2.2 — 2026-05-04

  • Dependency hygiene: the four causal-ML extras (econml, doubleml, stochtree, causalml) are now pinned to a single patch each — floors at the exact version validated by CI on the most recent main-branch run, caps at the next patch. Motivated by stochtree #376, where a patch release (0.4.0 → 0.4.2) silently changed the semantics of BCFModel.predict(terms="tau") and broke 0.2.0. Patch-level caps mean every upstream release lands outside the cap, triggers a Dependabot PR, and runs pytest -m integration before we widen — closing the silent-install hole that produced the 0.2.1 hotfix.

0.2.1 — 2026-05-04

  • Bug fix: StochtreeAdapter now calls BCFModel.predict(..., terms="cate") instead of terms="tau". With stochtree 0.4.2 (which added a parametric treatment-intercept term in the BCF sampler), terms="tau" returned the forest-only piece and excluded the parametric component, producing wildly seed-sensitive ATEs that disagreed sharply with the rest of the default ensemble. terms="cate" returns the full conditional treatment effect — including parametric and random-slope components, when present — for any BCF configuration. Fixes upstream issue stochtree #376 on the metacausal side.

0.2.0 — 2026-05-04

New

  • Outcome-type handling: CausalEnsemble auto-detects continuous vs binary Y at fit(), materialises the right default pool, and routes nuisance through predict_proba for binary. Override via outcome_type="continuous"|"binary". Public metacausal.infer_outcome_type(Y) utility; binarize_y={"median","positive"} on load_lalonde().

  • Subsample bootstrap (bootstrap(method="subsample")): m-out-of-n without replacement, T-stratified, with Politis–Romano scaled-percentile CIs. Eliminates duplicate-unit leakage across cross-fit folds.

  • Structured warning hierarchy: ComponentFailureWarning, ComponentExclusionWarning, BootstrapWarning under a common MetaCausalWarning umbrella.

  • CausalMLAdapter accepts a propensity_model= kwarg, forwarding a fitted propensity to non-TMLE meta-learners.

Breaking changes for custom-strategy / custom-adapter authors

  • AggregationStrategy and family are abc.ABC with a unified aggregate entry point. Subclasses now implement aggregate rather than per-mode methods.

  • Every adapter must declare supported_outcome_types and implement validate_outcome_type(detected); the injectable fit_nuisance_fn gains an outcome_type parameter.

Other

  • Bounded version constraints on econml, doubleml, causalml, stochtree (capped at next minor; floors anchored to tested versions).

  • requires-python raised to >=3.11 (causalml 0.16 floor).

  • Tier-2 integration tests via pytest -m integration.

  • Bug fixes: load_lalonde no longer leaks a file handle; EconMLAdapter suppresses the upstream DataConversionWarning from DRLearner(discrete_outcome=True).

  • PyPI metadata polish (classifiers, license badge).

Tested against: doubleml 0.11.2, econml 0.16.0, causalml 0.16.0, stochtree 0.4.0.

0.1.0 — 2026-04-25

Initial public release.