Intermediate22 min

Distributions and plotting

Generate and inspect data distributions, then chart them with Matplotlib — plots render right here in the browser.

Why this matters in AI / ML / GenAI

Weight initialisation, dropout, and train/test splits all draw from distributions. Plotting a histogram of your features or a scatter of predictions against truth catches problems no summary statistic will reveal.

Common distributions

np.random.default_rng(seed) creates a generator. Use it rather than the older np.random.seed global.

  • Uniform — every value in a range equally likely. rng.uniform(low, high, size).
  • Normal (Gaussian) — the bell curve, defined by mean and standard deviation. rng.normal(loc, scale, size). Roughly 68% of values fall within one standard deviation and 95% within two.
  • Integersrng.integers(low, high, size) for synthetic labels.
  • Choicerng.choice(options, size, p=probabilities) for weighted sampling, which is how you simulate class imbalance.

Neural network weights are initialised from scaled normal or uniform distributions; that scaling is what keeps activations from exploding or vanishing in deep networks.

Plotting with Matplotlib

The convention is import matplotlib.pyplot as plt.

Prefer the object-oriented style: fig, ax = plt.subplots() then ax.plot(...). It scales to multiple panels, unlike the stateful plt.plot shortcut.

Four charts cover most ML work:

  • ax.hist(values, bins=30) — distribution shape, skew, and outliers
  • ax.scatter(x, y) — relationship between two variables, or predicted vs actual
  • ax.plot(steps, losses) — training curves over time
  • ax.bar(labels, counts) — class distribution

Always set ax.set_title, ax.set_xlabel, and ax.set_ylabel. An unlabelled chart is unreadable a week later, and a reviewer cannot check your claim against it.

The compiler on this page renders figures below the output, so you can run every example and see the result immediately.

Reading what you plot

A histogram tells you the shape: symmetric, skewed, or bimodal. Bimodal usually means two populations are mixed together and should be modelled separately.

A predicted-vs-actual scatter should hug the diagonal. Systematic curvature means the model is underfitting the relationship.

A training curve where training loss keeps falling while validation loss rises is overfitting, and it is the single most useful plot in machine learning.

Plot the distribution of your features before and after scaling. Confirming the transform did what you expected takes ten seconds and saves hours.

Copy-paste examples

Copy into your own editor, or load one into the compiler below and press Run.

Sampling from distributions

Check the empirical mean and std against what you asked for.

import numpy as np

rng = np.random.default_rng(42)

uniform = rng.uniform(0, 1, 1000)
normal = rng.normal(loc=100, scale=15, size=1000)
labels = rng.choice(["positive", "negative", "neutral"], size=1000, p=[0.7, 0.25, 0.05])

print(f"uniform  mean={uniform.mean():.4f}  min={uniform.min():.4f}  max={uniform.max():.4f}")
print(f"normal   mean={normal.mean():.2f}   std={normal.std():.2f}")

within_1_sd = np.mean(np.abs(normal - normal.mean()) < normal.std())
within_2_sd = np.mean(np.abs(normal - normal.mean()) < 2 * normal.std())
print(f"within 1 sd: {within_1_sd:.1%} (expect ~68%)")
print(f"within 2 sd: {within_2_sd:.1%} (expect ~95%)")

unique, counts = np.unique(labels, return_counts=True)
print("\nlabel distribution:", dict(zip(unique.tolist(), counts.tolist())))

Histogram — the chart renders below the output

First run downloads Matplotlib, so allow it a few extra seconds.

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
latencies = np.concatenate([
    rng.normal(200, 30, 900),
    rng.normal(1200, 200, 100),
])

fig, ax = plt.subplots(figsize=(7, 3.5))
ax.hist(latencies, bins=40, color="#1d4ed8", edgecolor="white")
ax.axvline(np.median(latencies), color="#ea580c", linewidth=2, label="median")
ax.axvline(np.percentile(latencies, 95), color="#dc2626", linewidth=2, linestyle="--", label="p95")
ax.set_title("Request latency distribution (n=1000)")
ax.set_xlabel("Latency (ms)")
ax.set_ylabel("Number of requests")
ax.legend()

print("bimodal — two populations are mixed here")
print("median:", round(float(np.median(latencies)), 1), "ms")
print("p95   :", round(float(np.percentile(latencies, 95)), 1), "ms")

Training curve showing overfitting

The gap opening between the two lines is the thing to look for.

import matplotlib.pyplot as plt
import numpy as np

epochs = np.arange(1, 21)
train_loss = 1.2 * np.exp(-0.25 * epochs) + 0.05
val_loss = 1.2 * np.exp(-0.25 * epochs) + 0.05 + 0.012 * np.clip(epochs - 8, 0, None) ** 1.5

fig, ax = plt.subplots(figsize=(7, 3.5))
ax.plot(epochs, train_loss, marker="o", color="#1d4ed8", label="training loss")
ax.plot(epochs, val_loss, marker="s", color="#ea580c", label="validation loss")
best = int(np.argmin(val_loss)) + 1
ax.axvline(best, color="#64748b", linestyle=":", label=f"best epoch = {best}")
ax.set_title("Training vs validation loss")
ax.set_xlabel("Epoch")
ax.set_ylabel("Loss (cross-entropy)")
ax.legend()

print("stop training at epoch", best, "- after that the model is memorising")

Scatter plot: predicted vs actual

Points should hug the diagonal. Curvature means underfitting.

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(7)
actual = rng.uniform(0, 100, 120)
predicted = actual * 0.92 + rng.normal(0, 6, 120) + 3

fig, ax = plt.subplots(figsize=(5.5, 5))
ax.scatter(actual, predicted, alpha=0.6, color="#1d4ed8", edgecolor="white")
lims = [0, 105]
ax.plot(lims, lims, color="#dc2626", linestyle="--", label="perfect prediction")
ax.set_xlim(lims)
ax.set_ylim(lims)
ax.set_title("Predicted vs actual values")
ax.set_xlabel("Actual value")
ax.set_ylabel("Predicted value")
ax.legend()

residuals = predicted - actual
print("mean absolute error:", round(float(np.abs(residuals).mean()), 2))
print("bias (mean residual):", round(float(residuals.mean()), 2))

Bar chart of class distribution

Class imbalance is obvious in a bar chart and easy to miss in a table.

import matplotlib.pyplot as plt

labels = ["positive", "negative", "neutral"]
counts = [700, 250, 50]

fig, ax = plt.subplots(figsize=(6, 3.2))
bars = ax.bar(labels, counts, color=["#1d4ed8", "#ea580c", "#64748b"])
for bar, count in zip(bars, counts):
    ax.text(bar.get_x() + bar.get_width() / 2, count + 12, str(count), ha="center", fontweight="bold")
ax.set_title("Training label distribution")
ax.set_xlabel("Class")
ax.set_ylabel("Number of examples")

print("imbalance ratio:", round(max(counts) / min(counts), 1), ": 1")
print("a model predicting only 'positive' scores", f"{max(counts) / sum(counts):.0%}", "accuracy")

Compare a feature before and after scaling

Try it — in-browser Python

Packages: numpy, matplotlib

Change the mean and scale of the raw feature, then re-run to see both panels shift.

Output

Python runs in your browser. First run downloads the runtime.

Press Run (or Ctrl+Enter) to execute.

CPython in WebAssembly. Stdlib works. NumPy, pandas, scikit-learn and Matplotlib load on demand, and charts render below. No input(), no GPU, no network installs.

Takeaways

  • Seed with np.random.default_rng(seed) and check empirical mean and std against expectations.
  • Use fig, ax = plt.subplots() and always label the title and both axes.
  • Histogram for shape, scatter for predicted vs actual, line for training curves.