SysDesignPrep.com
Interactive concepts

Multi-armed bandits vs A/B tests: an interactive Thompson sampling simulator

An A/B test splits traffic evenly until it ends, so the worse options keep getting their full share while you learn. A multi-armed bandit learns and earns at the same time: it shows the option that looks best most of the time, keeps exploring the others a little, and shifts traffic as evidence accumulates. It suits decisions where the reward during the test matters more than a precise measurement: headlines, thumbnails, recommendations.

The playground serves three headlines 6,000 times. Compare an even split with epsilon-greedy and Thompson sampling, averaged over 20 runs, and click one to see where its traffic went.

Three headlines with true click rates of 4% for A, 5% for B, 6% for C, shown 6,000 times; nobody knows the rates in advance. An even split shows each a third of the time whatever the data says, earning 302 clicks and losing 60 against always showing C. The bandits shift traffic to whatever is doing best as they learn: epsilon-greedy earns 336 and Thompson sampling 339, giving C 65.4% and 68.2% of impressions. At the end, the headline with the best observed rate really is C in 95% of runs for the even split, 80% for epsilon-greedy and 95% for Thompson sampling.

StrategyClicksClicks lostShown CPicked C
Thompson sampling: who got each block of 200 impressions, in one run
impressions 1 to 200: A 35impressions 1 to 200: B 49impressions 1 to 200: C 116impressions 201 to 400: A 48impressions 201 to 400: B 43impressions 201 to 400: C 109impressions 401 to 600: A 51impressions 401 to 600: B 20impressions 401 to 600: C 129impressions 601 to 800: A 12impressions 601 to 800: B 7impressions 601 to 800: C 181impressions 801 to 1000: A 5impressions 801 to 1000: B 10impressions 801 to 1000: C 185impressions 1001 to 1200: A 11impressions 1001 to 1200: B 14impressions 1001 to 1200: C 175impressions 1201 to 1400: A 6impressions 1201 to 1400: B 9impressions 1201 to 1400: C 185impressions 1401 to 1600: A 11impressions 1401 to 1600: B 25impressions 1401 to 1600: C 164impressions 1601 to 1800: A 6impressions 1601 to 1800: B 25impressions 1601 to 1800: C 169impressions 1801 to 2000: A 17impressions 1801 to 2000: B 25impressions 1801 to 2000: C 158impressions 2001 to 2200: A 25impressions 2001 to 2200: B 22impressions 2001 to 2200: C 153impressions 2201 to 2400: A 33impressions 2201 to 2400: B 16impressions 2201 to 2400: C 151impressions 2401 to 2600: A 33impressions 2401 to 2600: B 11impressions 2401 to 2600: C 156impressions 2601 to 2800: A 25impressions 2601 to 2800: B 11impressions 2601 to 2800: C 164impressions 2801 to 3000: A 15impressions 2801 to 3000: B 9impressions 2801 to 3000: C 176impressions 3001 to 3200: A 15impressions 3001 to 3200: B 9impressions 3001 to 3200: C 176impressions 3201 to 3400: A 22impressions 3201 to 3400: B 5impressions 3201 to 3400: C 173impressions 3401 to 3600: A 16impressions 3401 to 3600: B 5impressions 3401 to 3600: C 179impressions 3601 to 3800: A 13impressions 3601 to 3800: B 6impressions 3601 to 3800: C 181impressions 3801 to 4000: A 10impressions 3801 to 4000: B 11impressions 3801 to 4000: C 179impressions 4001 to 4200: A 24impressions 4001 to 4200: B 8impressions 4001 to 4200: C 168impressions 4201 to 4400: A 23impressions 4201 to 4400: B 12impressions 4201 to 4400: C 165impressions 4401 to 4600: A 19impressions 4401 to 4600: B 22impressions 4401 to 4600: C 159impressions 4601 to 4800: A 10impressions 4601 to 4800: B 18impressions 4601 to 4800: C 172impressions 4801 to 5000: A 9impressions 4801 to 5000: B 22impressions 4801 to 5000: C 169impressions 5001 to 5200: A 27impressions 5001 to 5200: B 10impressions 5001 to 5200: C 163impressions 5201 to 5400: A 21impressions 5201 to 5400: B 10impressions 5201 to 5400: C 169impressions 5401 to 5600: A 24impressions 5401 to 5600: B 12impressions 5401 to 5600: C 164impressions 5601 to 5800: A 26impressions 5601 to 5800: B 10impressions 5601 to 5800: C 164impressions 5801 to 6000: A 21impressions 5801 to 6000: B 11impressions 5801 to 6000: C 1680%50%100%02,0004,0006,000
headline A, 4%headline B, 5%headline C, 6%Table: averages of 20 runs. Click a row to chart that strategy.

Exploration against exploitation

With headlines converting at 4%, 5% and 6%, the even split earns about 302 clicks, 60 fewer than always showing the best one would. Thompson sampling earns about 339, losing 24, because it gives the best headline 68% of impressions on average and the worst one little once the evidence is clear. When one headline is bad, at 1%, the split still gives it a third of the traffic and loses 102 clicks; the bandits lose about 10.

  • Epsilon-greedy shows the best-looking option and a random one 10% of the time. Simple, but it keeps spending 10% on options it already knows are bad, and early bad luck can leave it favouring the wrong one: it picked the best headline in 80% of runs against the split’s 95%.
  • Thompson sampling keeps a probability distribution of each option’s rate and shows each with the probability that it is the best. Uncertain options get tried; clearly worse ones fade out on their own.
  • UCB (upper confidence bound) picks the option with the highest optimistic estimate, mean plus an uncertainty bonus, deterministically.

The trade-off is measurement. A bandit gathers little data on the losing options, so its estimates of how much worse they are stay rough, and with close options it is no better than a test at telling them apart. Use an A/B test when you need to know the effect, for instance before a permanent product change; use a bandit when you just want the most clicks while you find out. Real systems add contextual bandits, which condition on the user and page, and discount old data so they adapt when tastes change.

Thompson sampling in code

import random

class ThompsonSampler:
    """One Beta(clicks + 1, misses + 1) belief per option."""
    def __init__(self, options):
        self.stats = {o: [0, 0] for o in options}      # clicks, impressions

    def choose(self):
        def draw(o):
            clicks, shown = self.stats[o]
            return random.betavariate(clicks + 1, shown - clicks + 1)
        return max(self.stats, key=draw)               # sample a plausible rate for each, take the best

    def record(self, option, clicked):
        self.stats[option][1] += 1
        self.stats[option][0] += int(clicked)

sampler = ThompsonSampler(["headline_a", "headline_b", "headline_c"])
choice = sampler.choose()
sampler.record(choice, clicked=False)

In a system design interview

Bandits come up in feed, ads and notification designs: “For each article, editors supply up to four headlines. The serving layer keeps clicks and impressions per headline in Redis, and a Thompson sampler picks one per request. Counts are updated asynchronously from the click stream, so the sampler works on numbers a few seconds old, which is fine. After a day the winner is locked in.” Expect the follow-ups: delayed rewards such as purchases, position bias in clicks, and how to keep the exploration from hurting new users.

Read the guide to bandits and exploration or experiments and feature flags, see A/B test peeking, or watch a strong candidate design a news feed.

Questions people ask

What is a multi-armed bandit?

A strategy for repeatedly choosing among options with unknown rewards, such as headlines or ads, that balances exploring options to learn about them with exploiting the one that currently looks best. Traffic shifts towards better options while the experiment runs.

What is the difference between a bandit and an A/B test?

An A/B test keeps a fixed split until it ends, gathering equal evidence on every option, which gives precise measurements but sends many users to worse options. A bandit shifts traffic to better options as it learns, earning more during the test but measuring the losing options less precisely.

How does Thompson sampling work?

For each option it keeps a probability distribution over its true rate, such as Beta(successes + 1, failures + 1) for click rates. On each request it draws one sample from every distribution and shows the option with the highest draw, so each option is shown in proportion to the probability that it is the best.

What is epsilon-greedy?

The simplest bandit: with probability epsilon, such as 10%, show a random option; otherwise show the one with the best observed rate. It is easy to implement but keeps spending epsilon on options known to be bad, and an unlucky start can lock it onto a worse option for a while.

What is regret in bandit algorithms?

The total reward lost compared with always choosing the best option, for example the clicks forgone while learning. Good bandit algorithms keep regret growing only logarithmically with the number of rounds, while a fixed even split loses a constant share of reward throughout.

When should you use a bandit instead of an A/B test?

When earning during the experiment matters more than an accurate measurement, and decisions are short-lived or frequent: headlines, thumbnails, promotions, recommendations. Use an A/B test when you need a trustworthy estimate of the effect, such as before a lasting product change.

What is a contextual bandit?

A bandit that chooses based on features of the situation, such as the user, device or page, learning which option works best in each context rather than overall. Recommendation and ad systems often use contextual bandits with models such as LinUCB or neural networks.