Sampling and the Central Limit Theorem (CLT)
Sampling
A sample is one possible view of the population.
Important ideas:
- Larger samples reduce noise.
- Random sampling reduces bias.
Central Limit Theorem (CLT)
CLT says (informally):
For many distributions, the distribution of the sample mean becomes approximately normal as sample size grows.
This enables:
- Confidence intervals
- Hypothesis tests
Demonstration in Python
Even if data is not normal (e.g., exponential), the means become close to normal.
import numpy as np
rng = np.random.default_rng(42)
# Non-normal population
population = rng.exponential(scale=1.0, size=200_000)
means = []
for _ in range(5000):
sample = rng.choice(population, size=50, replace=False)
means.append(sample.mean())
means = np.array(means)
print(means.mean(), means.std())import numpy as np
rng = np.random.default_rng(42)
# Non-normal population
population = rng.exponential(scale=1.0, size=200_000)
means = []
for _ in range(5000):
sample = rng.choice(population, size=50, replace=False)
means.append(sample.mean())
means = np.array(means)
print(means.mean(), means.std())Standard error
The standard error of the mean is:
[ SE(\bar{x}) = \frac{s}{\sqrt{n}} ]
- Bigger n → smaller SE
Visualize it
The CLT is one of the most surprising facts in statistics: take the mean of a handful of random values, do it thousands of times, and those means pile up into a bell curve — even though each sample came from a flat, non-bell distribution. Watch the histogram of sample means fill in:
Practical guidance
- Use bootstrap (resampling) if formulas are hard or assumptions unclear.
The CLT pipeline
flowchart LR A["Any population
(skewed, uniform, etc.)"] --> B["Draw a sample
of size n"] B --> C["Compute the
sample mean"] C --> D["Repeat
thousands of times"] D --> E["Histogram of means
looks normal"]
🧪 Try It Yourself
Exercise 1 – Sample size and standard error
# Task: show that standard error shrinks as sample size grows
import numpy as np
rng = np.random.default_rng(7)
population = rng.exponential(scale=2.0, size=100_000)
for n in [10, 100, 1000]:
sample = rng.choice(population, size=n, replace=False)
se = sample.std(ddof=1) / np.sqrt(n) # replace ___ with sqrt(n) if blanked
print(f"n={n}: SE={round(se, 3)}")
# Expected output (values will vary slightly, but shrink as n grows):
# n=10: SE=0.6...
# n=100: SE=0.2...
# n=1000: SE=0.06...# Task: show that standard error shrinks as sample size grows
import numpy as np
rng = np.random.default_rng(7)
population = rng.exponential(scale=2.0, size=100_000)
for n in [10, 100, 1000]:
sample = rng.choice(population, size=n, replace=False)
se = sample.std(ddof=1) / np.sqrt(n) # replace ___ with sqrt(n) if blanked
print(f"n={n}: SE={round(se, 3)}")
# Expected output (values will vary slightly, but shrink as n grows):
# n=10: SE=0.6...
# n=100: SE=0.2...
# n=1000: SE=0.06...Exercise 2 – Means of a skewed population look normal
# Task: collect 2000 sample means from a skewed population and check their shape
import numpy as np
rng = np.random.default_rng(11)
population = rng.exponential(scale=1.0, size=200_000)
means = [rng.choice(population, size=40, replace=False).mean() for _ in range(2000)]
means = np.array(means)
print("population skew ~ exponential, not normal")
print("mean of sample means:", round(means.mean(), 2))
print("std of sample means:", round(means.std(), 2))
# Expected output:
# population skew ~ exponential, not normal
# mean of sample means: ~1.0
# std of sample means: a small number, close to 1/sqrt(40)# Task: collect 2000 sample means from a skewed population and check their shape
import numpy as np
rng = np.random.default_rng(11)
population = rng.exponential(scale=1.0, size=200_000)
means = [rng.choice(population, size=40, replace=False).mean() for _ in range(2000)]
means = np.array(means)
print("population skew ~ exponential, not normal")
print("mean of sample means:", round(means.mean(), 2))
print("std of sample means:", round(means.std(), 2))
# Expected output:
# population skew ~ exponential, not normal
# mean of sample means: ~1.0
# std of sample means: a small number, close to 1/sqrt(40)Exercise 3 – Bootstrap a confidence range
# Task: use resampling (bootstrap) to estimate uncertainty in a mean
import numpy as np
rng = np.random.default_rng(3)
data = np.array([12, 15, 14, 10, 13, 16, 11, 14, 12, 15])
boot_means = []
for _ in range(2000):
resample = rng.choice(data, size=len(data), replace=True)
boot_means.append(resample.mean())
boot_means = np.array(boot_means)
lower = np.percentile(boot_means, 2.5)
upper = np.percentile(boot_means, 97.5)
print("bootstrap 95% range:", round(lower, 2), "to", round(upper, 2))
# Expected output:
# bootstrap 95% range: <roughly 12.0 to 14.6># Task: use resampling (bootstrap) to estimate uncertainty in a mean
import numpy as np
rng = np.random.default_rng(3)
data = np.array([12, 15, 14, 10, 13, 16, 11, 14, 12, 15])
boot_means = []
for _ in range(2000):
resample = rng.choice(data, size=len(data), replace=True)
boot_means.append(resample.mean())
boot_means = np.array(boot_means)
lower = np.percentile(boot_means, 2.5)
upper = np.percentile(boot_means, 97.5)
print("bootstrap 95% range:", round(lower, 2), "to", round(upper, 2))
# Expected output:
# bootstrap 95% range: <roughly 12.0 to 14.6>Next
Continue to Confidence Intervals (CI) to turn the standard error into a range you can report with confidence.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
