Monocurl Essays
Series Download

The normal distribution and the 68-95-99.7 rule

A histogram with ever narrower bins settles onto a curve whose areas are probabilities. For the bell curve, those areas give the 68-95-99.7 rule and z-scores.

20 min·High school and early college·

AssumesMean, median and standard deviation: what each one saysWhat is an integral? Adding up to get area

A simulation of the heights of 40,000 men, drawn as a histogram whose bins narrow from 8 cm to half a centimeter. Each bar’s height is the fraction of men in the bin divided by the bin’s width. The bars settle onto a smooth bell-shaped curve.
Monocurl source
let N = 40000
let COUNTS = height_counts(N, 1)
let GROUPS = [16, 8, 4, 2, 1]
let fh = Frame(LO, HI, 0, 0.065, [-0.05, 0.1, 0], 6.4, 2.9)
let Refine = |m| {
    let k = to_int(floor(m + 0.0001))
    return [
        HistBars(fh, COUNTS, N, GROUPS[k], 1, AZURE),
        center{at(fh, 196, 0.05)} scale{1.3} color{INK} Number(GROUPS[k] * FINE, 1)
    ]
}
mesh frame = [
    HeightAxes(fh, [0.02, 0.04, 0.06], 2, "fraction of men per cm"),
    center{at(fh, 196, 0.059)} Cap("bin width (cm)", 0.76, INK),
    center{at(fh, 158, 0.059)} Note("simulation:", 0.74),
    center{at(fh, 158, 0.052)} Note("40,000 heights", 0.74)
]
mesh bars = Refine(m: 0)
mesh curve = []

slide "refine"
    play Wait(0.8)
    bars.m = 4.6
    play Lerp(6.5, [&bars], linear)
    curve = Graph(fh, |x| bell(x, MU, SD), LO, HI, EMBER, 4.5)
    play Fade(0.9, [&curve])
    play Wait(1.8)

Uses the shared definitions for this lesson's figures.

What fraction of men are exactly 180 centimeters tall? Not 180 to the nearest centimeter, but 180.000... with zeros forever. Hardly any, and with enough decimal places demanded, none at all. The same goes for 176, for 190, and for every other height. So each exact height has probability zero, and yet every man has one, and heights near 180 are plainly more common than heights near 200.

The earlier probability lessons had no such trouble. A die has six outcomes and each gets a positive probability. Height is a measurement on a continuous scale, and on a continuous scale the question “how likely is this exact value?” has the same useless answer everywhere. We need a different way to say where the values tend to fall. This lesson builds it, and then studies the most important example, the bell curve, far enough to see where the numbers 68, 95 and 99.7 come from.

From a histogram to a curve

Our data are simulated: 40,000 heights of men, generated so that their mean is 176 cm and their standard deviation is 7 cm, which is roughly right for adult men in many countries. Recall from the first lesson that the mean is the balance point of the data and the standard deviation is the root of the mean squared deviation, a typical distance from the mean.

The standard picture of such data is a histogram. Cut the height axis into bins, and over each bin draw a bar whose height is the fraction of the data that falls in it. With bins 8 cm wide, the tallest bar covers 172 to 180 cm and holds about 43% of the men.

We would like the picture to be as detailed as possible, so we should narrow the bins.

Pause and try

The bin from 176 to 184 cm holds 37.1% of the men. Split it into two bins 4 cm wide, then split again. Roughly what fraction does a bin 1 cm wide hold? What happens to every bar’s height if we keep narrowing?

Show answerHide answer

Halving a bin’s width roughly halves its contents, a little more or less depending on which side of the peak it is on. The bin from 176 to 180 holds 21.5%, from 176 to 178 it is 11.2%, and from 176 to 177 it is 5.6%. Each halving of the width roughly halves every bar, so the whole histogram sinks toward the axis. In the limit every bar has height zero, which is the opening puzzle again.

The same 40,000 simulated heights, with each bar’s height equal to the plain fraction of men in its bin. As the bins narrow from 8 cm to half a centimeter, every bar shrinks, and the picture collapses toward the axis. The dashed gray outline, labelled “before”, is the 8 cm histogram it started from.
Monocurl source
let N = 40000
let COUNTS = height_counts(N, 1)
let GROUPS = [16, 8, 4, 2, 1]
let ff = Frame(LO, HI, 0, 0.5, [-0.05, 0.1, 0], 6.4, 2.9)
let TOPS = map(GROUPS, |g| {
    var best = 0
    for (j in range(0, NFINE // g)) {
        var c = 0
        for (q in range(0, g)) {
            c = c + COUNTS[j * g + q]
        }
        best = max(best, c / N)
    }
    return best
})
let Sink = |m| {
    let k = to_int(floor(m + 0.0001))
    return [
        HistBars(ff, COUNTS, N, GROUPS[k], 0, ROSE),
        center{at(ff, 196, 0.36)} scale{1.3} color{INK} Number(GROUPS[k] * FINE, 1),
        center{at(ff, 155.3, 0.36)} scale{1.3} color{ROSE} Number(TOPS[k], 3)
    ]
}
mesh frame = [
    HeightAxes(ff, [0.1, 0.2, 0.3, 0.4], 1, "fraction of men in the bin"),
    center{at(ff, 196, 0.44)} Cap("bin width (cm)", 0.76, INK),
    center{at(ff, 155.3, 0.44)} Cap("tallest bar", 0.76, ROSE)
]
# the 8 cm histogram as a dashed outline with no fill: a reference, not a second histogram
let GHOST = tint(SLATE, 0.55)
mesh ghost = block {
    var pts = [at(ff, LO, 0)]
    for (j in range(0, NFINE // 16)) {
        var c = 0
        for (q in range(0, 16)) {
            c = c + COUNTS[j * 16 + q]
        }
        pts .= at(ff, LO + j * 8, c / N)
        pts .= at(ff, LO + (j + 1) * 8, c / N)
    }
    pts .= at(ff, HI, 0)
    . stroke{GHOST, 2} dashed{[0.09, 0.07]} Polyline(pts)
    . center{at(ff, 185.2, 0.35)} color{GHOST} Text("before", 0.72, "Charter")
}
mesh bars = Sink(m: 0)

slide "sink"
    play Wait(1)
    bars.m = 4.6
    play Lerp(6, [&bars], linear)
    play Wait(1.6)

Uses the shared definitions for this lesson's figures.

The collapse has a simple cause, and the cause suggests the repair. A bar shrinks because its bin shrinks. The fraction in a narrow bin is roughly proportional to the bin’s width, so the quantity that stays put is the fraction divided by the width: the fraction of men per centimeter of height. For the bins starting at 176 that ratio is $0.371/8 \approx 0.046$, then $0.215/4 \approx 0.054$, then $0.112/2 = 0.056$, then $0.056/1 = 0.056$ per centimeter. It is settling down.

So redraw the histogram with bar height equal to fraction divided by bin width. That is the figure at the top of the page. Now narrowing the bins sharpens the picture without flattening it, and the tops of the bars close in on a curve. With this scaling a bar’s area has a meaning: height times width is (fraction per cm) times (cm), which is the fraction of men in the bin. The heights of the bars are no longer probabilities, but their areas are.

Area is probability

  1. With bins 2 cm wide, the five bars from 180 to 190 cm have total area 0.2596: that fraction of the 40,000 simulated men is in the range.
  2. Replace the bars by the curve they were settling onto. The area under the curve from 180 to 190 is 0.2611.
  3. Narrow the range to 180 to 181 cm. The area, and so the probability, drops to 0.0464.
  4. Narrow it to a tenth of a centimeter and the area is 0.0048. A single exact height is a range of width zero, with area zero.
Areas as probabilities for the heights. In the histogram, the bars between 180 and 190 cm have total area equal to the fraction of the sample in that range. Under the limiting curve, the same range has almost the same area. As the range closes down on the single height 180, its area goes to zero.
Monocurl source
let N = 40000
let COUNTS = height_counts(N, 1)
let fh = Frame(LO, HI, 0, 0.065, [-0.05, 0.1, 0], 6.4, 2.9)
let f = |x| bell(x, MU, SD)
let IN_RANGE = sum(map(range(64, 84), |i| COUNTS[i])) / N
let Shade = |b| [
    Under(fh, f, 180, b, tint(EMBER, 0.55)),
    Seg(at(fh, 180, 0), at(fh, 180, f(180)), EMBER, 3),
    Seg(at(fh, b, 0), at(fh, b, f(b)), EMBER, 3),
    center{at(fh, 195.5, 0.047)} scale{1.35} color{EMBER} Number(simpson(f, 180, b, 40), 4)
]
mesh frame = HeightAxes(fh, [0.02, 0.04, 0.06], 2, "fraction of men per cm")
mesh bars = [HistBars(fh, COUNTS, N, 4, 1, AZURE, 0, 180), HistBars(fh, COUNTS, N, 4, 1, AZURE, 190, 1000)]
mesh chosen = HistBars(fh, COUNTS, N, 4, 1, EMBER, 180, 190)
mesh curve = []
mesh shade = []
mesh label = []

slide "With bins 2 cm wide, the five bars from 180 to 190 cm have total area 0.2596: that fraction of the 40,000 simulated men is in the range."
    label = [
        center{at(fh, 195.5, 0.06)} Cap("in the sample", 0.76, EMBER),
        center{at(fh, 195.5, 0.054)} Cap("180 to 190 cm:", 0.76, EMBER),
        center{at(fh, 195.5, 0.047)} Math(to_string(IN_RANGE, 4), 1.05, EMBER)
    ]
    play Fade(0.6)

slide "Replace the bars by the curve they were settling onto. The area under the curve from 180 to 190 is 0.2611."
    label = []
    bars = []
    chosen = []
    play Fade(0.5, [&label, &bars, &chosen])
    curve = Graph(fh, f, LO, HI, AZURE, 4.5)
    shade = Shade(b: 190)
    label = [
        center{at(fh, 195.5, 0.06)} Cap("area under", 0.76, EMBER),
        center{at(fh, 195.5, 0.054)} Cap("the curve", 0.76, EMBER)
    ]
    play Fade(0.7, [&curve, &shade, &label])

slide "Narrow the range to 180 to 181 cm. The area, and so the probability, drops to 0.0464."
    shade.b = 181
    play Lerp(1.8, [&shade])

slide "Narrow it to a tenth of a centimeter and the area is 0.0048. A single exact height is a range of width zero, with area zero."
    shade.b = 180.1
    play Lerp(1.8, [&shade])

Uses the shared definitions for this lesson's figures.

This resolves the opening puzzle. The probability of a range of heights is an area, and a single exact height is a range of width zero, so its area is zero however tall the curve is there. The height of the curve at 180 is a rate, probability per centimeter, and it becomes a probability only when multiplied by a width.

To state this exactly we need the area under a curve, which is what an integral is. Recall from the lesson on integrals that $\int_a^b f(x)\,dx$ is the area between the graph of $f$ and the axis from $x = a$ to $x = b$, defined as the limit of the total area of thin rectangles of height $f(x)$ as their widths shrink to zero. A histogram with narrow bins is such a collection of rectangles.

The two conditions are the two rules of probability in new clothes: probabilities are never negative, and the probability that $X$ lands somewhere is 1. The integral over the whole line means the limit of $\int_{-R}^{R} f$ as $R$ grows. And since single points have area zero, $P(a \le X \le b)$ and $P(a < X < b)$ are equal, so for densities we can stop worrying about whether endpoints are included.

Pause and try

A density has the value 3 at some point. Does that contradict the rule that probabilities are at most 1? To decide, consider the density that equals 3 on the interval from 0 to $\frac13$ and equals 0 everywhere else.

Show answerHide answer

There is no contradiction. That function is never negative, and its total area is $3 \times \frac13 = 1$, so it is a density. The probability of landing between 0 and 0.1 is the area $3 \times 0.1 = 0.3$. A density can be as tall as you like on a narrow enough range, because only areas are probabilities.

The mean and standard deviation carry over from data to densities with sums replaced by integrals. The mean is $\mu = \int x f(x)\,dx$, the balance point of the region under the curve, and the variance is $\sigma^2 = \int (x - \mu)^2 f(x)\,dx$, both taken over the whole line. (For some densities these integrals do not settle on a finite value, and then the mean or the variance does not exist; the next lesson meets one. Every density used below has both.)

The formula for the bell curve

The curve that the height histogram settled onto has a formula. It is built from the function $e^{-z^2/2}$, where $e \approx 2.718$ is the base of natural logarithms, and each of the remaining pieces is there for a reason that we can now state.

  1. The function e^(-z^2/2) is 1 at z = 0 and falls away on both sides, slowly at first and then very fast: 0.61, 0.14 and 0.011 at z = 1, 2 and 3.
  2. The area under it, found here by adding up thin slices, is 2.5066. A density must have area 1.
  3. Divide every height by 2.5066. The peak drops to 0.3989 and the area becomes 1. This is the standard normal density.
Building the standard bell curve. The function $e^{-z^2/2}$ has the right shape, but the area under it is about 2.5066. Dividing by that number, which equals $\sqrt{2\pi}$, makes the area 1; the dashed curve is the original for comparison.
Monocurl source
let fz = Frame(-4, 4, 0, 1.15, [-0.05, 0.1, 0], 6.4, 2.9)
let raw = |z| exp(-z * z / 2)
let AREA = simpson(raw, -8, 8, 400)
let Bump = |s| Graph(fz, |z| s * raw(z), -4, 4, AZURE, 4.5)
let Fill = |s| Under(fz, |z| s * raw(z), -4, 4, tint(AZURE, 0.3))
mesh frame = [
    GridLines(fz, [-3, -2, -1, 1, 2, 3], [0.25, 0.5, 0.75, 1]),
    Seg(at(fz, -4, 0), at(fz, 4, 0), SLATE, 2.5),
    Seg(at(fz, 0, 0), at(fz, 0, 1.15), SLATE, 2.5),
    map([-3, -2, -1, 0, 1, 2, 3], |v| center{[fx(fz, v), fy(fz, 0) - 0.3, 0]} Math(to_string(v), 0.72, SLATE)),
    map([0.25, 0.5, 0.75, 1], |v| center{[fx(fz, -4) - 0.4, fy(fz, v), 0]} Math(to_string(v, 2), 0.72, SLATE)),
    center{[fx(fz, 0), fy(fz, 0) - 0.66, 0]} Math("z", 0.85, SLATE)
]
mesh fill = []
mesh ghost = []
mesh bump = Bump(s: 1)
mesh notes = []

slide "The function e^(-z^2/2) is 1 at z = 0 and falls away on both sides, slowly at first and then very fast: 0.61, 0.14 and 0.011 at z = 1, 2 and 3."
    notes = [
        map([0, 1, 2, 3], |z| Pt(at(fz, z, raw(z)), EMBER, 0.09)),
        center{at(fz, 0.55, 1.06)} Math("1", 0.85, EMBER),
        center{at(fz, 1.65, 0.66)} Math(to_string(raw(1), 2), 0.85, EMBER),
        center{at(fz, 2.6, 0.2)} Math(to_string(raw(2), 2), 0.85, EMBER),
        center{at(fz, 3.3, 0.12)} Math(to_string(raw(3), 3), 0.85, EMBER),
        center{at(fz, -2.6, 0.95)} Math("e^{-z^2/2}", 1.1, AZURE)
    ]
    play Fade(0.6)

slide "The area under it, found here by adding up thin slices, is 2.5066. A density must have area 1."
    fill = Fill(s: 1)
    notes .= center{at(fz, 2.75, 0.95)} Cap("area", 0.8, AZURE)
    notes .= center{at(fz, 2.75, 0.8)} Math(to_string(AREA, 4), 1.0, AZURE)
    play Fade(0.6, [&fill, &notes])

slide "Divide every height by 2.5066. The peak drops to 0.3989 and the area becomes 1. This is the standard normal density."
    notes = []
    play Fade(0.3, [&notes])
    ghost = stroke{SLATE, 2.5} dashed{[0.12, 0.09]} Polyline(map(sample(-4, 4, 100), |z| at(fz, z, raw(z))))
    play Fade(0.4, [&ghost])
    fill.s = 1 / AREA
    bump.s = 1 / AREA
    play Lerp(1.8, [&fill, &bump])
    notes = [
        center{at(fz, 2.75, 0.95)} Cap("area", 0.8, AZURE),
        center{at(fz, 2.75, 0.8)} Math(to_string(simpson(raw, -8, 8, 400) / AREA, 4), 1.0, AZURE),
        center{at(fz, -2.5, 0.92)} Math("\frac{1}{\sqrt{2\pi}}\,e^{-z^2/2}", 1.0, AZURE),
        Pt(at(fz, 0, 1 / AREA), EMBER, 0.09),
        center{at(fz, -0.62, 0.47)} Math(to_string(1 / AREA, 4), 0.85, EMBER)
    ]
    play Fade(0.5, [&notes])

Uses the shared definitions for this lesson's figures.

The exponent $-z^2/2$ is what makes the bell. Squaring makes the curve symmetric about zero, and the minus sign makes it fall as $|z|$ grows. It falls extremely fast: $e^{-z^2/2}$ is 0.61 at $z = 1$ but about $3.7 \times 10^{-6}$ at $z = 5$. That speed is the feature of the normal distribution that matters most in practice, and it is why values many standard deviations from the mean essentially never happen.

The constant in front only fixes the area. The area under $e^{-z^2/2}$ is exactly $\sqrt{2\pi} \approx 2.5066$. That is a theorem (the Gaussian integral) whose usual proof needs integrals in two variables and is beyond this lesson; the figure checks it numerically. Dividing by $\sqrt{2\pi}$ gives the standard normal density

$$ \varphi(z) = \frac{1}{\sqrt{2\pi}}\,e^{-z^2/2}. $$

It has mean 0, by symmetry, and standard deviation 1. The second fact also needs a computation that I am leaving out, an integration by parts showing $\int z^2\varphi(z)\,dz = 1$; a numerical integration agrees to four decimal places.

A bell for heights must be centered at 176 and be 7 times as wide. Moving the center is a shift: replace $z$ by $x - \mu$. Widening by a factor $\sigma$ means replacing $z$ by $(x - \mu)/\sigma$, so that $x$ has to travel $\sigma$ units to do what $z$ did in one. But stretching a region sideways by a factor $\sigma$ multiplies its area by $\sigma$, and the area must stay 1, so every height is divided by $\sigma$ to compensate. Shifting and stretching act on the mean and standard deviation as they did for data in the first lesson: if $Z$ has mean 0 and standard deviation 1, then $\sigma Z + \mu$ has mean $\mu$ and standard deviation $\sigma$, which is why the two parameters in the definition below carry those names.

One normal density as its two parameters change. Changing $\sigma$ from 7 to 3.5 to 10 makes the bell narrow and tall, then wide and low, with area 1 throughout. The orange bar is one standard deviation long. Changing $\mu$ slides the whole bell without changing its shape.
Monocurl source
let fb = Frame(LO, HI, 0, 0.125, [-0.05, 0.1, 0], 6.4, 2.9)
let Bell = |mu, sd| [
    Under(fb, |x| bell(x, mu, sd), LO, HI, tint(AZURE, 0.3)),
    Graph(fb, |x| bell(x, mu, sd), LO, HI, AZURE, 4.5, 220),
    Seg(at(fb, mu, 0), at(fb, mu, bell(mu, mu, sd)), LEAF, 3.5),
    Seg(at(fb, mu, bell(mu + sd, mu, sd)), at(fb, mu + sd, bell(mu + sd, mu, sd)), EMBER, 5),
    center{at(fb, 158.6, 0.108)} scale{1.3} color{LEAF} Number(mu, 0),
    center{at(fb, 199.8, 0.108)} scale{1.3} color{EMBER} Number(sd, 1),
    center{at(fb, 199, 0.078)} scale{1.2} color{AZURE} Number(bell(mu, mu, sd), 3)
]
mesh frame = [
    HeightAxes(fb, [0.04, 0.08, 0.12], 2, "probability per cm"),
    center{at(fb, 153, 0.108)} Math("\mu =", 0.95, LEAF),
    center{at(fb, 194.3, 0.108)} Math("\sigma =", 0.95, EMBER),
    center{at(fb, 193.5, 0.078)} Cap("peak", 0.78, AZURE)
]
mesh b = Bell(mu: 176, sd: 7)

slide "shape"
    play Wait(0.8)
    b.sd = 3.5
    play Lerp(1.6, [&b])
    play Wait(0.3)
    b.sd = 10
    play Lerp(2, [&b])
    play Wait(0.3)
    b.sd = 7
    play Lerp(1.3, [&b])
    b.mu = 166
    play Lerp(1.5, [&b])
    b.mu = 176
    play Lerp(1.3, [&b])
    play Wait(1.2)

Uses the shared definitions for this lesson's figures.

For the heights, the peak is $\frac{1}{7\sqrt{2\pi}} \approx 0.057$ per cm, which matches the top of the histogram. Halve $\sigma$ and the peak doubles.

Measuring in standard deviations

Every normal density is the standard one, shifted and stretched. That suggests measuring every normal quantity on a common scale.

The z-score says how many standard deviations $x$ lies above the mean, with negative values below. A height of 190 cm has $z = \frac{190 - 176}{7} = 2$, and a height of 169 cm has $z = -1$. The point of the definition is the following theorem, which says that for a normal quantity, probabilities depend on z-scores and on nothing else.

Each rectangle became $\sigma$ times narrower and $\sigma$ times taller, which is the stretching argument made exact. The theorem reduces every question about every normal distribution to areas under one curve.

Where 68, 95 and 99.7 come from

So we need areas under $\varphi$. Here there is an obstacle of a kind you may not have met before: the function $e^{-z^2/2}$ has no antiderivative that can be written with the usual functions (powers, exponentials, logarithms, trigonometric functions). This is a theorem of Liouville from the 1830s. Nobody is missing a trick. The areas are perfectly definite numbers, but they have to be computed numerically, by adding up thin slices, and that is what the next figure does.

Pause and try

Before looking: the area under $\varphi$ between $-1$ and $1$ is the probability that a normal quantity lands within one standard deviation of its mean. Use the peak height $\varphi(0) \approx 0.40$ and the height $\varphi(1) \approx 0.24$ to decide whether that probability is nearer 0.5, 0.65 or 0.8.

Show answerHide answer

The region is 2 wide. Its height falls from 0.40 in the middle to 0.24 at each end, so its area is between $2 \times 0.24 = 0.48$ and $2 \times 0.40 = 0.80$. The curve is rounded outward over that range, so it stays closer to 0.40 than a straight line would, and the area should be above the midpoint 0.64 of those bounds. Nearer 0.65 is the right choice; the computed value is 0.6827.

  1. Within one standard deviation of the mean, from z = -1 to z = 1, the area is 0.6827. About 68%% of men are between 169 and 183 cm.
  2. Within two standard deviations the area is 0.9545. About 95%% of men are between 162 and 190 cm.
  3. Within three standard deviations the area is 0.9973. Only 0.27%% of men, about 1 in 370, are shorter than 155 cm or taller than 197 cm.
The areas under the standard normal density within 1, 2 and 3 standard deviations of the mean, each computed in the figure by Simpson’s rule. The second row of labels gives the corresponding heights of men, for mean 176 cm and standard deviation 7 cm.
Monocurl source
let fz = Frame(-4, 4, 0, 0.45, [0.1, 0.3, 0], 6.6, 2.9)
let within = |k| simpson(phi, -k, k, 200)
let Band = |k, col| Under(fz, phi, -k, k, col)
let Edges = |k, col| [
    Seg(at(fz, -k, 0), at(fz, -k, phi(k)), col, 3),
    Seg(at(fz, k, 0), at(fz, k, phi(k)), col, 3)
]
let Readout = |k| [
    center{at(fz, -2.9, 0.435)} Math(str_join(["|z| \le ", to_string(k)]), 1.0, AZURE),
    center{at(fz, -2.9, 0.35)} Math(str_join([to_string(100 * within(k), 2), "\%%"]), 1.25, AZURE),
    center{at(fz, 2.9, 0.435)} Cap("outside", 0.8, ROSE),
    center{at(fz, 2.9, 0.35)} Math(str_join([to_string(100 * (1 - within(k)), 2), "\%%"]), 1.25, ROSE)
]
mesh grid = GridLines(fz, [-3, -2, -1, 0, 1, 2, 3], [0.1, 0.2, 0.3, 0.4])
mesh band3 = []
mesh band2 = []
mesh band1 = []
mesh frame = [
    Seg(at(fz, -4, 0), at(fz, 4, 0), SLATE, 2.5),
    map([-3, -2, -1, 0, 1, 2, 3], |v| [
        Seg([fx(fz, v), fy(fz, 0) - 0.07, 0], [fx(fz, v), fy(fz, 0), 0], SLATE, 2),
        center{[fx(fz, v), fy(fz, 0) - 0.3, 0]} Math(to_string(v), 0.76, SLATE),
        center{[fx(fz, v), fy(fz, 0) - 0.68, 0]} Math(to_string(MU + SD * v), 0.72, SLATE)
    ]),
    center{[fx(fz, -4) - 0.05, fy(fz, 0) - 0.3, 0]} Math("z", 0.85, SLATE),
    center{[fx(fz, -4) - 0.05, fy(fz, 0) - 0.68, 0]} Note("cm", 0.72),
    map([0.1, 0.2, 0.3, 0.4], |v| center{[fx(fz, -4) - 0.3, fy(fz, v), 0]} Math(to_string(v, 1), 0.72, SLATE))
]
mesh curve = Graph(fz, phi, -4, 4, AZURE, 4.5)
mesh edges = []
mesh readout = []

slide "Within one standard deviation of the mean, from z = -1 to z = 1, the area is 0.6827. About 68%% of men are between 169 and 183 cm."
    band1 = Band(1, tint(AZURE, 0.6))
    edges = Edges(1, AZURE)
    readout = Readout(1)
    play Fade(0.7)

slide "Within two standard deviations the area is 0.9545. About 95%% of men are between 162 and 190 cm."
    readout = []
    play Fade(0.3, [&readout])
    band2 = Band(2, tint(AZURE, 0.35))
    edges = Edges(2, AZURE)
    readout = Readout(2)
    play Fade(0.7, [&band2, &edges, &readout])

slide "Within three standard deviations the area is 0.9973. Only 0.27%% of men, about 1 in 370, are shorter than 155 cm or taller than 197 cm."
    readout = []
    play Fade(0.3, [&readout])
    band3 = Band(3, tint(AZURE, 0.16))
    edges = Edges(3, AZURE)
    readout = Readout(3)
    play Fade(0.7, [&band3, &edges, &readout])

Uses the shared definitions for this lesson's figures.

The simulated sample agrees: of the 40,000 heights, 68.05% fell within 7 cm of 176, 95.49% within 14 cm and 99.74% within 21 cm.

Compare this with the only promise that holds for every shape, which was a problem in the first lesson (Chebyshev’s inequality). For any data set whatever, at most $\frac14$ of the values lie two or more standard deviations from the mean. For normal data the true share is under 5%. The general bound has to allow for every possible shape, and the normal shape is far more concentrated than the worst case.

Worked examples

The worked example as areas. The middle region, within two standard deviations of the mean, has area 0.9545. What remains is split equally between two tails, so the fraction of men taller than 190 cm is about 0.0228.
The worked example as areas. The middle region, within two standard deviations of the mean, has area 0.9545. What remains is split equally between two tails, so the fraction of men taller than 190 cm is about 0.0228.
Monocurl source
let fz = Frame(-4, 4, 0, 0.45, [0.1, 0.3, 0], 6.6, 2.9)
let MID = simpson(phi, -2, 2, 200)
let TAIL = simpson(phi, 2, 8, 200)
mesh grid = GridLines(fz, [-3, -2, -1, 0, 1, 2, 3], [0.1, 0.2, 0.3, 0.4])
mesh regions = [
    Under(fz, phi, -2, 2, tint(AZURE, 0.3)),
    Under(fz, phi, 2, 4, tint(ROSE, 0.8)),
    Under(fz, phi, -4, -2, tint(ROSE, 0.8))
]
mesh frame = [
    Seg(at(fz, -4, 0), at(fz, 4, 0), SLATE, 2.5),
    map([-3, -2, -1, 0, 1, 2, 3], |v| [
        Seg([fx(fz, v), fy(fz, 0) - 0.07, 0], [fx(fz, v), fy(fz, 0), 0], SLATE, 2),
        center{[fx(fz, v), fy(fz, 0) - 0.3, 0]} Math(to_string(v), 0.76, SLATE),
        center{[fx(fz, v), fy(fz, 0) - 0.68, 0]} Math(to_string(MU + SD * v), 0.72, SLATE)
    ]),
    center{[fx(fz, -4) - 0.05, fy(fz, 0) - 0.3, 0]} Math("z", 0.85, SLATE),
    center{[fx(fz, -4) - 0.05, fy(fz, 0) - 0.68, 0]} Note("cm", 0.72),
    map([0.1, 0.2, 0.3, 0.4], |v| center{[fx(fz, -4) - 0.3, fy(fz, v), 0]} Math(to_string(v, 1), 0.72, SLATE))
]
mesh curve = [
    Graph(fz, phi, -4, 4, AZURE, 4.5),
    Seg(at(fz, 2, 0), at(fz, 2, phi(2)), ROSE, 3),
    Seg(at(fz, -2, 0), at(fz, -2, phi(2)), ROSE, 3)
]
mesh labels = [
    center{at(fz, 0, 0.15)} Math(to_string(MID, 4), 1.15, AZURE),
    center{at(fz, 3.0, 0.2)} Cap("taller than 190", 0.76, ROSE),
    center{at(fz, 3.0, 0.13)} Math(to_string(TAIL, 4), 1.1, ROSE),
    Tip(at(fz, 2.9, 0.095), at(fz, 2.45, 0.02), ROSE, 2.5, 0.14),
    center{at(fz, -3.0, 0.2)} Cap("shorter than 162", 0.76, ROSE),
    center{at(fz, -3.0, 0.13)} Math(to_string(TAIL, 4), 1.1, ROSE),
    Tip(at(fz, -2.9, 0.095), at(fz, -2.45, 0.02), ROSE, 2.5, 0.14)
]

Uses the shared definitions for this lesson's figures.

When the rule does not apply

The 68-95-99.7 rule is a property of one particular shape. It is tempting to apply it to any data with a mean and a standard deviation, and it can fail badly.

Take the waiting time for a bus that arrives at random moments, five minutes apart on average. The standard model for such waits, the exponential distribution, is very different from a bell: short waits are the most common, and long ones trail off to the right. Its mean and its standard deviation are both 5 minutes.

A simulation of 20,000 waiting times with mean 5 minutes and standard deviation 5 minutes, and the normal density with the same mean and standard deviation (dashed). The band within one standard deviation of the mean holds about 86% of the waits, where the normal rule says 68%, and the normal curve puts 16% of its area on waits below zero.
A simulation of 20,000 waiting times with mean 5 minutes and standard deviation 5 minutes, and the normal density with the same mean and standard deviation (dashed). The band within one standard deviation of the mean holds about 86% of the waits, where the normal rule says 68%, and the normal curve puts 16% of its area on waits below zero.
Monocurl source
let NW = 20000
let fw = Frame(-8, 28, 0, 0.21, [0, 0.1, 0], 6.6, 2.9)
var counts = map(range(0, 28), |i| 0)
var inside = 0
for (i in range(1, NW + 1)) {
    let w = -5 * ln(max(rnd(i, 7), 0.000000001))
    if (w < 28) {
        counts[to_int(floor(w))] = counts[to_int(floor(w))] + 1
    }
    if (w <= 10) {
        inside = inside + 1
    }
}
let CW = counts
let INSIDE = inside / NW
let g = |x| bell(x, 5, 5)
mesh band = Region([at(fw, 0, 0), at(fw, 10, 0), at(fw, 10, 0.21), at(fw, 0, 0.21)], tint(LEAF, 0.16))
mesh frame = [
    GridLines(fw, [-5, 5, 10, 15, 20, 25], [0.05, 0.1, 0.15, 0.2]),
    Seg(at(fw, -8, 0), at(fw, 28, 0), SLATE, 2.5),
    Seg(at(fw, 0, 0), at(fw, 0, 0.21), SLATE, 2.5),
    map([-5, 0, 5, 10, 15, 20, 25], |v| center{[fx(fw, v), fy(fw, 0) - 0.3, 0]} Math(to_string(v), 0.72, SLATE)),
    map([0.05, 0.1, 0.15, 0.2], |v| center{[fx(fw, -8) - 0.4, fy(fw, v), 0]} Math(to_string(v, 2), 0.72, SLATE)),
    center{[fx(fw, 10), fy(fw, 0) - 0.68, 0]} Note("wait (minutes)", 0.74),
    center{[fx(fw, -8) + 1.3, fy(fw, 0.21) + 0.32, 0]} Note("fraction per minute", 0.74)
]
mesh bars = map(range(0, 28), |j| Box(at(fw, j + 0.5, CW[j] / NW / 2), [6.6 / 36 * 0.94, max(CW[j] / NW, 0.0008) * 2.9 / 0.21], AZURE, 0.4, 1.5))
mesh normal = stroke{ROSE, 4} dashed{[0.16, 0.1]} Polyline(map(sample(-8, 22, 120), |x| at(fw, x, g(x))))
mesh labels = [
    center{at(fw, 19.5, 0.176)} Cap("within one sd of the mean:", 0.74, LEAF),
    center{at(fw, 19.5, 0.142)} Cap("these waits", 0.78, AZURE),
    center{at(fw, 19.5, 0.119)} Math(str_join([to_string(100 * INSIDE, 1), "\%%"]), 1.1, AZURE),
    center{at(fw, 19.5, 0.088)} Cap("a normal curve", 0.78, ROSE),
    center{at(fw, 19.5, 0.063)} Math(str_join([to_string(100 * simpson(phi, -1, 1, 200), 1), "\%%"]), 1.1, ROSE)
]

Uses the shared definitions for this lesson's figures.

The normal model is wrong here in a way that needs no calculation: it assigns probability 0.16 to waits shorter than zero. Incomes, city sizes and anything else with a long tail on one side fail in the same way. Even symmetric data can fail, if extreme values are more common than the fast-falling bell allows; daily changes in stock prices are a well-known case, with far too many “five standard deviation” days.

Heights of adults of one sex, measurement errors, and averages of many small independent effects are well described by a normal distribution. Why that last category is so large is the subject of the next lesson. In every case the normal curve is a model, to be checked against a histogram before its percentages are trusted.

Stepping back

The hard step was the first one: giving up on the probability of a single value and assigning probability to ranges instead, as area under a density. After that, the normal distribution is one function, $\varphi$, together with two operations, shift by $\mu$ and stretch by $\sigma$. The z-score undoes those operations, and that is why a single short list of areas, 68, 95 and 99.7, serves for heights, exam scores and anything else that is normal. The same reduction will be used constantly from here on: convert to z-scores, then look up an area.

Check yourself

Bottles are filled with a mean of 500 ml and a standard deviation of 4 ml, and the amounts are normally distributed. About what fraction of bottles contain less than 492 ml?

Problems

The first three use the rule directly. The next three ask you to reason about densities. The last two go further.

Problem 1

Scores on a test are normal with mean 100 and standard deviation 15. Using the 68-95-99.7 rule, find the approximate fraction of scores (a) between 85 and 115, (b) above 130, (c) below 55.

Show solutionHide solution

(a) These are $z = -1$ and $z = 1$: about 68%. (b) $z = 2$. About 95% of scores are within two standard deviations, so about 5% are outside, half of them above: about 2.5% (more exactly, $0.0455/2 = 2.3\%$). (c) $z = -3$. About 0.27% are outside three standard deviations, half of them below: about 0.135%, or 1 in 740.

Problem 2

A machine fills bags with a mean of 1000 g and a standard deviation of 5 g, normally distributed. (a) What range contains the middle 95% of bags? (b) In a shipment of 2000 bags, about how many weigh more than 1015 g?

Show solutionHide solution

(a) Two standard deviations either side of the mean: 990 g to 1010 g. (b) 1015 g is $z = 3$. The fraction above is half of $1 - 0.9973 = 0.0027$, that is, 0.00135. Out of 2000 bags that is $2000 \times 0.00135 = 2.7$, so about 3 bags.

Problem 3

Compute the standard normal density $\varphi(z) = \frac{1}{\sqrt{2\pi}}e^{-z^2/2}$ at $z = 0$, $1$ and $2$, using $\sqrt{2\pi} \approx 2.5066$, $e^{-1/2} \approx 0.6065$ and $e^{-2} \approx 0.1353$. Then find the peak height of the normal density with $\sigma = 0.1$. Is anything wrong with a height above 1?

Show solutionHide solution

$\varphi(0) = 1/2.5066 \approx 0.3989$, $\varphi(1) = 0.6065/2.5066 \approx 0.2420$ and $\varphi(2) = 0.1353/2.5066 \approx 0.0540$. The peak of a normal density is $\frac{1}{\sigma}\varphi(0)$, which for $\sigma = 0.1$ is about 3.99. Nothing is wrong: the density is a rate. Almost all of the area sits within $3\sigma = 0.3$ of the mean, and a region 0.6 wide can have area 1 only if it is tall.

Problem 4

Let $f(x) = \frac{1}{10}$ for $0 \le x \le 10$ and $f(x) = 0$ otherwise. (a) Check that $f$ is a density. (b) Find $P(2 \le X \le 5)$ and $P(X = 3)$. (c) What fraction of the probability lies within one standard deviation of the mean, given that the standard deviation is $10/\sqrt{12} \approx 2.89$? Compare with 68%.

Show solutionHide solution

(a) $f \ge 0$ everywhere, and the region under it is a rectangle 10 wide and $\frac{1}{10}$ tall, with area 1. (b) The area over $[2, 5]$ is $3 \times \frac{1}{10} = 0.3$. A single point has width zero, so $P(X = 3) = 0$. (c) The mean is 5 by symmetry. The range $5 \pm 2.89$ has width 5.77 and lies inside $[0, 10]$, so its probability is $5.77 \times \frac{1}{10} \approx 0.58$. That is noticeably less than 68%: the flat density is not normal, and the rule does not apply to it.

Problem 5

The heights of men are normal with mean 176 and standard deviation 7. Without computing any new areas, find the height that is exceeded by about 16% of men, and the height below which about 0.135% of men fall.

Show solutionHide solution

The area within one standard deviation is 0.6827, so the two tails beyond hold 0.3173 together and 0.1587 each. About 16% of men are therefore above $z = 1$, which is $176 + 7 = 183$ cm. Similarly the tails beyond three standard deviations hold $0.0027/2 = 0.00135$ each, so 0.135% of men are below $z = -3$, which is $176 - 21 = 155$ cm.

Problem 6

Suppose the standard deviation of the heights were 3.5 cm, half its real value, with the mean still 176. (a) What happens to the peak height of the density? (b) What fraction of men would then be between 169 and 183 cm? (c) Explain why the answer to (b) had to go up.

Show solutionHide solution

(a) The peak is $\frac{1}{\sigma\sqrt{2\pi}}$, so halving $\sigma$ doubles it, from about 0.057 to about 0.114 per cm. (b) Now 169 and 183 are $z = \pm 2$, so the fraction is about 95.45%. (c) The total area is fixed at 1. A narrower bell piles more of that area near the mean, so any fixed range around the mean captures more of it. The same 14 cm range that was two standard deviations wide is now four.

Problem 7

Estimate the area under $\varphi$ from $z = 0$ to $z = 1$ by hand, using two trapezoids of width $\frac12$ and the values $\varphi(0) = 0.3989$, $\varphi(0.5) = 0.3521$, $\varphi(1) = 0.2420$. Double your answer and compare with 0.6827. Is your estimate too high or too low, and why?

Show solutionHide solution

A trapezoid of width $w$ with parallel sides $p$ and $q$ has area $w(p + q)/2$. The two trapezoids have areas $\frac12 \cdot \frac{0.3989 + 0.3521}{2} = 0.18775$ and $\frac12 \cdot \frac{0.3521 + 0.2420}{2} = 0.14853$, totalling $0.3363$. Doubling gives 0.6726, about 1.5% below 0.6827. The estimate is too low because between $z = -1$ and $z = 1$ the curve bulges above each chord joining two of its points, so every trapezoid lies under the curve. (The next problem shows that this bulging stops exactly at $z = \pm 1$.) More and thinner trapezoids close the gap, and that is how the areas above were computed.

Problem 8

(Requires derivatives.) Show that $\varphi'(z) = -z\,\varphi(z)$ and $\varphi''(z) = (z^2 - 1)\,\varphi(z)$. Conclude that the bell curve changes from bending downward to bending upward exactly at $z = \pm 1$, so that for a general normal density the standard deviation is the distance from the mean to the points where the bending changes.

Show solutionHide solution

By the chain rule, the derivative of $e^{-z^2/2}$ is $e^{-z^2/2}$ times the derivative of $-z^2/2$, which is $-z$. The constant $\frac{1}{\sqrt{2\pi}}$ carries through, so $\varphi'(z) = -z\varphi(z)$. Differentiating again with the product rule, $\varphi''(z) = -\varphi(z) - z\varphi'(z) = -\varphi(z) + z^2\varphi(z) = (z^2 - 1)\varphi(z)$. Since $\varphi(z) > 0$ always, the sign of $\varphi''$ is the sign of $z^2 - 1$: negative for $|z| < 1$, where the curve bends downward, and positive for $|z| > 1$, where it bends upward. The change happens at $z = \pm 1$, which for a normal density with parameters $\mu$ and $\sigma$ are the points $x = \mu \pm \sigma$. So $\sigma$ can be read off a drawing of the bell: it is the horizontal distance from the center to the point where the curve is steepest.

Shared definitions used by this lesson's figures
# house kit: prepended to every essay figure after the std imports

# palette. AZURE and EMBER are the two actors, LEAF marks an answer, ROSE a problem
let PAPER = [1, 1, 1, 1]
let INK = [0.08, 0.09, 0.11, 1]
let SLATE = [0.40, 0.44, 0.50, 1]
let MIST = [0.85, 0.86, 0.88, 1]
let AZURE = [0.13, 0.42, 0.68, 1]
let EMBER = [0.89, 0.42, 0.12, 1]
let LEAF = [0.13, 0.55, 0.36, 1]
let ROSE = [0.78, 0.22, 0.24, 1]
let PLUM = [0.47, 0.30, 0.66, 1]
let GOLD = [0.85, 0.62, 0.08, 1]

background = PAPER

# an opaque pastel of col; use for fills so overlaps do not darken
let tint = |col, amount = 0.18| [
    1 - (1 - col[0]) * amount,
    1 - (1 - col[1]) * amount,
    1 - (1 - col[2]) * amount,
    1
]

# text: Cap for labels, Note for quiet labels, Math for typeset maths
let Cap = |txt, size = 0.75, col = INK| color{col} Text(txt, size, "Charter")
let Note = |txt, size = 0.65| color{SLATE} Text(txt, size, "Charter")
let Math = |tex, size = 0.9, col = INK| color{col} Tex(tex, size)

# a paper rectangle to sit behind text that crosses lines
let Plate = |pos, size| center{pos} fill{PAPER} stroke{alpha{0} PAPER, 0} Rect(size)

# shapes with fill and stroke both set, so no stray black appears
let Pt = |pos, col = INK, r = 0.075| center{pos} fill{col} stroke{col, 1} Circle(r, 48)
let Ring = |pos, col = INK, r = 0.1, w = 3| center{pos} fill{PAPER} stroke{col, w} Circle(r, 48)
let Seg = |a, b, col = INK, w = 3| stroke{col, w} Line(a, b)
let Path = |pts, col = INK, w = 3| stroke{col, w} Polyline(pts)
let Region = |pts, col| fill{col} stroke{alpha{0} col, 0} Polygon(pts)
let Outline = |pts, col = INK, w = 3| fill{alpha{0} PAPER} stroke{col, w} Polygon(pts)
let Shape = |pts, col, amount = 0.18, w = 3| fill{tint(col, amount)} stroke{col, w} Polygon(pts)
let Disc = |pos, r, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Circle(r, 96)
let Box = |pos, size, col, amount = 0.18, w = 3| center{pos} fill{tint(col, amount)} stroke{col, w} Rect(size)
# a line from a to b ending in a small solid arrowhead
let Tip = |a, b, col = INK, w = 3, head = 0.17| {
    let d = normalize([b[0] - a[0], b[1] - a[1], 0])
    let n = [-d[1], d[0], 0]
    let base = [b[0] - d[0] * head, b[1] - d[1] * head, 0]
    return [
        stroke{col, w} Line(a, base),
        fill{col} stroke{col, 1} Polygon([
            b,
            [base[0] + n[0] * head * 0.42, base[1] + n[1] * head * 0.42, 0],
            [base[0] - n[0] * head * 0.42, base[1] - n[1] * head * 0.42, 0]
        ])
    ]
}

# plots. a frame is [x0, x1, y0, y1, centre, width, height]: a data window and the scene box it fills
let Frame = |x0, x1, y0, y1, c = [0, 0, 0], w = 6.4, h = 3.6| [x0, x1, y0, y1, c, w, h]
let fx = |fr, x| fr[4][0] + (x - (fr[0] + fr[1]) / 2) * fr[5] / (fr[1] - fr[0])
let fy = |fr, y| fr[4][1] + (y - (fr[2] + fr[3]) / 2) * fr[6] / (fr[3] - fr[2])
let at = |fr, x, y| [fx(fr, x), fy(fr, y), 0]

# axes through the data origin (or the nearest edge of the window), with optional names
let Axes = |fr, x_name = "", y_name = "", col = SLATE| {
    let ox = clamp(fr[0], 0, fr[1])
    let oy = clamp(fr[2], 0, fr[3])
    var parts = [
        Tip(at(fr, fr[0], oy), [fx(fr, fr[1]) + 0.25, fy(fr, oy), 0], col, 2.5, 0.15),
        Tip(at(fr, ox, fr[2]), [fx(fr, ox), fy(fr, fr[3]) + 0.25, 0], col, 2.5, 0.15)
    ]
    if (x_name != "") {
        parts .= center{[fx(fr, fr[1]) + 0.5, fy(fr, oy), 0]} Math(x_name, 0.8, col)
    }
    if (y_name != "") {
        parts .= center{[fx(fr, ox), fy(fr, fr[3]) + 0.5, 0]} Math(y_name, 0.8, col)
    }
    return parts
}

# tick marks with numbers at the listed data values
let XTicks = |fr, xs, col = SLATE, places = 0| block {
    let oy = fy(fr, clamp(fr[2], 0, fr[3]))
    for (x in xs) {
        . Seg([fx(fr, x), oy - 0.07, 0], [fx(fr, x), oy + 0.07, 0], col, 2)
        . center{[fx(fr, x), oy - 0.33, 0]} Math(to_string(x, places), 0.72, col)
    }
}
let YTicks = |fr, ys, col = SLATE, places = 0| block {
    let ox = fx(fr, clamp(fr[0], 0, fr[1]))
    for (y in ys) {
        . Seg([ox - 0.07, fy(fr, y), 0], [ox + 0.07, fy(fr, y), 0], col, 2)
        . center{[ox - 0.36, fy(fr, y), 0]} Math(to_string(y, places), 0.72, col)
    }
}

# faint grid lines at the listed data values
let GridLines = |fr, xs, ys, col = MIST| block {
    for (x in xs) {
        . Seg(at(fr, x, fr[2]), at(fr, x, fr[3]), col, 1.5)
    }
    for (y in ys) {
        . Seg(at(fr, fr[0], y), at(fr, fr[1], y), col, 1.5)
    }
}

# the graph of f over [a, b], drawn in the frame. values are clamped to the window
let Graph = |fr, f, a, b, col = AZURE, w = 4, n = 160|
    stroke{col, w} Polyline(map(sample(a, b, n), |x| at(fr, x, clamp(fr[2], f(x), fr[3]))))

# the region between the graph of f and the x axis over [a, b]
let AreaUnder = |fr, f, a, b, col = AZURE, amount = 0.2, n = 80| {
    var pts = [at(fr, a, 0)]
    for (x in sample(a, b, n)) {
        pts .= at(fr, x, clamp(fr[2], f(x), fr[3]))
    }
    pts .= at(fr, b, 0)
    return fill{tint(col, amount)} stroke{alpha{0} col, 0} Polygon(pts)
}

# a double sine hash: std seeded_random is too overdispersed for tail counts
let frac = |x| mod(mod(x, 1) + 1, 1)
let rnd = |i, salt| frac(sin(i * 39.3468 + salt * 11.135 + frac(sin(i * 12.9898 + salt * 78.233) * 43758.5453) * 6.2831853) * 24634.6345)
# heights of men in centimeters: the running example
let MU = 176
let SD = 7
let LO = 148
let HI = 204
let FINE = 0.5
let NFINE = 112
let phi = |z| exp(-z * z / 2) / sqrt(TAU)
let bell = |x, mu, sd| phi((x - mu) / sd) / sd
# Simpson's rule with n (even) slices
let simpson = |f, a, b, n| {
    let h = (b - a) / n
    var s = f(a) + f(b)
    for (i in range(1, n)) {
        s = s + f(a + i * h) * (2 + 2 * mod(i, 2))
    }
    return s * h / 3
}
# one simulated standard normal value (Box-Muller)
let std_normal = |i, salt| sqrt(-2 * ln(max(rnd(i, salt), 0.000000001))) * cos(TAU * rnd(i, salt + 1))
# n simulated heights, counted in bins half a centimeter wide from LO to HI
let height_counts = |n, salt| {
    var c = map(range(0, NFINE), |i| 0)
    for (i in range(1, n + 1)) {
        let b = to_int(floor((MU + SD * std_normal(i, salt) - LO) / FINE))
        if (b >= 0 and b < NFINE) {
            c[b] = c[b] + 1
        }
    }
    return c
}
# bars made of `group` fine bins each. per_cm = 1 divides each fraction by the bin width
let HistBars = |fr, counts, n, group, per_cm, col, first = 0, last = 1000| block {
    let w = group * FINE
    for (j in range(0, NFINE // group)) {
        var c = 0
        for (q in range(0, group)) {
            c = c + counts[j * group + q]
        }
        var h = c / n
        if (per_cm) {
            h = h / w
        }
        let x = LO + (j + 0.5) * w
        if (h * fr[6] / (fr[3] - fr[2]) > 0.01 and x > first and x < last) {
            . Box(at(fr, x, h / 2), [w * fr[5] / (fr[1] - fr[0]) * 0.94, h * fr[6] / (fr[3] - fr[2])], col, 0.4, 1.5)
        }
    }
}
let HeightAxes = |fr, ys, places, y_name| [
    GridLines(fr, [150, 160, 170, 180, 190, 200], ys),
    Axes(fr),
    map([150, 160, 170, 180, 190, 200], |v| [
        Seg([fx(fr, v), fy(fr, 0) - 0.07, 0], [fx(fr, v), fy(fr, 0) + 0.07, 0], SLATE, 2),
        center{[fx(fr, v), fy(fr, 0) - 0.32, 0]} Math(to_string(v), 0.72, SLATE)
    ]),
    YTicks(fr, ys, SLATE, places),
    center{[fx(fr, 176), fy(fr, 0) - 0.68, 0]} Note("height (cm)", 0.74),
    center{[fx(fr, fr[0]) + 1.35, fy(fr, fr[3]) + 0.42, 0]} Note(y_name, 0.74)
]
# the region under f between a and b
let Under = |fr, f, a, b, col| {
    let n = to_int(max(2, ceil((fx(fr, b) - fx(fr, a)) / 0.03)))
    var pts = [at(fr, a, 0)]
    for (x in sample(a, b, n)) {
        pts .= at(fr, x, f(x))
    }
    pts .= at(fr, b, 0)
    return fill{col} stroke{alpha{0} col, 0} Polygon(pts)
}