Closing the loop after PolicyLearner: batched Thompson sampling on the treatment-effect posterior (scoping, following #833)
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 20/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- numpy, python
- Domain
- data, machine-learning
Research direction
This is a design proposal that asks maintainers to decide whether a batched Thompson sampler belongs in causalml/optimize/bandit.py before any code is written. Read the PolicyLearner code in causalml/optimize and the "Choosing an Estimator" docs page to see how randomized, known-propensity data is handled. Done means a maintainer agrees on placement and scope, then a sampler and tests that use only numpy and scipy.
Written by the indexing model from the issue text.
Description
When #833 was closed, you suggested agreeing first on where multi-armed bandits fit relative to causalml.optimize. Here is a narrow proposal, and the question of whether it fits.
Where it would sit. CausalML today takes one dataset, estimates the effect, validates it, and decides who to treat (PolicyLearner, value optimization). The proposal adds the step after that decision: run it as the next randomized batch, update the effect estimate, and decide again. It is one method, not a general bandit collection, and the loop is built around a treatment-effect estimate.
The core: an effect posterior carried from batch to batch.
- Each batch is a randomized experiment with known, fixed assignment shares.
- Within a batch, the method fits a logistic regression with the batch's own intercept and each arm's log odds ratio against a control arm. Only the effects are carried forward: their posterior becomes the prior for the next batch, and the intercept is refit every batch. Anything that moves all arms together within a batch, such as a promotion or a weekday, goes into the intercept instead of the effects.
- The next batch's shares come from Thompson sampling on the effect posterior.
- Because the shares depend only on the batch, the batch intercept stratifies on exactly what the assignment depended on. It is the comparison a propensity-stratified estimate makes, done through the design.
The logs are ordinary causalml input. Every unit was randomized with a known probability, its batch's share for the arm it received (p = shares[treatment]). The accumulated log (X, treatment, y, p, batch) can therefore be analysed afterwards by the existing learners with p passed in, as the "Choosing an Estimator" page describes for randomized data with a known assignment probability. The bandit handles allocation online; CATE estimation, validation and policy_risk stay with the existing tools.
Scale. Uplift in causalml is mostly a risk difference, while this method works on log odds. Within a segment both rank the arms the same way, so allocation is unaffected; reporting a risk difference needs the segment's base rate.
Suggested first PR, following your note on #833: causalml/optimize/bandit.py with
- the batched effect-posterior sampler for a binary outcome and discrete arms, without covariates;
- a Beta-Bernoulli Thompson sampler as the baseline;
- tests, formatted with
black, using only numpy and scipy.
I'd suggest marking it experimental so that it stays outside the v1.0 API freeze. Segments (each segment's effect shrunk toward the common effect by an amount estimated from the data), simulation utilities and a notebook would follow as separate PRs, and only if the first one fits.
A rough shape, with names open to change:
sampler = BatchedEffectThompsonSampler(n_arms=3, control_name=0, random_state=0)
shares = sampler.allocate() # assignment shares for the next batch
sampler.update(treatment=t, y=y) # outcomes observed in that batch
Background: the method is described in arXiv:2609.19709, and I maintain an MIT implementation (orts on PyPI). I would write the causalml version to the library's conventions rather than add a dependency. If this doesn't fit, or belongs somewhere other than optimize, I'd rather know before writing code.
- Dominant language
- Python
- Stars
- 6k
- Forks
- 889
- 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 16 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 25/100
-
cate_scoring default learner is oversized for its own per-fold, per-arm call patternPossibly taken @su-jin1425 claimed this 77 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 77 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
-
documentation type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
add entry to blockOpen
Difficulty 1/5 Under an hour Newbie friendliness 65/100
StevenBlack/hosts#3299 ·
-
`garak --report` overwrites the input report when its name has no ".report"Possibly taken @0z5a claimed this today. Openneeds-triage
Difficulty 2/5 Half a day Newbie friendliness 72/100
Maintainers usually reply within 2 days
-
trace: Pi's 1-hour cache writes are priced at 1.25x, but billed at 2xPossibly taken @Shizoqua claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
microsoft/TokenEconomizer#221 ·
Maintainers usually reply within 1 day
-
plot_graph crashes when a mesh node has no m2m edgePossibly taken @tygvi claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
mllam/neural-lam#883 · 1 comment ·
Maintainers usually reply within 2 days