Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
This week the review queue is larger than the team can label. Reviewers can draw 100 flagged signups at random and confirm 16 abuse cases. The dashboard wants to report 16%, while the review lead needs to decide how much that number could move before changing capacity.
Bayesian Inference from Evidence combined ten coding-agent outcomes with an explicit prior. Here we'll ask how a sample-based estimate moves across repeated audits, before returning to the role of a prior.
The earlier probability lesson counted 95 abusive flags among all 590 flags in a fully labeled signup snapshot. That was a population calculation. This week's queue is much larger and mostly unlabeled: 16 / 100 is evidence about an unknown rate, not an exact count of the whole queue.
Start with the target behind 16%
Before doing arithmetic, finish this sentence: "16 of 100 what?" Both numbers refer to flagged signups, so the sample has a precise target:
| Name | Meaning here |
|---|---|
| Population | every flagged signup entering the manual-review queue this week |
| Sample | 100 flagged signups selected at random and reviewed by humans |
| Success | reviewer confirms the signup is abusive |
| Unknown rate | , also called review-queue precision |
The population is the full set you want to describe. This week's queue is that population.
The sample is the 100 signups you managed to label. It's evidence about the queue, not a smaller name for the queue itself.
Now name the target quantity: the estimand is the queue's unknown abuse rate, .
The estimator is the rule applied to labels, here . This sample produces one realized estimate, . A different random sample could produce a different estimate even if the queue's true rate stayed fixed.
The hat on says "estimate." It doesn't say the full queue is exactly 16 percent abuse. For a Bernoulli rate, is also the maximum-likelihood estimate: the value of that makes these observed labels most likely under the model.
This is deliberately not overall accuracy. If only 1 percent of all signups are abusive, a useless model that labels every signup clean is 99 percent accurate. The review decision needs a different denominator: how much of the flagged queue is worth a human review?
Run that smallest possible report before adding any uncertainty machinery:
1reviewed_flags = 100
2confirmed_abuse = 16
3
4precision_estimate = confirmed_abuse / reviewed_flags
5always_clean_accuracy = 0.99
6
7print(f"review queue: {confirmed_abuse}/{reviewed_flags} confirmed abuse")
8print(f"estimated P(abuse | flagged): {precision_estimate:.1%}")
9print(f"misleading rare-event baseline accuracy: {always_clean_accuracy:.1%}")
10
11assert precision_estimate == 0.161review queue: 16/100 confirmed abuse
2estimated P(abuse | flagged): 16.0%
3misleading rare-event baseline accuracy: 99.0%Why is 16 / 100 a better running metric here than detector accuracy over all signups?
Answer
You are deciding whether flagged signups deserve review, so the useful quantity is the abusive share inside the flagged queue. With rare abuse, overall accuracy can look high even for a detector that never catches abuse.
Let the next hundred challenge the point estimate
The first report gave us a center, not a promise. If the queue's true risk were 16 percent, what should the next random 100 labels contain? Not always 16. One batch might contain 12, another 19, another 17.
The simulation holds the rate fixed at 0.16 and changes only which signups get drawn. Each draw is an independent coin flip with success probability 0.16, called a Bernoulli outcome. The number of successes in 100 such draws has a binomial distribution. Will all eight estimates stay close to 0.16?
1import numpy as np
2
3rng = np.random.default_rng(17)
4labels = rng.random((8, 100)) < 0.16 # eight batches, 100 labels each
5confirmed_counts = labels.sum(axis=1)
6estimates = confirmed_counts / 100
7
8print("confirmed abuse counts:", confirmed_counts.tolist())
9print("precision estimates: " + ", ".join(f"{value:.2f}" for value in estimates))
10print(f"range across batches: {min(estimates):.2f} to {max(estimates):.2f}")1confirmed abuse counts: [13, 16, 16, 17, 19, 16, 10, 18]
2precision estimates: 0.13, 0.16, 0.16, 0.17, 0.19, 0.16, 0.10, 0.18
3range across batches: 0.10 to 0.19The batches don't all return 16 percent. That movement isn't model drift because the simulated rate never changed. It's sampling variation.
One measured rate answers a question about one sample. A production claim needs both the center and the amount that center can move.
The simulation knew the true rate because we wrote it into the code. A real audit doesn't. It has to estimate sampling movement from the labels already in hand.
Replay sampling with the labels in hand
Our real audit has sixteen 1 values and eighty-four 0 values. The true queue risk remains unknown.
The bootstrap repeatedly samples 100 rows with replacement from those observed labels. Each resample gives another value of , so its distribution approximates how the estimate might move if we could redraw the audit. It treats the measured sample as the best available stand-in for the population, not as new evidence.[1][2]
For example, one resample might draw a particular abusive signup three times and omit another entirely. It needn't preserve exactly 16 successes. rng.choice(..., replace=True) performs that draw; each of its 5,000 rows is a resampled audit. Taking the 2.5th and 97.5th percentiles gives a percentile bootstrap interval.[3]
1import numpy as np
2
3reviewed_labels = np.array([1] * 16 + [0] * 84)
4rng = np.random.default_rng(7)
5
6resampled_labels = rng.choice(reviewed_labels, size=(5_000, 100), replace=True)
7resampled_estimates = resampled_labels.mean(axis=1)
8
9low, high = np.quantile(resampled_estimates, [0.025, 0.975])
10
11print(f"sample estimate: {sum(reviewed_labels) / len(reviewed_labels):.3f}")
12print(f"bootstrap 95% range: [{low:.3f}, {high:.3f}]")
13print(f"resampled min and max: {min(resampled_estimates):.3f}, {max(resampled_estimates):.3f}")
14
15zero_labels = np.zeros(10, dtype=int)
16zero_resamples = rng.choice(zero_labels, size=(100, 10), replace=True).mean(axis=1)
17print("bootstrap estimates from 0/10:", np.unique(zero_resamples).tolist())1sample estimate: 0.160
2bootstrap 95% range: [0.090, 0.230]
3resampled min and max: 0.050, 0.300
4bootstrap estimates from 0/10: [0.0]The central bootstrap range is much wider than a single point. It approximates sampling uncertainty by treating these labels as the population. That approximation can fail: with ten zero labels, every resample also contains ten zeros, so the percentile interval collapses to [0, 0]. Resampling can't invent an unobserved abuse case. We'll need a different interval for that boundary case.
Extra bootstrap repetitions only reduce Monte Carlo jitter in those percentile cut points. They don't add reviewed signups or repair a biased sample.
Row-by-row resampling also assumes the rows act like independent draws. If the audit selects whole groups of linked signups, keep those groups together when estimating uncertainty; ten related rows aren't necessarily ten independent pieces of evidence. Match the resampling unit to the sampling design.[4]
Bootstrap gives us an empirical picture. For a binary rate, we can also estimate its typical movement with a short formula.
Put a scale on the movement
Bootstrap gave us a distribution of possible estimates. For this binary rate, we can also get a quick scale for that movement from the 16 successes in 100 labels.
With 16 / 100, the estimated variance of one binary label is 0.16 × 0.84 = 0.1344. Averaging 100 independent labels divides that variance by 100. Take the square root to put it back on the rate's scale:
That's about 3.7 percentage points of typical sampling movement, not a 3.7 percent relative error. Under the independent-binomial model, the estimator's variance is . Substituting the observed rate for the unknown gives the general standard-error estimate:
Standard error is the estimated standard deviation of the estimator across repeated samples. It isn't the spread of individual labels.
Notice the in the denominator: standard error scales as . Cutting your typical sampling margin in half requires as many labels. Cutting it by a factor of 10 requires reviewing as many signups. This square-root penalty governs labeling budgets across machine learning: statistical certainty gets progressively more expensive as you demand narrower margins.
The central limit theorem explains why a sample mean can approach a normal shape as enough independent observations accumulate. It doesn't rescue tiny samples, dependent rows, or rates near 0 and 1.
These examples treat the live queue as much larger than the sample. Sampling a substantial fraction of a fixed queue without replacement needs a finite-population correction, because inspecting more of the queue leaves fewer unknown labels. A census of all its labels has no sampling uncertainty about that queue, though future traffic and label errors remain uncertain.
A familiar first approximation turns that scale into the Wald interval:
Look at the midpoint before running the code. The Wald midpoint is the observed rate, 0.16; the standard error supplies the margin on each side.
1from math import sqrt
2
3successes = 16
4n = 100
5p_hat = successes / n
6se = sqrt(p_hat * (1 - p_hat) / n)
7low = p_hat - 1.96 * se
8high = p_hat + 1.96 * se
9
10print(f"estimate: {p_hat:.3f}")
11print(f"standard error: {se:.4f}")
12print(f"quick interval: [{low:.3f}, {high:.3f}]")1estimate: 0.160
2standard error: 0.0367
3quick interval: [0.088, 0.232]The output makes the scale visible: 0.16 plus or minus about 0.072. A larger n shrinks standard error because the same kind of estimator moves less across repeated samples.
Keep this calculation as a useful first pass, not as the default report. Its symmetric endpoints can become impossible or collapse at the edges.
Wald collapses at 0/10; Wilson doesn't
The Wald interval breaks when the rate is near 0 or 1. If reviewers inspect ten flagged signups and find zero abuse, , the standard error is zero, and the interval becomes [0, 0]. That result looks decisive. Should ten clean labels count as proof that abuse risk is exactly zero? They shouldn't.
A Wilson score confidence interval handles that case better. Instead of treating the observed zero as certainty, it asks which candidate population rates remain compatible with the count. Its two-sided 95% interval for 0 / 10 reaches from 0 to about 0.278.
Wilson's construction inverts a score test for a binomial proportion: the test uses each candidate rate to calculate its standard error, not the observed .[5] For 16 / 100, the interval's adjusted center is about 0.173 and its margin is about 0.072, giving [0.101, 0.244]. The general calculation is:
For a rough 95 percent interval, use . The center isn't because the score construction adds a small adjustment toward 0.5.
With and , , so the center is about . For 0 / 10, the same adjustment moves the center off zero and keeps the margin positive. The code below uses exactly and returns center - margin and center + margin, clipped to .
Predict the three rows before running it: Wald should collapse at both endpoints, while Wilson should leave room above 0 after zero successes and below 1 after ten successes.
1from math import sqrt
2
3def wald_interval(successes: int, n: int, z: float = 1.96) -> tuple[float, float]:
4 if n <= 0 or not 0 <= successes <= n:
5 raise ValueError("require 0 <= successes <= n and n > 0")
6 p_hat = successes / n
7 se = sqrt(p_hat * (1 - p_hat) / n)
8 return p_hat - z * se, p_hat + z * se
9
10def wilson_interval(successes: int, n: int, z: float = 1.96) -> tuple[float, float]:
11 if n <= 0 or not 0 <= successes <= n:
12 raise ValueError("require 0 <= successes <= n and n > 0")
13 p_hat = successes / n
14 denominator = 1 + z * z / n
15 center = (p_hat + z * z / (2 * n)) / denominator
16 margin = z / denominator * sqrt(
17 p_hat * (1 - p_hat) / n + z * z / (4 * n * n)
18 )
19 return max(0.0, center - margin), min(1.0, center + margin)
20
21for successes, n in [(16, 100), (0, 10), (10, 10)]:
22 wald = wald_interval(successes, n)
23 wilson = wilson_interval(successes, n)
24 print(
25 f"{successes:>2}/{n:<3} wald=[{wald[0]:.3f}, {wald[1]:.3f}] "
26 f"wilson=[{wilson[0]:.3f}, {wilson[1]:.3f}]"
27 )116/100 wald=[0.088, 0.232] wilson=[0.101, 0.244]
2 0/10 wald=[0.000, 0.000] wilson=[0.000, 0.278]
310/10 wald=[1.000, 1.000] wilson=[0.722, 1.000]
A 95% confidence level describes repeated use of the procedure: draw a sample, calculate an interval, and check whether it contains the true rate. Wilson targets 95% coverage under the binomial model and typically improves on Wald, but its coverage isn't exactly 95% for every sample size and rate.[6][5]
That guarantee is about the procedure across replications. After seeing one realized interval, don't read it as "probability 0.95 that the true rate sits inside these two numbers." It isn't a posterior probability on these fixed endpoints.
For the review decision, if a threshold lies inside this two-sided 95% interval, the interval doesn't establish that the rate is above or below it. Inclusion isn't proof of equality. Keep the sample size and confidence level fixed in advance; repeatedly checking until an interval clears a threshold requires a different analysis.
For this binary report, use the Wilson interval. The immediate question is now practical: how much does a larger, still-representative sample narrow it?
Why should a report avoid the quick Wald interval for 0 / 10 reviewed abuse cases?
Answer
It returns [0, 0], which treats ten observed non-abuse cases as proof that abuse risk is exactly zero. A score interval preserves uncertainty and correctly leaves a nonzero upper bound.
More reviews only help if they match the queue
The Wilson interval tells us how much random movement remains. Suppose later labeling keeps the same center: 16 percent of reviewed flagged signups are abuse. If the added labels are still representative, increasing the sample size should narrow the interval.
Call the direction before running the next example: the estimate should stay at 0.16, while the interval should contract as n grows.
1from math import sqrt
2
3def wilson_interval(successes: int, n: int, z: float = 1.96) -> tuple[float, float]:
4 if n <= 0 or not 0 <= successes <= n:
5 raise ValueError("require 0 <= successes <= n and n > 0")
6 p_hat = successes / n
7 denominator = 1 + z * z / n
8 center = (p_hat + z * z / (2 * n)) / denominator
9 margin = z / denominator * sqrt(
10 p_hat * (1 - p_hat) / n + z * z / (4 * n * n)
11 )
12 return max(0.0, center - margin), min(1.0, center + margin)
13
14for successes, n in [(16, 100), (160, 1_000), (1_600, 10_000)]:
15 low, high = wilson_interval(successes, n)
16 print(f"{successes:>4}/{n:<6} estimate={successes / n:.3f} interval=[{low:.3f}, {high:.3f}]")116/100 estimate=0.160 interval=[0.101, 0.244]
2 160/1000 estimate=0.160 interval=[0.139, 0.184]
31600/10000 estimate=0.160 interval=[0.153, 0.167]16 / 100 is an early read. 1,600 / 10,000 is much tighter evidence about the population, assuming both samples represent the queue you intend to serve. More labels reduced random movement without changing the center.
Here, bias means the estimator's average over repeated samples minus the target rate. For simple random sampling, , so the sample fraction is unbiased. One particular sample can still miss . Its variance shrinks as grows, which is why these intervals tighten. This is greater statistical precision, distinct from the classifier metric also called precision.
But n only buys precision when the labels target the same population. A large sample from one slice can be confidently wrong about the queue.
A domestic-only sample is precise about the wrong pile
Suppose the next labeling budget goes to 2,000 routine domestic flags. Those labels describe that slice well, but they don't establish the rate in parts of the queue they never cover.
Selection bias appears when the selection process systematically shifts the estimate away from its target. To make the missing information visible, suppose we know the true rates in this separate, hypothetical queue. The slices must not overlap: classify high-value signups first, then divide the remainder into domestic and international signups.
| Flagged-signup slice | Share of queue | Confirmed-abuse rate |
|---|---|---|
| routine domestic signups | 70 percent | 12 percent |
| international signups | 20 percent | 25 percent |
| high-value signups | 10 percent | 42 percent |
A review project that samples only routine domestic flags can return a very tight estimate near 12 percent while missing higher-risk parts of the queue. That 12 percent is an estimate for the domestic slice.
Picture 1,000 flags with this mix: 700 domestic flags contribute 84 abuse cases, 200 international flags contribute 50, and 100 high-value flags contribute 42. The total is 176 / 1,000 = 17.6%. Equivalently, weight each rate by its share: 0.70 × 0.12 + 0.20 × 0.25 + 0.10 × 0.42 = 0.176. Domestic-only sampling has bias 12% - 17.6% = -5.6 percentage points for this queue target. More domestic labels don't remove it.
To estimate overall queue precision, sample each important slice or sample randomly from the actual queue, then account for the slice mix.
1slices = [
2 ("routine domestic", 0.70, 0.12),
3 ("international", 0.20, 0.25),
4 ("high value", 0.10, 0.42),
5]
6
7overall_precision = sum(share * rate for _, share, rate in slices)
8domestic_only_precision = slices[0][2]
9
10print(f"domestic-only estimate: {domestic_only_precision:.1%}")
11print(f"queue-weighted estimate: {overall_precision:.1%}")
12print(f"bias from wrong slice: {domestic_only_precision - overall_precision:+.1%}")
13
14assert round(overall_precision, 3) == 0.1761domestic-only estimate: 12.0%
2queue-weighted estimate: 17.6%
3bias from wrong slice: -5.6%In a real audit, the slice rates are estimates too. Confirm that the shares describe the target queue and that labels were selected randomly within each slice. If you deliberately review equal numbers from unequal-sized slices, don't pool their raw counts as though the result were a simple random sample of the queue. Weight the rates by population shares, and calculate uncertainty using the sampling design rather than applying the unweighted Wilson formula to the pooled counts.[4]
This trade-off has a clean mathematical foundation: the bias-variance decomposition of Mean Squared Error (). For any estimator targeting the unknown queue rate :
where .
When you sample randomly from the actual queue, your estimator is unbiased: . Mean squared error equals sampling variance, . Gathering more labels drives variance down as (and standard error down as ), converging toward zero error.
When you sample only routine domestic flags, your estimator converges to instead of the true queue rate . That leaves an irreducible bias of . Even if you label 10,000 domestic signups so variance collapses near zero:
The squared bias term creates an error floor that extra labels can't shrink. Spending budget on more domestic reviews only buys high-confidence delusion about the broader queue.

Random uncertainty, sampling design, and label quality need different fixes. Use the symptom to choose the repair:
| Failure | Symptom | Correct response |
|---|---|---|
| Too few representative reviews | interval is wide | label more randomly selected queue items |
| Wrong slices reviewed | interval may be narrow but misses deployment risk | repair sampling plan and report slice coverage |
| Labels change after model output is seen | metric drifts toward model guesses | lock rubric and review ambiguous labels independently |
A domestic-only sample of 2,000 flags yields a tight interval around 12%. The queue mix is 70/20/10 at 12%, 25%, and 42%. Can you treat 12% as queue precision?
Answer
No. The queue-weighted rate is 17.6%. Extra domestic labels shrink variance around the wrong center.
A prior pulls the center for a different reason
With one abuse case in five reviews, MLE gives 1 / 5 = 0.20. With 160 in 1,000, it gives 0.16. It uses only the observed labels. More generally, k positive and n - k negative labels contribute likelihood proportional to . This expression is maximized at , the same sample fraction we've used throughout.
Maximum a posteriori estimation (MAP) picks the posterior's highest-density point, or mode. With the prior from the Bayesian lesson, one abuse case in five reviews gives a posterior. Its mode is (3 - 1) / (3 + 6 - 2) = 2/7, about 0.286. That differs from its mean, 3/9, about 0.333.[7]
For k successes in n labels, this prior gives posterior . Substituting those parameters into the mode formula gives:
The mode formula acts as if one success and one failure were added to the observed counts. The posterior-mean formula instead uses (k + 2) / (n + 4). Neither rule adds actual reviews. This symmetric prior is an illustration, not a recommendation for a low-abuse queue: a defensible prior should reflect relevant knowledge, and its influence should be reported.
Run the same three counts through both rules. The labels stay fixed; only the source of the center changes.
1def validate_counts(successes: int, n: int) -> None:
2 if n <= 0 or not 0 <= successes <= n:
3 raise ValueError("require 0 <= successes <= n and n > 0")
4
5def mle_rate(successes: int, n: int) -> float:
6 validate_counts(successes, n)
7 return successes / n
8
9def map_rate_beta_2_2(successes: int, n: int) -> float:
10 validate_counts(successes, n)
11 return (successes + 1) / (n + 2)
12
13for successes, n in [(16, 100), (1, 5), (160, 1_000)]:
14 mle = mle_rate(successes, n)
15 map_estimate = map_rate_beta_2_2(successes, n)
16 posterior_mean = (successes + 2) / (n + 4)
17 print(
18 f"{successes}/{n:<4} MLE={mle:.3f} "
19 f"MAP={map_estimate:.3f} posterior_mean={posterior_mean:.3f}"
20 )116/100 MLE=0.160 MAP=0.167 posterior_mean=0.173
21/5 MLE=0.200 MAP=0.286 posterior_mean=0.333
3160/1000 MLE=0.160 MAP=0.161 posterior_mean=0.161The output makes the scale of the prior's influence visible: 1 / 5 moves from 0.200 to 0.286, while 160 / 1,000 moves from 0.160 to about 0.161.
Wilson's center also blends toward 0.5, but that's a confidence-interval construction, not a posterior mode. Don't hide weak evidence behind a convenient prior. State the prior and show how much it changes the result.[8][9]
So far the target has been a rate inside the flagged queue. A score adds another question: when it says 20 percent, do outcomes in that score bucket really average 20 percent?
A four-point calibration gap can still be noise
The Bayesian lesson asked whether forecasts line up with outcomes on new tasks. That's calibration: among many comparable cases assigned a probability near 20 percent, do about 20 percent have positive outcomes?
Use a separate audit of 100 randomly selected signups all assigned risk 0.20 before reviewers see their outcomes. Suppose 16 are abusive. These are new labels, chosen from one score bucket; the same 16 / 100 arithmetic applies. The observed gap is four percentage points. Does this bucket have enough evidence to rule out 20 percent?
1from math import sqrt
2
3def wilson_interval(successes: int, n: int, z: float = 1.96) -> tuple[float, float]:
4 if n <= 0 or not 0 <= successes <= n:
5 raise ValueError("require 0 <= successes <= n and n > 0")
6 p_hat = successes / n
7 denominator = 1 + z * z / n
8 center = (p_hat + z * z / (2 * n)) / denominator
9 margin = z / denominator * sqrt(
10 p_hat * (1 - p_hat) / n + z * z / (4 * n * n)
11 )
12 return max(0.0, center - margin), min(1.0, center + margin)
13
14predicted_risk = 0.20
15observed_abuse = 16
16n = 100
17low, high = wilson_interval(observed_abuse, n)
18
19print(f"predicted bucket risk: {predicted_risk:.1%}")
20print(f"observed abuse rate: {observed_abuse / n:.1%}")
21print(f"observed interval: [{low:.1%}, {high:.1%}]")
22print(f"20% is plausible here: {low <= predicted_risk <= high}")1predicted bucket risk: 20.0%
2observed abuse rate: 16.0%
3observed interval: [10.1%, 24.4%]
420% is plausible here: TrueThe interval includes 20 percent, so this preselected bucket hasn't established a calibration failure by the two-sided 95% comparison. It hasn't established exact calibration either. A wide interval can hide an operationally important gap.
Real buckets contain nearby, not identical, predictions. Compare their average prediction with their observed rate and preserve the number of labels in each bucket. Use held-out outcomes, choose bins before inspecting them, and don't treat one compatible bucket as evidence that the whole model is calibrated. Searching many buckets for a mismatch also needs a multiple-comparison plan.[10]
Deep networks can be miscalibrated even when accuracy looks strong, so calibration has to be measured rather than assumed.[11] Accuracy and calibration answer different questions, and neither replaces representative labels.
We now have the pieces of an honest audit: a rate, an interval, a sampling plan, and a calibration comparison. Put those pieces into a report without letting the dashboard hide context.
A risk bucket predicts 20 percent, while 16 of 100 reviewed cases are positive and the observed Wilson interval is 10.1 to 24.4 percent. Can this sample establish miscalibration?
Answer
No. The interval still includes 20 percent, so the four-point observed gap is compatible with finite-sample variation. Gather more representative labels before declaring a calibration failure.
Print the counts, the interval, and the sampling plan
A report should let someone reconstruct the estimate before they act. This function prints the numerator, denominator, estimate, interval width, and sampling method.
It doesn't decide whether to launch. That decision still needs costs, thresholds, and slice coverage.
There's no universal label count that makes an audit adequate. A 14-percentage-point interval may be enough for rough staffing and too wide for a close policy decision. Show the width and let the decision's tolerance determine whether more labels are needed. A slice name alone also isn't a sampling plan: say how its reviewed cases were selected.
1from math import sqrt
2
3def wilson_interval(successes: int, n: int, z: float = 1.96) -> tuple[float, float]:
4 if n <= 0 or not 0 <= successes <= n:
5 raise ValueError("require 0 <= successes <= n and n > 0")
6 p_hat = successes / n
7 denominator = 1 + z * z / n
8 center = (p_hat + z * z / (2 * n)) / denominator
9 margin = z / denominator * sqrt(
10 p_hat * (1 - p_hat) / n + z * z / (4 * n * n)
11 )
12 return max(0.0, center - margin), min(1.0, center + margin)
13
14def report_precision(successes: int, n: int, slice_name: str, sampling_plan: str) -> str:
15 low, high = wilson_interval(successes, n)
16 return (
17 f"{slice_name}: {successes}/{n} = {successes / n:.1%}; "
18 f"95% Wilson interval [{low:.1%}, {high:.1%}]\n"
19 f" width={(high - low) * 100:.1f} percentage points; sample={sampling_plan}"
20 )
21
22print(report_precision(16, 100, "weekly queue", "simple random flags from this week"))
23print(report_precision(25, 100, "international slice", "simple random flags within this slice"))
24
25try:
26 report_precision(2, 0, "empty slice", "no reviews")
27except ValueError as error:
28 print(error)1weekly queue: 16/100 = 16.0%; 95% Wilson interval [10.1%, 24.4%]
2 width=14.3 percentage points; sample=simple random flags from this week
3international slice: 25/100 = 25.0%; 95% Wilson interval [17.5%, 34.3%]
4 width=16.8 percentage points; sample=simple random flags within this slice
5require 0 <= successes <= n and n > 0The international line is a separate sample of 100 international flags, not a subset of the mixed 16/100 draw. Neither line should claim to measure next month's traffic without evidence that the population is comparable.
Also record which important slices still lack labels. A number without its collection process invites overconfidence.
Tighter intervals don't fix the wrong labels
An interval around queue precision measures estimation uncertainty: how much a finite reviewed sample can move. It doesn't identify every reason a detector can fail.
Ask what happens when the interval is narrow but reviewers saw model predictions before resolving ambiguous cases. The arithmetic can be precise while the labels no longer represent an independent review process.
| Problem | What it looks like | Does a tighter interval fix it? |
|---|---|---|
| finite representative sample | measured precision moves across random label batches | yes, more representative labels narrow it |
| sampling bias | only one routine slice was reviewed | no, change the sample design |
| ambiguous labels | reviewers disagree on whether a signup is abuse | no, improve rubric and measure agreement |
| unfamiliar deployment traffic | new payment path or geography appears | no, add coverage and retrain or route safely |
| miscalibrated score | predicted-risk buckets disagree with observed rates | no, measure and calibrate on held-out labels |
Some literature groups irreducible input or label ambiguity under aleatoric uncertainty, and lack of knowledge about unfamiliar cases under epistemic uncertainty. Those names can be useful, but the repair matters more: don't use an interval for sampling noise as if it solved biased sampling, unclear labels, or unfamiliar traffic.
Practice with 24/120 high-value flags
Transfer the audit to one new slice. You review 24 / 120 flagged high-value signups and confirm abuse. Before calculating, predict whether 120 labels can settle a 25 percent policy threshold.
- Compute the point estimate and run the
wilson_intervalfunction for24, 120. - Decide whether this sample alone proves the high-value slice meets a 25 percent minimum precision requirement.
- Your routine-domestic slice has
120 / 1000abuse. Explain why you can't replace one slice with the other. - A predicted-risk bucket averages 25 percent risk and contains
24 / 120abuse. Decide whether this is enough to declare it miscalibrated.
Solution checks:
| Item | Check |
|---|---|
| Point estimate | 24 / 120 = 0.20 |
| Wilson interval | approximately [0.138, 0.280] |
| 25 percent requirement | the interval crosses 25 percent, so this sample doesn't establish that precision is below or above the requirement |
| Slice swap | high-value and routine-domestic flags represent different populations; combine them only with a valid sampling/weighting plan |
| Calibration call | no; 25 percent remains plausible within this finite bucket's interval |
The original 16 / 100 wasn't wrong; it was incomplete. Counts plus an interval describe sampling uncertainty. The sampling method and label process determine which population that description deserves to represent.