S-, T- and X-learner bootstrap confidence intervals are not reproducible
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 52/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Quiet
- Tech stack
- numpy, python, scikit-learn
- Domain
- machine-learning
Research direction
Start with dependency #1035 and BaseLearner.bootstrap() in base.py:117, then inspect the S-, X- and legacy T-learner call sites listed in slearner.py, xlearner.py and tlearner.py. Done means the affected learner classes expose the seeded parameter, bootstrap intervals are reproducible independently of the global NumPy stream, and clone()/get_params round-trip coverage is present.
Written by the indexing model from the issue text.
Description
Depends on #1035 — that issue establishes the single check_random_state normalization path this one threads through the S- and X-learner families. Landing this first would mean either duplicating that decision or redoing it.
BaseLearner.bootstrap() (base.py:117) takes an optional rng and falls back to the global numpy stream when it is not supplied (base.py:133-137). #1032 fixed this for the R-learner (#1029). The remaining callers still pass no rng, so on master the R-learner is the only meta-learner whose bootstrap confidence intervals are reproducible:
slearner.py:215,:308xlearner.py:305,:422tlearner.py:297,:395— the legacy loop only. T-learner'sfit(store_bootstraps=True)→fit_bootstrap_ensemble()path from #886 is already seeded, and that is whatpredict(return_ci=True)uses.
DR-learner is unaffected: it threads an explicit per-call seed.
So BaseSRegressor(...).estimate_ate(..., bootstrap_ci=True) returns different intervals on every call, and unrelated consumption of the global numpy stream between calls changes the result. There is currently no way for a caller to pin them.
Note this is not purely a threading fix: grep -c random_state is 0 for both slearner.py and xlearner.py, so they have no random_state parameter to thread. Adding one is a backward-compatible API addition, which is why #1029 scoped itself to the R-learner.
Since #912 these are BaseEstimators, so the new parameter must be stored verbatim in __init__ per the scikit-learn convention, and each affected class wants clone() / get_params round-trip coverage. That is seven classes across two families: BaseSLearner, BaseSRegressor, BaseSClassifier, LRSRegressor (which constructs StatsmodelsOLS for the caller and needs care), plus BaseXLearner, BaseXRegressor, BaseXClassifier.
Suggested order: land #1035 first, then add random_state to BaseSLearner and BaseXLearner and pass rng at the six call sites.
Observed on master (477cd0a).
🤖 Generated with Claude Code
- 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
-
BaseRClassifier ignores cv_n_jobs during outcome cross-validationPossibly taken @ayuxy027 claimed this 17 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 25/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
-
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