The parent note gave you the tools. This page drills them across every case class you can meet: left tails, right tails, middle bands, one- vs two-sided tests, degenerate inputs (zero difference, zero spread), boundary probabilities (p = 0 , p = 1 ), and real-world word problems. Guess before you compute — the "Forecast" line trains your instinct.
Parent: scipy.stats — distributions, hypothesis tests .
Every question this topic throws at you falls into one of these cells. The worked examples below each carry a [cell] tag so you can see the whole grid gets covered.
#
Case class
What makes it distinct
Covered by
A
Left tail — .cdf
want P ( X ≤ x ) , area to the left
Ex 1
B
Right tail — .sf
want P ( X > x ) , upper tail
Ex 2
C
Middle band — cdf minus cdf
want P ( a ≤ X ≤ b )
Ex 3
D
Inverse / quantile — .ppf
given probability, find the cutoff value
Ex 4
E
Degenerate: zero effect
x ˉ = μ 0 , so t ≈ 0 , p → 1
Ex 5
F
Two-sample difference
clear gap → tiny p
Ex 6
G
Categorical counts — chi-square
discrete faces, not a continuous x
Ex 7
H
Boundary probabilities
p = 0 and p = 1 give ± ∞
Ex 8
I
Word problem + one-sided
translate English → H 0 , pick tail
Ex 9
J
Exam twist: non-normal scale
expon where scale = 1/λ, not λ
Ex 10
We use the standard normal picture repeatedly, so pin it down first.
Definition Notation we will lean on
Write F ( x ) = P ( X ≤ x ) for the CDF — "the fraction of the population at or below x ." In scipy this is exactly .cdf(x); we use F and .cdf interchangeably.
On the bell curve above:
.cdf(x) = F ( x ) = ==area shaded to the LEFT of x == (mint zone).
.sf(x) = 1 − F ( x ) = ==area shaded to the RIGHT of x == (coral zone), and cdf + sf = 1 .
.ppf(p) = F − 1 ( p ) = the reverse (lavender) arrow: you hand it the mint area p and it hands back the x line.
z-score — one ruler for every normal
For a normal with centre μ and spread σ , the z-score of a value x is
z = σ x − μ .
In words: "how many spreads σ is x away from the centre μ ?" Subtracting μ moves the centre to 0 ; dividing by σ makes one unit equal one spread. Why bother? Every normal, after this shift-and-stretch, becomes the same standard bell (the one drawn in s01). So one picture and one table of areas answer questions about all of them. A z of + 1 always sits one spread right of centre — mint area 0.8413 to its left — no matter the original μ , σ .
Prerequisites worth a glance: Normal Distribution , p-values and Significance , Chi-square Distribution , Maximum Likelihood Estimation .
[cell A — left tail] IQ scores, μ = 100 , σ = 15 . What fraction score at most 115?
Forecast: 115 is μ + 1 σ . The 68–95 rule says ~68% sit within ± 1 σ , so ~34% between 100 and 115, plus 50% below 100 → guess ≈ 84%.
Set up the object: stats.norm(loc=100, scale=15).
Why this step? loc is the centre μ , scale is the spread σ — for norm they literally are μ , σ .
We want P ( X ≤ 115 ) — the mint left zone of figure s01 → use .cdf (this is F ( 115 ) ).
Why this step? "at most" = "up to and including" = left tail = the C in the CDF mnemonic.
Compute the z-score to place 115 on the standard bell: z = 15 115 − 100 = 1 — exactly one spread right of centre.
Why this step? Standardizing (defined above) lets us read the answer off the one standard bell instead of a custom one.
So stats.norm(100,15).cdf(115) = area left of z = 1 = 0.8413 .
Why this step? The mint zone up to one spread right of centre is always 0.8413.
Verify: z = 1 exactly; my 84% forecast matched. Sanity: answer is between 0 and 1 ✓, and above 0.5 because 115 is above the mean ✓.
[cell B — right tail] Same IQ. What fraction score above 130?
Forecast: 130 = μ + 2 σ ; the 95 rule → 5% outside ± 2 σ , half of that (2.5%) in the upper tail. Guess ≈ 0.023.
We want P ( X > 130 ) — the coral right zone of figure s01 → use .sf (survival), which equals 1 − F ( 130 ) .
Why this step? The far-right area is what "survives" past x ; .sf is more accurate here than 1-.cdf.
z-score: z = 15 130 − 100 = 2 — two spreads right of centre.
Why this step? Same one-ruler idea; two spreads out is a familiar landmark.
Compute: stats.norm(100,15).sf(130) = coral area right of z = 2 = 0.02275 .
Verify: .cdf(130)+.sf(130) should equal 1: 0.97725 + 0.02275 = 1.0 ✓ (checked in VERIFY). The mint + coral zones of s01 always tile the whole bell.
[cell C — middle band] IQ between 85 and 115 (i.e. within ± 1 σ )?
Forecast: the 68 rule — guess ≈ 0.68.
The band is the strip of figure s01 between two vertical lines. Its area = (mint area left of 115) − (mint area left of 85), i.e. F ( 115 ) − F ( 85 ) .
Why this step? F ( b ) − F ( a ) (recall F = .cdf): chop off everything at or below b , then remove the part also at or below a , leaving just the middle strip.
Compute: .cdf(115) - .cdf(85) = F ( 115 ) − F ( 85 ) = 0.8413 − 0.1587 = 0.6827 .
Why this step? By symmetry .cdf(85) = .sf(115) = 0.1587 (85 is one spread left , mirror of 115).
Verify: 0.6827 matches the textbook 68.27% within one sd ✓. It lies in ( 0 , 1 ) and is the biggest of the three so far because it grabs the fat middle of s01 ✓.
[cell D — inverse / quantile] What score marks the top 5% (the 95th percentile)?
Forecast: the famous "1.645 for one-sided 5%" on the standard scale → back on IQ scale, 100 + 1.645 × 15 ≈ 124.7 .
We show it two equivalent ways ; pick one lane and stay in it.
Lane A — work on the scaled object directly. Given probability 0.95 below, ask for the value → use .ppf, the lavender reverse-arrow of s01: stats.norm(100,15).ppf(0.95) → 124.67 .
Why this step? .ppf(0.95) on the IQ-scaled object answers "which IQ score has 95% below it?" directly — no hand-standardizing.
Lane B — standardize first, then un-standardize. On the standard bell, stats.norm.ppf(0.95) = z ∗ = 1.6449 . Then invert the z-formula z = σ x − μ to get x = μ + z ∗ σ = 100 + 1.6449 × 15 = 124.67 .
Why this step? This exposes why the answer is 124.67 — it is exactly 1.6449 spreads right of centre.
Both lanes give the same 124.67; never mix a standard-bell ppf with a scaled-object one in the same line.
Verify: feed Lane A back — .cdf(124.67) should return ≈ 0.95 ✓ (round-trip check in VERIFY). Units: IQ points, plausibly above the mean ✓.
[cell E — degenerate, zero effect] One-sample t-test, data [5.1,4.9,5.3,5.0,4.8], claim mean = 5.0 .
Forecast: the values scatter roughly symmetrically around 5.0, so x ˉ is basically 5.0 → t ≈ 0 → p ≈ 1 (no surprise at all).
Sample mean: x ˉ = ( 5.1 + 4.9 + 5.3 + 5.0 + 4.8 ) /5 = 25.1/5 = 5.02 .
Why this step? The t-statistic needs x ˉ to measure the gap from μ 0 = 5.0 .
Sample standard deviation s , defined as s = n − 1 1 ∑ i ( x i − x ˉ ) 2 . Deviations from 5.02 are { 0.08 , − 0.12 , 0.28 , − 0.02 , − 0.22 } ; their squares sum to 0.0064 + 0.0144 + 0.0784 + 0.0004 + 0.0484 = 0.148 . Divide by n − 1 = 4 : 0.037 ; square-root: s = 0.037 ≈ 0.1924 .
Why this step? s estimates the spread σ we don't know. Why divide by n − 1 , not n ? We already "spent" one fact computing x ˉ from the data, so only n − 1 deviations are free to vary — this is the degrees of freedom df = n − 1 = 4 , and dividing by it removes bias.
Statistic t = s / n x ˉ − μ 0 = 0.1924/ 5 0.02 = 0.0860 0.02 ≈ 0.2325 .
Why this step? This is the z-trick with s replacing σ ; the tiny numerator forces t near 0. (scipy reports t ≈ 0.2325 ; the ± sign depends only on which side of 5.0 the mean falls.)
stats.ttest_1samp(data, 5.0) → t ≈ 0.2325 , p ≈ 0.8278 .
Why this step? Two-sided p = 2 P ( T 4 > ∣ t ∣ ) with df = 4 ; a small ∣ t ∣ leaves almost all the mass beyond it.
Verify: p ≈ 0.83 ≫ 0.05 → fail to reject — exactly what "the data agrees with the claim" should give. (The parent's -0.0 / 1.0 was a stylized illustration; here x ˉ = 5.02 = 5.0 so t is small but nonzero.) VERIFY confirms t , p .
[cell F — two-sample difference] Group a=[20,22,19,24,25], b=[28,27,30,26,29]. Do they differ?
Forecast: the group means are a ˉ = 22 and b ˉ = 28 , a clean gap of 6 , and every b value beats every a value → huge separation relative to within-group wobble → expect big ∣ t ∣ , tiny p .
stats.ttest_ind(a,b) compares two independent means.
Why this step? Two separate samples, not one sample vs a fixed number → the independent two-sample test.
Pooled standard deviation. With equal-size groups, pool the two variances: s a 2 = 6.5 , s b 2 = 2.5 , so the pooled variance is s p 2 = n a + n b − 2 ( n a − 1 ) s a 2 + ( n b − 1 ) s b 2 = 8 4 ⋅ 6.5 + 4 ⋅ 2.5 = 4.5 , giving s p = 4.5 ≈ 2.121 .
Why this step? The pooled (default equal_var=True) test assumes both groups share one true spread, so it averages their sample variances to estimate it.
Statistic t = s p 1/ n a + 1/ n b a ˉ − b ˉ = 2.121 1/5 + 1/5 22 − 28 = 2.121 × 0.6325 − 6 = 1.3416 − 6 ≈ − 4.47 , with df = n a + n b − 2 = 8 .
Why this step? Standardize the mean-gap by the pooled standard error; the sign is negative because a ˉ < b ˉ .
Edge case — the variance assumption. If the spreads were doubtful you would pass equal_var=False for Welch's correction , which adjusts the degrees of freedom instead of pooling.
Why this step? Here both groups have modest, similar spread so the default pooled test is fine; on wildly unequal spreads the pooled p-value can mislead.
Result (pooled default): t ≈ − 4.47 , two-sided p ≈ 0.0021 .
Why this step? Two-sided p = 2 P ( T 8 > ∣ t ∣ ) ; a ∣ t ∣ of 4.47 on df = 8 leaves very little tail mass.
Verify: p ≈ 0.0021 < 0.05 → reject "equal means" ✓. Direction: t < 0 says group b is higher, matching the eyeball ✓. Gap b ˉ − a ˉ = 6 ✓. VERIFY checks t ≈ − 4.47 , p ≈ 0.0021 , and the means.
[cell G — categorical counts] Is a die fair? 60 rolls give obs=[8,9,19,5,8,11], fair expects [10]*6.
Forecast: face 3 shows up 19 times (expected 10) — that's a big overshoot. Suspect the die is loaded → smallish p .
Statistic ∑ ( O − E ) 2 / E over all six faces.
Why this step? Here x isn't a continuous value — it's counts in categories , so we compare observed vs expected counts, not tail areas of a bell. That's the chi-square test's job.
Term-by-term: 10 ( 8 − 10 ) 2 + 10 ( 9 − 10 ) 2 + 10 ( 19 − 10 ) 2 + 10 ( 5 − 10 ) 2 + 10 ( 8 − 10 ) 2 + 10 ( 11 − 10 ) 2 = 0.4 + 0.1 + 8.1 + 2.5 + 0.4 + 0.1 = 11.6 .
Why this step? The ( 19 − 10 ) 2 /10 = 8.1 term dominates — face 3 is the culprit.
stats.chisquare([8,9,19,5,8,11],[10]*6) → statistic = 11.6 , p ≈ 0.0407 , df = 5 .
Why this step? df = categories − 1 = 5; p is the upper-tail area of the Chi-square Distribution beyond 11.6.
Verify: my hand-sum 11.6 equals scipy's statistic ✓. p ≈ 0.041 < 0.05 → die looks loaded ✓. VERIFY checks statistic and p .
[cell H — boundary probabilities] What are norm.ppf(0) and norm.ppf(1)?
Forecast: "the value with 0% below it" must be as far left as possible; "100% below it" as far right as possible → − ∞ and + ∞ .
stats.norm.ppf(0.0) → − ∞ .
Why this step? No finite x has zero mint area to its left on a bell that stretches forever (look at s01 — the mint zone never fully vanishes); the limit is − ∞ .
stats.norm.ppf(1.0) → + ∞ .
Why this step? Symmetric argument on the right coral zone.
stats.norm.cdf(-inf)=0, stats.norm.cdf(+inf)=1.
Why this step? These are the two anchor values every CDF F must hit — it climbs from 0 to 1.
Verify: ppf and cdf are inverses, and their round-trip respects the endpoints ✓. VERIFY checks the limits of F .
[cell I — word problem, one-sided] A factory claims bolts are at least 50 mm. A sample of 8 has x ˉ = 49.6 , s = 0.5 . At α = 0.05 , are bolts too short?
Forecast: x ˉ is below the claim by 0.4 mm with SE = 0.5/ 8 ≈ 0.177 , so t ≈ − 2.26 . One-sided → guess p around 0.03, borderline.
Frame it: H 0 : μ ≥ 50 vs H 1 : μ < 50 — a one-sided (left) test because we only care if bolts are too short .
Why this step? The English "at least" and the worry "too short" pick a direction; two-sided would waste half our sensitivity.
t = 0.5/ 8 49.6 − 50 = 0.1768 − 0.4 = − 2.2627 , with df = n − 1 = 7 .
Why this step? Standardize the gap by the estimated SE s / n ; the given s = 0.5 plays the role of the sample sd from Ex 5.
One-sided p = P ( T 7 < t ) = stats.t.cdf(-2.2627, df=7) → 0.0292 .
Why this step? Left tail only, so use .cdf (not doubled) — we test just the "too short" direction.
Verify: p ≈ 0.029 < 0.05 → reject H 0 : bolts are significantly short ✓. Two-sided would give 2 × 0.0292 = 0.0584 > 0.05 (fail!) — showing why choosing the correct side matters. VERIFY checks the one-sided p .
[cell J — exam twist, non-normal scale] Bus waits are exponential with rate λ = 0.5 buses/min. What fraction of waits exceed 3 minutes?
Forecast: exponential mean = 1/ λ = 2 min; 3 min is 1.5 means, moderately long → guess ≈ 0.22.
Build the object: stats.expon(scale=2) because for expon the scale argument is 1/ λ = 1/0.5 = 2 , not λ .
Why this step? scipy parametrizes expon by the mean-like scale; only when scale = 1/ λ does its survival match e − λ x . Setting scale=0.5 would silently model the wrong distribution.
We want P ( X > 3 ) — "exceed" is an upper tail , the same coral-zone idea as Ex 2 but on an exponential curve → use .sf, giving stats.expon(scale=2).sf(3).
Why this step? "exceed 3" = right tail = survival function; .cdf(3) would answer the opposite (waits shorter than 3).
Evaluate: survival of an exponential is the clean formula P ( X > x ) = e − x / scale = e − 3/2 = e − 1.5 , so stats.expon(scale=2).sf(3) → 0.2231 .
Why this step? The exponential's memoryless shape gives this exact closed form; plugging x = 3 , scale = 2 lands on e − 1.5 .
Verify: hand formula e − 1.5 = 0.2231 matches scipy ✓, and it sits between 0 and 1 as a probability must ✓. If you'd wrongly set scale=0.5 you'd get e − 6 = 0.0025 — off by ~90×, the classic exam mistake. VERIFY checks 0.2231.
Common mistake Doubling a one-sided p-value (or forgetting to).
Why it feels right: the t-test's default output is two-sided.
The fix: two-sided p = 2 P ( T > ∣ t ∣ ) ; one-sided uses just the single tail. Ex 9 flips the verdict depending on which you use.
Recall Quick self-quiz over the matrix
Which method for P ( X ≤ x ) ? ::: .cdf, i.e. F ( x )
Which method for P ( X > x ) ? ::: .sf, i.e. 1 − F ( x )
Given a percentile, find the value? ::: .ppf, i.e. F − 1
What is a z-score? ::: z = ( x − μ ) / σ — how many spreads σ the value x is from centre μ
norm.ppf(0) equals? ::: − ∞
For expon, scale equals? ::: 1/ λ , not λ
Chi-square statistic formula? ::: ∑ ( O − E ) 2 / E
Why divide by n − 1 for s ? ::: one degree of freedom is spent computing x ˉ , leaving df = n − 1 ; it de-biases the estimate
ttest_ind default assumption? ::: equal_var=True (pooled); use equal_var=False for Welch when spreads differ