Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Statistics & probability

Each block is the whole file — copy it into x.manic and run manic x.manic (live) or --record out (video).

statistics — the whole story in three ideas

A guided lesson, not a feature demo: describe a dataset, meet the normal curve, then see why the bell is everywhere (the Central Limit Theorem). The stats companion to the linear-algebra lesson. Start here.

// ============================================================================
//  statistics.manic  —  Statistics, from data to the bell (a lesson)
// ----------------------------------------------------------------------------
//  The stats-rung capstone, companion to linear-algebra.manic. Three chapters on
//  one stage, chaptered with section():
//     1. DESCRIBE   — any dataset has a shape, a centre, a spread   (histogram)
//     2. THE BELL   — measurements often pile into a normal curve   (bellcurve)
//     3. WHY (CLT)  — averages of anything become a bell            (clt)
//
//  Each chapter reveals its whole builtin with `untraced(tag)` → `draw(tag)`
//  (preserving faint band opacities) and clears with `fade(tag)`.
// ============================================================================

title("Statistics: from data to the bell");
canvas("16:9");

let sy = cy + 30;

// 1. a real dataset (exam scores, already roughly bell-shaped)
histogram(hg, (cx, sy),
  "68 72 75 71 74 77 73 70 76 72 74 78 71 73 75 69 74 72 76 73 71 75 70 74 72 73 74 71 75 73", 12, 620, 230);
untraced(hg);
// 2. the idealised normal curve
bellcurve(bc, (cx, sy - 24), 100, 15, 78);
untraced(bc);
// 3. why the bell is everywhere — the Central Limit Theorem
clt(ct, (cx, sy), 5, 1000, 7, 620, 250);
untraced(ct);

text(cap, (cx, h - 44), ""); size(cap, 22); color(cap, dim); display(cap);

// ---- 1. describe ----
section("Describe the data");
draw(hg, 1.3);
say(cap, "any dataset has a shape, a centre, and a spread", 0.5);
wait(1.8);
say(cap, "these exam scores cluster around the middle, tailing off either side", 0.5);
wait(2.0);
fade(hg, 0.6);
wait(0.3);

// ---- 2. the bell ----
say(cap, "", 0.2);
section("The normal curve");
draw(bc, 1.3);
say(cap, "so many measurements pile into the same bell — the normal distribution", 0.5);
wait(2.0);
say(cap, "68% within one standard deviation, 95% within two, 99.7% within three", 0.5);
wait(2.2);
fade(bc, 0.6);
wait(0.3);

// ---- 3. why ----
say(cap, "", 0.2);
section("Why? The Central Limit Theorem");
draw(ct, 1.4);
say(cap, "average five dice, a thousand times — however flat one die is…", 0.5);
wait(2.0);
say(cap, "…the averages form a bell. THAT is why the normal is everywhere", 0.5);
wait(2.4);

histogram

The shape of a dataset: a list of numbers binned into bars that stagger in one at a time, with the mean marked and the range labelled (histogram). Paste your own numbers into the data string — grades, prices, heights, times.

// ============================================================================
//  histogram.manic  —  the shape of a dataset (stats Tier 1)
// ----------------------------------------------------------------------------
//  `histogram(id, (cx,cy), "v1 v2 v3 ...", [bins], [width], [height], [color])`
//  bins a list of numbers into bars — the SHAPE of the data. Bars are
//  `{id}.bar{k}` (tagged `{id}.bars`) so they stagger in and recolour as a
//  group; `{id}.meanline` + `{id}.mean` mark the mean, `{id}.min`/`{id}.max`
//  label the range. Default bin count ≈ √n.
//
//  TO ADAPT: paste your own numbers into the data string (grades, prices,
//  heights, times…). More `bins` = finer shape; fewer = smoother. Pass a colour
//  as the last argument, or `rainbow` to give every bar its own hue (below).
// ============================================================================

title("The shape of a dataset");
canvas("16:9");

let n = 14;

text(hdr, (cx, 60), "35 exam scores — where do they cluster?");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);

// `rainbow` colours each bar across the spectrum — no loop needed
histogram(hg, (cx, cy + 10),
  "72 85 90 68 95 88 76 91 83 79 84 60 97 81 78 86 74 89 82 93 71 87 80 77 92 85 83 88 75 90 66 82 84 79 88",
  n, 640, 300, rainbow);
untraced(hg.bars);                 // bars sketch in one at a time
hidden(hg.meanline); hidden(hg.mean);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

// ---- bars build up, left to right ----
say(cap, "each bar counts how many scores fall in that range", 0.5);
stagger(0.06) { for k in 0..n { draw(hg.bar{k}, 0.35); } }
wait(1.0);
say(cap, "the shape emerges: most scores cluster in the 80s, tailing off below", 0.5);
wait(1.6);

// ---- reveal the mean ----
say(cap, "and the average sits right in the thick of it", 0.5);
show(hg.meanline, 0.5); show(hg.mean, 0.4);
pulse(hg.mean);
wait(1.8);

summary

Describe a dataset in one call: the numbers as dots on a number line, with the mean, median and mode marked, a ±1σ spread band, and readouts of the range, variance and standard deviation (summary). Central tendency and dispersion, together.

// ============================================================================
//  summary.manic  —  describe a dataset (stats Tier 1)
// ----------------------------------------------------------------------------
//  `summary(id, (cx,cy), "v1 v2 v3 …", [width], [color])` is the descriptive-
//  statistics workhorse: the data as dots on a number line, with the **mean**
//  (gold), **median** (magenta) and **mode** (lime) marked, a translucent
//  **±1σ spread band**, and a readout of **n / range / variance / std**.
//  Pieces: `{id}.dots` (the cloud), `{id}.meanmark`/`.medianmark`/`.modemark`
//  (+ `.*lbl`), `{id}.band`, `{id}.min`/`.max`, `{id}.readout`.
//
//  TO ADAPT: paste your own numbers. Central tendency (mean/median/mode) and
//  dispersion (range/variance/std) come out in one call.
// ============================================================================

title("Describe a dataset");
canvas("16:9");

text(hdr, (cx, 64), "20 daily temperatures — centre and spread");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);

summary(sm, (cx, cy - 10),
  "18 21 20 22 19 23 21 20 21 24 22 21 19 20 23 21 22 20 21 25", 640);
// reveal in beats
untraced(sm.line);
hidden(sm.dots);
hidden(sm.band);
hidden(sm.meanmark); hidden(sm.meanlbl);
hidden(sm.medianmark); hidden(sm.medianlbl);
hidden(sm.modemark); hidden(sm.modelbl);
hidden(sm.readout);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "start with the raw numbers, laid out on a line", 0.5);
draw(sm.line, 0.6);
show(sm.dots, 0.6);
wait(1.4);

say(cap, "the centre: mean, median and mode", 0.5);
show(sm.meanmark, 0.4); show(sm.meanlbl, 0.4);
show(sm.medianmark, 0.4); show(sm.medianlbl, 0.4);
show(sm.modemark, 0.4); show(sm.modelbl, 0.4);
wait(1.6);

say(cap, "the spread: most values fall within one standard deviation of the mean", 0.5);
to(sm.band, opacity, 0.12, 0.6);
show(sm.readout, 0.4);
wait(2.0);

boxplot

The five-number summary as a box-and-whisker: the box spans Q1→Q3 (its width is the interquartile range), a line marks the median, the whiskers reach the rest, and a value far outside is flagged as an outlier (boxplot).

// ============================================================================
//  boxplot.manic  —  the five-number summary (stats Tier 1)
// ----------------------------------------------------------------------------
//  `boxplot(id, (cx,cy), "v1 v2 v3 …", [width], [color])` draws a box-and-whisker:
//  the box spans Q1→Q3 (its width IS the interquartile range), a line marks the
//  median, whiskers reach the extreme non-outliers (within 1.5·IQR), and points
//  beyond are flagged as outliers. Pieces: `{id}.box`, `{id}.med`,
//  `{id}.whiskerlo`/`.whiskerhi` (+ `.caplo`/`.caphi`), `{id}.outliers`,
//  `{id}.iqr` and the value labels.
//
//  TO ADAPT: paste your numbers. A value far from the box (here 40) shows up as
//  an outlier automatically.
// ============================================================================

title("Box-and-whisker: the five-number summary");
canvas("16:9");

text(hdr, (cx, 70), "response times (seconds) — one slow outlier");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);

boxplot(bp, (cx, cy), "12 15 14 10 18 16 13 15 14 17 40 11 16 15 14", 700);
untraced(bp.whiskerlo); untraced(bp.whiskerhi); untraced(bp.caplo); untraced(bp.caphi); untraced(bp.med);
hidden(bp.box);
hidden(bp.outliers);
hidden(bp.iqr); hidden(bp.lmin); hidden(bp.lmax); hidden(bp.lmed); hidden(bp.lq1); hidden(bp.lq3);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "the box holds the middle half of the data — Q1 to Q3, the IQR", 0.5);
to(bp.box, opacity, 0.2, 0.6);
show(bp.lq1, 0.3); show(bp.lq3, 0.3); show(bp.iqr, 0.3);
wait(1.5);

say(cap, "the line inside is the median; the whiskers reach the rest", 0.5);
draw(bp.med, 0.4); show(bp.lmed, 0.3);
par { draw(bp.whiskerlo, 0.5); draw(bp.whiskerhi, 0.5); }
draw(bp.caplo, 0.3); draw(bp.caphi, 0.3);
show(bp.lmin, 0.3); show(bp.lmax, 0.3);
wait(1.6);

say(cap, "and a value far outside the whiskers is flagged as an outlier", 0.5);
show(bp.outliers, 0.4); pulse(bp.outliers);
wait(1.9);

skew

Which way does the tail point? A histogram with the mean and median marked and a labelled skewness — when the mean is dragged right of the median, the data is right-skewed (skew).

// ============================================================================
//  skew.manic  —  the shape of a dataset: skewness (stats Tier 1)
// ----------------------------------------------------------------------------
//  `skew(id, (cx,cy), "v1 v2 v3 …", [bins], [width], [height], [color])` draws a
//  histogram with the **mean** (gold) and **median** (magenta) marked and a
//  labelled skewness. The tell: when the mean sits right of the median, a right
//  tail is pulling it — the data is right-skewed (positive); left of it, left-
//  skewed; on top of it, symmetric.
//
//  TO ADAPT: paste your numbers. Incomes, wait times and prices are classically
//  right-skewed (a few big values pull the mean up).
// ============================================================================

title("Skewness: which way does the tail point?");
canvas("16:9");

text(hdr, (cx, 66), "monthly salaries (k) — a few big earners pull the mean up");
size(hdr, 23); color(hdr, cyan); bold(hdr); display(hdr);

skew(sk, (cx, cy - 10),
  "28 30 32 31 29 33 35 30 31 34 42 38 30 32 31 33 30 36 55 68 90 30 34 31", 14, 560, 240);
untraced(sk.bars);
hidden(sk.meanline); hidden(sk.meanlbl);
hidden(sk.medianline); hidden(sk.medianlbl);
hidden(sk.skewlbl);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "most salaries cluster low, with a long tail of high earners", 0.5);
stagger(0.05) { for k in 0..14 { draw(sk.bar{k}, 0.3); } }
wait(1.2);

say(cap, "the median sits in the cluster — but the mean is dragged toward the tail", 0.5);
show(sk.medianline, 0.4); show(sk.medianlbl, 0.3);
show(sk.meanline, 0.4); show(sk.meanlbl, 0.3);
wait(1.6);

say(cap, "mean to the right of median = right-skewed (positive skew)", 0.5);
show(sk.skewlbl, 0.4);
wait(1.8);

bellcurve

The normal (Gaussian) bell curve and the 68-95-99.7 rule: the bell draws in, then the ±1σ / ±2σ / ±3σ bands shade one at a time, showing that 68% of values fall within one standard deviation, 95% within two, and 99.7% within three (bellcurve, alias gaussian).

// ============================================================================
//  normal.manic  —  the bell curve & the 68-95-99.7 rule (stats Tier 2)
// ----------------------------------------------------------------------------
//  `bellcurve(id, (cx,cy), mu, sigma, [unit], [color])` (alias `gaussian`) draws
//  bell curve and shades the ±1σ/±2σ/±3σ bands — the 68-95-99.7 rule. Pieces:
//  `{id}.curve` (the bell), `{id}.band1/2/3` (tagged `{id}.bands`), `{id}.mean`,
//  `{id}.p1/p2/p3` (the percentages), `{id}.t{-3..3}` (value ticks).
//  `unit` = pixels per σ; the bell is standardised, μ/σ set the axis values.
//
//  TO ADAPT: change mu/sigma to your distribution (heights, IQ, measurement
//  error…). The 68-95-99.7 rule holds for every normal.
// ============================================================================

title("The bell curve & the 68-95-99.7 rule");
canvas("16:9");

// IQ scores: mean 100, standard deviation 15
bellcurve(nd, (cx, cy + 10), 100, 15, 95);
untraced(nd.curve);                // the bell draws in
hidden(nd.mean);
hidden(nd.bands);                  // the three σ-bands reveal one at a time
hidden(nd.p1); hidden(nd.p2); hidden(nd.p3);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "IQ scores: mean 100, standard deviation 15 — the classic bell", 0.5);
draw(nd.curve, 1.1);
show(nd.mean, 0.4);
wait(1.2);

say(cap, "68% of values fall within ONE standard deviation of the mean", 0.5);
to(nd.band1, opacity, 0.16, 0.6); show(nd.p1, 0.4);
wait(1.7);

say(cap, "95% fall within two — almost everyone", 0.5);
to(nd.band2, opacity, 0.16, 0.6); show(nd.p2, 0.4);
wait(1.7);

say(cap, "and 99.7% within three: the tails are tiny", 0.5);
to(nd.band3, opacity, 0.16, 0.6); show(nd.p3, 0.4);
wait(1.9);

clt

The Central Limit Theorem — the flagship: however flat a single die is, the average of five dice, taken 1200 times, piles into a bell that hugs the normal curve (clt). Seeded, so it renders the same every time.

// ============================================================================
//  clt.manic  —  the Central Limit Theorem (stats Tier 3, the flagship payoff)
// ----------------------------------------------------------------------------
//  `clt(id, (cx,cy), samplesize, trials, [seed], [width], [height])` runs `trials`
//  experiments — each the average of `samplesize` dice — and histograms those
//  averages. However flat a single die is, the averages pile into a BELL. Draws
//  the histogram of sample means (`{id}.bar{k}`, tagged `{id}.bars`), the normal
//  curve they converge to (`{id}.curve`), the mean line, ticks, and an info
//  label. Seeded → the render is identical every time.
//
//  TO ADAPT: change `samplesize` (bigger n → tighter bell) or `trials` (more →
//  smoother). `seed` picks the reproducible random sequence.
// ============================================================================

title("The Central Limit Theorem");
canvas("16:9");

clt(ct, (cx, cy + 20), 5, 1200, 7, 660, 300);
untraced(ct.bars);      // the sample means pile up, bar by bar
untraced(ct.curve);     // the normal curve draws in last
hidden(ct.mean);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "one die is flat — 1 to 6 are equally likely. but average FIVE dice…", 0.5);
stagger(0.03) { for k in 0..30 { draw(ct.bar{k}, 0.3); } }
wait(1.0);

say(cap, "…and repeat 1200 times: the averages pile up around the middle", 0.5);
show(ct.mean, 0.4);
wait(1.6);

say(cap, "they trace a bell — the Central Limit Theorem: averages are normal", 0.5);
draw(ct.curve, 1.1);
wait(2.0);

correlation

Do two things move together? The scatter of paired data, the best-fit line, and the Pearson correlation r — near +1 a tight upward line, near −1 downward, near 0 a shapeless blob (correlation).

// ============================================================================
//  correlation.manic  —  how strongly two variables move together (stats T3)
// ----------------------------------------------------------------------------
//  `correlation(id, (cx,cy), unit, "x1 y1  x2 y2 …", [color])` scatters the
//  points, fits the best line, and reports the **Pearson correlation r** with a
//  strong/moderate/weak · positive/negative reading. `unit` = pixels per data
//  unit (x and y share it, so use data with comparable ranges). Points
//  `{id}.p{k}` (tagged `{id}.points`), the fit `{id}.line`, and `{id}.r`.
//
//  TO ADAPT: paste your paired data. r near ±1 = a tight line; near 0 = a blob.
// ============================================================================

title("Correlation: do they move together?");
canvas("16:9");

text(hdr, (cx, 70), "hours studied vs grade (out of 10)");
size(hdr, 24); color(hdr, cyan); bold(hdr); display(hdr);

correlation(co, (cx, cy - 10), 42,
  "1 3  2 4  3 4  4 6  5 6  6 7  7 8  8 8  9 10  10 9");
hidden(co.points); untraced(co.line); hidden(co.r);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "each dot is one student: hours studied, and the grade they got", 0.5);
show(co.points, 0.6);
wait(1.3);

say(cap, "the cloud slopes up — more study tends to mean a higher grade", 0.5);
draw(co.line, 0.9);
wait(1.3);

say(cap, "r measures how tightly the points track that line: near +1 is strong", 0.5);
show(co.r, 0.4); pulse(co.r);
wait(1.8);

lln

The Law of Large Numbers: flip a fair coin over and over and track the running proportion of heads. It swings wildly at first, then settles onto the true 0.5 as the trials pile up (lln). Draw the curve in to watch it converge.

// ============================================================================
//  lln.manic  —  the Law of Large Numbers (stats Tier 3)
// ----------------------------------------------------------------------------
//  `lln(id, (cx,cy), trials, [seed], [width], [height])` plots the running
//  proportion of heads over many coin flips: wild at first, settling onto the
//  true 0.5. Draws `{id}.curve`, the reference line `{id}.ref`, axis labels, and
//  the final value. Seeded → the render is identical every time.
//
//  TO ADAPT: change `trials` (more → tighter settling) or `seed` (a different
//  reproducible run).
// ============================================================================

title("The Law of Large Numbers");
canvas("16:9");

lln(ll, (cx, cy + 10), 600, 3, 720, 300);
untraced(ll.curve);              // the proportion traces in — watch it settle
hidden(ll.finallbl);
hidden(ll.truelbl);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "flip a fair coin over and over; track the proportion of heads so far", 0.5);
show(ll.truelbl, 0.4);
wait(0.8);
say(cap, "early on it swings wildly — a few flips prove nothing", 0.5);
draw(ll.curve, 2.6);
wait(0.4);
say(cap, "but over many trials it settles onto the true probability, 0.5", 0.5);
show(ll.finallbl, 0.4);
wait(1.9);

hypothesis

Is a result surprising enough to be real? Under the null hypothesis the test statistic follows the standard normal; the observed z cuts off tails whose area is the p-value. Smaller than α, reject (hypothesis).

// ============================================================================
//  hypothesis.manic  —  a significance test & the p-value (stats Tier 5)
// ----------------------------------------------------------------------------
//  `hypothesis(id, (cx,cy), z, [alpha], [unit])` — under the null hypothesis the
//  test statistic is standard-normal; the observed z cuts off tails whose area is
//  the p-value. If that area is smaller than alpha, the result is too surprising
//  to be chance — reject. Pieces: `{id}.curve`, `{id}.tails`, `{id}.zline`,
//  `{id}.p`, `{id}.verdict`.
// ============================================================================

title("Hypothesis testing: is this surprising?");
canvas("16:9");

hypothesis(hy, (cx, cy - 10), 2.3, 0.05, 92);
untraced(hy.curve);
hidden(hy.tails); hidden(hy.zline); hidden(hy.zlbl);
hidden(hy.p); hidden(hy.verdict);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "if nothing's going on, the test statistic follows this bell", 0.5);
draw(hy.curve, 1.0);
wait(1.2);
say(cap, "we observed z = 2.3 — out here in the tails", 0.5);
show(hy.zline, 0.4); show(hy.zlbl, 0.3);
wait(1.4);
say(cap, "the shaded tail area is the p-value: how likely a result this extreme is", 0.5);
to(hy.tails, opacity, 0.5, 0.6); show(hy.p, 0.4);
wait(1.6);
say(cap, "p = 0.021 < 0.05, so it's too surprising for chance — reject the null", 0.5);
show(hy.verdict, 0.4);
wait(1.9);

covariance

Covariance as signed area: a cross at the means, and a rectangle from each point to the centre — cyan where x and y agree, magenta where they disagree. Their balance is the covariance (covariance).

// ============================================================================
//  covariance.manic  —  covariance as signed area (stats Tier 5)
// ----------------------------------------------------------------------------
//  `covariance(id, (cx,cy), unit, "x1 y1 x2 y2 …", [color])` — a cross at the
//  means splits the plane into quadrants; each point draws a rectangle to the
//  mean-corner, cyan where (x-x̄)(y-ȳ) > 0 (agreeing) and magenta where negative.
//  Their signed-area balance IS the covariance. `{id}.points`, `{id}.rects`,
//  `{id}.cross`, `{id}.cov`.
// ============================================================================

title("Covariance: signed area about the mean");
canvas("16:9");

covariance(cv, (cx, cy - 10), 34, "1 2  2 1  3 4  4 3  5 6  6 5  7 8  8 7  9 9");
hidden(cv.points); hidden(cv.rects);
untraced(cv.crossv); untraced(cv.crossh);
hidden(cv.cov);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "mark each point, then draw the cross at the mean of x and the mean of y", 0.5);
show(cv.points, 0.5);
par { draw(cv.crossv, 0.5); draw(cv.crossh, 0.5); }
wait(1.4);
say(cap, "each point makes a rectangle to the centre — cyan if x and y agree", 0.5);
to(cv.rects, opacity, 0.13, 0.7);
wait(1.6);
say(cap, "more agreeing (cyan) area than disagreeing = positive covariance", 0.5);
show(cv.cov, 0.4);
wait(1.9);

bayes

Bayesian updating: a prior belief about a coin’s bias, the likelihood from the data, and the posterior that combines them — pulled toward the evidence and sharpening as it accumulates (bayes).

// ============================================================================
//  bayes.manic  —  Bayesian updating (stats Tier 5)
// ----------------------------------------------------------------------------
//  `bayes(id, (cx,cy), heads, tails, [width], [height])` — belief about a coin's
//  bias: a mild PRIOR, the LIKELIHOOD from the data, and the POSTERIOR that
//  combines them (pulled toward the data, sharpening as evidence grows). Pieces:
//  `{id}.prior`, `{id}.likelihood`, `{id}.posterior`, `{id}.mean`.
// ============================================================================

title("Bayes: updating a belief with data");
canvas("16:9");

bayes(by, (cx, cy + 10), 7, 2, 640, 250);
untraced(by.prior); untraced(by.likelihood); untraced(by.posterior);
hidden(by.mean); hidden(by.priorlbl); hidden(by.postlbl); hidden(by.datalbl);

text(cap, (cx, h - 46), ""); size(cap, 22); color(cap, dim); display(cap);

say(cap, "before any flips, a mild belief: the coin is probably fair-ish (the prior)", 0.5);
draw(by.prior, 0.8); show(by.priorlbl, 0.3);
wait(1.4);
say(cap, "then we flip: 7 heads, 2 tails — the data favours a biased coin (likelihood)", 0.5);
draw(by.likelihood, 0.8); show(by.datalbl, 0.3);
wait(1.6);
say(cap, "combine them and the posterior lands between: p about 0.69", 0.5);
draw(by.posterior, 0.8); show(by.mean, 0.4); show(by.postlbl, 0.3);
wait(2.0);

probability

A probability & sampling playground in four chapters: named distributions (uniform / exponential / binomial / Poisson), a confidence interval, a Monte-Carlo estimate of π, and a random walk (distribution, confidence, montecarlo, randomwalk).

// ============================================================================
//  probability.manic  —  a probability & sampling playground (stats)
// ----------------------------------------------------------------------------
//  Four ideas on one stage, chaptered with section():
//     1. named DISTRIBUTIONS  (uniform / exponential / binomial / poisson)
//     2. CONFIDENCE intervals (an estimate ± a margin)
//     3. MONTE-CARLO           (estimate pi by throwing darts)
//     4. RANDOM WALK           (a path that wanders)
//  Each chapter reveals with untraced(tag) -> draw(tag) and clears with fade(tag).
//  The seeded builtins (montecarlo, randomwalk) render identically every time.
// ============================================================================

title("A probability playground");
canvas("16:9");

// ---- 1. named distributions (a 2x2 gallery) ----
distribution(du, (cx - 350, cy - 120), "uniform", 2, 6);       untraced(du);
distribution(de, (cx + 350, cy - 120), "exponential", 1);       untraced(de);
distribution(db, (cx - 350, cy + 180), "binomial", 12, 0.4);    untraced(db);
distribution(dp, (cx + 350, cy + 180), "poisson", 4);           untraced(dp);
// ---- 2. a confidence interval ----
confidence(ci, (cx, cy), 50, 8, 25, 95, 620); untraced(ci); hidden(ci.estimate);
// ---- 3. monte-carlo pi ----
montecarlo(mc, (cx, cy - 10), 1200, 7, 210); untraced(mc);
// ---- 4. a random walk ----
randomwalk(rw, (cx, cy), 500, 4, 12); untraced(rw);

text(cap, (cx, h - 44), ""); size(cap, 22); color(cap, dim); display(cap);

section("Named distributions");
par { draw(du, 0.9); draw(de, 0.9); draw(db, 0.9); draw(dp, 0.9); }
say(cap, "flat, decaying, discrete counts — the classic shapes of chance", 0.5);
wait(2.2);
par { fade(du, 0.5); fade(de, 0.5); fade(db, 0.5); fade(dp, 0.5); }
wait(0.3);

say(cap, "", 0.2);
section("Confidence intervals");
draw(ci, 0.9); show(ci.estimate, 0.4);
say(cap, "an estimate is never exact — the interval says how sure we are", 0.5);
wait(2.2);
fade(ci, 0.6);
wait(0.3);

say(cap, "", 0.2);
section("Monte Carlo");
draw(mc, 1.2);
say(cap, "throw darts at random: the fraction landing in the circle gives pi", 0.5);
wait(2.4);
fade(mc, 0.6);
wait(0.3);

say(cap, "", 0.2);
section("Random walk");
draw(rw, 1.4);
say(cap, "each step a random direction — chance draws a wandering path", 0.5);
wait(2.2);