causalml Contributions
PR #935
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
View Pull Request ↗
PR #933
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
View Pull Request ↗
PR #930
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
View Pull Request ↗
PR #923
Add return_components support to the R-Learner
Merged July 2026
  • 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
View Pull Request ↗
PR #925
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
View Pull Request ↗
PR #924
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
View Pull Request ↗
PR #921
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
View Pull Request ↗
PR #901
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)
View Pull Request ↗
PR #912
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
View Pull Request ↗
PR #886
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
View Pull Request ↗
PR #890
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
View Pull Request ↗
PR #887
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
View Pull Request ↗
PR #860
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
View Pull Request ↗