Samples, uncertainty and confidence intervals
You rarely measure everything. You count stock in 40 stores, audit 400 orders or survey 1,000 customers, and then someone asks what that says about all of Mangrove Market. This lesson shows you how a fair sample speaks for the whole, how much it can wobble, and how to report a number with an honest range around it.
By the end you can
- Tell a random sample from a convenience sample, and draw a random one in a spreadsheet, SQL or pandas
- Explain the standard error with a simulation, and why four times the sample only halves it
- Compute a 95% confidence interval for a mean (with t for small samples) and for a proportion
- Say what a confidence interval does and does not mean, and check it with a bootstrap
The everyday picture
A cook making a big pot of lentil soup wants to know if it needs salt. She doesn't drink the whole pot. She stirs it well, takes one spoonful and tastes it.
Three things make that spoonful trustworthy. First, she stirred: every part of the pot had the same chance of ending up in the spoon. A spoonful skimmed from the top without stirring tells her about the top, however carefully she tastes it. Second, a bigger spoon gives a steadier answer than a tiny one. Third, the size of the pot barely matters. One well-stirred spoonful tells her as much about a 20-litre pot as about a 5-litre one.
And she knows a single spoonful can mislead a little. If the soup tastes "just about right", she might add a pinch more and taste again. Statistics gives you the same habits with numbers: stir (sample at random), use a big enough spoon (sample size), and say how much the answer could be off (a confidence interval).
Population, sample and how you pick it
The population is everything you want to know about: all 320 Mangrove stores, every app order in October, every registered customer. The number you want, such as the average stock accuracy across all 320 stores, is fixed but unknown. A sample is the part you measure. You compute the same number on the sample and use it as your estimate.
Why sample at all? Because measuring everything costs time and money. Stock accuracy is the share of counted items where the system's stock figure matches what is on the shelf; at Mangrove it is about 82%. To check it, staff count items by hand. Counting every item in every store would stop the stores for days. Counting 200 random items in 40 random stores takes an afternoon per store.
How you pick the sample matters more than how big it is. In a random sample, chance decides which units get in, so every store has the same chance. In a convenience sample, people choose: the stores near head office, the managers who volunteer, the customers who answer a survey. Those units often differ from the rest. Managers who volunteer for a stock count tend to run tidy stores. That error is called bias, and a bigger sample does not fix it. Ask 400 volunteers instead of 40 and you get the same wrong answer, only more confidently.
Drawing a random sample takes a minute in any tool:
- Spreadsheet: next to the list of 320 stores, add a column with
=RAND(), then sort by it and take the first 40 rows.RANDchanges every time the sheet recalculates, so copy the column and paste it as values before you sort. - SQL (Postgres):
SELECT id FROM site WHERE kind = 'store' ORDER BY random() LIMIT 40; - pandas:
stores.sample(n=40, random_state=2026). Therandom_statenumber makes the draw repeatable, so a colleague gets the same 40 stores.
If cities differ a lot, you can also draw at random within each city in proportion to its store count. That is a stratified sample, and it makes sure no city is left out by bad luck.
Quick check
Last year the stock-accuracy audit used the 40 stores whose managers volunteered. Tomás suggests counting in 120 volunteer stores this year, "so the result is more reliable". What is wrong with the plan?
Sample size shrinks random wobble, never bias. Volunteers choose themselves, so they tell you about stores whose managers like counting. There is no rule that a sample must stay under 10% of the population; a larger share only makes the estimate a little more precise.
How much does a sample wobble? Simulate it
Draw a different sample and you get a slightly different answer. You can see how much by trying it many times on a population you know. In Describing data honestly you met Mangrove's app baskets: average about $18.40, median about $14, with a long right tail from big monthly shops. Tomás built a simulated population of one million baskets with that shape. Its standard deviation is about $15.70.
Then he did what you can never do with real data: he drew a random sample of 100 baskets, wrote down its mean, and repeated that 1,000 times. The 1,000 sample means look like this:
- They pile up around the true $18.40. No single sample is far off very often.
- They spread much less than single baskets do. Their standard deviation is $1.55, against $15.70 for single baskets.
- The middle 95% of them fall between $15.71 and $21.55. The lowest was $13.51 and the highest $26.95.
- Their histogram is close to a bell shape, even though single baskets are heavily skewed.
That pile of possible answers is the sampling distribution. Its standard deviation has its own name, the standard error (SE), and you don't need 1,000 samples to know it. For a mean:
standard error = s / √n s = standard deviation of the values, n = sample size
baskets: s = $15.70, n = 100 → SE = 15.70 / √100 = 15.70 / 10 = $1.57
the simulation measured $1.55: the formula predicts the wobble from ONE sampleThe s comes from your one sample (STDEV.S in a spreadsheet, stddev_samp in Postgres, .std() in pandas, which divides by n − 1 by default). So one sample tells you both an estimate and how much to trust it.
Why √n? Errors in single values partly cancel when you average them: a big basket here, a small one there. They cancel more slowly than the sample grows. Tomás's simulation, repeated for three sample sizes, shows the pattern:
sample size SE by formula ($15.70 / √n) SD of 1,000 simulated means
25 $3.14 $3.12
100 $1.57 $1.55
400 $0.79 $0.80Four times the sample halves the error, because √4 = 2. To cut the error to a tenth you need 100 times the data. This is why precision gets expensive fast, and why a sample of 1,000 is common in surveys: going to 4,000 costs four times as much and only halves the error.
The bell shape at the bottom of the diagram is the central limit theorem at work. In plain words: averages of enough random values pile up in a bell shape, even when the values themselves are lopsided. How many is "enough" depends on the skew. For mildly skewed data 30 often does; for baskets with a long tail, means of 25 are still a little lopsided and 100 or more look like a bell. The theorem is about averages; it never makes the baskets themselves normal.
Quick check
A sample of 100 baskets gives a mean with a standard error of about $1.57. Imran wants the standard error under $0.80. About how many baskets does he need?
The error falls with √n, so halving it takes four times the sample: $15.70 / √400 = $0.79. Doubling the sample only divides the error by √2, about 1.41. The total number of baskets barely matters when the sample is a tiny share of it, like the well-stirred pot.
From standard error to a confidence interval
Because sample means form a bell, about 95% of them land within 1.96 standard errors of the true value. Turn that around and you get a recipe for a range around your one estimate, the confidence interval (CI):
95% CI ≈ estimate ± 1.96 × SE the part after ± is the margin of errorWith small samples you also estimate the spread s from few values, which adds uncertainty. So instead of 1.96 you use a slightly bigger multiplier from the t distribution with n − 1 degrees of freedom: 2.262 for 10 values, 2.201 for 12, 2.023 for 40, and 1.984 for 100. Above a few hundred values it is practically 1.96. A spreadsheet gives it with =T.INV.2T(0.05, n-1), Python with scipy.stats.t.ppf(0.975, n - 1).
For a proportion (a share of yes or no answers), the standard error has its own formula, because the spread of yes/no data depends only on the share itself:
SE = √( p × (1 − p) / n )
Store 17 counts 200 random items; 158 match the system: p = 158 / 200 = 0.79
SE = √(0.79 × 0.21 / 200) = 0.0288
95% CI = 0.79 ± 1.96 × 0.0288 = 0.79 ± 0.056 → 73.4% to 84.6%This shortcut works when there are at least about 10 yes and 10 no answers. With fewer, or with a share near 0% or 100%, it can even give a range below zero. Then use the Wilson interval, which statsmodels offers as proportion_confint(count, n, method="wilson"). For store 17 it gives 72.8% to 84.1%, close to the shortcut. For 1 cut item in 20 orders, the shortcut says −4.6% to 14.6% and Wilson says 0.9% to 23.6%.
A useful sense of scale: a poll of 1,000 people on a question near 50/50 has a margin of error of about 3 points, because 1.96 × √(0.25 / 1,000) = 0.031. You will hear "plus or minus 3 points" in news polls for that reason.
What a confidence interval does and does not mean
The "95%" describes the method. If you repeat the whole process (draw a fresh random sample, compute the interval) many times, about 95% of the intervals will contain the true value. The second diagram shows it: 19 of these 20 intervals catch $18.40. Your one interval either contains the true value or it doesn't, and you can't tell which. That is why careful people say "we are 95% confident" and describe the method behind it.
Four readings to avoid:
- "95% of stores lie in the interval." The interval is about the average. Single stores spread far wider: in the audit below, the stores ranged from 63.5% to 97%, while the interval for the average is about 5 points wide.
- "The interval covers every kind of error." It covers only the luck of the draw. A convenience sample gives an interval just as narrow around the wrong value. Miscounted shelves and a wrong system figure are not in it either.
- "Values near the edge are as likely as the middle." Values near the centre fit the data best; the edges are the least plausible values still in range.
- "The method is exactly 95% every time." It is close to 95% when its assumptions hold. With skewed data and small samples it misses more often. In Tomás's simulation the intervals caught the true basket mean about 91% of the time with samples of 25, about 93% with 100 and about 95% with 400.
The width is the useful part. A result of "81.9%, between 79.4% and 84.4%" tells a manager whether the answer is precise enough to act on. A bare "81.9%" hides that.
Quick check
From a random sample of 40 stores, the 95% CI for average stock accuracy is 79.4% to 84.4%. Which sentence is right?
The interval is about the average of all 320 stores, and its 95% describes the method across many samples. Individual stores spread much more widely than the interval. And the interval only covers sampling luck, not counting mistakes or bias.
The bootstrap: a confidence interval by resampling
The formulas above work for means and proportions. For a median, a 90th percentile or a ratio, the formula is hard or missing. The bootstrap, introduced by Bradley Efron in 1979, gets an interval by simulation from the one sample you have:
- Treat your sample as a small model of the population.
- Draw a new sample of the same size from it with replacement (a store can be picked twice, another not at all).
- Compute your statistic (mean, median, anything) on it.
- Repeat 10,000 times. The middle 95% of the 10,000 results is a 95% interval (the percentile method).
It is the simulation from earlier, run on your sample instead of on a population you can't see. It needs no bell-shape formula, so it works for medians and ratios. It still needs a random sample, and with very small samples (say under 20) it tends to be too narrow, because a small sample shows too little of the population's spread.
In Python, a few lines do it by hand, and SciPy (version 1.7 or later) has it built in:
import numpy as np
from scipy import stats
values = np.array(accuracy_pct) # the 40 measured stores
rng = np.random.default_rng(7) # a seed makes the result repeatable
boot = [rng.choice(values, size=len(values), replace=True).mean()
for _ in range(10_000)] # 10,000 resampled means
print(np.percentile(boot, [2.5, 97.5])) # middle 95% = the interval
# built in; its default method is "BCa", a corrected version of the percentile method
res = stats.bootstrap((values,), np.mean, confidence_level=0.95, n_resamples=10_000)
print(res.confidence_interval)Spreadsheets have no bootstrap function. You can build one with RANDBETWEEN and INDEX across many columns, but it is slow and fragile; this is a good place to move to Python (the same analysis in Python).
Do it
Estimate one number for the whole chain from a random sample, and report it with a 95% confidence interval.
Name the population, the unit and the number
"All 320 stores; the unit is a store; the number is average stock accuracy." If you will measure items inside stores, the unit is still the store.
Draw the sample at random, and save it
Fix the seed so the draw can be repeated and checked. In Postgres,
setseedtakes a value between −1 and 1 and fixes the followingrandom()calls in the same session.SQLSELECT setseed(0.2026); -- makes the draw repeatable CREATE TABLE audit_sample AS SELECT id AS site_id, city FROM site WHERE kind = 'store' -- stores only, no distribution centres ORDER BY random() -- shuffle all 320 LIMIT 40; -- keep the first 40Measure every sampled unit, including the awkward ones
A store that is "too busy this week" stays in the sample; replacing it with a willing store brings the bias back.
Compute mean, standard error and the interval
One value per store in column B (rows 2 to 41):
Outputcell label formula result E1 n =COUNT(B2:B41) 40 E2 mean =AVERAGE(B2:B41) 81.9 E3 s =STDEV.S(B2:B41) 7.8 E4 SE =E3/SQRT(E1) 1.23 E5 t =T.INV.2T(0.05, E1-1) 2.023 E6 margin =E5*E4 2.5 E7 low =E2-E6 79.4 E8 high =E2+E6 84.4 One-step check: =CONFIDENCE.T(0.05, E3, E1) returns the same margin.Pythonimport numpy as np from scipy import stats acc = counts["accuracy_pct"] # one row per sampled store n = len(acc) mean = acc.mean() se = acc.std() / np.sqrt(n) # pandas .std() divides by n - 1 t = stats.t.ppf(0.975, n - 1) # 2.023 for 39 degrees of freedom print(mean - t * se, mean + t * se) # the same in one call stats.t.interval(0.95, n - 1, loc=mean, scale=se)Check it with a bootstrap
Run the resampling code from the bootstrap section. If the two intervals differ a lot, your data is skewed or small; report the bootstrap and say why.
Write the sentence
Markdown**Stock accuracy averages about 82% (95% CI 79% to 84%).** Method: 40 of 320 stores drawn at random (seed saved), 200 random items counted per store, store-level average with a t interval; bootstrap agrees (79.5% to 84.3%). Not covered by the interval: counting mistakes, and stores that changed after the count. Decision: the 85% target is not met; next, the 10 lowest stores.
Bring it to your team
Ask these whenever a number comes from part of the data.
- How was the sample chosen: by chance, or by whoever was easy to reach?
- What is the unit we sampled, and is the n in the formula that unit?
- What is the confidence interval, and is it narrow enough for the decision we have to make?
- Did any sampled unit drop out, and could the dropouts differ from the rest?
- If we doubled the sample, what would it cost, and would the narrower interval change the decision?
Red flags
- A single number with no n and no range.
- "We asked the stores that replied" presented as a chain-wide result.
- An interval built on items or orders when stores or customers were sampled.
- "95% of stores fall in the confidence interval."
- A busy store swapped out of the sample for a willing one.
Practice
A confidence interval in SQL
The table stock_count(site_id, city, in_sample, lines_counted, lines_accurate) holds this month's counts. Twelve stores were drawn at random (in_sample = true); three more volunteered and counted anyway (in_sample = false). Using only the random stores, compute each store's accuracy as a percentage, then return one row with n_stores, mean_pct, se_pct (standard deviation of the store percentages ÷ √n), ci_low and ci_high (mean ∓ 2.201 × SE; 2.201 is the t value for 11 degrees of freedom). Round the last four to one decimal.
CREATE TABLE stock_count (site_id int PRIMARY KEY, city text NOT NULL, in_sample boolean NOT NULL, lines_counted int NOT NULL, lines_accurate int NOT NULL); INSERT INTO stock_count VALUES (104, 'North', true, 200, 170), (117, 'North', true, 200, 158), (122, 'East', true, 200, 149), (138, 'East', true, 200, 181), (145, 'East', true, 200, 165), (159, 'South', true, 200, 152), (163, 'South', true, 200, 176), (178, 'South', true, 200, 139), (186, 'West', true, 200, 162), (201, 'West', true, 200, 168), (233, 'Central', true, 200, 155), (276, 'Harbour', true, 200, 171), (110, 'North', false, 200, 186), (150, 'East', false, 200, 183), (190, 'West', false, 200, 188);
-- 95% confidence interval for average stock accuracy (random stores only)
WITH store AS (
SELECT site_id,
100.0 * lines_accurate / lines_counted AS pct -- one value per store
FROM stock_count
-- add: keep only the randomly sampled stores
)
SELECT count(*) AS n_stores,
round(avg(pct), 1) AS mean_pct
-- add: se_pct = stddev_samp(pct) / sqrt(count(*))
-- add: ci_low and ci_high = avg(pct) -/+ 2.201 * se
-- tip: wrap a float in ::numeric before round(), e.g. round((x)::numeric, 1)
FROM store;
[{"n_stores": 12, "mean_pct": 81.1, "se_pct": 1.7, "ci_low": 77.2, "ci_high": 84.9}]
Show a solution
WITH store AS (
SELECT site_id,
100.0 * lines_accurate / lines_counted AS pct
FROM stock_count
WHERE in_sample -- volunteers would add bias
), s AS (
SELECT count(*) AS n,
avg(pct) AS mean,
stddev_samp(pct) / sqrt(count(*)) AS se -- s / √n
FROM store
)
SELECT n AS n_stores,
round(mean, 1) AS mean_pct,
round(se::numeric, 1) AS se_pct,
round((mean - 2.201 * se)::numeric, 1) AS ci_low,
round((mean + 2.201 * se)::numeric, 1) AS ci_high
FROM s;The random stores average 81.1%, with an interval from 77.2% to 84.9%. With the three volunteers included the average would be 83.4%: a 15-store sample that looks bigger and is less trustworthy. The SE uses the store as the unit (12 values), not the 2,400 items. sqrt(count(*)) returns a float, which is why the results are cast to numeric before rounding.
A confidence interval for a proportion in JavaScript
Write proportionCI(yes, n) that returns { p, se, low, high } as fractions: p = yes / n, se = √(p(1 − p) / n), and low/high = p ∓ 1.96 × se. A share can't be below 0 or above 1, so clamp low at 0 and high at 1. Example: store 17 counted 200 items and 158 matched, so the interval is about 0.734 to 0.846.
function proportionCI(yes, n) {
const p = yes / n;
// add: se = square root of p * (1 - p) / n (Math.sqrt)
// add: low = p - 1.96 * se, but never below 0 (Math.max)
// add: high = p + 1.96 * se, but never above 1 (Math.min)
return { p };
}
test('store 17: 158 of 200 gives p 0.79 and se about 0.0288', () => {
const r = proportionCI(158, 200);
expect(r.p).toBeCloseTo(0.79, 4);
expect(r.se).toBeCloseTo(0.0288, 4);
});
test('store 17: interval is about 0.7336 to 0.8464', () => {
const r = proportionCI(158, 200);
expect(r.low).toBeCloseTo(0.7336, 3);
expect(r.high).toBeCloseTo(0.8464, 3);
});
test('order audit: 36 of 400 gives about 0.0620 to 0.1180', () => {
const r = proportionCI(36, 400);
expect(r.low).toBeCloseTo(0.0620, 3);
expect(r.high).toBeCloseTo(0.1180, 3);
});
test('low is clamped at 0 for 1 of 20', () => {
const r = proportionCI(1, 20);
expect(r.low).toBe(0);
expect(r.high).toBeCloseTo(0.1455, 3);
});
test('high is clamped at 1 for 19 of 20', () => {
expect(proportionCI(19, 20).high).toBe(1);
});
Show a solution
function proportionCI(yes, n) {
const p = yes / n;
const se = Math.sqrt(p * (1 - p) / n); // standard error of a share
const low = Math.max(0, p - 1.96 * se); // a share can't go below 0
const high = Math.min(1, p + 1.96 * se); // or above 1
return { p, se, low, high };
}The clamping hides a deeper problem: with 1 yes in 20 the shortcut is unreliable, because there are fewer than 10 yes answers. The Wilson interval for 1 in 20 is about 0.009 to 0.236, much wider on the high side than the clamped 0 to 0.146.
Check yourself
Tomás surveys 1,000 randomly chosen app customers out of about 2 million. A colleague in a smaller company surveys 1,000 random customers out of 50,000. Both measure a share near 50%. Whose margin of error is smaller?
The margin is about 1.96 × √(0.25 / 1,000), roughly 3 points, for both. The population size only matters when the sample is a sizeable share of it, and even 2% changes the margin by about 1%. That is the well-stirred pot: one spoonful works for a small pot and a big one.
An audit of 400 random app orders found 36 with an item cut for being out of stock: 9%, with a margin of error of about 2.8 points. The operations lead wants a margin of about 1.4 points. Roughly how many orders must be audited?
Halving the margin needs four times the sample, because the error falls with √n: 1.96 × √(0.09 × 0.91 / 1,600) is about 1.4 points. Doubling to 800 only divides the margin by about 1.41, to roughly 2 points.
You need a 95% interval for the median basket from a random sample of 300 baskets. What is the most sensible approach?
The bootstrap works for any statistic computed from a random sample. The s / √n formula is for means, and for skewed baskets the mean ($18.40) sits well above the median (about $14). The percentiles of the baskets describe where single baskets fall, a much wider range than the uncertainty of the median.
Flashcards
Key takeaways
- How you pick the sample matters first: chance must decide which units get in, or a bigger sample only repeats the bias.
- The standard error, s / √n, says how much an estimate wobbles between samples; four times the data halves it.
- Means of enough random values form a bell (the central limit theorem), which is why estimate ± 1.96 SE works; use t for small samples and √(p(1 − p) / n) for proportions.
- The 95% describes the method, not single stores, and the interval covers only sampling luck, never bias or counting errors.
- Use the unit you sampled as n, and bootstrap when there is no simple formula, such as for a median.
Sources and further reading
- OpenIntro Statistics, David Diez, Mine Çetinkaya-Rundel and Christopher Barr, free textbook (chapters on foundations for inference)
- Seeing Theory: Frequentist Inference, Brown University, interactive pictures of sampling and confidence intervals
- Confidence Limits for the Mean, NIST/SEMATECH e-Handbook of Statistical Methods
- Bootstrap Methods: Another Look at the Jackknife, Bradley Efron, The Annals of Statistics, 1979
- scipy.stats.bootstrap and statsmodels proportion_confint, library documentation