Bayesian rates: trusting small numbers
Priors as pseudo-counts, Beta posteriors, shrinkage for small samples, and which of two rates is really higher.
Create a free account Try part of it here
Why it matters
A new product page converted 3 of its first 4 visitors: 75%. A page that has run for a year converts 4%. Should the new page get the whole ad budget? Small samples swing wildly, and Bayes' rule gives a principled way to combine a little new evidence with what you already knew. This lesson turns that idea into a few lines of Python: a prior, some data, and an estimate that pulls wild small-sample rates toward sensible ones. It's how review sites, A/B testing tools and recommendation systems avoid being fooled by tiny numbers.
What you'll be able to do
- Explain why rates from a handful of trials swing wildly
- Combine a prior with new data by adding counts, and read the posterior estimate
- Choose how strong a prior should be, and see it fade as data grows
- Rank items fairly with a Bayesian average, and get an interval from the Beta distribution
What it covers
- Small samples swing
- Prior plus evidence
- Shrink three rates
- How strong should the prior be?
- The whole picture: a Beta distribution
- A credible interval from samples
- Where this sits in Bayes' rule
- Bayesian rates in AI and product work
Show the answer
B's 6%: it rests on far more visitors
With 4 visitors, one more or one fewer sale moves the rate by 25 points. B's rate comes from 5,000 visitors, so it can't swing like that. This lesson shows how to use a small sample without either ignoring it or believing it completely.
Small samples swing
Imagine a page whose true conversion rate is 5%. Show it to 4 visitors, note the rate, and do that again and again. Most of the time nobody buys and the rate reads 0%; now and then one visitor buys and it reads 25%:
import random random.seed(3) for trial in range(8): sales = sum(1 for _ in range(4) if random.random() < 0.05) print(sales / 4, end=" ")
With 4 visitors, the only rates you can ever see are 0%, 25%, 50%, 75% and 100%. None of them is close to the true 5%. And each new visitor moves the rate a lot: going from 1 sale in 4 visits to 1 in 5 drops it from 25% to 20%, while a page with 5,000 visits barely notices one more. A rate's reliability depends on the number of trials behind it, which is the same lesson the samples lesson taught with confidence intervals. The question here is what to do with a small sample, other than wait.
print([k / 4 for k in range(5)])
Show the answer
[0.0, 0.25, 0.5, 0.75, 1.0]
With 4 visitors, 0 to 4 of them can buy, giving rates of 0, 0.25, 0.5, 0.75 or 1.0. A true rate of 0.05 can't show up exactly; you only ever see a coarse, swinging approximation.
Prior plus evidence
Bayes' rule, in its friendliest form, says: start from what you already believe, then update it with the new data. For rates, both can be written as counts.
The prior is what you knew before. If pages like this one usually convert around 4%, you can write that as if you'd already seen 2 sales in 50 visits. The new data is what this page did: 3 sales in 4 visits. The updated belief, the posterior, just adds them:
prior_sales, prior_visits = 2, 50 sales, visits = 3, 4 raw = sales / visits posterior = (prior_sales + sales) / (prior_visits + visits) print(round(raw, 3), round(posterior, 3))
The raw rate says 75%; the posterior says about 9%. The page looks promising, and it gets credit for that, but 4 visitors can't outweigh everything you know about pages like it. As visits pile up, the data will dominate.
The posterior always lands between the prior's rate and the raw rate, closer to whichever has more visits behind it. That's the whole behavior in one sentence, and it's why this is called shrinkage: noisy estimates are shrunk toward a sensible center, more when they're noisier.
The prior acts like visits you've already seen. A big prior needs lots of data to move; a small one moves quickly.
Shrink three rates
Pages like these usually convert about 4%, which you'll use as a prior of 2 sales in 50 visits. Set post_a, post_b and post_c: each page's posterior rate, rounded to 3 decimal places.
prior_sales, prior_visits = 2, 50 a_sales, a_visits = 3, 4 # a brand-new page b_sales, b_visits = 0, 20 # a page with a slow start c_sales, c_visits = 240, 5000 # a page that has run for months post_a = 0 post_b = 0 post_c = 0 print(post_a, post_b, post_c)
How strong should the prior be?
The prior's visits are its weight. A prior of 2 in 50 says "I'm about as sure as 50 visits would make me". Choose it from history: if past pages converted around 4%, and their rates varied about as much as 50 visits' worth of noise, then 2 in 50 is a fair summary.
Two failure modes are worth knowing. A prior that's too weak (say 0.04 sales in 1 visit) hardly shrinks anything, and tiny samples fool you again. A prior that's too strong (400 in 10,000) drowns real differences, and a genuinely great page takes forever to be believed. Whatever you pick, enough data always wins: with thousands of real visits, any sensible prior fades into the background.
Be careful where the prior comes from. Taking it from the very data you're judging counts that data twice. A fair prior comes from history, from similar items, or from a deliberate, stated assumption that others can question.
Show the answer
Hardly at all: 502 / 10,050 is still about 5%
502 / 10,050 ≈ 4.995%, against the raw 5%. Fifty imagined visits are nothing next to 10,000 real ones; the prior matters only while data is scarce.
The whole picture: a Beta distribution
The posterior is more than one number. It's a whole spread of plausible rates, a Beta distribution named by two counts: α = prior sales + sales and β = prior non-sales + non-sales. Its mean is α / (α + β), the estimate you've been computing, and its width says how sure you can be.
Python can draw samples from it, which gives an interval with no formulas at all:
import random random.seed(4) alpha = 2 + 3 # prior sales + sales beta = (50 - 2) + (4 - 3) # prior non-sales + non-sales draws = sorted(random.betavariate(alpha, beta) for _ in range(4000)) print(round(draws[100], 3), round(draws[3899], 3)) # the middle 95%
Bayesians call this a credible interval: given the prior and the data, the true rate is probably in there. With 4 visitors it's wide; with 5,000 it would be narrow. That width is useful in its own right: two pages whose intervals barely overlap are probably different, while two whose intervals mostly overlap may not be.
alpha, beta = 5, 49 print(round(alpha / (alpha + beta), 3))
Show the answer
0.093
The mean is α / (α + β) = 5 / 54 ≈ 0.093: the same estimate as adding the counts, 5 sales in 54 visits.
A credible interval from samples
A page made 12 sales in 200 visits, and you'll use the prior of 2 in 50. Set alpha and beta for the posterior, draw 4,000 samples with random.betavariate(alpha, beta), sort them, and set low and high to the values at positions 100 and 3899.
import random random.seed(5) prior_sales, prior_visits = 2, 50 sales, visits = 12, 200 alpha = 0 beta = 0 low = 0 high = 0 print(alpha, beta, round(low, 3), round(high, 3))
Where this sits in Bayes' rule
The conditional probability lesson wrote Bayes' rule as P(A given B) = P(B given A) × P(A) / P(B): an updated belief is the old belief, P(A), reweighted by how well it explains the evidence. Adding counts to a prior is that same rule, worked out once and for all for rates. Statisticians call a prior that keeps its shape after updating "conjugate"; the Beta prior for rates is the classic example, which is why the update is just addition.
Show the answer
About 15%
Out of 1,000 messages, 10 are spam and 9 of them get flagged; of the 990 normal ones, about 50 get flagged too. So 9 of about 59 flagged messages are spam: roughly 15%. The rare base rate dominates.
Bayesian rates in AI and product work
Rankings use a Bayesian average so a product with one perfect review doesn't top the list. A/B testing tools report the probability that B beats A from two Beta posteriors. Thompson sampling decides which variant to show next by drawing from each variant's posterior, so promising newcomers get tried without wasting traffic, and clear losers are quietly dropped as their posteriors settle low. And when you report an eval score from a handful of test cases, the same honesty applies: say how many cases there were, or shrink the score toward what similar systems usually get. Small numbers deserve modest claims, always, however exciting the first few results look.
prior_n, prior_mean = 10, 4.0 n, total = 1, 5.0 print(round((prior_n * prior_mean + total) / (prior_n + n), 2))
Show the answer
4.09
(10 × 4.0 + 5) / (10 + 1) = 45 / 11 ≈ 4.09. One perfect review nudges it up a little, instead of putting it at the top of the list.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 6-question quiz
- 5 review cards for spaced repetition
Try a quiz question
As a page collects more and more visits, what happens to the prior's influence?