CSCI 1109 — Practical Data Science
Motivating story (hospital wait times)
A hospital reports:
“Average emergency wait time last week was 42 minutes.”
Real questions decision-makers care about:
Goal of this module
After this module, you should be able to…
Key concepts
Note
The sampling distribution of a statistic is the distribution you’d see if you could repeat your entire sampling process many times.
They let us answer questions like:
Without some model of sampling variation, statements like
“The average is 42 minutes”
are incomplete. We need something like:
“Average is 42 minutes, and typical sampling variation is around ±5 minutes.”
Instead of:
we:
This is our simulation-based stand-in for the true sampling distribution.
A manager says:
“We measured 50 patients. The average wait was 37.6 minutes.
Therefore, from now on, we’ll just say the true average is 37.6.”
Issues:
🙅🏽 Takeaway anti-pattern
Bootstrap & sampling simulations help us visualize and quantify that uncertainty.
Let
\[ \bar{X} = \frac{1}{n}\sum_{i=1}^n X_i \]
In theory (under some assumptions):
But:
Bootstrap idea: approximate the sampling distribution of \(\bar{X}\) by resampling our data.
For a more formal treatment of the probability and statistics ideas behind this, see Deisenroth, Faisal, and Ong (2020).
Given a sample \(x_1, \dots, x_n\):
Compute the original statistic:
\[ \hat{\theta} = \bar{x} = \frac{1}{n}\sum_{i=1}^n x_i. \]
For \(b = 1, \dots, B\) (e.g., \(B = 2000\)):
Use the collection \(\{\hat{\theta}^{(1)}, \dots, \hat{\theta}^{(B)}\}\) to:
This same pattern works for lots of statistics: medians, proportions, differences, etc.
The bootstrap works best when:
If these conditions hold (roughly):
In more advanced courses, you’ll see:
For this course, we treat the bootstrap as a practical simulation tool
and focus on when it’s appropriate and how to interpret it.
For a deeper, classic treatment of bootstrap theory and practice, see Efron and Tibshirani (1994).
We’ll simulate data for 120 ER visits. Two triage levels:
"urgent": typically shorter waits"non-urgent": typically longer waitsn = 120
triage = rng.choice(["urgent", "non-urgent"], size=n, p=[0.4, 0.6])
wait_times = np.where(
triage == "urgent",
rng.normal(loc=30, scale=8, size=n), # urgent: shorter waits
rng.normal(loc=50, scale=10, size=n) # non-urgent: longer waits
)
# Ensure no negative or crazy-small times
wait_times = np.clip(wait_times, 5, None)
er = pd.DataFrame({"triage": triage, "wait_min": wait_times})
er.head()| triage | wait_min | |
|---|---|---|
| 0 | urgent | 28.277796 |
| 1 | urgent | 37.777020 |
| 2 | urgent | 41.910784 |
| 3 | urgent | 20.331177 |
| 4 | non-urgent | 35.585020 |
| triage | wait_min | |
|---|---|---|
| count | 120 | 120.000000 |
| unique | 2 | NaN |
| top | non-urgent | NaN |
| freq | 67 | NaN |
| mean | NaN | 40.769709 |
| std | NaN | 12.752208 |
| min | NaN | 15.771329 |
| 25% | NaN | 31.257241 |
| 50% | NaN | 39.099323 |
| 75% | NaN | 49.376112 |
| max | NaN | 76.009549 |
Quick visualization:
We want a 95% bootstrap confidence interval for the mean wait time.
n_boot = 2000
boot_means = []
for _ in range(n_boot):
sample = er["wait_min"].sample(
frac=1.0,
replace=True,
random_state=rng.integers(0, 1_000_000)
)
boot_means.append(sample.mean())
boot_means = np.array(boot_means)
orig_mean = er["wait_min"].mean()
ci_low, ci_high = np.percentile(boot_means, [2.5, 97.5])
orig_mean, (ci_low, ci_high)(40.76970860065877, (38.508473145919304, 43.01662206678892))

Interpretation:
orig_mean minutes.[ci_low, ci_high] minutes.Suppose:
Cases:
The bootstrap doesn’t magically “prove” anything;
it quantifies uncertainty so we can make better decisions.
Scenario:
converted (1 if user signed up; 0 otherwise).Simulated data:
| variant | converted | |
|---|---|---|
| 0 | old | 0 |
| 1 | old | 0 |
| 2 | old | 0 |
| 3 | old | 0 |
| 4 | old | 0 |
| mean | sum | count | |
|---|---|---|---|
| variant | |||
| new | 0.13 | 26 | 200 |
| old | 0.09 | 18 | 200 |
Let:
0.04000000000000001
Question: Is this difference plausibly just noise, or real?
We’ll treat each bootstrap sample as:
n_boot = 4000
boot_diffs = []
for _ in range(n_boot):
sample = ab.sample(
frac=1.0,
replace=True,
random_state=rng.integers(0, 1_000_000)
)
mean_old = sample.loc[sample["variant"] == "old", "converted"].mean()
mean_new = sample.loc[sample["variant"] == "new", "converted"].mean()
boot_diffs.append(mean_new - mean_old)
boot_diffs = np.array(boot_diffs)
ci_low, ci_high = np.percentile(boot_diffs, [2.5, 97.5])
delta_obs, (ci_low, ci_high)(0.04000000000000001, (-0.023277309956720487, 0.10006241450669022))
ax = plt.hist(boot_diffs, bins=40, density=True)
plt.axvline(delta_obs, linestyle="--", label=f"Observed diff = {delta_obs:.3f}")
plt.axvline(ci_low, color="black", linestyle=":", label=f"2.5% = {ci_low:.3f}")
plt.axvline(ci_high, color="black", linestyle=":", label=f"97.5% = {ci_high:.3f}")
plt.axvline(0.0, color="red", linestyle="-.", label="No difference")
plt.xlabel("Bootstrapped difference in conversion (new - old)")
plt.ylabel("Density")
plt.title("Bootstrap distribution of conversion difference")
plt.legend();
Interpretation:
Before trusting a bootstrap-based conclusion, ask:
Diagnostics you can run:
scipy.stats.bootstrap implements the generic bootstrap pattern you coded by hand.np.mean), and it returns a bootstrap confidence interval.from scipy.stats import bootstrap
import numpy as np
rng = np.random.default_rng(1109)
x = rng.normal(loc=75, scale=8, size=50)
# SciPy expects a tuple of arrays
res = bootstrap(
(x,),
np.mean,
confidence_level=0.95,
n_resamples=2000,
method="percentile",
random_state=rng,
)
res.confidence_interval.low, res.confidence_interval.highBaggingClassifier, BaggingRegressorRandomForestClassifier, RandomForestRegressorsklearn.utils.resample for general-purpose resampling:from sklearn.utils import resample
# X, y could be any dataset (features, labels)
X_boot, y_boot = resample(X, y, replace=True, random_state=1109)Simple mitigations:
This module is an on-ramp to:
In practice, resampling-style ideas show up in:
Key mindset:
Rather than memorizing formulas, think:
“Can I simulate what would happen if I repeated this process many times?”
Conceptual
You have a random sample of 200 patients’ blood pressures.
Which of the following is closest to the idea of a bootstrap distribution of the mean?
A. The distribution of the 200 observed blood pressures.
B. The distribution of means from many resamples with replacement of those 200 values.
C. The distribution of means from 200 new patients in the future.
D. The distribution of differences between two unrelated datasets.
Numeric (thought only)
You bootstrap the mean wait time and find a 95% CI of [38, 46] minutes.
Which statement is most reasonable?
A. There is a 95% chance the true mean is exactly 42 minutes.
B. With this sampling process, intervals built this way will cover the true mean about 95% of the time.
C. The true mean must be between 38 and 46 minutes, guaranteed.
D. At least 95% of future wait times will be between 38 and 46 minutes.
Design
Which situation breaks the basic bootstrap assumptions the most?
A. 500 independent product ratings from different customers.
B. 500 daily closing prices of one stock (strong time dependence).
C. 500 independent survey responses from randomly sampled voters.
D. 500 machine sensor readings, each from a different machine.
Interpreting a CI for a difference
In an A/B test, the bootstrap 95% CI for (new − old) conversion rate is [−0.01, 0.05].
Which is the best conclusion?
A. New page is definitely worse.
B. New page is definitely better.
C. Data are consistent with small harm, no change, or modest benefit; we need more data or other evidence.
D. New and old pages are identical in every way.
