5.4.14 · D4Scientific Computing (Python)

Exercises — scipy.stats — distributions, hypothesis tests

3,026 words14 min readBack to topic

Throughout, we assume:

from scipy import stats
import numpy as np

Two pieces of notation used everywhere below, defined once here:


Level 1 — Recognition

You just need to name the right tool. No derivation.

Recall Solution L1.1

Each question maps to exactly one of the four functions built from the CDF .

  • (a) "height at a point" = PDF: n.pdf(100). (The bell peak equals .)
  • (b) "area up to a value" = CDF: n.cdf(115). (, so ~84%.)
  • (c) "upper tail" = survival .sf: n.sf(130).
  • (d) "value at a percentile" = PPF (inverse CDF): n.ppf(0.90).
Recall Solution L1.2

True. Recall the survival function defined above. By symmetry of the standard bell about , the area to the left of equals the area to the right of . Both are . Formally, for a distribution symmetric about , .


Level 2 — Application

Plug numbers into a known recipe.

Recall Solution L2.1

Standardise with (this collapses every normal onto one standard curve): Then = the upper tail of the standard normal:

stats.norm(100,15).sf(130)     # 0.0227501
stats.norm(0,1).sf(2.0)        # 0.0227501  (same, by z-transform)

Answer: , about 2.3%. Note this is less than the "5%/2 = 2.5%" you'd guess from the 95% rule, because the rule really uses , not .

Recall Solution L2.2
  • Mean: .
  • Deviations from : . Squares are and sum to . Sample variance , so .
  • Standard error . Why ? This is an exact algebraic fact for any : for independent values each with variance , the variance of their average is , so its sd is . (The Central Limit Theorem is a separate statement — it says the shape of that averaged quantity approaches a normal as grows — but the shrinkage here needs no CLT, only the variance-of-a-sum rule.)
  • t-statistic: .
  • p-value (two-sided, df): :
stats.ttest_1samp([5.1,4.9,5.3,5.0,4.8], 5.0)
# statistic ≈ 0.23250, pvalue ≈ 0.8278

Answer: , . Huge p → no evidence the mean differs from 5.0.


Level 3 — Analysis

Interpret, compare, decide.

Recall Solution L3.1
  • , . Gap .
  • The equal-variance (pooled) two-sample t-statistic compares that gap to the pooled within-group noise. scipy's ttest_ind gives:
stats.ttest_ind([20,22,19,24,25],[28,27,30,26,29])
# statistic ≈ -4.4721, pvalue ≈ 0.002085, df = 8
  • is large because a 6-unit gap dwarfs the within-group spread. . Decision: reject . Group B is genuinely higher; see p-values and Significance. (We rebuild this exact number from raw sums in L5.2.)
Recall Solution L3.2

The statistic sums scaled squared mismatches: With : contributions are . Degrees of freedom (counts must sum to 60, one constraint). p-value = upper tail of :

stats.chisquare([8,9,19,5,8,11],[10]*6)
# statistic = 11.6, pvalue ≈ 0.04073

Answer: , → reject "fair."

Check the assumption first. The chi-square approximation is only trustworthy when every expected count is reasonably large — the standard rule of thumb is (and ideally all , with none below ). Here every , comfortably above 5, so the approximation is safe. When some fall below 5 you have two fixes: (i) merge sparse categories into a combined bucket until each expected count clears 5, or (ii) use an exact test instead (e.g. stats.fisher_exact for a table, or a Monte-Carlo / permutation p-value). Ignoring the rule inflates the false-positive rate because the true sampling distribution of the statistic is then no longer close to .

How to read the figure below. Each white bar is an observed count for one face; the dashed black line is the flat expectation of 10. The chi-square statistic is literally the sum of . Scan the bars: five of them sit within a couple of units of the line (tiny contributions), but the red bar for face 3 towers at 19 — its gap of squared and scaled gives , which is of the whole statistic. The picture makes visible why the die is flagged: one face, not a diffuse drift, carries the evidence.

Figure — scipy.stats — distributions, hypothesis tests

Level 4 — Synthesis

Combine several ideas into one pipeline.

Recall Solution L4.1

"Two-sided, total tail area " means in each tail. So the left boundary sits where the CDF equals : The PPF answers exactly "which value has this much area below it?"

stats.norm.ppf(0.975)     # 1.959964  ≈ 1.96
stats.norm.cdf(1.959964)  # 0.975     round-trip check

Answer: . This is why is the famous magic number — it's the 97.5th percentile of the standard normal.

Recall Solution L4.2

The test defines , so , i.e. the upper 2.5% point of with df. Invert with the PPF of the t-distribution:

stats.t.ppf(0.975, df=9)   # 2.262157

Answer: .

How to read the figure below. Two densities are drawn on the same axis: the black curve is the standard normal, the red curve the Student-t with df. Look at where they differ — near the centre they almost coincide, but in the tails (far left and right) the red curve sits visibly above the black one. That extra tail mass is the whole story: to fence off the same of area on the right, you must move the cutoff further out, from the normal's dotted mark at to the t's dotted mark at . Reading the picture, the heavier the tail, the larger the critical value — the price you pay for estimating from data instead of knowing it.

Figure — scipy.stats — distributions, hypothesis tests

Level 4½ — Unequal variances (Welch)

The pooled test of L3.1 secretly assumed both groups have the same spread. What if they don't?

Recall Solution L4.3

Diagnose first. Eyeball or measure the two sample variances: C's values huddle near 20, D's swing from 5 to 40. A quick ratio of sample variances (or a formal stats.levene(C, D) test for equal spread) reveals D's variance is many times C's — the equal-variance assumption behind the pooled statistic (which blends both groups' spread into one ) is violated.

Why pooling is wrong when spreads differ. Pooling averages the two variances into a single number and then applies it to both groups. If one group is genuinely noisier, that shared estimate is too small for the noisy group and too large for the calm one, distorting the standard error and giving a mis-calibrated p-value (wrong Type-I error rate).

The fix — Welch's t-test. It keeps the two variances separate: and uses the Welch–Satterthwaite approximate degrees of freedom (generally not an integer, and smaller than ). In scipy you flip one flag:

C = [20,21,19,20,22,18,21]
D = [10,40,25,5,35,15,30]
stats.ttest_ind(C, D, equal_var=False)   # Welch: separate variances
stats.ttest_ind(C, D, equal_var=True)    # pooled: the L3.1 default

Rule of thumb: when in doubt, prefer equal_var=False. Welch is nearly as powerful as the pooled test when spreads are equal, and far more reliable when they are not — many statisticians make it their default.


Level 5 — Mastery

Build the machinery yourself; explain every step.

Recall Solution L5.1

.fit returns the maximum-likelihood estimates . Maximum-likelihood estimation (abbreviated MLE) picks the parameter pair that makes the observed sample most probable under the normal model. For the normal these have closed forms: (sample mean) and (the -divisor MLE spread, not the one).

sample = stats.norm(50,5).rvs(size=1000, random_state=0)
mu_hat, sd_hat = stats.norm.fit(sample)
# mu_hat ≈ 50.09, sd_hat ≈ 4.905

They land near because with samples the MLE estimates concentrate tightly around the truth — consistency. (Uses numpy.random under the hood for the draws.)

Recall Solution L5.2

First, the notation: for a group with mean , define its sum of squared deviations i.e. add up how far each value sits from its own group mean, squared. Now build every piece:

  1. Means: , . Both .
  2. .
  3. .
  4. Pooled variance (share spread information across groups), dividing by df:
  5. Standard error of the difference:
  6. Statistic: .
import numpy as np
a=np.array([20,22,19,24,25]); b=np.array([28,27,30,26,29])
SSa=((a-a.mean())**2).sum(); SSb=((b-b.mean())**2).sum()
sp2=(SSa+SSb)/(5+5-2); SE=np.sqrt(sp2*(1/5+1/5))
t=(a.mean()-b.mean())/SE
p=2*stats.t.sf(abs(t), df=8)
# t ≈ -4.4721, p ≈ 0.002085

Answer: , , — exactly the value reported in L3.1, now rebuilt from raw sums.


Recall Edge cases & when the recipes bend

Small (say ) ::: the t-test assumes roughly normal data; with few points you cannot check that assumption and the p-value is fragile. Prefer a non-parametric alternative — the Wilcoxon signed-rank (stats.wilcoxon) for one sample / paired data, or the Mann–Whitney U (stats.mannwhitneyu) for two independent groups — which rank the data instead of assuming a shape. Heavy-tailed / outlier-prone data ::: the mean and are both outlier-sensitive, so can swing wildly; a single extreme value can flip the decision. Use the rank-based tests above (they compare medians / orderings, so one giant value barely moves them). Unequal group spreads ::: do not use the pooled two-sample test; switch to Welch with stats.ttest_ind(a, b, equal_var=False) (see L4.3). Diagnose spread differences with stats.levene. Discrete or small counts (chi-square) ::: the approximation needs expected counts per cell; with tiny expected counts merge sparse categories or use an exact test (stats.fisher_exact for a table, or a Monte-Carlo p-value). One-sided vs two-sided ::: two-sided splits into both tails (cutoff ppf(1-α/2)); one-sided puts all of in the tail you predicted in advance (cutoff ppf(1-α)). Never pick the side after seeing the data. Paired vs independent ::: if each A value is matched to a B value (before/after on the same subject), use stats.ttest_rel, not ttest_ind — pairing removes between-subject noise and is more powerful.

Recall Score yourself

Level cleared ::: the highest level where you solved before revealing. L1–L2 comfortable ::: you know the toolbox; drill L3+ interpretation. L3–L4 comfortable ::: you can run and read tests; L5 to truly own the machinery. L5 clean ::: you can rebuild scipy's tests from first principles — mastery.