This page is an ipython notebook, meaning you can download it and run the executable cells. It directly follows Activity 5.3. Reference answers appear after the questions, challenges, predictions, and reflections. They show one reasonable solution or interpretation, but other correct approaches may exist.
Bootstrap & Permutation Procedures from Scratch¶
Tools: Python, NumPy, pandas, matplotlib, SciPy, Jupyter/VS Code
Learning objectives¶
By the end of this tutorial, you should be able to:
generate bootstrap samples manually;
construct bootstrap distributions;
estimate bootstrap standard errors;
construct percentile bootstrap confidence intervals;
bootstrap means, medians, and paired statistics;
preserve observational units during resampling;
create permutation null distributions;
calculate one- and two-sided permutation p-values;
distinguish exact and Monte Carlo permutation logic;
work with paired resampling structures;
write reusable resampling functions;
compare from-scratch results with SciPy.
Import:python import numpy as np import pandas as pd import matplotlib.pyplot as plt
Create:python rng = np.random.default_rng(556)
Define:python N_BOOT = 10000 N_PERM = 10000
python x = np.array([ 4.2, 5.1, 3.8, 6.0, 4.9, 5.4, 4.5, 5.7 ])
Inspect:python x.mean() np.median(x) x.std(ddof=1)
### Question
What do we know about the population that generated `x`?
What do we *not* know?python bootstrap_sample = rng.choice( x, size=len(x), replace=True )
Inspect:python bootstrap_sample
### Questions
1. Is the sample the same size as `x`?
2. Do any observations repeat?
3. Are any original observations absent?Reference answer¶
The nonparametric bootstrap treats the observed sample as an empirical approximation to the unknown population. We know the observed values but not the true population distribution that generated them.
python rng.choice( x, size=len(x), replace=False )
Question¶
What has this actually done?
Why does it fail to create meaningful bootstrap variation in the sample values?
Reference answer¶
A bootstrap sample has the same size as the original and is drawn with replacement, so observations can repeat and others can be omitted. Replacement creates bootstrap variation.
python bootstrap_mean = ( bootstrap_sample.mean() )
Compare:python x.mean() bootstrap_mean
The bootstrap mean is one replicate of the estimator.
------------------------------------------------------------------------Reference answer¶
Sampling the whole dataset without replacement only reorders the observations. Statistics such as the mean are unchanged, so it does not approximate sampling variability.
python bootstrap_means = np.empty( N_BOOT )
Then:python for b in range(N_BOOT): sample_b = rng.choice( x, size=len(x), replace=True )
bootstrap_means[b] = (
sample_b.mean()
)
Inspect:python bootstrap_means[:10]
python plt.hist( bootstrap_means, bins=30 )
plt.axvline( x.mean() )
plt.xlabel(“Bootstrap mean”) plt.ylabel(“Frequency”) plt.show()
### Reflection
How does the bootstrap distribution differ from a histogram of the
original observations?python bootstrap_se = ( bootstrap_means.std( ddof=1 ) )
Now calculate the ordinary analytical SE:python analytical_se = ( x.std(ddof=1) / np.sqrt(len(x)) )
Compare:python bootstrap_se analytical_se
Why are they similar but not identical?
------------------------------------------------------------------------Reference answer¶
The bootstrap distribution is a distribution of a statistic across resamples, not a distribution of raw observations. Its standard deviation estimates the statistic’s sampling standard error.
python bootstrap_ci = np.quantile( bootstrap_means, [0.025, 0.975] )
bootstrap_ci
Add these limits to the histogram.
### Question
What does the percentile procedure actually do computationally?python bootstrap_bias = ( bootstrap_means.mean() - x.mean() )
bootstrap_bias
Interpret the sign.Reference answer¶
A percentile interval takes empirical quantiles of the bootstrap statistic distribution, such as the 2.5th and 97.5th percentiles for a nominal 95% interval.
python bootstrap_samples = rng.choice( x, size=( N_BOOT, len(x) ), replace=True )
Inspect:python bootstrap_samples.shape
Calculate:python bootstrap_means_vec = ( bootstrap_samples.mean( axis=1 ) )
Compare:python bootstrap_means_vec.mean() bootstrap_means_vec.std(ddof=1)
with the loop implementation.Reference answer¶
Bootstrap bias is estimated as the mean bootstrap statistic minus the original-sample statistic. Its sign gives the estimated direction of bias.
text 1,000,000 bootstrap samples × 10,000 observations
### Reflection
Why might a loop or batched method be preferable even if vectorization
is faster?python bootstrap_medians = np.empty( N_BOOT )
Fill it using repeated bootstrap samples.
Then calculate:python median_se = ( bootstrap_medians.std( ddof=1 ) )
median_ci = np.quantile( bootstrap_medians, [0.025, 0.975] )
### Question
Why is bootstrapping particularly attractive for statistics without a
simple elementary SE formula?Reference answer¶
Full vectorization can require a huge resample matrix. A loop or batched implementation may use much less memory and can therefore be preferable for large problems.
python def bootstrap_statistic( data, statistic, n_boot, rng ): results = np.empty( n_boot )
for b in range(n_boot):
sample = rng.choice(
data,
size=len(data),
replace=True
)
results[b] = statistic(
sample
)
return results
Test:python means = bootstrap_statistic( x, np.mean, N_BOOT, rng )
Then:python medians = bootstrap_statistic( x, np.median, N_BOOT, rng )
### Connection
Which Week 7 concept allows `statistic` itself to be passed as an
argument?Reference answer¶
Bootstrap is attractive when an analytic sampling distribution or SE formula is unavailable or inconvenient. A generic bootstrap function should accept data, a statistic, number of resamples, and an RNG.
python df = pd.DataFrame({ “age”: [22, 25, 31, 35, 41, 48], “score”: [70, 75, 80, 86, 91, 94] })
Suppose we want the correlation.
Observed:python observed_r = ( df[“age”] .corr( df[“score”] ) )
### Wrong idea
Resampling the two columns separately would destroy the original
age-score pairs.python indices = rng.choice( len(df), size=len(df), replace=True )
Use:python sample_b = df.iloc[ indices ]
Compute:python sample_b[ “age” ].corr( sample_b[“score”] )
### Question
Why do row indices preserve the data structure?python bootstrap_correlations
Then calculate:
- bootstrap SE;
- percentile 95% CI.
Plot the distribution.Reference answer¶
For correlation, resample row indices so paired (x,y) observations stay together. Resampling columns independently would destroy the joint structure.
python group_a = np.array([ 82, 88, 91, 85, 90, 87, 93, 89 ])
group_b = np.array([ 75, 80, 78, 84, 79, 77, 81, 76 ])
Calculate:python observed_difference = ( group_a.mean() - group_b.mean() )
observed_difference
text H0:
For this simple permutation test, the null mechanism treats the
observations as exchangeable between groups.
### Question
What would the group labels mean if `H0` were true?Reference answer¶
The null hypothesis is that group membership is not associated with the outcome/statistic in the relevant sense; under the null, group labels are exchangeable.
python combined = np.concatenate([ group_a, group_b ])
Inspect:python combined len(combined)
Record:python n_a = len(group_a)
Reference answer¶
If H0 is true, the observed labels carry no systematic outcome information. Pooling and randomly reallocating labels simulates that null world.
python permuted = rng.permutation( combined )
Split:python a_perm = permuted[ :n_a ]
b_perm = permuted[ n_a: ]
Calculate:python perm_difference = ( a_perm.mean() - b_perm.mean() )
Compare with the observed difference.python null_distribution = np.empty( N_PERM )
Then:python for b in range(N_PERM): permuted = rng.permutation( combined )
a_perm = permuted[
:n_a
]
b_perm = permuted[
n_a:
]
null_distribution[b] = (
a_perm.mean()
- b_perm.mean()
)Reference answer¶
Compare the observed statistic with the permutation distribution. An observed value deep in a tail is unusual under the null.
python plt.hist( null_distribution, bins=30 )
plt.axvline( observed_difference )
plt.xlabel( “Difference in means under H0” )
plt.ylabel(“Frequency”) plt.show()
### Question
Where is the null distribution centered?
Why?text H1: mean_A > mean_B
Calculate:python p_greater = ( 1 + np.sum( null_distribution >= observed_difference ) ) / ( N_PERM + 1 )
p_greater
Interpret.Reference answer¶
A one-sided p-value is the fraction of null statistics at least as extreme as the observed statistic in the prespecified direction.
python p_two_sided = ( 1 + np.sum( np.abs( null_distribution ) >= np.abs( observed_difference ) ) ) / ( N_PERM + 1 )
p_two_sided
### Question
What does "at least as extreme" mean here?Reference answer¶
A two-sided p-value counts null statistics whose magnitude is at least as large as the observed statistic’s magnitude (or an equivalent valid two-tail definition).
python np.mean( null_distribution >= observed_difference )
with the corrected Monte Carlo expression.
### Reflection
Why is reporting a p-value of exactly zero inappropriate when only a
finite random sample of permutations was generated?Reference answer¶
‘At least as extreme’ means at least as incompatible with H0 according to the chosen statistic and alternative. Adding one avoids reporting an estimated Monte Carlo p-value of exactly zero.
text 100 1,000 10,000
Store each result.
### Question
How stable is the estimated p-value?
When might more permutations matter?Reference answer¶
A finite permutation count introduces Monte Carlo error into the p-value estimate. More permutations make the estimate more stable.
python def difference_in_means( x, y ): return ( np.mean(x) - np.mean(y) )
Use it for both:python observed_difference
and every permutation replicate.
### Principle
Separate:text statistic
from:text resampling mechanism
Reference answer¶
A statistic function isolates the quantity of interest so the same permutation engine can be reused with means, medians, correlations, and other statistics.
python def permutation_distribution( x, y, statistic, n_perm, rng ): combined = np.concatenate([ x, y ])
n_x = len(x)
results = np.empty(
n_perm
)
for b in range(n_perm):
permuted = rng.permutation(
combined
)
x_perm = permuted[
:n_x
]
y_perm = permuted[
n_x:
]
results[b] = statistic(
x_perm,
y_perm
)
return results
Test it.Reference answer¶
A generic independent permutation test pools outcomes, reallocates them into groups of the original sizes, recomputes the statistic, repeats, and compares the observed statistic with the resulting null distribution.
python def difference_in_medians( x, y ): return ( np.median(x) - np.median(y) )
Compare the resulting null distribution and p-value with the difference
in means.
### Reflection
The permutation mechanism is unchanged.
What changed?Reference answer¶
Changing the statistic changes what aspect of the groups is being tested, so the null distribution and p-value can change even with the same data.
python x_small = np.array([ 1, 2 ])
y_small = np.array([ 4, 5 ])
Pool:text 1, 2, 4, 5
Manually list all distinct ways to choose two observations for the first
group.
How many are there?
Hint:text C(4,2)
Calculate the difference in means for every assignment.
This is an exact permutation distribution.Reference answer¶
For tiny samples, enumerate every distinct label allocation to obtain an exact permutation distribution rather than relying on random permutations.
text all possible assignments
with:text randomly sampled assignments
### Questions
1. When is exact enumeration feasible?
2. Why does it rapidly become impossible as sample size increases?
3. What approximation is introduced by Monte Carlo permutation?Reference answer¶
Exact permutation evaluates all possible allocations and has no Monte Carlo error; Monte Carlo permutation samples a feasible subset of allocations.
python before = np.array([ 80, 72, 88, 91, 77, 85 ])
after = np.array([ 84, 75, 90, 94, 76, 89 ])
Calculate:python differences = ( after - before )
Inspect:python differences differences.mean()
Reference answer¶
Paired data must preserve the pairing. Use a paired null operation such as random sign flips of within-pair differences rather than pooling all observations.
Part 33 --- Preserve the pairing¶
Incorrect¶
Pool all before/after observations and randomly split them.
Better for a paired sign-randomization logic¶
For each difference, randomly choose:
text +d_i or -d_i
Generate signs:python signs = rng.choice( [-1, 1], size=len(differences) )
permuted_difference = ( differences * signs )
Calculate its mean.Part 34 --- Paired null distribution¶
Repeat the random sign flip N_PERM times.
Store mean differences.
Compare the observed paired mean difference with the resulting null distribution.
Question¶
Why does this preserve more of the original study design than pooling all observations?
Reference answer¶
The paired procedure preserves subject-level dependence and therefore respects the study design more closely than an independent-label permutation.
python from scipy.stats import ( permutation_test )
Define a compatible statistic:python def statistic( x, y ): return ( np.mean(x) - np.mean(y) )
Run:python result = permutation_test( ( group_a, group_b ), statistic, n_resamples=N_PERM, alternative=“two-sided”, rng=np.random.default_rng( 556 ) )
Inspect:python result.statistic result.pvalue
Compare with your own implementation.python from scipy.stats import bootstrap
Run an appropriate bootstrap for the sample mean.
Compare its confidence interval with your percentile interval.
### Reflection
Do not expect every library default to match your from-scratch choices
exactly.
Check:
- interval method;
- confidence level;
- RNG;
- number of resamples.Reference answer¶
SciPy should give a similar bootstrap result when statistic, confidence level, method, and randomization choices are aligned. Small numerical differences can arise from implementation details.
Part 37 --- Bootstrap vs. permutation challenge¶
For each goal, choose bootstrap or permutation.
A¶
Estimate the standard error of the median.
B¶
Test whether two randomized groups differ in mean outcome under a no-group-effect null.
C¶
Estimate uncertainty in a regression coefficient.
D¶
Construct a null distribution for a difference in groups.
E¶
Estimate a confidence interval for a correlation.
Explain each choice.
Reference answer¶
Bootstrap estimates sampling uncertainty around an observed statistic; permutation builds a null distribution under an exchangeability assumption for hypothesis testing.
python rng = np.random.default_rng(556)
control = rng.normal( 70, 10, size=30 )
treatment = rng.normal( 76, 10, size=30 )
Complete:
## Bootstrap
1. Estimate the observed difference in means.
2. Bootstrap the two groups appropriately.
3. Construct the bootstrap distribution of the difference.
4. Estimate the bootstrap SE.
5. Construct a percentile 95% CI.
## Permutation
6. Pool the observations.
7. Construct a permutation null distribution.
8. Estimate a two-sided p-value.
## Interpretation
9. Explain why the two empirical distributions answer different
questions even though both contain differences in means.
------------------------------------------------------------------------text 1,000 resamples 10,000 resamples
Compare:
- SE;
- CI endpoints;
- p-value.
### Question
Which quantities are most sensitive to the number of resamples?text What is the observational unit? What statistic is being computed? What is resampled? Is replacement used? What null assumption justifies permutation? Is pairing/clustering preserved? How many resamples are used? How is randomness controlled? What empirical distribution is being constructed? What inferential quantity is extracted?
Reference answer¶
A resampling audit should state the observational unit, statistic, what is resampled, replacement rule, null hypothesis if relevant, number of resamples, and RNG/seed strategy.
bash git status
Stage:bash git add .
Commit:bash git commit -m “Complete Week 11 bootstrap and permutation exercises”
Push:bash git push
Part 42 --- Final reflection¶
Answer in Markdown.
1. Bootstrap¶
Why does a bootstrap sample use replacement?
2. Empirical population¶
What population does the nonparametric bootstrap sample from?
3. Bootstrap distribution¶
What does it approximate?
4. Standard error¶
How do you estimate a bootstrap SE?
5. Percentile interval¶
How is it constructed?
6. Permutation¶
What does a permutation null distribution represent?
7. Exchangeability¶
Why is it central to a permutation test?
8. p-value¶
How is a Monte Carlo permutation p-value calculated?
9. Pairing¶
Why can we not arbitrarily shuffle observations in a paired study?
10. Comparison¶
What is the most important conceptual difference between bootstrap and permutation resampling?
Completion checklist¶
Created Week 11 notebook
Generated one bootstrap sample
Explained why replacement is required
Constructed a bootstrap distribution with a loop
Visualized the bootstrap distribution
Calculated bootstrap SE
Compared bootstrap and analytical SE
Constructed a percentile CI
Estimated bootstrap bias
Implemented vectorized bootstrap sampling
Discussed memory tradeoffs
Bootstrapped a median
Wrote a generic bootstrap function
Preserved row pairings when resampling
Bootstrapped a correlation
Calculated an observed group difference
Constructed one random permutation
Built a permutation null distribution
Calculated one-sided p-value
Calculated two-sided p-value
Used a finite-resampling correction
Examined permutation-count stability
Wrote reusable statistic functions
Wrote a generic permutation function
Compared mean and median test statistics
Constructed a small exact permutation distribution
Compared exact and Monte Carlo permutation tests
Preserved paired design using sign randomization
Validated permutation output with SciPy
Validated bootstrap output with SciPy
Completed the bootstrap-vs-permutation challenge
Completed the mini resampling study
Compared results across resampling counts
Audited the resampling design
Committed Week 11 work to Git
Pushed work to GitHub
What you should now understand¶
text Observed data ↓ Choose statistic ↓ Bootstrap sample with replacement ↓ sampling uncertainty ↓ SE / confidence interval
or
Permutation rearrange under H0 ↓ null distribution ↓ p-value
Next week we move into **numerical optimization**, including root
finding, Newton--Raphson, gradient descent, and statistical objective
functions.Reference answer¶
Bootstrap samples with replacement from the empirical distribution to approximate sampling variability. Permutation rearranges labels/signs under a null. Both require resampling the correct observational unit.