BaseRClassifier ignores cv_n_jobs during outcome cross-validation
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 25/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Stale
- Tech stack
- python, scikit-learn
- Domain
- machine-learning
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
mastersource - 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
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from uber/causalml
-
Add a pickle-size regression test for fitted learners (v1.0 M1)Possibly taken @techi101 claimed this 7 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 67/100
-
Difficulty 5/5 Over a week Newbie friendliness 20/100
-
cate_scoring default learner is oversized for its own per-fold, per-arm call patternPossibly taken @su-jin1425 claimed this 78 days ago. Openenhancement
Difficulty 4/5 3-5 days Newbie friendliness 48/100
-
S-, T- and X-learner bootstrap confidence intervals are not reproduciblePossibly taken @su-jin1425 claimed this 78 days ago. Openbug
Difficulty 4/5 3-5 days Newbie friendliness 52/100
-
sklearn metadata routing: let `treatment` reach a learner inside a PipelineMay be free again @su-jin1425 claimed this 90 days ago, and no pull request is open. Openenhancement
Difficulty 4/5 3-5 days Newbie friendliness 55/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
pierreaubert/gpui-toolkit#114 ·
Maintainers usually reply within 1 day
-
intake mcp-intake needs-human-review priority:high type:bug
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Ikalus1988/MisakaNet#4903 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
FinanceFlash/unvibecode#819 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day