Chapter 62 · Solutions
Probability Sampling — Worked Solutions ✅
Five challenges, each verified in code.
⚙️ Setup & population¶
In [1]:
import numpy as np, pandas as pd
rng = np.random.default_rng(620)
# two strata with very different means
A = rng.normal(40, 6, 60000); B = rng.normal(80, 6, 40000)
pop = np.concatenate([A,B]); strat = np.array(["A"]*60000+["B"]*40000)
MU = pop.mean(); print(f"true mean = {MU:.2f}")
true mean = 55.99
CHALLENGE 1
SRS is unbiased
Draw 3,000 simple random samples of size 200 and confirm the average estimate equals the true mean.
In [2]:
means=np.array([pop[rng.choice(len(pop),200,replace=False)].mean() for _ in range(3000)])
print(f"average of SRS estimates = {means.mean():.2f} (true {MU:.2f})")
print(f"SRS standard error = {means.std():.3f}")
average of SRS estimates = 55.96 (true 55.99) SRS standard error = 1.442
CHALLENGE 2
Stratified beats SRS
Draw stratified samples (proportional allocation, n=200) and compare the standard error to SRS.
In [3]:
idxA=np.where(strat=="A")[0]; idxB=np.where(strat=="B")[0]
def stratified(n=200):
a=pop[rng.choice(idxA, round(n*0.6), replace=False)]
b=pop[rng.choice(idxB, round(n*0.4), replace=False)]
return np.concatenate([a,b]).mean()
sm=np.array([stratified() for _ in range(3000)])
srs=np.array([pop[rng.choice(len(pop),200,replace=False)].mean() for _ in range(3000)])
print(f"stratified SE = {sm.std():.3f}")
print(f"SRS SE = {srs.std():.3f}")
print(f"variance cut = {(1-(sm.std()/srs.std())**2)*100:.0f}%")
stratified SE = 0.428 SRS SE = 1.449 variance cut = 91%
CHALLENGE 3
The cluster penalty
Split the sorted population into homogeneous clusters of 100 and sample 2 clusters. Show the SE is larger than SRS at the same n.
In [4]:
order=np.argsort(pop); clustered=pop[order]; cl=np.arange(len(pop))//100
def cluster(n_cl=2):
chosen=rng.choice(cl.max()+1, n_cl, replace=False)
return clustered[np.isin(cl, chosen)].mean()
cm=np.array([cluster() for _ in range(3000)])
print(f"cluster SE (2x100 = n=200) = {cm.std():.2f}")
print(f"SRS SE (n=200) = {srs.std():.2f}")
print("clusters of similar units carry less information -> higher variance")
cluster SE (2x100 = n=200) = 14.33 SRS SE (n=200) = 1.45 clusters of similar units carry less information -> higher variance
CHALLENGE 4
The periodicity trap
Build a list whose values repeat with period k and show systematic sampling (every k-th) returns a biased estimate.
In [5]:
k=10; cycle=np.tile([10,20,30,40,50,60,70,80,90,100], 1000) # period 10
print(f"full-list mean = {cycle.mean():.1f}")
for start in [0, 3, 7]:
est=cycle[start::k].mean()
print(f"every {k}-th from start {start}: estimate = {est:.1f} (off by {est-cycle.mean():+.1f})")
print("each systematic pass hits ONE repeated value -> wildly biased")
full-list mean = 55.0 every 10-th from start 0: estimate = 10.0 (off by -45.0) every 10-th from start 3: estimate = 40.0 (off by -15.0) every 10-th from start 7: estimate = 80.0 (off by +25.0) each systematic pass hits ONE repeated value -> wildly biased
CHALLENGE 5
Proportional allocation
Verify that proportional allocation samples each stratum in proportion to its share of the population.
In [6]:
shareA=len(idxA)/len(pop); n=500
print(f"stratum A is {shareA*100:.0f}% of the population")
print(f"proportional allocation draws {round(n*shareA)} of {n} from A ({round(n*shareA)/n*100:.0f}%)")
print("the sample mirrors the population composition by construction")
stratum A is 60% of the population proportional allocation draws 300 of 500 from A (60%) the sample mirrors the population composition by construction
Statistics, Data Science and AI: A Visual Handbook · © 2026 John Fisher