viewof dist_kind = Inputs.select(
["Normal", "Exponential", "Uniform", "Beta", "Gamma", "Chi-square"],
{label: "Distribution"}
)
viewof p1 = Inputs.range([-3, 10], {value: 0, step: 0.1, label: "Parameter 1 (mean / rate / a / shape)"})
viewof p2 = Inputs.range([0.1, 5], {value: 1, step: 0.05, label: "Parameter 2 (sd / b / scale)"})Demos
Browser-side · no install · move the sliders
Interactive widgets that bring the chapter methods to life. Every demo simulates fresh data on every interaction and runs the estimator in Observable JS, client-side. No server, no install.
Distribution sampler
Pick a distribution and its parameters. The PDF / PMF appears on the left; a histogram of 5,000 simulated draws appears on the right. Move the parameter sliders to see how shape, location, and scale change.
dist_samples = {
const n = 5000
const out = []
for (let i = 0; i < n; i++) {
let x
const u = Math.random()
if (dist_kind === "Normal") {
// Box-Muller
const v = Math.random()
x = p1 + p2 * Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v)
} else if (dist_kind === "Exponential") {
x = -Math.log(1 - u) / Math.max(0.01, p1 + 1)
} else if (dist_kind === "Uniform") {
x = p1 + (p2 - p1) * u
} else if (dist_kind === "Beta") {
// Use two Gamma draws via inverse CDF approximation
// Approximation: Beta(a, b) ≈ sample from sum approach. Use ratio of gammas via marsaglia.
// For teaching simplicity: numerical inverse via Newton on the regularized incomplete beta is overkill.
// We'll do a quick rejection-sampling fallback.
const a = Math.max(0.5, p1 + 1), b = Math.max(0.5, p2)
let proposal, accept
do {
proposal = Math.random()
accept = Math.random()
} while (accept > Math.pow(proposal, a - 1) * Math.pow(1 - proposal, b - 1) / Math.max(0.001, Math.pow(0.5, a - 1) * Math.pow(0.5, b - 1)))
x = proposal
} else if (dist_kind === "Gamma") {
// Marsaglia-Tsang
const shape = Math.max(0.5, p1 + 1), scale = Math.max(0.1, p2)
const d = shape - 1/3
const c = 1 / Math.sqrt(9 * d)
let g = -1
while (g < 0) {
const z = Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random())
const v = Math.pow(1 + c * z, 3)
if (v > 0 && Math.log(Math.random()) < 0.5 * z * z + d - d * v + d * Math.log(v)) {
g = d * v * scale
}
}
x = g
} else if (dist_kind === "Chi-square") {
// Sum of squared normals
const df = Math.max(1, Math.round(p1 + 1))
let s = 0
for (let k = 0; k < df; k++) {
const v = Math.random()
const z = Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * v)
s += z * z
}
x = s
}
out.push(x)
}
return out
}{
const mean = d3.mean(dist_samples)
const sd = d3.deviation(dist_samples)
const p50 = d3.quantile(dist_samples.slice().sort(d3.ascending), 0.50)
const p025 = d3.quantile(dist_samples.slice().sort(d3.ascending), 0.025)
const p975 = d3.quantile(dist_samples.slice().sort(d3.ascending), 0.975)
return html`<div style="background: var(--paper-2, #f3efe3); border-left: 4px solid #0d3b66; padding: 0.7rem 1rem; margin: 1rem 0; border-radius: 4px; font-variant-numeric: tabular-nums;">
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap;">
<div><strong>Mean</strong> = ${mean.toFixed(3)}</div>
<div><strong>SD</strong> = ${sd.toFixed(3)}</div>
<div><strong>Median</strong> = ${p50.toFixed(3)}</div>
<div><strong>2.5% / 97.5%</strong> = [${p025.toFixed(3)}, ${p975.toFixed(3)}]</div>
<div><strong>n</strong> = ${dist_samples.length}</div>
</div>
</div>`
}Central Limit Theorem animator
Draw many samples of size n from a non-normal source (uniform, exponential, lognormal, t with df=3). Plot the histogram of sample means — watch it converge to a normal as n grows.
viewof source_dist = Inputs.select(
["Uniform[0,1]", "Exponential(1)", "Lognormal(0,1)", "t(df=3)"],
{label: "Source distribution"}
)
viewof sample_n = Inputs.range([2, 200], {value: 30, step: 1, label: "Sample size n"})
viewof n_replications = Inputs.range([100, 5000], {value: 1000, step: 100, label: "Number of sample means to draw"})function drawOne(dist) {
const u = Math.random()
const v = Math.random()
if (dist === "Uniform[0,1]") return u
if (dist === "Exponential(1)") return -Math.log(1 - u)
if (dist === "Lognormal(0,1)") return Math.exp(Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v))
if (dist === "t(df=3)") {
const z = Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random())
const chi = (() => {
let s = 0
for (let k = 0; k < 3; k++) {
const r1 = Math.random(), r2 = Math.random()
const zk = Math.sqrt(-2 * Math.log(r1)) * Math.cos(2 * Math.PI * r2)
s += zk * zk
}
return s
})()
return z / Math.sqrt(chi / 3)
}
}
clt_means = {
const means = []
for (let r = 0; r < n_replications; r++) {
let s = 0
for (let k = 0; k < sample_n; k++) s += drawOne(source_dist)
means.push(s / sample_n)
}
return means
}Plot.plot({
width: 760, height: 320, marginLeft: 50,
x: {label: `↑ sample mean (n=${sample_n}) →`, grid: true},
y: {label: "density", grid: true},
marks: [
Plot.rectY(clt_means, Plot.binX({y: "proportion"}, {x: d => d, thresholds: 40, fill: "#0d3b66", fillOpacity: 0.7})),
// Overlay a normal density (matched to sample mean and SD)
Plot.line(d3.range(d3.min(clt_means), d3.max(clt_means), (d3.max(clt_means)-d3.min(clt_means))/100), {
x: d => d,
y: d => {
const m = d3.mean(clt_means), s = d3.deviation(clt_means)
return (1 / (s * Math.sqrt(2 * Math.PI))) * Math.exp(-0.5 * Math.pow((d - m) / s, 2)) * (d3.max(clt_means) - d3.min(clt_means)) / 40
},
stroke: "#b85c38", strokeWidth: 2.4
})
]
}){
const m = d3.mean(clt_means), s = d3.deviation(clt_means)
// Shapiro-style heuristic: skewness should be small
const cubed = clt_means.map(x => Math.pow((x - m) / s, 3))
const skew = d3.mean(cubed)
const verdict = Math.abs(skew) < 0.15 ? 'looks normal' : Math.abs(skew) < 0.4 ? 'getting there' : 'not yet normal'
const color = Math.abs(skew) < 0.15 ? '#4f7942' : Math.abs(skew) < 0.4 ? '#d4a72c' : '#b85c38'
return html`<div style="background: var(--paper-2, #f3efe3); border-left: 4px solid ${color}; padding: 0.7rem 1rem; margin: 1rem 0; border-radius: 4px; font-variant-numeric: tabular-nums;">
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap;">
<div><strong>Mean of means</strong> = ${m.toFixed(3)}</div>
<div><strong>SD of means</strong> = ${s.toFixed(3)}</div>
<div><strong>Skewness</strong> = ${skew.toFixed(3)}</div>
<div style="color: ${color}; font-weight: 600;">${verdict}</div>
</div>
<div style="font-size: 0.92rem; color: #555; margin-top: 0.4rem;">
The orange curve is the normal approximation with sample mean and SD. As n grows, the histogram should match the curve. Heavy-tailed sources (t(df=3), lognormal) need larger n than light-tailed sources.
</div>
</div>`
}OLS by hand
A small simulated dataset of 30 points. The line below is the OLS fit. Drag the parameter sliders to move a candidate line; watch the residual-sum-of-squares update. The optimum (lowest RSS) is the OLS solution.
viewof true_intercept = Inputs.range([-3, 3], {value: 1.0, step: 0.1, label: "True intercept (data-generating)"})
viewof true_slope = Inputs.range([-2, 2], {value: 0.5, step: 0.05, label: "True slope (data-generating)"})
viewof noise = Inputs.range([0.1, 2], {value: 0.6, step: 0.05, label: "Noise SD"})
viewof candidate_intercept = Inputs.range([-3, 3], {value: 0.5, step: 0.05, label: "Candidate intercept (yours)"})
viewof candidate_slope = Inputs.range([-2, 2], {value: 0.3, step: 0.02, label: "Candidate slope (yours)"})ols_data = {
const data = []
for (let i = 0; i < 30; i++) {
const x = (Math.random() - 0.5) * 8
const eps = Math.sqrt(-2 * Math.log(Math.random())) * Math.cos(2 * Math.PI * Math.random()) * noise
data.push({x, y: true_intercept + true_slope * x + eps})
}
return data
}
ols_fit = {
const n = ols_data.length
const xbar = d3.mean(ols_data, d => d.x)
const ybar = d3.mean(ols_data, d => d.y)
let num = 0, den = 0
for (const d of ols_data) { num += (d.x - xbar) * (d.y - ybar); den += (d.x - xbar) ** 2 }
const beta = num / den
const alpha = ybar - beta * xbar
let rss = 0
for (const d of ols_data) rss += Math.pow(d.y - (alpha + beta * d.x), 2)
let rss_candidate = 0
for (const d of ols_data) rss_candidate += Math.pow(d.y - (candidate_intercept + candidate_slope * d.x), 2)
return {alpha, beta, rss, rss_candidate, n}
}Plot.plot({
width: 760, height: 380, marginLeft: 50,
x: {label: "x →", domain: [-5, 5], grid: true},
y: {label: "↑ y", grid: true},
marks: [
Plot.dot(ols_data, {x: "x", y: "y", fill: "#0d3b66", r: 3.5}),
// OLS fit
Plot.line([{x: -5, y: ols_fit.alpha + ols_fit.beta * -5}, {x: 5, y: ols_fit.alpha + ols_fit.beta * 5}],
{x: "x", y: "y", stroke: "#b85c38", strokeWidth: 2.4}),
// candidate
Plot.line([{x: -5, y: candidate_intercept + candidate_slope * -5}, {x: 5, y: candidate_intercept + candidate_slope * 5}],
{x: "x", y: "y", stroke: "#5b4fc7", strokeWidth: 2, strokeDasharray: "5 5"})
]
}){
return html`<div style="background: var(--paper-2, #f3efe3); border-left: 4px solid #0d3b66; padding: 0.8rem 1rem; margin: 1rem 0; border-radius: 4px; font-variant-numeric: tabular-nums;">
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap;">
<div style="color: #b85c38;"><strong>OLS fit</strong></div>
<div>α̂ = ${ols_fit.alpha.toFixed(3)}</div>
<div>β̂ = ${ols_fit.beta.toFixed(3)}</div>
<div>RSS = ${ols_fit.rss.toFixed(3)}</div>
</div>
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap; margin-top: 0.3rem;">
<div style="color: #5b4fc7;"><strong>Your candidate</strong></div>
<div>α = ${candidate_intercept.toFixed(3)}</div>
<div>β = ${candidate_slope.toFixed(3)}</div>
<div>RSS = ${ols_fit.rss_candidate.toFixed(3)}</div>
<div style="color: ${ols_fit.rss_candidate <= ols_fit.rss * 1.001 ? '#4f7942' : '#b85c38'}; font-weight: 600;">${ols_fit.rss_candidate <= ols_fit.rss * 1.001 ? '★ at the optimum' : `${(ols_fit.rss_candidate / ols_fit.rss - 1).toFixed(3) * 100}% above optimum`}</div>
</div>
<div style="font-size: 0.92rem; color: #555; margin-top: 0.4rem;">
OLS finds the (α, β) that minimizes RSS. Drag your sliders to find them. The closed form is α̂ = ȳ − β̂x̄ and β̂ = Σ(xᵢ−x̄)(yᵢ−ȳ) / Σ(xᵢ−x̄)².
</div>
</div>`
}Bootstrap distribution
Resample a small dataset with replacement (B = 2000 times by default). The histogram of bootstrap medians appears below, with the percentile 95% CI marked.
viewof boot_n = Inputs.range([20, 500], {value: 80, step: 5, label: "Sample size"})
viewof boot_B = Inputs.range([200, 5000], {value: 2000, step: 100, label: "Bootstrap replications B"})
viewof boot_source = Inputs.select(["Normal(0,1)", "Lognormal", "Exponential(1)"], {label: "Source distribution"})boot_data = {
const out = []
for (let i = 0; i < boot_n; i++) {
const u = Math.random(), v = Math.random()
const z = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v)
let x
if (boot_source === "Normal(0,1)") x = z
else if (boot_source === "Lognormal") x = Math.exp(z)
else if (boot_source === "Exponential(1)") x = -Math.log(1 - u)
out.push(x)
}
return out
}
boot_medians = {
const arr = []
for (let b = 0; b < boot_B; b++) {
const sample = []
for (let i = 0; i < boot_n; i++) sample.push(boot_data[Math.floor(Math.random() * boot_n)])
sample.sort(d3.ascending)
const m = boot_n % 2 === 1 ? sample[(boot_n - 1) / 2] : 0.5 * (sample[boot_n/2 - 1] + sample[boot_n/2])
arr.push(m)
}
return arr
}
boot_ci = {
const sorted = boot_medians.slice().sort(d3.ascending)
return {lo: d3.quantile(sorted, 0.025), hi: d3.quantile(sorted, 0.975), point: d3.median(sorted)}
}Plot.plot({
width: 760, height: 320, marginLeft: 50,
x: {label: "↑ bootstrap median estimate →", grid: true},
y: {label: "frequency", grid: true},
marks: [
Plot.rectY(boot_medians, Plot.binX({y: "count"}, {x: d => d, thresholds: 40, fill: "#4f7942", fillOpacity: 0.6})),
Plot.ruleX([boot_ci.lo, boot_ci.hi], {stroke: "#b85c38", strokeWidth: 2, strokeDasharray: "4 4"}),
Plot.ruleX([boot_ci.point], {stroke: "#0d3b66", strokeWidth: 2.2})
]
}){
return html`<div style="background: var(--paper-2, #f3efe3); border-left: 4px solid #4f7942; padding: 0.7rem 1rem; margin: 1rem 0; border-radius: 4px; font-variant-numeric: tabular-nums;">
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap;">
<div><strong>Point estimate (median)</strong> = ${boot_ci.point.toFixed(3)}</div>
<div><strong>95% percentile CI</strong> = [${boot_ci.lo.toFixed(3)}, ${boot_ci.hi.toFixed(3)}]</div>
<div><strong>B</strong> = ${boot_B}</div>
<div><strong>n</strong> = ${boot_n}</div>
</div>
<div style="font-size: 0.92rem; color: #555; margin-top: 0.4rem;">
The bootstrap approximates the sampling distribution of the median by resampling with replacement. The percentile CI is the simplest version; BCa corrects for bias and skew but is harder to demo in 30 lines.
</div>
</div>`
}What’s coming
These are the live ones. The roadmap below tracks demos in progress; each will land in the relevant chapter and be linked here.
- p-value visualizer (Ch. 5): a test-statistic slider showing the rejection region under the null and the p-value as the shaded tail.
- Influence and leverage (Ch. 7): drag a point, watch Cook’s distance and DFBETAs update.
- GLM probabilities (Ch. 8): pick a logit coefficient, see predicted probability curves; marginal effects displayed.
- PCA explorer (Ch. 10): a 4-variable correlated dataset, click to standardize, see PC1 and PC2 in real time.
- MCMC trace (Ch. 11): Metropolis-Hastings on a bimodal target, see acceptance rate and convergence diagnostics.