Add R-loss (rlearner_score) for CATE model evaluation
Merged July 2026
Added rlearner_score() to causalml.metrics, enabling standalone R-loss evaluation for fitted CATE models with bootstrap confidence intervals and feature parity with EconML's RScorer
Introduced compute_r_residuals() for reusable cross-fitted outcome and treatment residual estimation, while resolving circular import issues between the metrics and meta-learner modules
Refactored BaseRLearner.fit() to reuse the shared residual computation pipeline, eliminating duplicated nuisance-fitting logic and improving maintainability
Optimized training performance by adding a compute_w_residual flag to avoid redundant propensity estimation, eliminating an approximately 4× slowdown identified during code review, and added comprehensive test coverage for correctness, confidence intervals, and imbalanced treatment scenarios
Improve SensitivityMSM robustness and test coverage
Merged July 2026
Replaced the learner blocklist with a subclass-aware validation mechanism, ensuring all supported S-, T-, and DR-Learner implementations are accepted while unsupported learner families continue to raise informative errors
Expanded SensitivityMSM test coverage to include S-, T-, and DR-Learners along with a concrete learner subclass, improving regression protection across supported learner families
Made MSM sensitivity tests deterministic by seeding synthetic data generation with the repository's shared RANDOM_SEED, ensuring reproducible test execution across environments
Added propensity clipping within the MSM bound computation to improve numerical stability when estimated propensity scores are extremely close to 0 or 1, preventing divide-by-zero and floating-point instability
Add DR and plug-in T scoring metrics for CATE model evaluation
Merged July 2026
Implemented compute_dr_pseudo_outcomes() to construct cross-fitted doubly robust (AIPW) pseudo-outcomes, enabling observed-data evaluation of CATE models without requiring access to true counterfactual outcomes
Added dr_score(), a new CATE evaluation metric that measures mean squared error between model-predicted treatment effects and DR pseudo-outcomes, providing a principled surrogate metric for model selection
Implemented plug_in_t_score(), which evaluates CATE estimators against a cross-fitted plug-in T-Learner proxy, offering a complementary baseline for treatment effect estimation accuracy
Designed reusable evaluation utilities so DR pseudo-outcomes can be computed once and shared across multiple metrics, integrating seamlessly with the existing rate_score() workflow without redundant nuisance model fitting
Added optional half-sample bootstrap confidence intervals for both scoring metrics, following the statistical inference framework already established by rate_score()
Resolved reviewer-identified robustness issues by replacing KFold with StratifiedKFold during cross-fitting, preventing failures on highly imbalanced treatment assignments common in observational datasets
Refined the API after code review by removing statistically uninformative p-values from MSE-based loss metrics, updating documentation, and ensuring consistency with the underlying statistical interpretation
Added comprehensive regression tests covering pseudo-outcome construction, model ranking, bootstrap confidence intervals, imbalanced treatment edge cases, compatibility with rate_score(), and overall numerical stability across supported Python versions
Implemented return_components support for both BaseRLearner and BaseRClassifier, extending predict() and fit_predict() with a consistent API matching the existing T- and X-Learners
Exposed the R-Learner's nuisance components with learner-specific semantics by returning yhat (outcome model predictions, E[Y|X]) and p (propensity score estimates, E[W|X]) required for CATE validation and diagnostics
Added the same API safeguards as other meta-learners by preventing simultaneous use of return_ci and return_components, ensuring consistent behavior across the library
Fitted the nuisance outcome model after cross-validation to enable inference-time retrieval of yhat without altering the original R-Learner estimation procedure
Resolved multiple review-driven edge cases by recomputing propensity scores only when return_components=True, preserving backward compatibility and the original performance of standard predict() while avoiding stale training propensities on unseen data
Added comprehensive regression tests covering BaseRLearner, BaseRClassifier, and XGBRRegressor, including different-sized prediction inputs, missing propensity model handling, and mutual exclusion between return_ci and return_components
Updated the synthetic dataset utilities to maintain backward compatibility after the API refinement and ensured all existing and new tests passed across supported Python versions before merge
Add sensitivity bounds for ATE via the Marginal Sensitivity Model (MSM)
Merged July 2026
Implemented SensitivityMSM, a new Sensitivity subclass exposing get_msm_bounds(gamma=[...]), giving closed-form ATE bounds under the Marginal Sensitivity Model (Tan 2006; Dorn & Guo 2023; Dorn, Guo & Kallus 2024), model-agnostic across any learner exposing propensity scores and fitted outcome regressions
Added get_potential_outcome_predictions() on the base Sensitivity class to extract μ0/μ1 potential-outcome predictions from meta-learner output
Fixed a correctness bug caught in review: the initial implementation misread the X-learner's return_components output (two CATE estimates from tau models, not potential outcomes), silently degrading the bound — resolved by explicitly rejecting unsupported learners (X-learner, R-learner) with a clear NotImplementedError rather than misinterpreting their output
Added a Γ=1 ≈ true-ATE regression assertion to the test suite specifically to catch this class of error going forward, and moved the test to BaseTLearner
Documented the Gamma (propensity odds-ratio) parameterization as distinct from the partial-R² robustness value used by EconML/DoWhy, framing the new method explicitly as fragility-diagnostic tooling consistent with the rest of the sensitivity module
Scoped deliberately to binary treatment and ATE only per maintainer discussion, closing Issue #916 with plot_msm_bounds() and breakdown_gamma() explicitly deferred as tracked follow-ups
Add CatBoost documentation and test coverage for Explainer
Merged July 2026
Investigated an issue requesting CatBoostRegressor support as the model_tau estimator in Explainer when method="auto", and verified that current CatBoost versions already expose feature_importances_ after fitting — the existing implementation already worked correctly
Updated the Explainer documentation to explicitly mention CatBoost support for model_tau, closing the gap between actual and documented behavior
Added a regression test using CatBoostRegressor with pytest.importorskip("catboost") to validate Explainer(method="auto") going forward
Refined the PR based on maintainer feedback, keeping the change scoped to documentation and test coverage rather than introducing unnecessary implementation changes
Closed Issue #826, improving CI reliability by validating CatBoost compatibility without altering existing functionality
Polish Polars support and add bootstrap CI test coverage
Merged July 2026
Hoisted X_new_c and X_new_t construction outside the S-Learner per-group loop, eliminating redundant matrix allocations and restoring the original optimization
Simplified the X-Learner by reusing the existing y_filt_np array instead of performing redundant filtering, avoiding unnecessary conversions at the scikit-learn boundary
Replaced the hardcoded RANDOM_STATE in the Polars test suite with the shared RANDOM_SEED constant from tests.const to align with project conventions
Added regression tests covering the store_bootstraps=True → predict(return_ci=True) workflow for both pandas and Polars DataFrame inputs, ensuring the previously fixed bootstrap path remains protected by CI
Added dedicated Polars compatibility tests for BaseDRClassifier, covering a previously untested execution path and expanding native Polars test coverage
Addressed all four follow-up items from Issue #920, improving performance, simplifying implementation, and strengthening regression coverage without introducing any breaking changes
Add native Polars DataFrame, Series, and LazyFrame support for all meta-learners
Merged July 2026
Added collect_if_lazy(X), n_rows(X), filter_mask(), filter_index(), prepend_column(), concat_treatment_col(), and to_numpy() to utils.py — a minimal abstraction layer that dispatches to the correct numpy/pandas/polars operation based on input type, with no external Polars dependency when it is not installed
Applied the native-X contract across all five learner families (S/T/X/R/DR): collect_if_lazy(X) called once at the top of every public method; feature matrices kept in their native format end-to-end; treatment/y/p/sample_weight normalised to numpy at method entry only
Fixed BaseLearner.bootstrap() and _fit_bootstrap_clone() in base.py to resample via filter_index(X, idxs) and n_rows(X) instead of X[idxs]/X.shape[0], fixing a regression for both pandas and polars DataFrames in the store_bootstraps=True → predict(return_ci=True) path
Fixed BaseLearner._set_propensity_models() to filter X natively via filter_mask and pass it as-is to sklearn and XGBoost, both of which accept pandas and Polars DataFrames natively at their pinned minimum versions (sklearn ≥1.6, XGBoost ≥3.1)
Removed top-level from causalml.inference.meta.utils import convert_pd_to_np from propensity.py which caused a circular import on cold import of causalml.propensity; kept convert_pd_to_np as a deprecated backward-compat alias in utils.py so explainer.py and other existing callers are unaffected
Fixed X-learner model_mu_c to be fitted once on the full control set and exposed as a shared-reference dict {group: self.model_mu_c}; self.var_c stored as a finite scalar; same pattern applied to BaseXClassifier; removed crash-causing X[bool_mask] preamble in both predict() overrides
Fixed DR-learner KFold cross-fit partitions to slice X natively via filter_index(X, idx) across all three cross-fit folds; fixed X = collect_if_lazy(X) buried inside the fit() docstring (dead text, never executed) and moved it to the method body
Fixed S-learner np.hstack replaced with type-safe concat_treatment_col/prepend_column; prepend_column converts to numpy for pandas DataFrames to avoid mixed int/str column name errors; control/treatment counterfactual frames hoisted above the per-group loop (2·N copies → 2)
Declared polars>=1.0.0 as [project.optional-dependencies] polars and self-referenced in the test extra so pytest.importorskip actually runs in CI; documented LightGBM caveat (known sklearn-API bug with Polars, lightgbm-org/LightGBM#6849)
Added 35 Polars support tests to tests/test_polars_support.py covering numpy == pandas == polars == LazyFrame equivalence for all five learner families and their classifier variants (BaseTClassifier, BaseSClassifier, BaseXClassifier), store_bootstraps=True → predict(return_ci=True) with DataFrame X, DR-classifier polars path, mixed inputs, and fit-on-numpy/predict-on-polars edge cases; addressed all blocking and non-blocking maintainer review comments across 8+ review rounds including merge conflict resolution across concurrent upstream refactors (#912, #910, #886)
Make meta-learners scikit-learn compliant via BaseEstimator
Merged Jun 2026
Made BaseLearner inherit sklearn.base.BaseEstimator, giving every subclass get_params/set_params for free and enabling Pipeline and GridSearchCV compatibility out of the box
Refactored all five learner families (S/T/X/R/DR) to store constructor arguments verbatim in __init__ with no logic or deepcopy; all model construction deferred to fit()
Replaced the bespoke _unfitted_clone/_model_*_template machinery introduced in #910 with a direct clone(self) call in the bootstrap path, eliminating the regression where clone(self, safe=False) deepcopied fitted models on every bootstrap iteration
Fixed XGBRRegressor to use an explicit named-parameter signature with xgb_kwargs=None instead of *args/**kwargs, deferring all XGBRegressor construction to fit() so get_params()/clone() work correctly
Moved learner-presence validation out of __init__ into fit() across all learners, since __init__-time assertions break clone()
Added self.propensity = {} sentinel to BaseXLearner and BaseDRLearner so estimate_ate(pretrain=True) before fit() raises a clean ValueError instead of AttributeError
Fixed BaseTClassifier.predict fail-fast ordering to match BaseTLearner.predict, checking mutually exclusive flags at the top before any computation
Made fit() return self across all learners for Pipeline/GridSearchCV method chaining; nested params now visible via get_params (e.g. learner__max_depth)
Added 31 sklearn compliance tests to test_meta_learners.py covering clone()/get_params() round-trips for all 8 learner variants, fit() returns self, XGBRRegressor bootstrap CI path, bit-identical equivalence guards, and propensity sentinel consistency
Addressed all blocking and non-blocking maintainer review comments across 6+ review rounds, including merge conflict resolution, Cython extension troubleshooting on Windows, architecture consistency, and statistical correctness
Add Post-Fit Confidence Intervals to BaseTLearner via store_bootstraps and return_ci
Merged May 2026
Added store_bootstraps=False to BaseTLearner.fit(), enabling storage of a bootstrap ensemble after training for train-once, score-many workflows
Added return_ci=False to BaseTLearner.predict(), allowing confidence intervals to be generated on new unseen datasets without retraining
Introduced a reusable bootstrap ensemble framework through BaseLearner.fit_bootstrap_ensemble(), making the implementation extensible to additional causal inference meta-learners
Refactored bootstrap training into module-level helper functions to eliminate joblib parallelization and pickling issues caused by nested functions
Replaced deepcopy() with sklearn.base.clone() following EconML-style design patterns for efficient model replication and reproducibility
Added support for reproducible bootstrap inference through random_state handling and parallel execution via joblib
Extended confidence interval support to BaseTClassifier.predict(), enabling uncertainty estimation for classification-based treatment effect models
Added comprehensive test coverage for reproducibility, parallel execution (n_jobs > 1), random seed behavior, BaseTLearner confidence intervals, and BaseTClassifier confidence intervals
Addressed all blocking and non-blocking maintainer review comments across multiple review rounds, including architecture refactoring, API consistency, parallelization safety, and statistical correctness
Add Bootstrap Confidence Intervals and P-values to rate_score()
Merged Apr 2026
Extended rate_score() in causalml/metrics/rate.py with return_ci=False, n_bootstrap=200, alpha=0.05, and random_state=None parameters following sklearn conventions
When return_ci=True, uses half-sample bootstrap (m = n // 2, without replacement) per the Yadlowsky et al. (2021) functional CLT, returning SE, CI bounds, and a two-sided p-value testing H0: RATE = 0
Refactored integration logic into a module-level _compute_rate_from_toc() helper to eliminate code duplication and avoid joblib pickle issues with nested functions
Added 4 new tests to tests/test_rate.py using existing synthetic_df and rct_df fixtures and RANDOM_SEED from tests/const.py; addressed all blocking and non-blocking review comments across two review rounds; passed black and CI checks
Bootstrap inference verified correct against the Yadlowsky et al. (2021) paper by the maintainer across two review rounds
Add Rank-weighted Average Treatment Effect (RATE) Metric
Merged Mar 2026
Added causalml/metrics/rate.py with three public functions — get_toc(), rate_score(), and plot_toc() — following the exact same API conventions as get_qini / qini_score / plot_qini
get_toc() computes the Targeting Operator Characteristic curve via O(n) cumulative sums; rate_score() computes the RATE scalar with AUTOC (1/q) or Qini (q) weighting; plot_toc() visualizes the TOC curve
Supported both oracle mode (simulated tau) and observed RCT mode (y + w); fixed normalize division-by-zero by using max(|TOC|) instead of TOC(1); added logger.warning for observed-outcome fallback
Added 20 unit tests in tests/test_rate.py; addressed all blocking and non-blocking review comments across two review rounds; passed black and pre-commit clean
Implementation verified correct against the Yadlowsky et al. (2021) paper and the grf R package reference by the maintainer
Add Native NaN Support for UpliftTree and UpliftRandomForest
Merged Mar 2026
Added native NaN routing logic to each candidate split, evaluating both left/right directions and learning the optimal routing per node — consistent with scikit-learn's decision tree behavior
Stored the learned NaN routing in each DecisionTree node and applied it consistently during training, pruning, filling, and prediction
Guarded all np.isnan() calls with np.issubdtype(..., np.number) to prevent TypeError on string/categorical columns
Added NaN-aware percentile calculation by filtering out NaN values before computing split thresholds
Added two targeted tests: one for NaN values in numeric columns, one for None values in object-dtype columns