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:
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
Depends on #1035 — that issue establishes the single
check_random_statenormalization 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 optionalrngand 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 norng, 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 Add post-fit confidence intervals toBaseTLearnerviastore_bootstrapsandreturn_ci#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_stateis 0 for bothslearner.pyandxlearner.py, so they have norandom_stateparameter 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 wantsclone()/get_paramsround-trip coverage. That is seven classes across two families:BaseSLearner,BaseSRegressor,BaseSClassifier,LRSRegressor(which constructsStatsmodelsOLSfor the caller and needs care), plusBaseXLearner,BaseXRegressor,BaseXClassifier.Suggested order: land #1035 first, then add
random_statetoBaseSLearnerandBaseXLearnerand passrngat the six call sites.Observed on master (
477cd0a).🤖 Generated with Claude Code