AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Statistical Hypothesis Testing for Machine Learning

📚 Machine Learning⏱️ 28 min read🎓 Grade 10
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 28 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

A bank's fraud team builds two models to flag suspicious UPI transactions before money leaves an account. On a held-out batch of transactions, Model A correctly catches 80% of the fraud cases and Model B catches 72%. Someone on the team says, "Ship Model A — it's clearly better." But is it? If you drew a different batch of transactions tomorrow, would Model A still win by 8 percentage points, or could that gap shrink to nothing, or even flip in Model B's favour? An accuracy number computed on a finite set of examples is not a fixed truth about a model — it is one roll of the dice, drawn from a spread of accuracies the model could have shown you depending on which examples happened to land in the test set. Hypothesis testing is the machinery that answers the real question buried underneath "which model is better": is the observed gap large compared to the noise a finite sample can produce on its own, or is it the kind of gap you'd see fairly often even if the two models were secretly identical?

This chapter builds that machinery from first principles — from a single correct/incorrect prediction, up through the variance of an accuracy estimate, to a formal test statistic, a p-value, and a decision rule — and then exposes a trap that catches even experienced ML engineers: the test you'd reach for by instinct is often the wrong test for how models are actually compared in practice, and using it silently throws away statistical power.

Why "Higher Accuracy" Isn't the Same as "Better Model"

Suppose Model A's true accuracy — its correctness rate over every UPI transaction it could ever see, not just the ones you happened to test it on — is exactly 76%, identical to Model B's true accuracy. If you evaluate both on a test set of 250 transactions each, you will almost never see exactly 76% and 76%. Sampling is noisy: which 250 transactions you happened to draw, and which of those happened to be the harder edge cases, shifts the measured accuracy up or down by a few points purely by chance. So even two identical models will typically show slightly different accuracies on any single test set. The question hypothesis testing answers is precise: given the size of the gap you observed and the size of the test set it was measured on, how surprising would this gap be if there were really no difference between the models at all? If the answer is "very surprising," you have evidence of a real difference. If the answer is "not surprising, this happens all the time by chance," the gap you saw is consistent with pure noise, and shipping Model A on that basis alone is a guess dressed up as a conclusion.

The Coin-Flip Intuition: From Batting Averages to Classifiers

Here is the cleanest way to see why a finite-sample accuracy behaves like a random, wobbly quantity rather than a fixed number: model each prediction as a coin flip. A cricket commentator might describe a batsman's "conversion rate" — the fraction of innings in which he crosses fifty runs — as roughly constant match to match, even though whether he crosses fifty in this particular innings depends on many things (pitch, bowling attack, form on the day) that behave, in aggregate, like a weighted coin. If his true long-run conversion rate is 40%, he won't cross fifty in exactly 40% of any small sample of innings — in ten innings he might do it three times, or six times, purely from the randomness of when good and bad days land.

A classifier's correctness on one test example is the same kind of weighted coin. Formally, model the outcome of prediction i as a random variable Xi, where Xi = 1 if the model is correct on example i and Xi = 0 if it is wrong, with Xi ~ Bernoulli(p) and p the model's true accuracy over the whole population of possible inputs. "True accuracy" here means the accuracy the model would show if you could test it on infinitely many transactions — a fixed but unknown constant. The accuracy you actually measure on n test examples is the sample mean:

p̂ = (X1 + X2 + ... + Xn) / n

p̂ is a random variable too, because it is built from n random coin flips. To know how much it can wobble around the true p, we need its variance — and this is worth deriving properly rather than quoting, because the entire rest of the chapter rests on it.

Deriving the Variance of an Accuracy Estimate

For a single Bernoulli(p) trial X, the expected value is E[X] = 1·p + 0·(1−p) = p. For the variance, use Var(X) = E[X²] − (E[X])². Because X only takes values 0 and 1, X² = X always (0² = 0, 1² = 1), so E[X²] = E[X] = p. Therefore:

Var(X) = p − p² = p(1 − p)

Now let T = X1 + X2 + ... + Xn be the total number of correct predictions out of n independent test examples. Because the Xi are independent, variances add:

Var(T) = Var(X1) + Var(X2) + ... + Var(Xn) = n·p(1 − p)

(T itself follows a Binomial(n, p) distribution — this is exactly where the binomial distribution you meet in NCERT probability comes from: a count of successes across n independent Bernoulli trials.) Since p̂ = T/n, and dividing a random variable by the constant n divides its variance by n²:

Var(p̂) = Var(T) / n² = n·p(1 − p) / n² = p(1 − p) / n

So the standard error of an accuracy estimate — the typical size of its wobble around the true accuracy — is:

SE(p̂) = √(p(1 − p) / n)

Two things fall straight out of this formula and are worth internalizing before moving on. First, SE shrinks as 1/√n, not 1/n — quadrupling your test set only halves the noise, which is why doubling a small test set rarely settles an argument. Second, SE is largest when p is near 0.5 and smallest when p is near 0 or 1 — a model that is almost always right or almost always wrong has a more stable measured accuracy than one hovering near a coin flip.

Because p̂ is an average of n independent random variables, the Central Limit Theorem says that for large enough n, p̂ is approximately normally distributed: p̂ ≈ N(p, p(1−p)/n). "Large enough" is usually checked with the rule of thumb np ≥ 5 and n(1−p) ≥ 5, comfortably satisfied whenever your test set has more than a few dozen examples and the model isn't near-perfect or near-worthless. This is the bridge that lets us replace an awkward binomial calculation with the clean, well-tabulated normal distribution — and it is the single idea that makes every z-test in this chapter work.

Setting Up a Formal Hypothesis Test

A hypothesis test always starts by stating two competing claims about the world:

  • Null hypothesis (H0): the "nothing interesting is happening" claim — here, that the two models have equal true accuracy, pA = pB.
  • Alternative hypothesis (H1): the claim you're trying to find evidence for — here, that pA ≠ pB (a two-sided test, since before looking at the data we didn't know which model would come out ahead).

You then compute a test statistic: a single number, built from the data, that measures how many standard errors the observed result sits away from what H0 predicts. For a normally-distributed quantity this is a z-score:

z = (observed difference − difference predicted by H0) / SE of the difference

Under H0, the difference predicted is 0, so z simply measures how many standard errors the observed gap is from zero. You fix a significance level α (almost always 0.05 in practice) before looking at the result — this is the probability of wrongly rejecting a true H0 that you're willing to tolerate. For a two-sided test at α = 0.05, the rejection region is |z| > 1.96 — a threshold that itself comes from the normal distribution, since 95% of a standard normal's probability mass lies between −1.96 and 1.96.

The p-value converts the test statistic into a probability: it is the chance of seeing a result at least as extreme as what you observed, if H0 were true. Formally, for a two-sided test, p-value = P(|Z| ≥ |zobserved|), computed using the standard normal distribution. If the p-value is smaller than α, you reject H0 and conclude the difference is statistically significant; otherwise you fail to reject H0 — which is not the same as proving the models are equal, only that this test, on this data, didn't find enough evidence to say otherwise.

Worked Example 1: Comparing Two Models on Independent Test Sets

Two teams build competing UPI fraud-flagging models and evaluate them the simplest possible way: each model is scored on its own separately, randomly drawn batch of 250 transactions, with no overlap between the two batches. Model A gets 200 of its 250 correct (p̂A = 0.80). Model B gets 180 of its 250 correct (p̂B = 0.72). Is an 8-point gap real, or noise?

Because the two samples are independent, Var(p̂A − p̂B) = Var(p̂A) + Var(p̂B) — variances of independent random variables add. Under H0, both models share one true accuracy p, which we don't know, so we estimate it by pooling both samples together:

pool = (xA + xB) / (n1 + n2) = (200 + 180) / (250 + 250) = 380 / 500 = 0.76

SE = √( p̂pool(1 − p̂pool) · (1/n1 + 1/n2) ) = √( 0.76 × 0.24 × (1/250 + 1/250) ) = √(0.1824 × 0.008) = √0.0014592 ≈ 0.0382

z = (p̂A − p̂B) / SE = (0.80 − 0.72) / 0.0382 ≈ 2.09

import math

def two_proportion_z_test(x1, n1, x2, n2):
    p1, p2 = x1 / n1, x2 / n2
    p_pool = (x1 + x2) / (n1 + n2)
    se = math.sqrt(p_pool * (1 - p_pool) * (1 / n1 + 1 / n2))
    z = (p1 - p2) / se
    p_value = 2 * (1 - 0.5 * (1 + math.erf(abs(z) / math.sqrt(2))))
    return z, p_value

z, p = two_proportion_z_test(200, 250, 180, 250)
print(f"z = {z:.3f}, p = {p:.3f}")
# z = 2.094, p = 0.036

z ≈ 2.09 exceeds the 1.96 threshold, and the two-sided p-value works out to ≈0.036 — smaller than α = 0.05. We reject H0: on this evidence, Model A's true accuracy is significantly higher than Model B's. Note carefully what this does and doesn't say. It does not say "there is a 3.6% chance Model B is actually as good as Model A" — that is a statement about H0, and a p-value never assigns a probability to a hypothesis. It says: if the two models truly had equal accuracy, a gap at least this large, on samples this size, would arise from sampling noise alone only about 3.6% of the time. That's rare enough that we treat it as evidence of a genuine difference rather than a fluke — but it remains possible we're wrong.

A Common Misconception, Corrected

The single most common misreading of a p-value, seen constantly in ML papers and product post-mortems alike, is treating "p = 0.036" as "there's a 96.4% chance Model A is better." A p-value is computed by assuming H0 is true and asking how likely the observed data (or something more extreme) would be under that assumption. It says nothing about the probability that H0 itself is true — that would require Bayesian reasoning with a prior belief about how likely A being better was before you saw any data, which classical hypothesis testing deliberately doesn't use. A p-value of 0.036 is a statement about the data given the hypothesis, not a statement about the hypothesis given the data. Getting this backwards is the single most consequential statistics mistake to avoid, in ML evaluation and everywhere else.

The Trap That Breaks Real ML Comparisons

Worked Example 1 evaluated the two models on two separate batches of transactions, and that independence was exactly what let us add the variances in the SE formula. But that is almost never how ML models are actually compared. In practice, you build one held-out test set and run both models on the exact same examples, because you want to compare like with like — testing on identical inputs removes differences caused by one batch happening to contain easier or harder cases than the other. This is good experimental practice. But it breaks the independent-samples z-test's core assumption.

Here's why. If both models are scored on the same n messages, their two accuracy numbers are no longer statistically independent: whatever makes message k easy or hard affects both models' correctness on that message simultaneously. A garbled, ambiguous transaction description might trip up both models at once; a clean, obvious fraud pattern might be caught by both. This shared exposure to the same set of "hard" and "easy" examples creates correlation between p̂A and p̂B — and the formula Var(p̂A − p̂B) = Var(p̂A) + Var(p̂B) is only valid when p̂A and p̂B are independent. Plugging correlated, paired accuracies into the independent-samples formula doesn't just give a slightly wrong answer — it typically overstates the noise, because it fails to cancel out the "shared difficulty" component of the variance that both models experience identically. The result is a test that is less likely to detect a real difference than it should be: real signal gets misread as noise.

Paired Data Needs a Paired Test: McNemar's Test

When two models are scored on the same set of examples, the right tool discards the examples that carry no information and analyzes only the ones that distinguish the models. Organize the paired outcomes into a 2×2 table of counts:

  • n11: both models correct on this example
  • n10: Model A correct, Model B wrong
  • n01: Model A wrong, Model B correct
  • n00: both models wrong

The concordant counts n11 and n00 — where the models agree — tell you nothing about which model is better, because they contribute identically to both accuracies and cancel out of the difference p̂A − p̂B. All of the information about which model is better lives in the discordant pairs: n10 (A wins, B loses) and n01 (A loses, B wins). This is the key insight McNemar's test is built on, and it's the reason the paired test can be so much more sensitive than the naive one: it isn't diluted by the concordant examples both models handle identically.

Under H0 (the models are equally accurate), consider only the discordant pairs and ask: among examples where the models disagree, is A equally likely to be the one that's right as B is? If the models are truly equal, each discordant pair should be a coin flip — a Bernoulli(0.5) trial as to whether A or B was the correct one. So n10, the count of "A right, B wrong" pairs, should follow Binomial(n10 + n01, 0.5) under H0. Using the same normal-approximation machinery derived earlier, with p = 0.5 and sample size n10 + n01, the test statistic simplifies to:

z = (n10 − n01) / √(n10 + n01)

Notice what this formula depends on: only the discordant count, n10 + n01 — which is typically far smaller than the full test set size n. A smaller effective sample size sounds like it should mean a weaker test, but because the concordant pairs contributed no signal in the first place, removing them from the noise calculation (rather than just from the signal) makes the test more powerful per unit of real information, not less.

Worked Example 2: The Same 200 Messages, Two Ways

Now return to a realistic setup: 200 UPI transaction descriptions, each scored by both models. Model A gets 159 correct (79.5%). Model B gets 151 correct (75.5%) — a 4-point gap, smaller than Worked Example 1's, on the same size test set. The paired outcomes break down as:

  • Both correct: n11 = 150
  • A correct, B wrong: n10 = 9
  • A wrong, B correct: n01 = 1
  • Both wrong: n00 = 40

(Check: 150 + 9 + 1 + 40 = 200; A's correct total = 150 + 9 = 159; B's correct total = 150 + 1 = 151 — consistent with the accuracies above.)

import math

def two_proportion_z_test(x1, n1, x2, n2):
    p1, p2 = x1 / n1, x2 / n2
    p_pool = (x1 + x2) / (n1 + n2)
    se = math.sqrt(p_pool * (1 - p_pool) * (1 / n1 + 1 / n2))
    z = (p1 - p2) / se
    p_value = 2 * (1 - 0.5 * (1 + math.erf(abs(z) / math.sqrt(2))))
    return z, p_value

def mcnemar_test(n10, n01):
    z = (n10 - n01) / math.sqrt(n10 + n01)
    p_value = 2 * (1 - 0.5 * (1 + math.erf(abs(z) / math.sqrt(2))))
    return z, p_value

# naive (wrong for paired data): treats the two 200-message
# scores as if drawn from independent samples
z_naive, p_naive = two_proportion_z_test(159, 200, 151, 200)
print(f"naive:   z = {z_naive:.3f}, p = {p_naive:.3f}")
# naive:   z = 0.958, p = 0.338

# correct: uses only the 10 examples where the models disagree
z_mc, p_mc = mcnemar_test(n10=9, n01=1)
print(f"McNemar: z = {z_mc:.3f}, p = {p_mc:.3f}")
# McNemar: z = 2.530, p = 0.011

The naive test — plugging the two 79.5%/75.5% accuracies into the independent-samples formula as if the 200 messages for A and the 200 messages for B were unrelated batches — gives z ≈ 0.96, p ≈ 0.338. Nowhere close to significant; you'd conclude the models are statistically indistinguishable. McNemar's test, using only the 10 discordant messages (9 where A alone was right, 1 where B alone was right), gives z ≈ 2.53, p ≈ 0.011. Comfortably significant at α = 0.05. Same 200 messages, same two accuracy numbers, same 4-point gap — opposite conclusions, because the naive test's variance formula was inflated by treating 190 shared, uninformative examples (150 both-right, 40 both-wrong) as if they were independent evidence of uncertainty, when in fact both models experienced those exact 190 examples identically. Stripping that out is precisely what makes McNemar's test the sharper instrument here: with 9-to-1 odds among the disagreements, that's a strongly one-sided split for a supposedly fair coin, and the test correctly flags it as unlikely to arise by chance.

The diagram below plots the standard normal curve used by both tests, with the ±1.96 rejection boundary for α = 0.05 shaded, and both worked test statistics marked to scale on the same axis.

Naive versus McNemar test statistics on the standard normal curve A standard normal curve with rejection regions shaded beyond z = -1.96 and z = 1.96. A vertical marker at z = 0.96 (amber) falls inside the fail-to-reject zone, representing the naive unpaired test on the 200-message paired dataset. A second marker at z = 2.53 (teal) falls inside the rejection zone, representing the correct McNemar test on the same data. Naive vs. paired test: same 4-point gap, different verdict 0 -1.96 1.96 Rejection region, |z| > 1.96 (α = 0.05, two-sided) Naive (unpaired) statistic: z ≈ 0.96 — fails to reject McNemar (paired) statistic: z ≈ 2.53 — rejects H₀

The lesson generalizes well beyond fraud detection: any time you evaluate two models, two prompts, or two versions of a pipeline on the identical set of test cases — which is the standard, correct way to run an ML comparison — you have paired data, and a paired test (McNemar's test for accuracy/classification, or a paired t-test for continuous metrics like loss or latency) is the statistically correct tool. Reaching for the independent two-sample test out of habit doesn't just risk a wrong answer occasionally; as this example shows, it can systematically bury a real, meaningful difference under overstated uncertainty.

Exam Connections

The building blocks of this chapter map cleanly onto material you already study, though it's worth being precise about which syllabus covers what, since the pieces are scattered. Core CBSE Class 11–12 Mathematics (the NCERT Probability unit) covers Bernoulli trials, the binomial distribution, conditional probability, and Bayes' theorem — exactly the foundation used to derive Var(X) = p(1−p) and Var(T) = np(1−p) above. That core syllabus, and JEE Main/Advanced and BITSAT mathematics alongside it, stops at discrete and combinatorial probability; none of them include the Central Limit Theorem, normal approximation to the binomial, z-tests, p-values, or formal hypothesis testing, so you won't find board or JEE/BITSAT questions phrased as "test whether this classifier's accuracy is significantly different from 90%." If you've opted for CBSE Applied Mathematics (code 241) rather than core Mathematics, that syllabus does include a dedicated Inferential Statistics unit with z-tests for proportions and means, which is the direct classroom cousin of Worked Example 1's calculation — worth checking your own subject combination rather than assuming. Further ahead, the GATE Data Science & AI paper explicitly examines hypothesis testing, p-values, and confidence intervals as core topics, and any serious ML or data-science coursework at the undergraduate level treats the material in this chapter as a prerequisite, not an elective. The honest framing: this chapter is not board-exam content, but it is exactly the statistical literacy every one of you will need the first time you A/B test a model, a feature, or a product change for real.

Active Recall

  1. A model scores 68% on a 100-example test set and another scores 74% on a different, independent 100-example test set. Set up H0 and H1, then compute p̂pool, the standard error, and the z-statistic. Is the difference significant at α = 0.05?
  2. Explain, without using the words "independent" or "paired," why testing two models on the exact same set of examples changes which formula for Var(p̂A − p̂B) is valid.
  3. In a paired comparison, why do the n11 (both-correct) and n00 (both-wrong) counts drop out of McNemar's test statistic entirely? What would happen to the test statistic if you mistakenly included them in the denominator?
  4. A paired evaluation gives n10 = 3 and n01 = 2 (only 5 discordant examples out of a 500-example test set). Compute the McNemar z-statistic. What does the result tell you about trying to detect small true differences when models rarely disagree, even with a large overall test set?
  5. A classmate says, "The p-value was 0.02, so there's a 98% chance my model is genuinely better." Identify precisely what is wrong with this statement and rewrite it correctly.

Summary

An accuracy measured on a finite test set is a random variable, not a fixed truth, because it's built from Bernoulli(p) outcomes whose sample mean has variance p(1−p)/n — a result derived directly from E[X²] = E[X] for a 0/1 variable. The Central Limit Theorem lets this sample proportion be treated as approximately normal for reasonably sized test sets, which is what makes z-tests and p-values well-defined. A hypothesis test compares an observed gap, measured in standard errors, against a rejection threshold (|z| > 1.96 for α = 0.05); the resulting p-value is the probability of the observed-or-more-extreme result under H0, never the probability that H0 is true. The single most consequential real-world subtlety is that comparing two models on the same test set — the normal, correct experimental setup — produces paired, correlated accuracies, which the naive independent-samples z-test analyzes incorrectly and typically under-powers; McNemar's test, built from only the discordant examples where the models disagree, is the statistically correct tool and can turn a "no significant difference" verdict into a confidently significant one on identical underlying data.

Think About It

Think about this: How would you explain statistical hypothesis testing for machine learning to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

← Markov Chains: Predicting the Future from the PresentBuilding a Neural Network from Scratch in Python →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn