Dictionary of Applied Machine Learning · probability distribution

probability distribution — Python demo

Numerical companion to the entry probability distribution: it recomputes what the entry states and prints one line per check

One block per paragraph of the entry (marked [P...]): each block verifies numerically what the corresponding statement asserts. Self-contained (numpy/matplotlib only), fixed seed.

Run it without installing anything:
uv run https://dictionaryofml.org/terms/probdist.py
uv downloads this script and the pinned NumPy and Matplotlib it needs, then runs it; the script fetches any input file it uses. To keep the output files, download probdist.py into a folder and run uv run probdist.py there. With NumPy and Matplotlib already installed, python3 probdist.py, from any directory — it writes its output files into the current directory. Fixed seeds, so the printed numbers reproduce exactly. Download probdist.py · Notebook · Open in Colab

The script, block by block

One cell per block of the script: the code, and what that code printed when it last ran here

setup

"""
probdist.py — numerical companion to the glossary entry
'probability distribution'.

One block per paragraph of the entry (marked [P...]): each block verifies
numerically what the corresponding statement asserts. Self-contained
(numpy/matplotlib only), fixed seed.

The method used throughout the first blocks is the simplest one
available: it reads a data set of numbers and delivers their average.
That keeps every quantity below available in closed form, so a check
compares a measured number against a formula rather than against
another simulation.

Blocks
------
[P-method]  One distribution, many data sets, a different output each
            time: the outputs of the method are realizations of another
            RV, whose spread shrinks as sigma/sqrt(m) with the data set
            size m. The distribution of that output RV depends on the
            common distribution P and on the method: a different method
            (taking the middle value of the sorted data set) run on the
            same data sets has a larger output spread.
[P-typical] The distribution decides which data points are typical
            (relative frequencies of a growing sample converge to it),
            and it can be estimated from a data set: the empirical
            frequencies computed from one large data set recover the
            distribution that generated it.
[P-specify] How a distribution is specified: a binary RV by the single
            probability P(y = 0), and a continuous real-valued RV by a
            pdf p, for which P(x in [a, b]) ~ p(a)|b - a| on a short
            interval.
[P-picture] The distribution visualized: the joint pdf of a data point
            (feature, label) as a grayscale over the plane, a
            two-component Gaussian mixture. Paralleling the entry's
            Fig. 1, three data sets of realizations of iid RVs with
            this distribution are drawn, and three hypothesis maps are
            learned from them by the same polynomial regression method.
            The checks verify that the plotted window carries the
            probability mass, that the data sets concentrate where the
            pdf is large, that the three learned maps differ, and that
            they differ most where the pdf is small.

Outputs
-------
probdist.png             : preview figure (checking only).
probdist_density.csv     : joint pdf on a grid (columns x, y, p; row-wise
                           in y).
probdist_train1.csv, probdist_train2.csv, probdist_train3.csv
                         : the three data sets (columns x, y).
probdist_hypotheses.csv  : the three learned hypothesis curves
                           (columns x, yhat1, yhat2, yhat3).

Data generated by pythondemos/probdist.py.
"""

import numpy as np
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path

OUT_DIR = Path(__file__).parent

rng = np.random.default_rng(42)
report = []

MU, SIGMA = 1.0, 2.0            # the common distribution: N(MU, SIGMA^2)
NR_DATASETS = 4000


def check(name, ok):
    report.append((name, bool(ok)))
    print(f"  [{'ok' if ok else 'FAIL'}] {name}")


def method(data):
    """The ML method: it delivers the average of the data set."""
    return float(np.mean(data))

P-method

One distribution, many data sets, a different output each time: the outputs of the method are realizations of another RV, whose spread shrinks as sigma/sqrt(m) with the data set size m. The distribution of that output RV depends on the common distribution P and on the method: a different method (taking the middle value of the sorted data set) run on the same data sets has a larger output spread.

print("[P-method] one distribution, many data sets, a different output each time")
outputs = {}
for m in (25, 400):
    outputs[m] = np.array([method(rng.normal(MU, SIGMA, m))
                           for _ in range(NR_DATASETS)])
spread = {m: float(np.std(out)) for m, out in outputs.items()}
print(f"    output spread: {spread[25]:.4f} at m=25, {spread[400]:.4f} at m=400"
      f"  (sigma/sqrt(m) = {SIGMA / np.sqrt(25):.4f}, "
      f"{SIGMA / np.sqrt(400):.4f})")
check("two data sets from the same distribution give different outputs",
      outputs[25][0] != outputs[25][1])
check("the spread of the outputs shrinks with the data set size",
      spread[400] < spread[25])
for m in (25, 400):
    check(f"the spread matches sigma/sqrt(m) within 5% at m={m}",
          abs(spread[m] - SIGMA / np.sqrt(m)) / (SIGMA / np.sqrt(m)) < 0.05)
# the distribution of the output RV depends on P and on the method: the
# middle value of a sorted Gaussian data set has a larger spread than
# the average
med_outputs = np.array([float(np.median(rng.normal(MU, SIGMA, 25)))
                        for _ in range(NR_DATASETS)])
print(f"    output spread at m=25: {spread[25]:.4f} (average), "
      f"{float(np.std(med_outputs)):.4f} (middle value)")
check("a different method (the middle value) has a different output "
      "distribution: its spread is larger",
      float(np.std(med_outputs)) > 1.1 * spread[25])
[P-method] one distribution, many data sets, a different output each time
    output spread: 0.4034 at m=25, 0.0979 at m=400  (sigma/sqrt(m) = 0.4000, 0.1000)
  [ok] two data sets from the same distribution give different outputs
  [ok] the spread of the outputs shrinks with the data set size
  [ok] the spread matches sigma/sqrt(m) within 5% at m=25
  [ok] the spread matches sigma/sqrt(m) within 5% at m=400
    output spread at m=25: 0.4034 (average), 0.4911 (middle value)
  [ok] a different method (the middle value) has a different output distribution: its spread is larger

P-typical

The distribution decides which data points are typical (relative frequencies of a growing sample converge to it), and it can be estimated from a data set: the empirical frequencies computed from one large data set recover the distribution that generated it.

print("[P-typical] typical data points; the distribution estimated from a data set")
p_true = np.array([0.5, 0.3, 0.2])              # distribution on {0, 1, 2}
errs = []
for m in (10**2, 10**4, 10**6):
    draws = rng.choice(3, size=m, p=p_true)
    errs.append(np.max(np.abs(np.bincount(draws, minlength=3) / m - p_true)))
print(f"    max |frequency - p| for m=1e2,1e4,1e6: "
      f"{errs[0]:.4f}, {errs[1]:.4f}, {errs[2]:.4f}")
check("relative frequencies converge to the distribution", errs[0] > errs[2])
check("the empirical frequencies of one large data set estimate the "
      "distribution that generated it (within 2e-3)", errs[2] < 2e-3)
[P-typical] typical data points; the distribution estimated from a data set
    max |frequency - p| for m=1e2,1e4,1e6: 0.0300, 0.0110, 0.0005
  [ok] relative frequencies converge to the distribution
  [ok] the empirical frequencies of one large data set estimate the distribution that generated it (within 2e-3)

P-specify

How a distribution is specified: a binary RV by the single probability P(y = 0), and a continuous real-valued RV by a pdf p, for which P(x in [a, b]) ~ p(a)|b - a| on a short interval.

print("[P-specify] one probability specifies a binary RV; a pdf a continuous one")
p0 = 0.73
y = (rng.uniform(size=10**6) >= p0).astype(int)   # P(y = 0) = p0
f0 = float(np.mean(y == 0))
check("empirical P(y = 0) recovers p0 = 0.73", abs(f0 - p0) < 2e-3)
check("P(y = 1) = 1 - P(y = 0)", np.isclose(np.mean(y == 1), 1 - f0))

pdf = lambda t: np.exp(-t ** 2 / 2) / np.sqrt(2 * np.pi)
x = rng.standard_normal(10**7)
a = 0.5
rel_errs = []
for width in (0.5, 0.1, 0.02):
    p_emp = np.mean((x >= a) & (x <= a + width))
    rel_errs.append(abs(p_emp - pdf(a) * width) / p_emp)
print(f"    relative approximation error for |b-a|=0.5,0.1,0.02: "
      f"{rel_errs[0]:.3f}, {rel_errs[1]:.3f}, {rel_errs[2]:.3f}")
check("the pdf approximation improves as the interval shrinks",
      rel_errs[0] > rel_errs[1] > rel_errs[2])
check("relative error below 1% for |b - a| = 0.02", rel_errs[2] < 0.01)
[P-specify] one probability specifies a binary RV; a pdf a continuous one
  [ok] empirical P(y = 0) recovers p0 = 0.73
  [ok] P(y = 1) = 1 - P(y = 0)
    relative approximation error for |b-a|=0.5,0.1,0.02: 0.174, 0.027, 0.007
  [ok] the pdf approximation improves as the interval shrinks
  [ok] relative error below 1% for |b - a| = 0.02

P-picture

The distribution visualized: the joint pdf of a data point (feature, label) as a grayscale over the plane, a two-component Gaussian mixture. Paralleling the entry's Fig. 1, three data sets of realizations of iid RVs with this distribution are drawn, and three hypothesis maps are learned from them by the same polynomial regression method. The checks verify that the plotted window carries the probability mass, that the data sets concentrate where the pdf is large, that the three learned maps differ, and that they differ most where the pdf is small.

print("[P-picture] grayscale pdf; three data sets, three learned maps")
# The joint pdf of a data point z = (x, y): the feature x follows a
# two-component Gaussian mixture, and the label y is the value of a fixed
# curve at x plus Gaussian noise.
MIX_W = np.array([0.5, 0.5])
MIX_MU = np.array([-2.0, 2.0])
MIX_S = np.array([0.7, 0.9])
NOISE = 0.35
M_TRAIN, NR_SETS = 40, 3


def gauss_pdf(t, mu, s):
    return np.exp(-((t - mu) ** 2) / (2 * s ** 2)) / (s * np.sqrt(2 * np.pi))


def pdf_feature(t):
    return sum(w * gauss_pdf(t, mu, s) for w, mu, s in zip(MIX_W, MIX_MU, MIX_S))


def pdf_joint(t, u):
    return pdf_feature(t) * gauss_pdf(u, np.tanh(t), NOISE)


def draw_points(m):
    comp = rng.choice(2, size=m, p=MIX_W)
    xx = rng.normal(MIX_MU[comp], MIX_S[comp])
    yy = np.tanh(xx) + NOISE * rng.standard_normal(m)
    return xx, yy


# three data sets of iid draws, and the map the same polynomial regression
# method learns from each (the degree-3 polynomial minimizing the average
# squared deviation on the data set, available in closed form)
x_curve = np.linspace(-4.4, 4.4, 200)
sets, curves = [], []
for _ in range(NR_SETS):
    x_tr, y_tr = draw_points(M_TRAIN)
    design = np.vander(x_tr, 4)
    w_hat = np.linalg.solve(design.T @ design, design.T @ y_tr)
    sets.append((x_tr, y_tr))
    curves.append(np.polyval(w_hat, x_curve))
curves = np.array(curves)

xs = np.linspace(-4.4, 4.4, 61)
ys = np.linspace(-1.9, 1.9, 41)
grid_x, grid_y = np.meshgrid(xs, ys)
grid_p = pdf_joint(grid_x, grid_y)

mass = float(grid_p.sum() * (xs[1] - xs[0]) * (ys[1] - ys[0]))
print(f"    probability mass inside the plotted window: {mass:.4f}")
check("the plotted window carries the probability mass (within 2%)",
      abs(mass - 1.0) < 0.02)

for i, (x_tr, y_tr) in enumerate(sets, 1):
    check(f"data set {i} concentrates where the pdf is large",
          float(pdf_joint(x_tr, y_tr).mean()) > float(grid_p.mean()))

gap01 = float(np.max(np.abs(curves[0] - curves[1])))
check("the three learned maps differ", gap01 > 0.05)
spread_curve = curves.std(axis=0)                  # pointwise over the maps
dens = pdf_feature(x_curve)
low, high = dens < np.median(dens), dens >= np.median(dens)
print(f"    map spread: {spread_curve[low].mean():.3f} where the pdf is "
      f"small, {spread_curve[high].mean():.3f} where it is large")
check("the maps differ most where the pdf is small",
      spread_curve[low].mean() > spread_curve[high].mean())

np.savetxt(OUT_DIR / "probdist_density.csv",
           np.column_stack([grid_x.ravel(), grid_y.ravel(), grid_p.ravel()]),
           delimiter=",", header="x,y,p", comments="", fmt="%.6f")
for i, (x_tr, y_tr) in enumerate(sets, 1):
    np.savetxt(OUT_DIR / f"probdist_train{i}.csv",
               np.column_stack([x_tr, y_tr]),
               delimiter=",", header="x,y", comments="", fmt="%.6f")
np.savetxt(OUT_DIR / "probdist_hypotheses.csv",
           np.column_stack([x_curve, curves[0], curves[1], curves[2]]),
           delimiter=",", header="x,yhat1,yhat2,yhat3", comments="",
           fmt="%.6f")

# ------------------------------------------------------------ preview
fig, ax = plt.subplots(1, 3, figsize=(12.6, 3.0))
for m, style in ((25, "--"), (400, "-")):
    ax[0].hist(outputs[m], bins=60, histtype="step", density=True,
               color="k", linestyle=style, label=f"m = {m}")
ax[0].set_xlabel("output of the method (the average)")
ax[0].set_ylabel("density over data sets")
ax[0].set_title("outputs of one method over 4000 data sets")
ax[0].legend(frameon=False)

t = np.linspace(-4, 4, 400)
ax[1].plot(t, pdf(t), "k-")
ax[1].fill_between(t, pdf(t), where=(t >= a) & (t <= a + 0.5),
                   facecolor="none", hatch="///", edgecolor="k")
ax[1].set_xlabel("value of the RV")
ax[1].set_ylabel("probability density p")
ax[1].set_title("P(x in [a, b]) is the shaded area")

ax[2].pcolormesh(xs, ys, grid_p, cmap="Greys", shading="nearest",
                 vmin=0.0, vmax=float(grid_p.max()) * 1.3)
for i, (ls, mk) in enumerate(zip(("-", "--", ":"), ("o", "s", "^"))):
    ax[2].plot(x_curve, curves[i], "k", linestyle=ls,
               label=f"learned map {i + 1}")
    ax[2].scatter(sets[i][0], sets[i][1], s=18, marker=mk,
                  facecolors=("black", "white", "0.5")[i],
                  edgecolors=("white", "black", "black")[i],
                  linewidths=0.4, label=f"data set {i + 1}", zorder=3)
ax[2].set_xlabel("feature x")
ax[2].set_ylabel("label y")
ax[2].set_title("pdf (grayscale), three data sets, three learned maps")
ax[2].legend(frameon=False, loc="upper left", fontsize=7)
fig.tight_layout()
fig.savefig(OUT_DIR / "probdist.png", dpi=110)

print(f"\n{sum(ok for _, ok in report)}/{len(report)} checks passed")
assert all(ok for _, ok in report)
[P-picture] grayscale pdf; three data sets, three learned maps
    probability mass inside the plotted window: 0.9963
  [ok] the plotted window carries the probability mass (within 2%)
  [ok] data set 1 concentrates where the pdf is large
  [ok] data set 2 concentrates where the pdf is large
  [ok] data set 3 concentrates where the pdf is large
  [ok] the three learned maps differ
    map spread: 0.105 where the pdf is small, 0.059 where it is large
  [ok] the maps differ most where the pdf is small

17/17 checks passed
Preview figure produced by probdist.py
The preview figure the block P-picture writes when the script runs