The Normal Distribution
The normal (Gaussian) distribution $X \sim \mathcal{N}(\mu, \sigma^2)$ has PDF:
$$f(x) = \frac{1}{\sigma\sqrt{2\pi}} \exp\!\left(-\frac{(x-\mu)^2}{2\sigma^2}\right), \quad x \in \mathbb{R}$$
The standard normal $Z = (X-\mu)/\sigma \sim \mathcal{N}(0, 1)$. The 68-95-99.7 rule: approximately 68%, 95%, and 99.7% of a normal distribution falls within 1, 2, and 3 standard deviations of the mean.
The normal distribution's dominance in statistics comes from the Central Limit Theorem: sums (and averages) of large numbers of independent, finite-variance random variables converge in distribution to normal, regardless of the original distribution. This is why measurement error, sample means, and many aggregate quantities are approximately normal.
from scipy.stats import norm
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-4, 4, 400)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# PDF
for mu, sig, label in [(0,1,'N(0,1)'), (0,2,'N(0,4)'), (2,1,'N(2,1)')]:
axes[0].plot(x*sig + mu*0 + mu, norm.pdf(x*sig + mu*0 + mu, mu, sig), label=label)
axes[0].set_title('Normal PDFs'); axes[0].legend()
# Q-Q plot check
rng = np.random.default_rng(0)
data = rng.normal(3, 1.5, 300)
from scipy.stats import probplot
probplot(data, dist='norm', plot=axes[1])
axes[1].set_title('Normal Q-Q plot')
plt.tight_layout()
The Q-Q Plot
A quantile-quantile (Q-Q) plot compares the quantiles of your sample to the theoretical quantiles of a reference distribution. If the sample follows the reference distribution, the points fall on a 45-degree line. Systematic S-curves indicate heavier tails; a bow shape indicates skewness.
The Exponential Distribution
The exponential distribution $X \sim \text{Exp}(\lambda)$ models the waiting time between events in a Poisson process:
$$f(x) = \lambda e^{-\lambda x}, \quad x \geq 0$$
Mean: $1/\lambda$. Variance: $1/\lambda^2$. Its defining property is memorylessness: $P(X > s + t \mid X > s) = P(X > t)$. The time remaining until the next event does not depend on how long you have already waited — which is realistic for certain failure modes but unrealistic for aging systems.
Typical use cases: service times in queueing models; time to first failure of electronic components; inter-arrival times in a Poisson process.
The Gamma and Weibull Distributions
The Gamma distribution generalizes the exponential. $X \sim \text{Gamma}(\alpha, \beta)$ is the sum of $\alpha$ independent $\text{Exp}(1/\beta)$ variables:
$$f(x) = \frac{x^{\alpha-1} e^{-x/\beta}}{\beta^\alpha \Gamma(\alpha)}, \quad x > 0$$
Mean: $\alpha\beta$. Variance: $\alpha\beta^2$. When $\alpha = 1$ it reduces to exponential; when $\alpha = n/2, \beta = 2$ it gives the chi-squared distribution with $n$ degrees of freedom, central to statistical testing.
The Weibull distribution models failure times with non-constant hazard rates:
$$f(x) = \frac{k}{\lambda}\left(\frac{x}{\lambda}\right)^{k-1} e^{-(x/\lambda)^k}, \quad x > 0$$
The shape parameter $k$ controls the hazard: $k < 1$ means decreasing hazard (infant mortality); $k = 1$ is exponential (constant hazard); $k > 1$ means increasing hazard (wear-out). It is the workhorse of reliability engineering.
Log-Normal and Heavy-Tailed Distributions
If $\ln X \sim \mathcal{N}(\mu, \sigma^2)$, then $X$ is log-normal. It arises naturally whenever a positive quantity is the product of many independent factors (by CLT on the log scale). Common examples: incomes, stock prices, particle sizes, file sizes, reaction times.
The Pareto and power-law distributions model truly heavy-tailed phenomena — where extreme values are far more common than a normal or exponential model predicts:
$$P(X > x) = \left(\frac{x_m}{x}\right)^\alpha, \quad x \geq x_m$$
If $\alpha \leq 2$ the variance is infinite; if $\alpha \leq 1$ even the mean is infinite. Web page requests, earthquake magnitudes, and city sizes follow power laws.
from scipy.stats import lognorm, expon, gamma, weibull_min
import numpy as np
import matplotlib.pyplot as plt
xg = np.linspace(0.01, 10, 500)
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(xg, expon.pdf(xg, scale=2), label='Exp(rate=0.5)')
ax.plot(xg, gamma.pdf(xg, a=2, scale=1), label='Gamma(2,1)')
ax.plot(xg, weibull_min.pdf(xg, c=2, scale=2), label='Weibull(k=2)')
ax.plot(xg, lognorm.pdf(xg, s=0.8, scale=np.exp(1)), label='LogNormal')
ax.set_xlabel('x'); ax.set_ylabel('f(x)')
ax.set_title('Positive continuous distributions')
ax.legend(); plt.tight_layout()
Choosing a Distribution: A Practical Guide
- Bounded integers: Binomial, Hypergeometric
- Unbounded counts: Poisson, Negative Binomial
- Symmetric, approximately normal: Normal
- Positive, right-skewed, moderate: Gamma, Log-normal
- Failure times, reliability: Weibull, Exponential
- Extreme values / fat tails: Pareto, Generalized Extreme Value
- Proportions / probabilities: Beta
Always start with domain knowledge (what process generated the data?) before fitting. Then use Q-Q plots and goodness-of-fit tests to validate.
Code Examples
Fitting distributions to data with scipy and comparing with Q-Q plots
Fit a gamma distribution to right-skewed data using MLE, compare to a normal fit, and evaluate both with Q-Q plots.
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
rng = np.random.default_rng(123)
data = rng.gamma(shape=3, scale=2, size=500) # true: Gamma(3, 2)
# Fit Gamma and Normal via MLE
alpha_hat, loc_hat, scale_hat = stats.gamma.fit(data, floc=0) # fix loc=0
mu_hat, sigma_hat = data.mean(), data.std(ddof=1)
print(f"Gamma MLE: shape={alpha_hat:.3f}, scale={scale_hat:.3f} (true: 3, 2)")
print(f"Normal MLE: mean={mu_hat:.3f}, std={sigma_hat:.3f}")
# Kolmogorov-Smirnov tests
ks_gamma = stats.kstest(data, 'gamma', args=(alpha_hat, loc_hat, scale_hat))
ks_norm = stats.kstest(data, 'norm', args=(mu_hat, sigma_hat))
print(f"\nKS test vs Gamma: stat={ks_gamma.statistic:.4f}, p={ks_gamma.pvalue:.4f}")
print(f"KS test vs Normal: stat={ks_norm.statistic:.4f}, p={ks_norm.pvalue:.4f}")
# Q-Q plots
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
stats.probplot(data, dist=stats.gamma(alpha_hat, loc_hat, scale_hat), plot=axes[0])
axes[0].set_title('Q-Q: data vs fitted Gamma')
stats.probplot(data, dist='norm', plot=axes[1])
axes[1].set_title('Q-Q: data vs Normal')
plt.tight_layout()
plt.savefig('qq_comparison.png', dpi=120)
OutputGamma MLE: shape=3.052, scale=1.981 (true: 3, 2)
Normal MLE: mean=6.046, std=3.477
KS test vs Gamma: stat=0.0198, p=0.9823
KS test vs Normal: stat=0.0721, p=0.0044
Distribution fitting in R with fitdistrplus
Fit gamma and lognormal distributions to positive data using fitdistrplus, compare AIC, and produce a descdist plot.
library(fitdistrplus)
set.seed(42)
x <- rgamma(500, shape=3, rate=0.5) # mean=6, var=12
# Descriptive plot (skewness-kurtosis plane)
descdist(x, discrete=FALSE, boot=100)
# Fit two candidate distributions
fit_gamma <- fitdist(x, "gamma", method="mle")
fit_lnorm <- fitdist(x, "lnorm", method="mle")
cat("=== Gamma fit ===\n")
print(summary(fit_gamma))
cat("\n=== Log-normal fit ===\n")
print(summary(fit_lnorm))
cat(sprintf("\nAIC Gamma: %.2f\n", fit_gamma$aic))
cat(sprintf("AIC Log-norm: %.2f\n", fit_lnorm$aic))
# Goodness-of-fit plots
par(mfrow=c(2,2))
plot.legend <- c("gamma", "lnorm")
denscomp(list(fit_gamma, fit_lnorm), legendtext=plot.legend)
qqcomp( list(fit_gamma, fit_lnorm), legendtext=plot.legend)
cdfcomp( list(fit_gamma, fit_lnorm), legendtext=plot.legend)
ppcomp( list(fit_gamma, fit_lnorm), legendtext=plot.legend)
par(mfrow=c(1,1))
Visualizing the Central Limit Theorem
Demonstrate that the sample mean of an exponential distribution converges to normal as sample size grows.
import numpy as np
from scipy.stats import norm, expon
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
fig, axes = plt.subplots(1, 4, figsize=(14, 4), sharey=False)
for ax, n in zip(axes, [1, 5, 30, 100]):
# Sample means of n exponential(1) variables, repeated 5000 times
means = rng.exponential(scale=1, size=(5000, n)).mean(axis=1)
ax.hist(means, bins=40, density=True, color='steelblue', alpha=0.7, edgecolor='white')
# Overlay theoretical normal
mu_clt = 1.0 # E[Exp(1)] = 1
sig_clt = 1.0 / np.sqrt(n) # std of sample mean
xg = np.linspace(means.min(), means.max(), 300)
ax.plot(xg, norm.pdf(xg, mu_clt, sig_clt), 'r-', lw=2)
ax.set_title(f'n = {n}')
ax.set_xlabel('Sample mean')
fig.suptitle('CLT: Sample mean of Exp(1) converges to Normal', y=1.02)
plt.tight_layout()
plt.savefig('clt_demo.png', dpi=120)
print("Saved clt_demo.png")
OutputSaved clt_demo.png
# Figure shows: n=1 is exponential shape, n=5 slightly skewed, n=30 closely normal, n=100 nearly perfect bell curve overlaid by red normal PDF.