statistics Module Complexity¶
The statistics module computes averages, spread, relations between two inputs, kernel density
estimates and normal-distribution quantities. What separates its functions is what they hold:
mean(), fmean(), geometric_mean() and the spread functions stream through their input keeping
only running sums, while the medians, quantiles and modes keep a copy or a count as large as the
data.
n is the data points (the length of data, or of each of x and y), u is the distinct
values among them, q is the number of intervals asked of a quantiles() method (its n
argument), s is the samples asked of NormalDist.samples() (its n argument), and w is the
data points within one bandwidth of the point a bounded-kernel estimate is evaluated at. Bounds
treat arithmetic, comparison and hashing of one data value as O(1), as they are for floats and
machine-size ints, and the bounds are for int and float data. The exact-arithmetic functions keep
one partial sum per distinct denominator, which stays bounded for ints and floats; Fraction data
can have n of them, and Fraction and Decimal arithmetic also grows with the digits. Space counts
what a call allocates, its result included, but not its input.
Complexity Reference¶
Averages and measures of central location¶
| Operation | Time | Space | Notes |
|---|---|---|---|
statistics.mean(data) |
O(n) | O(1) | One pass with exact arithmetic; accepts an iterator |
statistics.fmean(data, weights=None) |
O(n) | O(1), O(n) for weights that are not a list or tuple | One pass in floats, so it is faster than mean(); other weights, such as an iterator or a range, are copied to a list |
statistics.geometric_mean(data) |
O(n) | O(1) | Accepts an iterator |
statistics.harmonic_mean(data, weights=None) |
O(n) | O(1), O(n) for iterator data or weights | An iterator data or weights is copied to a list first |
statistics.median(data) |
O(n log n) | O(n) | Sorts a copy; O(n) time when data is already sorted |
statistics.median_low(data), statistics.median_high(data) |
O(n log n) | O(n) | As median(), returning a data point rather than an average |
statistics.median_grouped(data, interval=1.0) |
O(n log n) | O(n) | Sorts a copy, then two binary searches |
statistics.mode(data) |
O(n) | O(u) | Counts every value, then returns the first most common one |
statistics.multimode(data) |
O(n) | O(u) | Every value with the highest count, in first-seen order |
statistics.quantiles(data, *, n=4, method='exclusive') |
O(n log n + q) | O(n + q) | Sorts a copy, then interpolates q - 1 cut points |
Measures of spread¶
| Operation | Time | Space | Notes |
|---|---|---|---|
statistics.variance(data, xbar=None), statistics.pvariance(data, mu=None) |
O(n) | O(1) | One pass with exact arithmetic; accepts an iterator |
statistics.stdev(data, xbar=None), statistics.pstdev(data, mu=None) |
O(n) | O(1) | The variance's pass, then one square root |
Relations between two inputs¶
| Operation | Time | Space | Notes |
|---|---|---|---|
statistics.covariance(x, y, /) |
O(n) | O(1) | x and y must be sequences of equal length |
statistics.correlation(x, y, /, *, method='linear') |
O(n) | O(n) | Holds centred copies of both inputs |
statistics.correlation(x, y, method='ranked') |
O(n log n) | O(n) | Sorts both inputs to rank them |
statistics.linear_regression(x, y, /, *, proportional=False) |
O(n) | O(n) | Holds a centred copy of x; O(1) space with proportional=True |
statistics.LinearRegression, LinearRegression.slope, LinearRegression.intercept |
O(1) | O(1) | The named tuple linear_regression() returns |
Kernel density estimation¶
| Operation | Time | Space | Notes |
|---|---|---|---|
statistics.kde(data, h, kernel='normal', *, cumulative=False) |
O(1) | O(1) | For normal (or gauss), logistic and sigmoid, which weight every point; data is held by reference |
statistics.kde(data, h, kernel) with a bounded kernel |
O(n log n) | O(n) | Every other kernel: sorts a copy of data once |
Calling an estimate built with normal, gauss, logistic or sigmoid |
O(n) | O(1) | Sums a kernel term for every data point |
| Calling an estimate built with a bounded kernel | O(log n + w) | O(w) | Binary searches the sorted copy, then sums the w points in reach; a call after len(data) changes first re-sorts, O(n log n) time and O(n) space |
statistics.kde_random(data, h, kernel='normal', *, seed=None) |
O(1) | O(1) | data is held by reference |
Calling a kde_random() result |
O(1) | O(1) | One random data point plus one kernel offset; the pick indexes data, so O(1) holds for a list or tuple |
NormalDist¶
| Operation | Time | Space | Notes |
|---|---|---|---|
statistics.NormalDist(mu=0.0, sigma=1.0) |
O(1) | O(1) | Holds two floats |
NormalDist.from_samples(data) |
O(n) | O(1) | The mean and sample standard deviation in one pass |
NormalDist.samples(n, *, seed=None) |
O(s) | O(s) | Returns a list of s floats |
NormalDist.pdf(x), NormalDist.cdf(x), NormalDist.inv_cdf(p), NormalDist.zscore(x) |
O(1) | O(1) | Closed forms and a fixed rational approximation |
NormalDist.quantiles(n=4) |
O(q) | O(q) | q - 1 calls to inv_cdf() |
NormalDist.overlap(other) |
O(1) | O(1) | |
NormalDist.mean, NormalDist.median, NormalDist.mode, NormalDist.stdev, NormalDist.variance |
O(1) | O(1) | Read-only properties |
+ and - with a constant or another NormalDist, * and / by a constant, unary + and - |
O(1) | O(1) | Each returns a new NormalDist |
Exceptions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
statistics.StatisticsError |
O(1) | O(1) | A ValueError subclass, raised for empty data and other invalid inputs |
Streaming and Sorting¶
One Pass or a Sorted Copy¶
mean(), fmean(), geometric_mean() and the variance family keep running sums and never hold
the data, so a generator over a large source costs O(1) memory on Python 3.11+. The median family
and quantiles() sort a copy first, so they hold all n values whatever you pass.
import statistics
def readings():
for i in range(1, 10_001):
yield i % 7
assert statistics.mean(readings()) == 2.9998 # O(n) time, O(1) memory
assert statistics.fmean(readings()) == 2.9998 # O(n) time, O(1) memory
assert round(statistics.pvariance(readings()), 3) == 3.999 # O(n) time, O(1) memory
assert statistics.median(readings()) == 3 # O(n log n) time, O(n) memory
assert statistics.quantiles(readings(), n=4) == [1.0, 3.0, 5.0] # O(n log n + q)
Sorted Input¶
The sort is Timsort, which finishes in a single linear scan on data that is already in order. So when several order statistics are needed from the same data, sort it once: every later call still copies it, but each copy sorts in O(n).
import statistics
data = [7, 1, 9, 3, 5, 3, 8]
ordered = sorted(data) # O(n log n), once
assert statistics.median(ordered) == 5 # O(n) - already sorted
assert statistics.median_low([1, 2, 3, 4]) == 2
assert statistics.median_high([1, 2, 3, 4]) == 3
assert statistics.median([1, 2, 3, 4]) == 2.5 # an even count averages the middle two
assert statistics.quantiles(ordered, n=4) == [3.0, 5.0, 8.0] # O(n + q)
Exact and Float Averages¶
mean(), variance() and pvariance() compute in exact integer ratios, and return a Fraction
for Fraction data and a Decimal for Decimal data. fmean() stays in floats and always
returns one. Both kinds are one pass, but the exact
arithmetic makes mean() the slower of the two.
import statistics
from fractions import Fraction
assert statistics.mean([1, 2, 3, 4]) == 2.5 # O(n)
assert statistics.mean([Fraction(1, 3), Fraction(2, 3)]) == Fraction(1, 2)
assert statistics.fmean([1, 2, 3, 4]) == 2.5 # O(n), always a float
assert statistics.geometric_mean([4, 9]) == 6.0 # O(n)
assert statistics.harmonic_mean([40, 60]) == 48.0 # O(n)
assert statistics.variance([1, 2, 3, 4, 5]) == 2.5 # O(n)
assert statistics.pstdev([2, 4, 4, 4, 5, 5, 7, 9]) == 2.0 # O(n)
assert round(statistics.stdev([1, 2, 3, 4, 5]), 4) == 1.5811 # O(n)
Counting Modes¶
mode() and multimode() count every value in a dictionary, so memory follows the distinct
values rather than the data. A tie is not an error: mode() returns the value seen first, and
multimode() returns all of them.
import statistics
assert statistics.mode(['red', 'blue', 'red']) == 'red' # O(n) time, O(u) memory
assert statistics.mode([1, 1, 2, 2]) == 1 # a tie returns the first seen
assert statistics.multimode([1, 1, 2, 2, 3]) == [1, 2] # O(n) time, O(u) memory
assert statistics.multimode([]) == []
try:
statistics.mode([])
except statistics.StatisticsError as error:
assert 'no mode' in str(error)
else:
raise AssertionError('mode() of empty data returned')
Relations Between Two Inputs¶
covariance(), correlation() and linear_regression() take len() of both inputs, so they
need sequences, not iterators. The ranked correlation replaces each input by its ranks, which is a
sort.
import statistics
x = [1, 2, 3, 4, 5]
y = [2, 4, 5, 4, 5]
assert statistics.covariance(x, y) == 1.5 # O(n)
assert round(statistics.correlation(x, y), 4) == 0.7746 # O(n)
assert statistics.correlation(x, [v ** 3 for v in x], method='ranked') == 1.0 # O(n log n)
fit = statistics.linear_regression(x, y) # O(n)
assert (fit.slope, fit.intercept) == (0.6, 2.2)
assert statistics.linear_regression(x, [2 * v for v in x], proportional=True).slope == 2.0
try:
statistics.covariance(iter(x), iter(y))
except TypeError as error:
assert 'len()' in str(error)
else:
raise AssertionError('covariance() accepted iterators')
Kernel Density Estimates¶
kde() returns a function, and the choice of kernel decides where the cost lands. The normal (or
gauss), logistic and sigmoid kernels give every point some weight, so building the estimate is
free and every evaluation sums over all n points. The bounded kernels sort a copy once, and each
evaluation then looks only at the points within the bandwidth.
import statistics
data = [-2.1, -1.3, -0.4, 1.9, 5.1, 6.2]
f = statistics.kde(data, h=1.5) # O(1) - normal kernel
assert round(f(0.0), 4) == 0.1099 # O(n) per call
g = statistics.kde(data, h=1.5, kernel='triangular') # O(n log n) - sorts once
assert round(g(0.0), 4) == 0.0963 # O(log n + w) per call
assert g(100.0) == 0.0 # nothing within reach
cdf = statistics.kde(data, h=1.5, kernel='triangular', cumulative=True)
assert cdf(100.0) == 1.0
draw = statistics.kde_random(data, h=1.5, seed=8675309) # O(1)
assert isinstance(draw(), float) # O(1) per call
Normal Distributions¶
NormalDist holds a mean and a standard deviation and nothing else. Fitting one to data is a
single pass; after that every probability, inv_cdf() value and combination is O(1), however
large the data it came from.
import statistics
from statistics import NormalDist
heights = NormalDist.from_samples([168, 172, 175, 181, 169, 177]) # O(n)
assert round(heights.mean, 1) == 173.7 # O(1)
iq = NormalDist(100, 15) # O(1)
assert round(iq.cdf(130), 4) == 0.9772 # O(1)
assert round(iq.inv_cdf(0.5), 6) == 100.0 # O(1)
assert round(iq.pdf(100), 5) == 0.0266 # O(1)
assert iq.zscore(130) == 2.0 # O(1)
assert [round(v, 2) for v in iq.quantiles(4)] == [89.88, 100.0, 110.12] # O(q)
assert round(iq.overlap(NormalDist(110, 15)), 4) == 0.7389 # O(1)
combined = NormalDist(10, 3) + NormalDist(20, 4) # O(1)
assert (combined.mean, combined.stdev) == (30.0, 5.0)
assert (2 * NormalDist(1, 1)).variance == 4.0
draws = NormalDist(0, 1).samples(1_000, seed=42) # O(s)
assert len(draws) == 1_000
try:
NormalDist(0, 0).pdf(0)
except statistics.StatisticsError as error:
assert 'sigma is zero' in str(error)
else:
raise AssertionError('pdf() of a zero-width distribution returned')
Common Patterns¶
Summarising a Sample¶
import statistics
def summarise(data):
ordered = sorted(data) # O(n log n), once
return {
'mean': statistics.fmean(ordered), # O(n)
'stdev': statistics.stdev(ordered), # O(n)
'median': statistics.median(ordered), # O(n) - already sorted
'quartiles': statistics.quantiles(ordered), # O(n + q)
'min': ordered[0], # O(1)
'max': ordered[-1], # O(1)
}
summary = summarise([10, 20, 30, 40, 50])
assert summary['median'] == 30
assert summary['quartiles'] == [15.0, 30.0, 45.0]
assert (summary['min'], summary['max']) == (10, 50)
Flagging Outliers¶
from statistics import NormalDist
baseline = [9.8, 10.1, 10.0, 9.9, 10.2, 10.0]
model = NormalDist.from_samples(baseline) # O(n), once
readings = [10.1, 12.5, 9.9]
outliers = [r for r in readings if abs(model.zscore(r)) > 3] # O(1) per reading
assert outliers == [12.5]
Performance Best Practices¶
✅ Do:
- Pass a generator to
mean(),fmean()orvariance()when the data does not fit in memory; on Python 3.11+ they hold running sums, not values - Use
fmean()when a float result is enough; it skips the exact arithmeticmean()does - Sort once and pass the sorted list when you need several of
median(),median_low(),median_high()andquantiles() - Fit a
NormalDistonce and reuse it: afterfrom_samples(), eachpdf(),cdf(),inv_cdf()andzscore()is O(1) - Choose a bounded kernel for
kde()when the estimate is evaluated many times over large data
❌ Avoid:
- Computing a median of a stream you only need the mean of: it holds all n values
- Appending to the data behind a bounded-kernel
kde()estimate between calls; each call after a length change re-sorts it
Version Notes¶
- Python 3.11+:
mean(),variance(),pvariance(),stdev(),pstdev()andNormalDist.from_samples()read an iterator in one pass; on 3.10 they copy it to a list first, O(n) space - Python 3.11+:
multimode()no longer sorts the counts: O(n) rather than O(n + u log u) - Python 3.11+:
fmean()acceptsweights, andlinear_regression()acceptsproportional - Python 3.12+:
correlation()acceptsmethod='ranked'; it andlinear_regression()hold centred copies of their inputs, O(n) space where 3.10 and 3.11 used O(1) - Python 3.12+:
fmean()copies any weights that are not a list or tuple; 3.11 copies only iterator weights, and arangecosts O(1) space - Python 3.13+: Added
kde()andkde_random() - Python 3.13+:
quantiles()of a single data point returns q - 1 copies of it instead of raisingStatisticsError