Skip to content

Moments and Skewness

Moments describe the shape of a distribution, with mean (1st moment), variance (2nd), skewness (3rd), and kurtosis (4th) quantifying center, spread, asymmetry, and tail heaviness respectively.


Moments Definition

1. Raw Moments

E[X^n]:

from scipy import stats
import numpy as np

data = np.random.normal(0, 1, 1000)

# First raw moment = mean
m1 = np.mean(data)
print(f"1st moment (mean): {m1:.3f}")

# Second raw moment
m2 = np.mean(data**2)
print(f"2nd moment: {m2:.3f}")

# Third raw moment
m3 = np.mean(data**3)
print(f"3rd moment: {m3:.3f}")

2. Central Moments

E[(X - μ)^n]:

mean = np.mean(data)

# Second central moment = variance
c2 = np.mean((data - mean)**2)
print(f"Variance: {c2:.3f}")

# Third central moment
c3 = np.mean((data - mean)**3)
print(f"3rd central moment: {c3:.3f}")

# Using scipy
print(stats.moment(data, moment=2))  # Variance
print(stats.moment(data, moment=3))  # 3rd central

3. Standardized Moments

E[(X - μ)^n / σ^n]:

std = np.std(data, ddof=1)

# Skewness (3rd standardized moment)
skew = np.mean(((data - mean) / std)**3)
print(f"Skewness: {skew:.3f}")

# Using scipy
print(stats.skew(data, bias=False))

4. Relationship

# Raw → Central
# μ₂' = E[X²] = Var(X) + (E[X])²
# μ₃' = E[X³] = μ₃ + 3μ(Var) + μ³

# Central → Standardized
# γ₃ = μ₃ / σ³ (skewness)
# γ₄ = μ₄ / σ⁴ - 3 (excess kurtosis)

5. Moment Generating Function

# MGF: M(t) = E[e^(tX)]
# Derivatives at t=0 give moments

# For normal distribution
t = 0.5
norm_mgf = np.exp(mean * t + 0.5 * std**2 * t**2)
print(f"Normal MGF at t={t}: {norm_mgf:.3f}")

6. Cumulants

# Cumulant generating function
# K(t) = log(M(t))

# First cumulant = mean
# Second cumulant = variance
# Third cumulant = third central moment

7. Sample vs Population

# Sample moments (unbiased)
sample_var = np.var(data, ddof=1)  # Divide by n-1

# Population moments (biased)
pop_var = np.var(data, ddof=0)     # Divide by n

print(f"Sample variance: {sample_var:.3f}")
print(f"Population variance: {pop_var:.3f}")

Skewness

1. Definition

Measure of asymmetry:

# Skewness = E[(X - μ)³] / σ³
skewness = stats.skew(data, bias=False)
print(f"Skewness: {skewness:.3f}")

# Interpretation:
# 0: Symmetric (normal)
# >0: Right-skewed (positive, long right tail)
# <0: Left-skewed (negative, long left tail)

2. Positive Skew

# Generate right-skewed data
right_skew = np.random.exponential(scale=1, size=1000)
print(f"Skewness: {stats.skew(right_skew):.3f}")  # ~2.0

# Visualization
plt.hist(right_skew, bins=50, density=True, alpha=0.7)
plt.axvline(np.mean(right_skew), color='r', label='Mean')
plt.axvline(np.median(right_skew), color='g', label='Median')
plt.legend()
plt.title('Positive Skew: Mean > Median')
plt.show()

# Mean pulled toward tail
print(f"Mean: {np.mean(right_skew):.3f}")
print(f"Median: {np.median(right_skew):.3f}")  # Mean > Median

3. Negative Skew

# Generate left-skewed data
left_skew = -np.random.exponential(scale=1, size=1000)
print(f"Skewness: {stats.skew(left_skew):.3f}")  # ~-2.0

# Mean < Median for left skew

4. Skewness Test

# Test if skewness significantly differs from 0
skewtest_stat, p_value = stats.skewtest(data)
print(f"Skew test: statistic={skewtest_stat:.3f}, p={p_value:.4f}")

# p < 0.05: Significant skewness (reject symmetry)

5. Pearson Skewness Coefficients

# Mode skewness
mean = np.mean(data)
mode = stats.mode(data, keepdims=True).mode[0]
std = np.std(data, ddof=1)
pearson_mode = (mean - mode) / std

# Median skewness (more robust)
median = np.median(data)
pearson_median = 3 * (mean - median) / std

print(f"Pearson mode skewness: {pearson_mode:.3f}")
print(f"Pearson median skewness: {pearson_median:.3f}")

6. Quantile Skewness

# Based on quartiles (very robust)
q1, q2, q3 = np.percentile(data, [25, 50, 75])
bowley_skew = (q3 + q1 - 2*q2) / (q3 - q1)
print(f"Bowley skewness: {bowley_skew:.3f}")

# Ranges from -1 to +1

7. Transformations

# Box-Cox transformation to reduce skewness
from scipy import stats

data_positive = np.abs(data) + 1  # Must be positive
transformed, lambda_param = stats.boxcox(data_positive)

print(f"Original skewness: {stats.skew(data_positive):.3f}")
print(f"Transformed skewness: {stats.skew(transformed):.3f}")
print(f"Lambda parameter: {lambda_param:.3f}")

Kurtosis

1. Definition

Measure of tail heaviness:

# Excess kurtosis = E[(X - μ)⁴] / σ⁴ - 3
kurtosis = stats.kurtosis(data, bias=False)
print(f"Excess kurtosis: {kurtosis:.3f}")

# Interpretation:
# 0: Normal (mesokurtic)
# >0: Heavy tails (leptokurtic)
# <0: Light tails (platykurtic)

2. Leptokurtic (Heavy Tails)

# t-distribution has heavy tails
heavy_tail = stats.t.rvs(df=5, size=1000)
print(f"Kurtosis: {stats.kurtosis(heavy_tail):.3f}")  # Positive

# More extreme values than normal

3. Platykurtic (Light Tails)

# Uniform distribution has light tails
light_tail = np.random.uniform(-1, 1, 1000)
print(f"Kurtosis: {stats.kurtosis(light_tail):.3f}")  # ~-1.2

# Fewer extreme values than normal

4. Kurtosis Test

# Test if kurtosis significantly differs from 0 (normal)
kurttest_stat, p_value = stats.kurtosistest(data)
print(f"Kurt test: statistic={kurttest_stat:.3f}, p={p_value:.4f}")

# p < 0.05: Significant deviation from normal kurtosis

5. Sample Kurtosis Bias

# Unbiased vs biased estimator
kurt_unbiased = stats.kurtosis(data, bias=False)  # Recommended
kurt_biased = stats.kurtosis(data, bias=True)

print(f"Unbiased: {kurt_unbiased:.3f}")
print(f"Biased: {kurt_biased:.3f}")

6. Outlier Sensitivity

# Kurtosis very sensitive to outliers
data_clean = np.random.normal(0, 1, 100)
data_outlier = np.concatenate([data_clean, [10, -10]])

print(f"Clean kurtosis: {stats.kurtosis(data_clean):.3f}")
print(f"With outliers: {stats.kurtosis(data_outlier):.3f}")  # Much higher

7. Robust Alternatives

# L-kurtosis (based on L-moments, more robust)
# Not in scipy, but conceptually important

# Or use quantile-based measure
q = np.percentile(data, [10, 25, 75, 90])
robust_kurt = (q[3] - q[0]) / (q[2] - q[1])
print(f"Robust kurtosis proxy: {robust_kurt:.3f}")

Normal Test

1. Jarque-Bera Test

# Tests both skewness and kurtosis
jb_stat, p_value = stats.jarque_bera(data)
print(f"Jarque-Bera: statistic={jb_stat:.3f}, p={p_value:.4f}")

# H0: Data is normally distributed
# p < 0.05: Reject normality

2. Anderson-Darling Test

# More powerful than Jarque-Bera
result = stats.anderson(data, dist='norm')
print(f"A-D statistic: {result.statistic:.3f}")

# Compare to critical values
for i, sl in enumerate(result.significance_level):
    print(f"{sl}%: {result.critical_values[i]:.3f}")

3. Shapiro-Wilk Test

# Most powerful test for normality (small samples)
sw_stat, p_value = stats.shapiro(data)
print(f"Shapiro-Wilk: statistic={sw_stat:.4f}, p={p_value:.4f}")

# W close to 1 indicates normality

4. Kolmogorov-Smirnov Test

# General distribution test
ks_stat, p_value = stats.kstest(data, 'norm', args=(np.mean(data), np.std(data)))
print(f"K-S test: statistic={ks_stat:.4f}, p={p_value:.4f}")

5. D'Agostino-Pearson Test

# Combines skew and kurtosis tests
k2, p_value = stats.normaltest(data)
print(f"D'Agostino: statistic={k2:.3f}, p={p_value:.4f}")

# Equivalent to combining skewtest and kurtosistest

6. Q-Q Plot

# Visual normality check
stats.probplot(data, dist="norm", plot=plt)
plt.title("Q-Q Plot")
plt.show()

# Points on line → Normal
# S-curve → Heavy tails
# Reverse S → Light tails

7. Omnibus Test

# Not directly in scipy, but can be constructed
skew = stats.skew(data)
kurt = stats.kurtosis(data)
n = len(data)

# Jarque-Bera statistic
jb = n/6 * (skew**2 + kurt**2/4)
p_value = 1 - stats.chi2.cdf(jb, df=2)
print(f"Omnibus: JB={jb:.3f}, p={p_value:.4f}")

Practical Applications

1. Financial Returns

# Stock returns often have fat tails
returns = np.random.standard_t(df=5, size=1000) * 0.02  # 2% daily vol

print(f"Skewness: {stats.skew(returns):.3f}")
print(f"Kurtosis: {stats.kurtosis(returns):.3f}")  # Positive → Fat tails

# Implications: Normal models underestimate risk

2. Income Distribution

# Income typically right-skewed
income = np.random.lognormal(mean=10.5, sigma=0.5, size=1000)

print(f"Skewness: {stats.skew(income):.3f}")  # Strongly positive
print(f"Mean: ${np.mean(income):.0f}")
print(f"Median: ${np.median(income):.0f}")  # Median < Mean

3. Outlier Detection

# High kurtosis suggests outliers
if stats.kurtosis(data) > 3:
    print("Warning: Heavy tails detected")
    # Consider robust statistics or outlier removal

4. Transformation Selection

# Choose transformation based on skewness
if stats.skew(data) > 1:
    # Strong right skew → log transform
    transformed = np.log(data + 1)
elif stats.skew(data) < -1:
    # Strong left skew → exponential transform
    transformed = np.exp(data)
else:
    # Mild skew → square root or Box-Cox
    transformed, _ = stats.boxcox(data + abs(data.min()) + 1)

5. Model Selection

# Normal vs t-distribution based on kurtosis
if abs(stats.kurtosis(data)) < 1:
    model = stats.norm
    print("Using normal distribution")
else:
    # Fit t-distribution for heavy tails
    df, loc, scale = stats.t.fit(data)
    model = stats.t(df=df, loc=loc, scale=scale)
    print(f"Using t-distribution (df={df:.1f})")

6. Risk Measures

# VaR underestimated if kurtosis ignored
mean, std = np.mean(returns), np.std(returns)

# Normal assumption
var_normal = stats.norm.ppf(0.05, loc=mean, scale=std)

# Empirical (accounts for actual distribution)
var_empirical = np.percentile(returns, 5)

print(f"VaR (normal): {var_normal:.4f}")
print(f"VaR (empirical): {var_empirical:.4f}")

7. Power Analysis

# Sample size for detecting skewness/kurtosis
from statsmodels.stats.power import normal_power_het

# Example: Detect skewness of 0.5 with power 0.8
# Requires large sample size

Summary

Moments: - 1st: Mean (location) - 2nd: Variance (spread) - 3rd: Skewness (asymmetry) - 4th: Kurtosis (tail heaviness)

Skewness: - 0: Symmetric - >0: Right-skewed (mean > median) - <0: Left-skewed (mean < median)

Kurtosis (excess): - 0: Normal tails - >0: Heavy tails (more extremes) - <0: Light tails (fewer extremes)

Key insight: Higher moments reveal distribution shape beyond mean and variance, with skewness indicating asymmetry (important for risk assessment) and kurtosis measuring tail thickness (critical for extreme event modeling).