BaseRClassifier ignores cv_n_jobs during outcome cross-validation

Open
#1,059 0 comments 0 reactions 0 assignees View on GitHub

@ayuxy027 is already working on this.

Since Oct 28, 2026.

  • #1060 by @ayuxy027 — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
25/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Stale

Research direction

The issue points to causalml/inference/meta/rlearner.py, where BaseRClassifier.fit() calls cross_val_predict with a hardcoded n_jobs=-1; compare this with the BaseRLearner regressor path, which uses self.cv_n_jobs. Start by reading those paths and the tests covering R-learner classifier fitting. Done means outcome cross-validation uses the configured job count while the default remains -1; check linked pull request #1060 for work already underway.

Written by the indexing model from the issue text.

Description

Describe the bug

BaseRClassifier.fit() ignores its inherited cv_n_jobs attribute and calls cross_val_predict(..., n_jobs=-1) unconditionally. Setting model.cv_n_jobs = 1 therefore does not limit cross-validation parallelism. This matters when fits are themselves dispatched in parallel: each outer worker can start its own inner joblib pool.

The behavior is present in CausalML 0.18.0 and in the current master: https://github.com/uber/causalml/blob/d59bb520fc78410c01c443bad0ac57ea972844b8/causalml/inference/meta/rlearner.py#L566-L567. The BaseRLearner regressor path uses self.cv_n_jobs, consistent with the intent of #714.

To reproduce

import numpy as np
from unittest.mock import patch
from sklearn.linear_model import LinearRegression, LogisticRegression
from causalml.inference.meta import BaseRClassifier
from causalml.inference.meta import rlearner

rng = np.random.default_rng(42)
X = rng.normal(size=(100, 3))
treatment = rng.integers(0, 2, size=100)
y = rng.integers(0, 2, size=100)
model = BaseRClassifier(
    outcome_learner=LogisticRegression(),
    effect_learner=LinearRegression(),
    random_state=42,
)
model.cv_n_jobs = 1
actual_cross_val_predict = rlearner.cross_val_predict
requested_jobs = []

def observe_jobs(*args, **kwargs):
    requested_jobs.append(kwargs["n_jobs"])
    # Avoid spawning a large pool while reproducing; this is instrumentation,
    # not a proposed library workaround.
    kwargs["n_jobs"] = 1
    return actual_cross_val_predict(*args, **kwargs)

with patch.object(rlearner, "cross_val_predict", side_effect=observe_jobs):
    model.fit(X=X, treatment=treatment, y=y, p=np.full(100, 0.5), verbose=False)

print("model.cv_n_jobs:", model.cv_n_jobs)
print("cross_val_predict n_jobs:", requested_jobs)

Actual output:

model.cv_n_jobs: 1
cross_val_predict n_jobs: [-1]

Expected behavior

The classifier's outcome cross-validation should use self.cv_n_jobs, so the example requests n_jobs=1. Retaining the existing -1 default would preserve the default level of parallelism.

Environment

  • CausalML 0.18.0; also verified against current master source
  • Python 3.11.7, Windows
  • scikit-learn 1.9.1, NumPy 2.4.6, joblib 1.6.0

Additional context

BaseRClassifier and BaseRRegressor do not currently expose cv_n_jobs in their concrete constructors, so the example sets the inherited attribute after construction. It may be useful to expose the parameter there as well, while preserving sklearn cloning compatibility. XGBRRegressor appears to have a separate hardcoded n_jobs=-1 call in its own fit path: https://github.com/uber/causalml/blob/d59bb520fc78410c01c443bad0ac57ea972844b8/causalml/inference/meta/rlearner.py#L785.

Dominant language
Python
Stars
6k
Forks
892
Avg merge
11h 53m
Merged PRs (30d)
1

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from uber/causalml

All issues in uber/causalml

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.