Every bootstrap does the same thing: draw n rows WITH replacement from your n rows, recompute the statistic, repeat R times, and look at the spread of the R values (its SD is the bootstrap SE). The three methods below differ only in HOW they draw the rows.
By the way, Methods 1 and 3 give identical numbers because pandas.sample(replace=True, random_state=rng) calls rng.integers(0, n, size=n) under the hood, so 5,000 calls of size n use the same random stream as one (5,000, n) draw. Method 2 uses numpy’s older random generator, so its draws (and computes its SE) differ slightly.
import os
import numpy as np
import pandas as pd
R = 5_000
Where rng first showed up (rng.ipynb,
cdf_and_pdf.ipynb, kernel_densities.ipynb, Lab
5). There was no data yet: we told numpy the distribution, drew samples
from it, and watched how a statistic behaves across many samples. That
is simulation (Monte Carlo), and it is the same move
the bootstrap makes later. - Make one stream with
rng = np.random.default_rng(seed), then draw everything
from it - Draw B samples of size K at once with
size=(B, K): each row is one simulated
sample, so .mean(axis=1) gives B sample means. This is
where Method 3’s (R, n) matrix comes from - Or use a loop
that appends one sample at a time (the first version in
rng.ipynb). Same numbers, just slower
rng = np.random.default_rng(100)
K, B = 100, 5_000
# Integers: a fair die. integers(low, high) EXCLUDES high, so 1..6 needs (1, 7)
rolls = rng.integers(1, 7, size=(B, K))
rolls.mean(axis=1).std() # ≈ 0.171 = sqrt(35/12) / sqrt(100)
# Normal: standard normal draws
z = rng.standard_normal(size=(B, K)) # or rng.normal(loc=mu, scale=sd, size=(B, K))
z.mean(axis=1).std() # ≈ 0.100 = 1 / sqrt(100)
# Choice with probabilities: categories with a PMF p
rng.choice(cars, size=1000, p=p) # the car-brand example
# Bernoulli and binomial: yes/no trials
flips = rng.binomial(1, 0.1, size=(B, 200)) # B samples of 200 individual 0/1 trials
counts = rng.binomial(200, 0.1, size=B) # B success counts out of 200, in one step
# flips.sum(axis=1) and counts have the same distribution
From simulation to the bootstrap. In real life we don’t know the distribution, only our n data points. The bootstrap uses the data’s own empirical distribution in its place: each observed value gets probability 1/n, and we sample n values from it with replacement. We’ve done 3 different methods to do this resampling.
Resample whole ROWS of a DataFrame (or values of a Series) - frac=1.0 means “draw n rows” (same as .sample(len(df), replace=True), which the A/B bridge used) - replace=True is what makes it a bootstrap: without it you just get the same rows shuffled - Seeding: pass random_state=rng to make it reproducible. Our Week 6 notebooks made an rng but never passed it to .sample(), so those results change every run.
rng = np.random.default_rng(5030)
reps_1 = []
for r in range(R):
df_r = df.sample(frac=1.0, replace=True, random_state=rng)
reps_1.append(df_r['Overall Survival (Months)'].mean())
reps_1 = np.array(reps_1)
Draw n random row positions from 0, 1, …, n-1 (randint excludes the upper bound n), then pull those rows. Using the SAME idx for every column keeps rows together, which is what you want for a regression: mlr(X.iloc[idx], y.iloc[idx]). This is numpy’s older “global” random state. np.random.seed() once, ABOVE the loop, makes the whole run reproducible while every resample is still different.
np.random.seed(5030)
reps_2 = []
for r in range(R):
idx = np.random.randint(0, n, n)
reps_2.append(x.iloc[idx].mean())
reps_2 = np.array(reps_2)
Same idea as Method 2, but with the newer Generator (rng) and all R resamples drawn at once: row r of idx is the r-th bootstrap sample’s row positions. No loop needed when the statistic can be computed along axis=1 (means, proportions, SDs). This works with a numpy array (x.to_numpy()), since x_np[idx] indexes by position.
rng = np.random.default_rng(5030)
x_np = x.to_numpy()
idx = rng.integers(0, n, size=(R, n))
reps_3 = x_np[idx].mean(axis=1)
# For a statistic that isn't vectorized (like a regression), Loop over the rows of idx:
# reps = np.array([mlr(X.iloc[i], y.iloc[i]) for i in idx])
rng.choice(x_np, size=n, replace=True) draws values directly instead of indices.
This is equivalent for a single column; for several columns, draw indices (Methods 2 and 3) so rows stay together.