Skip to content

Statistical Operations

Once numerical data have been created or imported, the next step is often to summarise them. NumPy provides functions for calculating the mean, standard deviation, minimum, maximum, and percentiles, the everyday building blocks of descriptive statistics.

These operations are especially useful in finance. For example, the mean can summarise average returns, the standard deviation can be interpreted as a measure of volatility (how much returns tend to swing around that average), and percentiles can help describe downside risk, for example, "what's the worst 5% of outcomes likely to look like?"

Python Code Examples: Statistical Operations

A quick note on floating-point precision

Before diving into statistics, it's worth being aware of a subtle quirk in how computers represent decimal numbers, since it occasionally causes small, confusing surprises:

import numpy as np

# Depending on tiny floating-point rounding errors, the array below may stop
# just short of 5/3, or may include it, even though the maths looks identical
print(np.arange(0, 5/3, 1/3))
print(np.arange(0, 5/3, 0.333333333))
print(np.arange(0, 5/3, 0.333333334))

This happens because computers store decimal numbers in binary, which can't represent values like 1/3 with perfect precision. It's rarely a serious problem, but it's a useful thing to have seen once, so it doesn't catch you off guard later when a boundary case behaves unexpectedly.

Basic Summary Statistics

a = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
print(a)

print("mean =", a.mean())

# Looping through several summary statistics at once
for func in (a.min, a.max, a.sum, a.prod, a.std, a.var):
    print(func.__name__, "=", func())

Here, a.min() and a.max() give the smallest and largest values, a.sum() and a.prod() give the total and the product of all elements, and a.std() and a.var() give the standard deviation and variance, a measure closely related to standard deviation, but expressed in squared units.

Summarising Along a Specific Axis

For multi-dimensional arrays, you can choose which direction to summarise across, rather than collapsing the whole array into a single number:

c = np.arange(24).reshape(4, 2, 3)   # a 3D array: 4 matrices, each 2 rows x 3 columns

print(c.sum(axis=0))        # sums corresponding elements across all 4 matrices
print(c.sum(axis=1))        # sums across rows, within each matrix
print(c.sum(axis=(0, 2)))    # sums across both matrices and columns at once

Getting comfortable with the axis argument is one of the more important skills in NumPy, axis=0 generally refers to the outermost dimension (often rows, or "which matrix" in higher dimensions), and increasing axis numbers move inward from there.

Element-wise Comparison and Matrix Summary Functions

a = np.array([1, 5, 2])
b = np.array([3, 2, 4])

# np.maximum() compares two arrays element by element, keeping the larger value each time
print(np.maximum(a, b))     # [3 5 4]

from numpy import linalg

m3 = np.array([[1, 2, 3], [5, 7, 11], [21, 29, 31]])

print(linalg.det(m3))        # the determinant of the matrix, a single summary number
                              # used in various linear algebra and statistical calculations

print(m3.trace())            # the sum of the diagonal elements
                              # equivalent to np.diag(m3).sum()

A Worked Example: Summarising Simulated Data

Bringing several of these ideas together, here's a more complete example in the style you'd use when summarising simulated financial data, such as simulated returns across several assets (columns) and time periods (rows):

Array_1 = np.array([[1, 2, 3], [4, 5, 6]])
Array_2 = np.array([[10, 20, 30], [40, 50, 60]])

Array_sum = Array_1 + Array_2
Array_sum_shape = Array_sum.shape

print(f'Array_sum = {Array_sum}, Shape = {Array_sum_shape}')

# Simulate a 50 x 10 matrix, e.g. 50 time periods across 10 simulated assets
np.random.seed(1)
Y = np.random.normal(0, 1, size=(50, 10))

# Mean: overall, by column, and by row
Y_mean = Y.mean()                  # a single number: the mean of every value in Y
Y_column_mean = Y.mean(axis=0)     # one mean per column (e.g. per asset)
Y_row_mean = Y.mean(axis=1)        # one mean per row (e.g. per time period)

print(f'Entire Mean = {Y_mean}, Column Mean = {Y_column_mean}, Row Mean = {Y_row_mean}')

# Standard deviation: overall, by column, and by row
Y_std = Y.std()
Y_column_std = Y.std(axis=0)
Y_row_std = Y.std(axis=1)

print(f'Entire Std = {Y_std}, Column Std = {Y_column_std}, Row Std = {Y_row_std}')

# Minimum and maximum: overall, by column, and by row
Y_min = Y.min()
Y_column_min = Y.min(axis=0)
Y_row_min = Y.min(axis=1)

Y_max = Y.max()
Y_column_max = Y.max(axis=0)
Y_row_max = Y.max(axis=1)

# '\n' inserts a line break when printed, useful for separating output visually
print(f'Entire Min = {Y_min}, Column Min = {Y_column_min}, Row Min = {Y_row_min}\n\n'
      f'Entire Max = {Y_max}, Column Max = {Y_column_max}, Row Max = {Y_row_max}')

Reading this example: notice the recurring pattern, calling a statistical method with no arguments summarises the entire array into one number, adding axis=0 summarises down each column, and axis=1 summarises across each row. Once this pattern clicks for one statistic (like the mean), it applies identically to nearly every other NumPy statistical function.

Percentiles

Percentiles describe the value below which a given percentage of observations fall, for example, the 50th percentile (the median) is the value that splits the data exactly in half.

# The 50th percentile, i.e. the median
Y_P50 = np.percentile(Y, 50)
Y_column_P50 = np.percentile(Y, 50, axis=0)
Y_row_P50 = np.percentile(Y, 50, axis=1)

print(f'Entire P50 = {Y_P50}, Column P50 = {Y_column_P50}, Row P50 = {Y_row_P50}')

# Multiple percentiles can be requested at once by passing a list
Percentile_List = [1, 5, 10, 25, 50, 75, 90, 95, 99]

# Percentile values are returned in the same order as Percentile_List
Y_Percentile = np.percentile(Y, Percentile_List)
Y_column_Percentile = np.percentile(Y, Percentile_List, axis=0)
Y_row_Percentile = np.percentile(Y, Percentile_List, axis=1)

print(f'Entire % = {Y_Percentile}, Column % = {Y_column_Percentile}, Row % = {Y_row_Percentile}')

Why this matters in finance: percentiles are the backbone of many risk measures. For example, the 5th percentile of a set of simulated portfolio returns is closely related to a common risk metric called Value at Risk (VaR), it tells you the return level below which only 5% of simulated outcomes fall, giving a sense of "how bad could things realistically get?" without needing to examine every single simulated scenario individually.