3 Real-World Distributions That Refuse to Follow the Bell Curve

3 Real-World Distributions That Refuse to Follow the Bell Curve
Image Author
 

The normal distribution is not a neutral default. It claims that extreme events are vanishingly rare, that the mean reliably represents the typical value, and that variance is finite and stable. In exchange, you get elegant math and a century of statistical tools. The problem is that a lot of real data breaks all three of those claims simultaneously, and the tools built on normality quietly fail when that happens.

Three distributions come up again and again in real-world data and are consistently misidentified as approximately normal: the Pareto, the log-normal, and the Cauchy. Each breaks the normal assumption differently. The Pareto has a tail so heavy that the mean may not even exist. The log-normal looks approximately normal until you notice that the mean is higher than what almost everyone in the dataset earns. The Cauchy has no mean at all — not a large one, an undefined one. Understanding where these three appear and what happens when you mistake them for normal is the point of this article.

Distribution 1: The Pareto Distribution

The Pareto distribution, named after the Italian economist Vilfredo Pareto, is a power-law distribution originally used to describe wealth concentration. The 80/20 rule is its most famous expression, but the underlying math is more general and often more extreme than that framing suggests. It is heavy-tailed because its probability density function decreases at a power rate rather than an exponential rate — exactly what makes it so different from the normal distribution and so dangerous to misidentify.

Where It Shows Up

The Pareto distribution follows the Power Law, where small events are common but extreme events have an outsized impact. It describes wealth distribution, website traffic, software bug frequency, earthquake magnitudes, city population sizes, word frequencies (Zipf’s law), and insurance losses where a single catastrophic claim can dwarf all smaller ones combined.

Why It Is Dangerous

The mean is only defined when the shape parameter alpha is greater than 1, and the variance only when alpha is greater than 2. For heavy-tailed Pareto data, the sample mean is not a reliable summary — it is dominated by rare extreme events and shifts dramatically whenever a new large observation enters the dataset.

A risk model built on normal assumptions will systematically underprice tail risk when the underlying loss distribution is Pareto. The normal distribution assigns near-zero probability to events that are genuinely possible and financially ruinous under a power law.

How to Recognize It

Plot the survival function (1 minus the CDF) on logarithmic axes. A linear trend on a log-log scale indicates Pareto behavior, and the slope estimates the shape parameter alpha. If the data is truly Pareto, that plot will be straight. If it is normal, it will curve sharply downward.

Python Code

 
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

np.random.seed(42)
n = 2000

# --- Generate samples from Pareto and Normal distributions ---
# Pareto shape parameter b = 1.5 (alpha = 1.5)
# Mean is defined (alpha > 1) but variance is not (alpha <= 2)
pareto_samples = stats.pareto.rvs(b=1.5, scale=1, size=n)

# Normal samples with the same sample mean and std for fair comparison
normal_samples = stats.norm.rvs(
    loc=pareto_samples.mean(),
    scale=pareto_samples.std(),
    size=n
)

# --- Plot 1: Histogram comparison ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Clip Pareto for histogram visibility -- extreme tail is the point
axes[0].hist(pareto_samples[pareto_samples < 20], bins=80, density=True, color="steelblue", alpha=0.7, label="Pareto (b=1.5)") x_range = np.linspace(0, 20, 300) axes[0].plot(x_range, stats.norm.pdf(x_range, loc=pareto_samples.mean(), scale=pareto_samples.std()), color="firebrick", linewidth=2, label="Normal (same mean/std)") axes[0].set_title("Pareto vs Normal: Histogram\n(Pareto tail continues far to the right)") axes[0].set_xlabel("Value") axes[0].set_ylabel("Density") axes[0].legend() axes[0].set_xlim(0, 20) # --- Plot 2: Log-log survival function (fingerprint of power law) --- # Sort samples and compute empirical survival function pareto_sorted = np.sort(pareto_samples) pareto_survival = 1 - np.arange(1, n + 1) / n normal_sorted = np.sort(np.abs(normal_samples)) # Abs for positive range normal_survival = 1 - np.arange(1, n + 1) / n axes[1].loglog(pareto_sorted, pareto_survival, color="steelblue", linewidth=1.5, label="Pareto") axes[1].loglog(normal_sorted, normal_survival, color="firebrick", linewidth=1.5, linestyle="--", label="Normal") axes[1].set_title("Log-Log Survival Function\n(Straight line = power law fingerprint)") axes[1].set_xlabel("Value (log scale)") axes[1].set_ylabel("P(X > x) (log scale)")
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("pareto_vs_normal.png", dpi=150)
plt.show()

# --- Show how the sample mean is unstable under Pareto ---
# Each new large observation shifts the mean significantly
running_mean_pareto = np.cumsum(pareto_samples) / np.arange(1, n + 1)
running_mean_normal = np.cumsum(normal_samples) / np.arange(1, n + 1)

plt.figure(figsize=(10, 4))
plt.plot(running_mean_pareto, color="steelblue", linewidth=1,
         label="Pareto running mean")
plt.plot(running_mean_normal, color="firebrick", linewidth=1,
         linestyle="--", label="Normal running mean")
plt.title("Running Sample Mean: Pareto vs Normal\n"
          "(Pareto mean jumps with each new extreme value)")
plt.xlabel("Number of observations")
plt.ylabel("Cumulative mean")
plt.legend()
plt.tight_layout()
plt.savefig("pareto_running_mean.png", dpi=150)
plt.show()

# Print summary statistics to show the mean/median gap
print("=== Pareto Distribution Statistics ===")
print(f"Sample mean:   {pareto_samples.mean():.2f}")
print(f"Sample median: {np.median(pareto_samples):.2f}")
print(f"Max value:     {pareto_samples.max():.2f}")
print(f"\nThe median is {np.median(pareto_samples):.2f} but the mean is "
      f"{pareto_samples.mean():.2f} -- pulled up by rare extreme values")

The Pareto Distribution
What this code does: Three things run here:

  • First, a histogram of Pareto samples is plotted with a normal curve overlaid using the same mean and standard deviation — the visual gap between the Pareto’s heavy right tail and the normal curve’s rapid drop-off makes the difference concrete
  • Second, the log-log survival function plot produces the power law’s fingerprint: the Pareto line is straight on log-log axes, while the normal curve falls away sharply
  • Third, the running mean comparison shows the Pareto mean jumping with each new extreme observation, while the normal mean stabilizes quickly. The print output shows the mean/median gap — the median reflects what most observations are near, but the mean is inflated far above it by a few large values

Distribution 2: The Log-Normal Distribution

A variable is log-normally distributed when its logarithm follows a normal distribution. The log-normal arises when the underlying process is multiplicative — when percentage changes compound, as they do in stock prices, population growth, and biological growth. This is the core distinction from the normal distribution, which arises from the sum of many small independent effects. The log-normal arises from their product. Any time a quantity grows by a percentage at each step rather than by a fixed amount, log-normal is the natural model.

Where It Shows Up

The log-normal distribution appears in finance for modelling stock prices and option pricing (Black-Scholes assumes log-normal returns), in biology for sizes of living organisms and incubation periods, in economics for income and wealth distribution, and in engineering for fatigue life of materials. In income distributions, most people earn below the average — a direct consequence of the right skew, where the median is always less than the mean.

Why It Is Dangerous

The danger is subtler than with the Pareto. The mean is defined and finite, and the data looks vaguely bell-shaped — just a bit skewed. That “just a bit” is where models go wrong.

In a log-normal distribution, the mean is always greater than the median. A policy calibrated to mean income will be calibrated to a number higher than what most people actually earn, because the right tail of high earners pulls the mean upward. Models such as linear regression and ANOVA perform poorly on log-normal data unless the outcome variable is log-transformed first; applying them to raw data produces biased coefficients and unreliable standard errors.

How to Recognize It

Log-transform the data and check whether the result looks normal. A Q-Q plot of the log-transformed values against a theoretical normal distribution should follow the diagonal reference line closely. A histogram of the original data showing a right skew with a long upper tail is a softer signal — log-normal is one of several right-skewed distributions, so the formal Q-Q check after log transformation is the more reliable test.

Python Code

 
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

np.random.seed(42)
n = 2000

# --- Generate log-normal data simulating income distribution ---
# mu=10.5, sigma=0.8 roughly corresponds to annual incomes in USD
# (median around $36,000, mean around $50,000 -- consistent with right-skewed income data)
mu_log = 10.5    # Mean of the underlying normal (log scale)
sigma_log = 0.8  # Std dev of the underlying normal (log scale)

# scipy.stats.lognorm: s = sigma of the underlying normal, scale = exp(mu)
income_samples = stats.lognorm.rvs(s=sigma_log, scale=np.exp(mu_log), size=n)

# --- Plot 1: Raw data histogram (log-normal) ---
fig, axes = plt.subplots(1, 3, figsize=(16, 5))

axes[0].hist(income_samples / 1000, bins=80, density=True,
             color="steelblue", alpha=0.7, edgecolor="none")

# Overlay a normal curve with the same mean and std -- shows how poorly normal fits
x_range = np.linspace(0, income_samples.max() / 1000, 400)
normal_fit = stats.norm.pdf(x_range,
                             loc=income_samples.mean() / 1000,
                             scale=income_samples.std() / 1000)
axes[0].plot(x_range, normal_fit, color="firebrick",
             linewidth=2, label="Normal fit")
axes[0].axvline(income_samples.mean() / 1000, color="firebrick",
                linestyle="--", linewidth=1.5, label=f"Mean: ${income_samples.mean()/1000:.0f}K")
axes[0].axvline(np.median(income_samples) / 1000, color="darkorange",
                linestyle="--", linewidth=1.5,
                label=f"Median: ${np.median(income_samples)/1000:.0f}K")
axes[0].set_title("Raw Data: Log-Normal Income\n(Right-skewed, mean > median)")
axes[0].set_xlabel("Income ($000s)")
axes[0].set_ylabel("Density")
axes[0].set_xlim(0, 250)
axes[0].legend(fontsize=8)

# --- Plot 2: Log-transformed data histogram ---
log_income = np.log(income_samples)

axes[1].hist(log_income, bins=60, density=True,
             color="seagreen", alpha=0.7, edgecolor="none")

# Overlay the normal fit on log scale -- should match closely
x_log = np.linspace(log_income.min(), log_income.max(), 300)
axes[1].plot(x_log, stats.norm.pdf(x_log, loc=log_income.mean(),
             scale=log_income.std()), color="firebrick",
             linewidth=2, label="Normal fit")
axes[1].set_title("Log-Transformed Data\n(Approximately normal after transformation)")
axes[1].set_xlabel("log(Income)")
axes[1].set_ylabel("Density")
axes[1].legend(fontsize=8)

# --- Plot 3: Q-Q plot of log-transformed values ---
stats.probplot(log_income, dist="norm", plot=axes[2])
axes[2].set_title("Q-Q Plot of Log(Income)\n(Points on diagonal confirm log-normality)")
axes[2].get_lines()[0].set(color="seagreen", alpha=0.5, markersize=3)
axes[2].get_lines()[1].set(color="firebrick", linewidth=2)

plt.tight_layout()
plt.savefig("lognormal_income.png", dpi=150)
plt.show()

# --- Print the mean vs median gap ---
print("=== Log-Normal Income Distribution ===")
print(f"Sample mean:   ${income_samples.mean():,.0f}")
print(f"Sample median: ${np.median(income_samples):,.0f}")
print(f"Mean/Median ratio: {income_samples.mean() / np.median(income_samples):.2f}")
print(f"\n{(income_samples < income_samples.mean()).mean()*100:.1f}% of observations "
      f"fall BELOW the mean income")
print("(This is what 'most people earn below average' actually looks like)")

The Log-Normal Distribution
What this code does: Three panels tell the full story:

  • The first shows the raw income distribution as a right-skewed histogram with a normal curve overlaid; the normal fit is visibly wrong, and the vertical lines for mean and median show the gap between them concretely
  • The second panel shows the same data after a log transformation, where the histogram now matches the normal curve closely
  • The third panel is a Q-Q plot of the log-transformed values: if the points follow the diagonal reference line, the original data is log-normal. The print output quantifies the mean/median gap and shows what percentage of observations fall below the mean

Distribution 3: The Cauchy Distribution

The Cauchy distribution looks fairly normal — symmetric, slightly more peaked — but it is actually the pathological cousin of the normal distribution. Its tails decay so slowly they can dominate any calculation. The Cauchy has no defined mean, no defined variance, and no defined moments of any order. Not “very large” — undefined. The integral that would compute the expected value does not converge. The Law of Large Numbers does not hold, and the Central Limit Theorem does not apply.

The canonical construction: the ratio of two independent standard normal random variables follows a Cauchy distribution. Any time you work with a ratio of normally distributed variables, you may be in Cauchy territory.

Where It Shows Up

Cauchy behavior appears in physics and signal processing (resonance phenomena, spectral line shapes), in certain financial return ratios, and in any context where you are analyzing the ratio of two normally distributed quantities. The Cauchy distribution is also known as the Lorentz distribution in physics, where it describes the shape of spectral lines in atomic emission.

Why It Is the Most Dangerous

It passes the eyeball test. A moderate-sized sample of Cauchy data looks like a slightly leptokurtic normal distribution — more peaked in the center, a bit more spread in the tails, but nothing obviously alarming. Then compute the running mean. It does not converge. Add a thousand more observations, and it still does not converge. A risk model calibrated on sample means and standard deviations from Cauchy data will produce estimates that never stabilize, no matter how much data you collect.

Python Code

 
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

np.random.seed(42)
n = 5000

# --- Generate Cauchy and Normal samples ---
cauchy_samples = stats.cauchy.rvs(loc=0, scale=1, size=n)
normal_samples = stats.norm.rvs(loc=0, scale=1, size=n)

# --- Plot 1: PDF comparison on standard scale ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

x = np.linspace(-8, 8, 500)
axes[0].plot(x, stats.cauchy.pdf(x, loc=0, scale=1),
             color="steelblue", linewidth=2.5, label="Cauchy(0, 1)")
axes[0].plot(x, stats.norm.pdf(x, loc=0, scale=1),
             color="firebrick", linewidth=2.5, linestyle="--", label="Normal(0, 1)")
axes[0].set_title("Cauchy vs Normal: Standard Scale\n"
                  "(They look similar -- this is the trap)")
axes[0].set_xlabel("Value")
axes[0].set_ylabel("Density")
axes[0].legend()
axes[0].set_ylim(0, 0.42)

# --- Plot 2: PDF comparison on log-scale (reveals true tail behavior) ---
axes[1].semilogy(x, stats.cauchy.pdf(x, loc=0, scale=1),
                 color="steelblue", linewidth=2.5, label="Cauchy(0, 1)")
axes[1].semilogy(x, stats.norm.pdf(x, loc=0, scale=1),
                 color="firebrick", linewidth=2.5, linestyle="--", label="Normal(0, 1)")
axes[1].set_title("Cauchy vs Normal: Log-Scale Y-axis\n"
                  "(Cauchy tail stays elevated -- Normal tail vanishes)")
axes[1].set_xlabel("Value")
axes[1].set_ylabel("Density (log scale)")
axes[1].legend()
axes[1].set_ylim(1e-10, 1)
axes[1].grid(True, alpha=0.3, which="both")

plt.tight_layout()
plt.savefig("cauchy_vs_normal_pdf.png", dpi=150)
plt.show()

# --- The key demonstration: running mean convergence ---
# Normal running mean converges. Cauchy running mean does not.
running_mean_normal = np.cumsum(normal_samples) / np.arange(1, n + 1)
running_mean_cauchy = np.cumsum(cauchy_samples) / np.arange(1, n + 1)

plt.figure(figsize=(12, 5))
plt.plot(running_mean_normal, color="firebrick", linewidth=1.2,
         label="Normal -- running mean converges to 0")
plt.plot(running_mean_cauchy, color="steelblue", linewidth=1.2, alpha=0.8,
         label="Cauchy -- running mean never converges")
plt.axhline(0, color="black", linewidth=1, linestyle="--", alpha=0.5)
plt.title(f"Running Sample Mean: Normal vs Cauchy (n={n:,})\n"
          "Normal converges quickly. Cauchy wanders indefinitely.")
plt.xlabel("Number of observations")
plt.ylabel("Cumulative sample mean")
plt.legend()
plt.tight_layout()
plt.savefig("cauchy_running_mean.png", dpi=150)
plt.show()

# --- Quantify just how heavy the Cauchy tails are ---
print("=== How Often Do Extreme Values Occur? ===")
for threshold in [5, 10, 20, 50]:
    p_normal = 2 * (1 - stats.norm.cdf(threshold))
    p_cauchy = 2 * (1 - stats.cauchy.cdf(threshold))
    print(f"|X| > {threshold:>2}:  Normal = {p_normal:.2e}  |  Cauchy = {p_cauchy:.4f}")

print("\n(The Cauchy assigns meaningful probability to events that Normal treats as impossible)")

# --- Canonical construction: ratio of two normals IS Cauchy ---
print("\n=== The Ratio Construction ===")
z1 = stats.norm.rvs(0, 1, 10_000)
z2 = stats.norm.rvs(0, 1, 10_000)
ratio = z1 / z2  # This ratio is exactly Cauchy(0,1) distributed

# Fit a Cauchy to the ratio and check the parameters
loc_fit, scale_fit = stats.cauchy.fit(ratio)
print(f"Z1/Z2 fitted Cauchy parameters: loc={loc_fit:.3f}, scale={scale_fit:.3f}")
print(f"Expected for Cauchy(0,1):        loc=0.000, scale=1.000")

The Cauchy Distribution
What this code does:

  • First, the standard-scale PDF comparison shows how similar Cauchy and normal look at a glance — this is the visual trap
  • Second, the log-scale y-axis comparison makes the tail difference unmistakable: the normal tail drops to near-zero by x = 5 or 6, while the Cauchy tail remains meaningfully elevated far beyond that
  • Third, the running mean plot is the most important demonstration: with 5,000 observations, the normal running mean stabilizes within the first few hundred, while the Cauchy running mean keeps jumping
  • Fourth, the probability table quantifies exactly how likely extreme events are under each distribution: an event at x = 10 has near-zero probability under the normal and non-trivial probability under the Cauchy
  • The final block demonstrates that the ratio of two independent standard normals is exactly Cauchy distributed, which you can verify by fitting the distribution to the ratio

How These Three Compare to the Normal

Property Normal Pareto Log-Normal Cauchy
Shape Symmetric bell Heavy right tail, power law Right-skewed Symmetric, heavy tails
Mean defined? Yes Only if alpha > 1 Yes No
Variance defined? Yes Only if alpha > 2 Yes No
CLT applies? Yes Partially Yes No
Typical domain Heights, errors, test scores Wealth, traffic, disasters Income, prices, biology Ratios, resonance, signal processing
Core danger Baseline Under-prices tail risk Mean misleads most people Estimates never converge

Conclusion

The normal distribution is the right model when data genuinely arises from the sum of many small independent effects, when extreme events are rare, and when the mean meaningfully represents the typical value. The problem is assuming those conditions hold by default.

Pareto, log-normal, and Cauchy data are not statistical edge cases — they dominate enormous swaths of finance, economics, biology, and risk modelling. Defaulting to normality in these domains produces confidently wrong estimates in ways that can be expensive to discover after the fact. Plot your data, check the tails, look at the mean-median gap, and run the log-transformation Q-Q check. Do not assume normal until you have actual evidence that it fits.

Leave a Reply

Your email address will not be published. Required fields are marked *