Beyond the mean and the variance
Two return series can have the same mean and the same volatility and still be very different to hold. One drifts up slowly and now and then drops 6% in a day; the other is symmetric and calm. Mean and variance are the first two moments of a distribution. The k-th central moment is the average of (x - mean) ** k. Moments 3 and 4 describe the shape: skewness says which tail is longer, kurtosis how heavy the tails are.
import numpy as np
def central_moment(x, k):
return float(np.mean((x - x.mean()) ** k))
# Calm days of about +1%, and one crash of -6%
returns = np.array([0.010, 0.012, 0.008, 0.011, 0.009, 0.010, 0.012, -0.060, 0.010, 0.009])
print(round(central_moment(returns, 2), 8)) # 0.00044389 the variance with ddof=0
print(round(central_moment(returns, 3), 10)) # -2.47823e-05 odd powers keep the sign: the crash dominates
print(round(central_moment(returns, 4), 12)) # 1.587946e-06 even powers are always positiveSkewness: which tail is longer
Cubing keeps the sign of each deviation and makes big ones count far more, so the third moment is dominated by the most extreme values. Dividing by m2 ** 1.5 removes the units, giving the skewness: 0 for a symmetric distribution, negative when the long tail is on the left (rare big losses), positive when it is on the right (rare big gains). Equity indices usually have negative skew: slow rises, sudden falls. The skewness of a sample of 100,000 normal values is about 0.
m2 = central_moment(returns, 2)
m3 = central_moment(returns, 3)
print(round(m3 / m2 ** 1.5, 3)) # -2.65 strongly negative: one big loss among calm gains
mirror = -returns # flip every return: the long tail moves to the right
print(round(central_moment(mirror, 3) / central_moment(mirror, 2) ** 1.5, 3)) # 2.65Kurtosis: how heavy are the tails
The fourth power makes extreme values count enormously, so m4 / m2 ** 2 grows with the tails. For a normal distribution it equals 3, which is why we quote the excess kurtosis, m4 / m2 ** 2 - 3: it is 0 for a normal distribution, positive for fat tails (extreme days happen more often than the bell predicts) and negative for thin tails (the thinnest possible, two equal spikes, gives -2). Daily returns of most assets have a clearly positive excess kurtosis, often 5 or more. Both statistics are unchanged when you rescale or shift the returns, so they describe shape only.
m4 = central_moment(returns, 4)
print(round(m4 / m2 ** 2 - 3, 3)) # 5.059 fat tails: one crash dominates the fourth moment
calm = np.array([-0.02, -0.01, -0.005, 0.0, 0.005, 0.01, 0.02, -0.015, 0.015, 0.0])
c2, c3, c4 = (central_moment(calm, k) for k in (2, 3, 4))
print(round(c3 / c2 ** 1.5, 3), round(c4 / c2 ** 2 - 3, 3)) # 0.0 -1.033 symmetric, slightly thin tails# Shape does not depend on scale or shift: multiply the returns by 3 and add 1
rescaled = returns * 3 + 1
print(round(central_moment(rescaled, 3) / central_moment(rescaled, 2) ** 1.5, 3)) # -2.65
print(round(central_moment(rescaled, 4) / central_moment(rescaled, 2) ** 2 - 3, 3)) # 5.059 the same two numbers- The k-th central moment is
mean((x - mean) ** k): moments 2, 3 and 4 give variance, skewness and kurtosis. - Skewness
m3 / m2 ** 1.5: negative means a long left tail (rare big losses); excess kurtosism4 / m2 ** 2 - 3is 0 for a normal. - Positive excess kurtosis means fat tails; both statistics are driven by a few extreme days, so they need lots of data.
Next up: fat tails and normality tests, checking formally whether returns follow the bell curve.
Lab · what's graded
- Compute the building block with
central_moment25 pts - Measure which tail is longer with
skewness25 pts - Measure how heavy the tails are with
excess_kurtosis25 pts - Put a name on the shape with
shape_label25 pts