Statistical Data Analysis and Inference › Descriptive Statistics for Financial Returns

Michael BrenndoerferOctober 22, 202518 min read
Quantitative FinanceAll chapters
Part 1 of 15Mathematical Foundations
Chapter 4Statistical Data Analysis and Inference › Descriptive Statistics for Financial Returns
Reading section 2 of 9 · Descriptive Statistics for Financial Returns · 18 minSection 2 of 9 · 18 min

Book contents

Section 2 of 9 · 18 min read

Statistical Data Analysis and Inference › Descriptive Statistics for Financial ReturnsOne section · Sections in this chapter2 / 9 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Descriptive statistics summarize the essential features of a dataset. For financial returns, we focus on four moment-based summaries: mean, volatility, skewness, and kurtosis. Together they describe complementary aspects of a return distribution.

Returns: The Foundation of Financial Analysis

Raw prices are difficult to compare across assets and time periods. A $10 move in a $100 stock is very different from a $10 move in a $1000 stock. The first represents a 10% change, while the second is only 1%. Returns normalize price changes, making relative moves comparable. Returns are often closer to stationary than price levels, but the transformation does not guarantee stationarity; that property must be checked for the series, horizon, and sample in question.

The choice to work with returns rather than prices can help with a basic statistical requirement: stationarity. Price levels often contain trends, so their distribution changes over time. Return means, variances, and dependence can also change across regimes, however. Model-based inference and forecasting therefore require checks for structural breaks, serial dependence, and time-varying volatility rather than an automatic assumption that returns are stationary.

Simple Returns vs. Log Returns

Simple returns measure the percentage change in price:

Rt=Pt−Pt−1Pt−1R_t = \frac{P_t - P_{t-1}}{P_{t-1}}

where:

  • RtR_t: simple return at time tt (expressed as a decimal, e.g., 0.05 for 5%)
  • PtP_t: price at time tt
  • Pt−1P_{t-1}: price at time t−1t-1

Log returns (continuously compounded returns) are defined as:

rt=ln⁡(PtPt−1)r_t = \ln\left(\frac{P_t}{P_{t-1}}\right)

where rtr_t is the log return at time tt. Here we use strictly positive prices for log returns; the simple-return formula requires a nonzero lagged price. The logarithm turns the price ratio into an additive measure.

Log returns are additive over time and approximately equal to simple returns for small values. Their additivity is convenient for time aggregation, while simple returns remain natural for portfolio accounting and cross-sectional aggregation. The appropriate convention depends on the analysis.

The distinction between simple and log returns deserves careful attention because it affects how we aggregate returns over time. With simple returns, compounding over multiple periods requires multiplication: a 10% gain followed by a 10% loss does not return you to your starting point (you end up at 99% of your original value). Log returns, however, are additive. The two-day log return is simply the sum of the individual daily log returns. Both definitions are widely used; the appropriate choice depends on whether the task emphasizes time aggregation, portfolio accounting, or another convention.

Let's calculate returns from price data:

In[3]:
Code
import numpy as np

# Simulated daily closing prices
np.random.seed(42)
prices = 100 * np.cumprod(1 + np.random.normal(0.0005, 0.02, 252))

# Calculate simple and log returns
simple_returns = np.diff(prices) / prices[:-1]
log_returns = np.diff(np.log(prices))

This seeded price path is positive. The normal simple-return generator is a fixed numerical illustration, not a globally positive price model: a normal draw can be below -1. The later log-return intervals illustrate inference conditional on a real-valued i.i.d. log-return model with the required population moments, not a guarantee about every possible path of this generator.

Out[4]:
Console
Number of daily returns: 251
Simple return (first day): -0.0023
Log return (first day): -0.0023
Difference: 0.000003

For small percentage moves, simple and log returns are nearly identical. The difference becomes material for larger moves: a 20% simple gain corresponds to a log return of about 18.2%, while a 20% simple loss corresponds to about -22.3%. The appropriate convention depends on the calculation rather than on a universal frequency threshold.

The First Moment: Mean Return

The arithmetic mean of returns measures the average performance over the sample period. This is the most intuitive statistic: add up all the returns and divide by how many there are. It summarizes the sample's first moment, but it need not equal an observed return, the most common return, or a value representative of a skewed or multimodal distribution.

rˉ=1n∑i=1nri\bar{r} = \frac{1}{n}\sum_{i=1}^{n} r_i

where:

  • rˉ\bar{r}: sample mean return
  • nn: number of observations in the sample
  • rir_i: individual return observation at time ii

The summation notation ∑i=1n\sum_{i=1}^{n} indicates that we add up all returns from the first observation (i=1i=1) through the last (i=ni=n). The factor 1n\frac{1}{n} in front converts this sum into an average. This formula produces a single measure of central tendency, but observations need not cluster around it.

Using the common 252-trading-day convention, we annualize daily mean log returns by multiplying by 252:

rˉannual=252×rˉdaily\bar{r}_{annual} = 252 \times \bar{r}_{daily}

Log returns add over time. With a common daily mean log return of 0.04%, the expected sum over 252 trading days is 252 × 0.04% = 10.08% in log-return units. This is not automatically expected compounded simple growth. For simple returns, multiplying the daily mean by 252 defines a linear annualized arithmetic mean, not the compounded annual return. Small daily values alone do not guarantee that compounding over a year is negligible.

In[5]:
Code
# Calculate mean returns
mean_daily = np.mean(log_returns)
mean_annual = mean_daily * 252
Out[6]:
Console
Mean daily log return: 0.000199 (0.0199%)
Annualized mean log return: 0.0500 (5.00%)

The sample mean estimates the population mean under a specified return model; it is not automatically an investor's forward expected reward. Historical sample means are unstable, and a few extreme days can shift the estimate significantly. This instability arises because the mean is computed by adding all observations, giving equal weight to every day including the outliers. A single day with a 10% move (positive or negative) can shift the annual mean estimate by several percentage points.

The Second Moment: Volatility

Volatility measures the dispersion of returns around the mean. It quantifies risk in the most commonly used sense: how much do returns deviate from what we expect? A high-volatility asset tends to have larger deviations around its mean, while a low-volatility asset tends to have smaller deviations. Dispersion alone does not determine whether returns are forecastable. This concept of risk as variability is central to modern portfolio theory.

For at least two observations, the sample variance is:

s2=1n−1∑i=1n(ri−rˉ)2s^2 = \frac{1}{n-1}\sum_{i=1}^{n} (r_i - \bar{r})^2

where:

  • s2s^2: sample variance
  • n−1n-1: degrees of freedom; for i.i.d. observations with a common finite variance, Bessel's correction gives the usual unbiased sample-variance estimator
  • ri−rˉr_i - \bar{r}: deviation of each return from the sample mean
  • nn: number of observations in the sample

The logic behind variance is straightforward. First, we calculate how far each return deviates from the mean by computing (ri−rˉ)(r_i - \bar{r}). Some deviations are positive (returns above average) and some are negative (returns below average). If we simply averaged these deviations, the positives and negatives would cancel out, giving us zero, which is not useful as a dispersion measure. By squaring each deviation, we ensure all terms are nonnegative, and larger deviations receive disproportionately more weight.

For i.i.d. observations with a common finite variance, we use n−1n-1 in the denominator rather than nn to obtain the usual unbiased estimator of that population variance. This adjustment, known as Bessel's correction, accounts for the fact that we estimated the mean from the same data. When we calculate deviations from the sample mean, the deviations are constrained to sum to zero, removing one algebraic degree of freedom. Under those sampling conditions, dividing by n−1n-1 accounts for that lost degree of freedom and makes the sample variance unbiased for the common population variance.

Volatility is the square root of variance, annualized by multiplying by 252\sqrt{252}:

σannual=σdaily×252\sigma_{annual} = \sigma_{daily} \times \sqrt{252}

where:

  • σannual\sigma_{annual}: annualized volatility
  • σdaily\sigma_{daily}: daily volatility (standard deviation of daily returns)
  • 252≈15.87\sqrt{252} \approx 15.87: scaling factor based on approximately 252 trading days per year (the square root arises because variance scales linearly with time, so standard deviation scales with the square root)

The square root scaling deserves explanation because it differs from how we annualize means. When we sum independent random variables, their variances add. If daily variance is σdaily2\sigma_{daily}^2, then annual variance (summing 252 independent daily variances) is 252×σdaily2252 \times \sigma_{daily}^2. Taking the square root to convert back to standard deviation gives us 252×σdaily\sqrt{252} \times \sigma_{daily}. This is why volatility scales with the square root of time rather than linearly.

In[7]:
Code
# Calculate volatility
std_daily = np.std(log_returns, ddof=1)  # ddof=1 for sample std
std_annual = std_daily * np.sqrt(252)
Out[8]:
Console
Daily volatility: 0.019319 (1.9319%)
Annualized volatility: 0.3067 (30.67%)

Volatility is central to finance: it appears in option pricing formulas, portfolio optimization, and risk metrics like Value at Risk. It is often more forecastable than mean return, but it remains time-varying and can shift sharply across regimes. Historical volatility is therefore an estimate with a horizon and model attached, not a stable constant.

The Third Moment: Skewness

Skewness measures one form of asymmetry in a distribution. Mean and variance describe center and spread but do not describe asymmetry, even though both are also defined for many asymmetric distributions. The third standardized moment records whether large standardized deviations contribute more on the positive or negative side.

Define the sample central moments with denominator nn as

mk=1n∑i=1n(ri−rˉ)k.m_k = \frac{1}{n}\sum_{i=1}^{n}(r_i-\bar r)^k.

The moment coefficient of sample skewness used by scipy.stats.skew when bias=True is

g1=m3m23/2.g_1 = \frac{m_3}{m_2^{3/2}}.

where:

  • m2m_2: second central sample moment using denominator nn
  • m3m_3: third central sample moment using denominator nn
  • g1g_1: dimensionless moment coefficient; SciPy can apply a finite-sample bias correction with bias=False

The coefficient requires m2>0m_2>0. A constant sample has zero dispersion, so its standardized skewness is undefined.

Understanding this formula requires recognizing the role of each component. Subtracting the mean centers each return, and dividing by m2\sqrt{m_2} expresses the deviation on a dimensionless scale. Cubing those standardized deviations preserves their signs and gives large deviations more influence.

The key step is raising these standardized returns to the third power. Unlike squaring (which makes everything positive), cubing preserves the sign: negative values remain negative after cubing, while positive values remain positive. Cubing also amplifies extreme values more than moderate ones. A z-score of 3 contributes 27 to the sum, while a z-score of 2 contributes only 8. This makes skewness heavily influenced by the distribution's tails, especially which tail extends farther.

The sign of the third standardized moment is interpreted as follows, with the tail descriptions serving as heuristics rather than complete tail orderings:

  • Zero skewness: The third central moment is zero. Symmetric distributions with positive variance and a finite third moment have zero skewness, but zero skewness does not imply symmetry.
  • Negative skewness: The third standardized moment is negative; this often accompanies a more influential left tail.
  • Positive skewness: The third standardized moment is positive; this often accompanies a more influential right tail.
In[9]:
Code
from scipy import stats

skewness = stats.skew(log_returns)
Out[10]:
Console
Skewness: 0.2353
Interpretation: Sample skewness is within the chosen ±0.5 threshold
Out[11]:
Visualization
Density plot of a negatively skewed distribution with a longer left tail.
Negative-skew example with a negative third standardized moment and a longer left side.
Density plot of a symmetric normal distribution.
Zero-skew example using a symmetric normal distribution.
Density plot of a positively skewed distribution with a longer right tail.
Positive-skew example with a positive third standardized moment and a longer right side.

Some equity return samples exhibit negative skewness, but its sign and magnitude vary by asset, horizon, estimator, and market regime. A symmetric model cannot represent an asymmetric fitted loss distribution, so risk analysis should compare the relevant left-tail probabilities or quantiles directly rather than infer every tail probability from skewness alone.

The Fourth Moment: Kurtosis

Kurtosis is the fourth standardized moment and is especially sensitive to large standardized deviations. While skewness records one form of asymmetry, kurtosis summarizes how strongly those large deviations influence the fourth moment. It does not, by itself, rank tail probabilities against a normal distribution at every threshold.

Using the same central moments, the raw moment coefficient of kurtosis is

g2raw=m4m22.g_2^{\mathrm{raw}} = \frac{m_4}{m_2^2}.

where:

  • m4m_4: fourth central sample moment using denominator nn
  • m22m_2^2: square of the second central sample moment
  • g2rawg_2^{\mathrm{raw}}: raw sample moment coefficient; finite-sample bias corrections use a different formula

Like skewness, this standardized coefficient requires positive dispersion: m2>0m_2>0. Population kurtosis also requires a finite fourth moment.

The fourth power creates an even more extreme amplification of outliers than the cube used in skewness. A standardized return of 3 (three standard deviations from the mean) contributes 81 to the sum, while a standardized return of 4 contributes 256. A single extreme observation can therefore increase the kurtosis estimate dramatically. This sensitivity is relevant when studying extremes, but the fourth moment is not a complete measure of tail behavior and has considerable sampling variability.

A normal distribution has a kurtosis of 3. Excess kurtosis subtracts 3 to make the normal distribution the baseline:

Excess Kurtosis=Kurtosis−3\text{Excess Kurtosis} = \text{Kurtosis} - 3

where:

  • Kurtosis\text{Kurtosis}: the raw kurtosis value computed from the sample
  • 33: the kurtosis of the standard normal distribution, used as the reference point (subtracting it makes the normal distribution have excess kurtosis of zero)

By subtracting 3, we create a measure where zero is the normal-distribution benchmark. Positive excess kurtosis means a larger fourth standardized moment than the normal benchmark; it does not, by itself, guarantee larger tail probability at every threshold. scipy.stats.kurtosis reports excess kurtosis by default because its fisher argument defaults to True; other software may use a different convention.

The usual labels summarize the fourth-moment comparison, but they do not replace a direct tail-probability comparison:

  • Zero: Same raw fourth standardized moment as the normal benchmark (mesokurtic)
  • Positive: Larger fourth standardized moment than the normal benchmark (leptokurtic)
  • Negative: Smaller fourth standardized moment than the normal benchmark (platykurtic)
In[12]:
Code
kurtosis = stats.kurtosis(log_returns)  # scipy returns excess kurtosis by default
Out[13]:
Console
Excess kurtosis: 0.4562
Interpretation: Sample excess kurtosis is within the chosen ±1 descriptive threshold
Out[14]:
Visualization
Line chart comparing a normal distribution and a fat-tailed t-distribution, with the tails highlighted to emphasize higher probability of extreme values.
Comparison of a normal distribution (excess kurtosis = 0) with a variance-matched Student's t distribution with five degrees of freedom (finite excess kurtosis = 6). The t distribution has a higher center and heavier far tails for this parameterization.

Many return samples have positive sample excess kurtosis, but the estimate depends on the asset, frequency, window, and estimator. A large estimate is evidence that the sample's fourth standardized moment differs from the normal benchmark. Establishing how often specified market moves occur requires a named dataset and horizon, with any model and dependence assumptions stated explicitly; the synthetic sample used in this chapter cannot establish those empirical frequencies.

Visualizing the Distribution

Let's visualize these statistics with a histogram and overlay a normal distribution for comparison:

Out[15]:
Visualization
Histogram of daily returns with overlaid normal distribution curve.
Distribution of simulated daily log returns compared with a normal distribution having the same sample mean and standard deviation. Both the histogram and fitted curve come from the chapter's seeded synthetic example.

Summary Statistics Table

Let's create a function that computes all descriptive statistics in one place:

In[16]:
Code
def validate_return_sample(returns):
    """Require a finite, one-dimensional, nonconstant sample with n >= 2."""
    values = np.asarray(returns, dtype=float)
    if values.ndim != 1 or values.size < 2:
        raise ValueError(
            "returns must be a one-dimensional sample with at least two observations"
        )
    if not np.all(np.isfinite(values)):
        raise ValueError("returns must contain only finite observations")
    dispersion = np.std(values, ddof=1)
    if not np.isfinite(dispersion) or dispersion <= 0:
        raise ValueError(
            "returns must have positive, numerically finite sample dispersion"
        )
    return values


def validate_periods_per_year(periods_per_year):
    """Require a positive, finite scalar annualization factor."""
    factor = np.asarray(periods_per_year, dtype=float)
    if factor.ndim != 0 or not np.isfinite(factor) or factor <= 0:
        raise ValueError("periods_per_year must be a positive finite scalar")
    return float(factor)


def compute_return_statistics(
    returns, periods_per_year=252, return_convention="simple"
):
    """Summarize supplied returns; the convention labels them, not converts them."""
    returns = validate_return_sample(returns)
    periods_per_year = validate_periods_per_year(periods_per_year)
    if return_convention not in {"simple", "log"}:
        raise ValueError("return_convention must be 'simple' or 'log'")
    std_return = np.std(returns, ddof=1)
    result = {
        "Count": len(returns),
        "Mean (daily)": np.mean(returns),
        "Mean (annualized, linear)": np.mean(returns) * periods_per_year,
        "Std Dev (daily)": std_return,
        "Std Dev (annualized)": std_return * np.sqrt(periods_per_year),
        "Skewness": stats.skew(returns),
        "Excess Kurtosis": stats.kurtosis(returns),
        "Min": np.min(returns),
        "Max": np.max(returns),
        f"Sharpe Ratio ({return_convention} returns, zero reference)": (
            np.mean(returns) * periods_per_year
        )
        / (std_return * np.sqrt(periods_per_year)),
    }
    if not np.all(np.isfinite(list(result.values()))):
        raise ValueError(
            "the requested statistics are not numerically finite for these inputs"
        )
    return result


summary = compute_return_statistics(log_returns, return_convention="log")
Out[17]:
Console
Return Statistics Summary
----------------------------------------
Count: 251
Mean (daily): 0.000199
Mean (annualized, linear): 0.050049
Std Dev (daily): 0.019319
Std Dev (annualized): 0.306673
Skewness: 0.235344
Excess Kurtosis: 0.456209
Min: -0.053290
Max: 0.074694
Sharpe Ratio (log returns, zero reference): 0.163199

The helper requires at least two finite, one-dimensional observations and positive sample dispersion. It raises a descriptive error for a constant sample because the standardized moments and ratio are undefined. return_convention labels the supplied data; it does not transform simple returns into log returns.

The ratio included above is explicitly based on log returns with a zero reference. The conventional ex-post Sharpe ratio uses arithmetic excess returns relative to a matching-period risk-free return; the worked example later uses simulated simple returns and a zero reference. Write the matching-period differential return as dt=Rt−Rf,td_t=R_t-R_{f,t}. The annualized ex-post ratio is:

SR^annual=252 dˉsd\widehat{SR}_{annual} = \sqrt{252}\,\frac{\bar d}{s_d}

where:

  • dˉ\bar d: sample mean of matching-period differential returns
  • sds_d: positive sample standard deviation of those differential returns
  • 252\sqrt{252}: annualization factor for the stated daily-data convention

Subtract the reference before computing dispersion. With a constant daily reference cc, sds_d equals the portfolio's sample standard deviation and the linear annualized reference is 252c252c. With a time-varying reference, the differential-return volatility generally differs from portfolio-return volatility. Square-root annualization assumes uncorrelated differential returns with a common variance; it is not an exact volatility formula for compounded annual wealth.

The Sharpe ratio addresses a fundamental question in investing: how much return are you earning per unit of risk taken? A strategy with high returns might simply be taking excessive risk. The Sharpe ratio normalizes performance by volatility, allowing comparisons across strategies when the return horizon and conventions match. A Sharpe ratio of 1.0 is dimensionless: excess return equals volatility numerically at the same horizon. Whether that value is attractive depends on the horizon, estimation method, peer set, costs, and constraints.