Skip to content

Q-Q Plots for Financial Data: Detecting Non-Normality in Asset Returns

Overview

Q-Q plots are particularly valuable in finance for diagnosing departures from normality in asset returns. Many financial models assume returns follow a normal distribution, but empirical data often exhibit heavy tails and skewness, leading to significant underestimation of tail risk. This section focuses on using Q-Q plots to visualize these departures in financial data.

Why Normality Matters in Finance

Financial models rely heavily on distributional assumptions:

  • Option pricing (Black-Scholes) assumes log-normal asset prices, equivalent to normally distributed log-returns
  • Value at Risk (VaR) and Expected Shortfall (ES) rely on tail behavior assumptions
  • Portfolio optimization uses variance-covariance approaches that assume multivariate normality

When actual returns deviate from normality, these models systematically underestimate the probability of large losses, especially during market stress periods.

Real-World Example: Netflix (NFLX) Log-Returns

Netflix stock provides an excellent case study of non-normal financial returns. The following example uses daily log-returns computed from closing prices:

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

# Simulate NFLX-like log-returns (or load real data)
np.random.seed(42)
# Use a distribution with slightly heavier tails than normal
# (Actual NFLX returns are even heavier-tailed)
returns = np.random.standard_t(df=8, size=1000) * 0.02

# Create Q-Q plot against normal distribution
fig, ax = plt.subplots(figsize=(8, 6))
stats.probplot(returns, dist="norm", plot=ax)

ax.set_title("Q-Q Plot: Daily Log-Returns vs Normal Distribution", fontsize=12)
ax.set_xlabel("Theoretical Normal Quantiles", fontsize=11)
ax.set_ylabel("Sample Quantiles (Observed Returns)", fontsize=11)

# Enhance aesthetics
ax.spines[["top", "right"]].set_visible(False)
ax.grid(True, alpha=0.3, linestyle='--')

plt.tight_layout()
plt.show()

Interpreting the Q-Q Plot for Financial Data

Perfect Normality

When data follow a normal distribution exactly, the Q-Q plot shows all points tightly clustered along the 45-degree reference line.

Heavy Tails (The Financial Reality)

Real stock returns exhibit heavy tails: both the left tail (large negative returns/losses) and right tail (large positive returns/gains) contain more observations than the normal distribution predicts.

Visual signature: The Q-Q plot "bends upward" in the right tail and "bends downward" in the left tail. This creates an S-shaped pattern.

# Simulate heavy-tailed returns (e.g., using Student's t distribution)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats

np.random.seed(42)
df = 5  # degrees of freedom; lower = heavier tails
heavy_tailed_returns = stats.t.rvs(df=df, scale=0.02, size=2000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# Left: Histogram with normal overlay
ax1.hist(heavy_tailed_returns, bins=50, density=True, alpha=0.6, label='Observed Returns')
x = np.linspace(heavy_tailed_returns.min(), heavy_tailed_returns.max(), 100)
ax1.plot(x, stats.norm.pdf(x, loc=heavy_tailed_returns.mean(),
                            scale=heavy_tailed_returns.std()),
         'r-', lw=2, label='Normal PDF')
ax1.set_xlabel('Daily Log-Return', fontsize=11)
ax1.set_ylabel('Density', fontsize=11)
ax1.set_title('Distribution Shape: Heavy Tails vs Normal', fontsize=12)
ax1.legend()
ax1.spines[["top", "right"]].set_visible(False)

# Right: Q-Q plot
stats.probplot(heavy_tailed_returns, dist="norm", plot=ax2)
ax2.set_title("Q-Q Plot: Revealing Heavy Tails", fontsize=12)
ax2.set_xlabel('Theoretical Normal Quantiles', fontsize=11)
ax2.set_ylabel('Sample Quantiles', fontsize=11)
ax2.spines[["top", "right"]].set_visible(False)
ax2.grid(True, alpha=0.3, linestyle='--')

plt.tight_layout()
plt.show()

Consequences of Ignoring Non-Normality

Underestimating Tail Risk

The normal distribution has a kurtosis of 3. Real financial returns typically have excess kurtosis > 1, meaning the tails are heavier than normal.

Example: If the normal distribution predicts a 5% loss with 0.1% probability, actual heavy-tailed returns might exhibit this loss with 0.5% probability — a 5x underestimation!

Implication for Risk Management

  • VaR models based on normality underestimate losses at the 99th or 99.9th percentile
  • Hedging strategies based on normal assumptions leave portfolios vulnerable to tail events
  • Capital requirements (e.g., Basel III) may be insufficient if built on normal assumptions

Practical Workflow: Diagnosing Return Distributions

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

# Step 1: Load or simulate returns
np.random.seed(42)
returns = np.random.standard_t(df=6, size=1500) * 0.025

# Step 2: Compute summary statistics
mean_ret = returns.mean()
std_ret = returns.std()
skewness = stats.skew(returns)
kurtosis = stats.kurtosis(returns)  # Excess kurtosis

print(f"Mean:     {mean_ret:.4f}")
print(f"Std Dev:  {std_ret:.4f}")
print(f"Skewness: {skewness:.4f}")
print(f"Ex. Kurtosis: {kurtosis:.4f}")

# Step 3: Normality tests
_, p_ks = stats.kstest(returns, 'norm', args=(mean_ret, std_ret))
_, p_ad = stats.anderson(returns, dist='norm')
_, p_jb = stats.jarque_bera(returns)

print(f"\nKolmogorov-Smirnov test p-value: {p_ks:.4f}")
print(f"Jarque-Bera test p-value: {p_jb:.4f}")

# Step 4: Q-Q plot
fig, ax = plt.subplots(figsize=(8, 6))
stats.probplot(returns, dist="norm", plot=ax)
ax.set_title("Diagnostics: Are Returns Normal?", fontsize=12)
ax.spines[["top", "right"]].set_visible(False)
plt.show()

Adjusting for Non-Normality

1. Alternative Distributions

Instead of normal, fit Student's \(t\) or Generalized Hyperbolic distributions:

from scipy.stats import t as student_t

# Fit Student's t distribution
df, loc, scale = student_t.fit(returns)
print(f"Fitted df: {df:.2f} (lower df → heavier tails)")

2. Non-Parametric Methods

Bootstrap and quantile-based approaches make no distributional assumptions.

3. Modified Risk Measures

Use Expected Shortfall (CVaR) instead of VaR; it better captures tail behavior for non-normal distributions.

Summary

  • Q-Q plots visually compare sample quantiles to theoretical quantiles, revealing distributional shape
  • Financial returns exhibit heavy tails, showing an S-shaped pattern in Q-Q plots
  • Ignoring non-normality leads to systematic underestimation of tail risk
  • Practical solutions: use alternative distributions, non-parametric methods, or robust risk measures tailored to observed tail behavior