Topic 14.4
Aggregations, Statistics and Random Numbers
In one line
Aggregations like sum, mean, std and argmax collapse an array along an axis into fewer numbers. A seeded random generator, np.random.default_rng(seed), gives random-looking data that you can repeat exactly.
Think of it like this
A class mark sheet. The total of one student is a sum across the columns. The class average of one subject is a mean down the rows. "Who got the highest mark?" is an argmax. Aggregations answer questions like these, and axis says which direction you add along.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Aggregation (reduction)
- An operation that collapses many numbers into fewer, like sum or mean.
- Mean
- The average: the sum divided by the count.
- Standard deviation (std)
- How spread out numbers are around their mean.
- argmax / argmin
- The position of the largest or smallest value.
- np.where
- A vector if/else: choose between two values per element, based on a condition.
- Seed
- A starting number for a random generator. The same seed gives the same sequence.
- Generator (rng)
- The object from np.random.default_rng that produces random numbers.
- keepdims
- Keep the reduced axis as size 1 so the result can broadcast back.
Step by step
01Totals, means and spread
For m = [[3, 1, 2], [9, 7, 8]], m.sum() adds all six numbers. m.mean(axis=0) gives one mean per column and m.mean(axis=1) one per row. m.std(axis=1) gives the spread of each row. The axis you name is the one that disappears.
m = np.array([[3.0, 1.0, 2.0],
[9.0, 7.0, 8.0]])
print(m.mean(axis=0)) # [6. 4. 5.]
print(m.mean(axis=1)) # [2. 8.]02Which one is biggest?
m.argmax(axis=1) returns the column index of the largest value in each row: [0 0]. Without an axis, argmin works on the flattened array and returns a position in that flat order (1 here). np.argsort(m[0]) returns [1 2 0]: the order of positions that sorts the row ascending.
03np.where as a vector if
np.where(m > 5, "high", "low") builds an array of labels in one go. With a number in both slots it replaces the values: np.where(m > 5, m, 0.0) keeps the big values and zeroes the rest.
04Reproducible randomness
Two generators created from seed 42 produce the same numbers, so any experiment that uses one can be re-run. Choose a seed once at the top, pass rng into functions that need randomness, and write the seed in your notes.
Different methods draw different kinds of numbers: random for floats in [0, 1), integers(low, high) with high excluded, normal for a bell curve, choice for sampling and permutation for shuffling.
rng = np.random.default_rng(42)
print(rng.random(3).round(4)) # same on every run05The law of large numbers, in miniature
A thousand draws from a normal distribution with mean 50 and std 10 give a sample mean close to 50 but not exactly. The more draws, the closer you get. Statistics on samples are estimates, which is why a model's score on 100 test examples is a rougher guide than on 100,000.
Try it yourself
- 1
Rank the scores
With
s = np.array([72, 55, 90, 41]), printnp.argsort(-s). What does the first number mean? (It is the position of the best score.) - 2
Roll a die 6000 times
Use
rng.integers(1, 7, size=6000)andnp.bincounton the result (np.bincountcounts how many times each whole number appears). Each face should come up near 1000 times, but not exactly. - 3
Same seed, same shuffle
Call
np.random.default_rng(1).permutation(5)twice in a row and compare. Then create one rng and callpermutation(5)twice on it. Why do the two calls differ?
Code & diagrams
Add axis=0 to argmax and predict the three indices.
Expected output
sum: 30.0 mean: 5.0
mean axis 0: [6. 4. 5.]
mean axis 1: [2. 8.]
std axis 1: [0.816 0.816]
argmax axis 1: [0 0] argmin: 1
min/max: 1.0 9.0
keepdims: [[2.0], [8.0]]
where: [['low', 'low', 'low'], ['high', 'high', 'high']]
where 3 args: [[0.0, 0.0, 0.0], [9.0, 7.0, 8.0]]
sorted idx: [1 2 0]Change the seed 42 to 43 and see every number change; keep 42 and the output never changes.
Expected output
same seed A: [0.774 0.4389 0.8586]
same seed B: [0.774 0.4389 0.8586]
integers: [6 4 5 6 4]
normal: [-0.891 -0.455 -0.992]
choice: ['d' 'c']
permutation: [3 8 4 6 5 2 0 7 1 9]
mean ~50: 49.33 std ~10: 9.45Some systems store "no reading" as a magic number such as -999. Averaging it in gives a nonsense mean. Convert it to NaN first, then use the nan-aware functions. A NaN in a plain mean makes the whole answer NaN, which is NumPy telling you that data is missing.
Expected output
naive mean: -313.1666666666667
with NaN: [31.5 nan 29. 30.5 nan 28. ]
mean of NaN data: nan
nanmean: 29.75
missing count: 2
NaN == NaN: False
argmax with NaN: 1 nanargmax: 0
max with NaN: nan nanmax: 31.5Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Expecting random to repeat without a seed
Compare two unseeded generators.
import numpy as np
a = np.random.default_rng().random(2)
b = np.random.default_rng().random(2)
print(np.array_equal(a, b))Myth vs fact
Myth
A seeded generator is not random.
Fact
It produces a fixed pseudo-random sequence that passes statistical tests but repeats given the same seed. That is exactly what testing needs. It is not suitable for passwords: use the secrets module.
Myth
np.std and pandas std are the same.
Fact
NumPy divides by n by default; pandas uses n - 1. Use ddof=1 in NumPy to match.
Myth
argmax returns the maximum.
Fact
It returns the position of the maximum. Use max for the value.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
default_rnguses the PCG64 bit generator and supports independent child streams withSeedSequence.spawn, so parallel workers can have separate, reproducible streams. Seeding each worker with the same number would give identical data. - ▸
Floating-point sums depend on the order of addition. NumPy uses pairwise summation, which is more accurate than a naive loop, but results can differ slightly between machines and array layouts, so compare floats with
np.allclose, never==. - ▸
Aggregations over a very large integer array can overflow its dtype (Topic 14.1).
np.meanon integers computes infloat64, whilenp.sumkeeps the integer type. Cast tofloat64orint64for large totals. - ▸
NaN propagates. Any arithmetic with
NaNgivesNaN, sonp.meanof data with one gap isnan, andNaN == NaNisFalse(test withnp.isnan). Thenan-aware family (np.nanmean,np.nansum,np.nanstd,np.nanmax,np.nanargmax) skips the gaps. Plainargmaxandmaxon data with NaN return the NaN, which is usually a bug. pandas does the opposite by default (skipna=True), see Topic 14.7. Integer arrays cannot hold NaN at all, which is why a column with a gap becomes float. - ▸
argmaxandargminreturn the first position when there are ties, andargsortis not stable by default: passkind="stable"when equal scores must keep their original order, for example when ranking search results.np.argpartition(-scores, k)[:k]finds the top k without fully sorting, which is cheaper for millions of items but gives them unordered. - ▸
The legacy API (
np.random.seed,np.random.rand,RandomState) shares one hidden global generator.RandomStatepromises the same stream forever, which is the reason old code and tutorials still use it. For new code usedefault_rng, and pass the generator explicitly so tests, threads and libraries do not disturb each other.
Remember this
- 1
The core aggregations are
sum,mean,min,max,stdandvar. The mean is the usual average: add everything up, divide by how many there are. The standard deviation (std) says how spread out the numbers are around the mean. Picture two classes that both average 50 marks. In class A everyone scored 48 to 52, so the std is small. In class B half scored 10 and half scored 90, so the std is large. The variance (var) is the std squared. Each works on the whole array by default and returns one number, or along oneaxis, returning an array with that axis removed. They exist as methods (m.sum()) and as functions (np.sum(m)). - 2
argmaxandargminreturn the position of the largest or smallest value, not the value. Along an axis they give one position per row or column.np.argsort(a)gives the positions that would sorta, which is how you rank items and how top-k search works in Topic 14.5. - 3
np.where(condition, x, y)is a vectorif: it picksxwhere the condition is true andyelsewhere. With only a condition it returns the positions where it is true. Use it to label data ("high"or"low") or to replace values without a loop. - 4
keepdims=Truekeeps the reduced axis with size 1. That makes the result broadcast back against the original array, for examplem - m.mean(axis=1, keepdims=True)centres every row. Without it you would need a reshape. - 5
Random numbers: create a generator with
rng = np.random.default_rng(seed), then callrng.random(3)(floats from 0 to 1),rng.integers(1, 7, size=5)(dice rolls, stop excluded),rng.normal(loc, scale, size)(numbers that cluster aroundlocand thin out symmetrically, the bell-curve distribution;scaleis the std),rng.choice(items, size, replace=False)(note that the default isreplace=True, so the same item can be drawn twice unless you say otherwise) andrng.permutation(n)(a shuffled 0..n-1). The same seed gives the same numbers on every run, which is how you make experiments reproducible. Keep the NumPy version fixed too: NumPy does not promise that aGeneratorproduces the same stream in every release. - 6
Prefer
default_rngover the oldernp.random.seed(...)andnp.random.randglobal functions. A generator is an object you pass around, so two parts of a program cannot disturb each other's random streams. For machine learning,default_rng(seed)is how you shuffle data into train and test sets repeatably (see the exercises). - 7
Statistics with a catch:
np.stddivides bynby default (it treats your numbers as the whole population), while pandasSeries.stddivides byn - 1(it treats them as a sample drawn from a bigger group). They give different numbers for the same data. Know which one a library uses. - 8
Recall from Topic 14.2:
axisis the direction you work along, and the axis you name disappears from the result's shape. Everything below uses that rule.
Explain it without notes
What does axis mean in m.sum(axis=0) versus m.sum(axis=1) for a table?
What is the difference between max and argmax, and where is argsort used?
Why use np.random.default_rng(seed) rather than unseeded randomness in a data experiment?
What does np.where(cond, a, b) do and why is it better than a loop?
A sensor file has gaps stored as -999. Why is values.mean() wrong, and what do you do instead?
Why do np.std and pandas Series.std disagree on the same data?
Practice
For m = np.array([[1.0, 2.0], [3.0, 4.0]]), print the column means and the row sums.
Using default_rng(0), make an array of 5 dice rolls and print how many are above 3.
Replace negative numbers in x = np.array([-2.0, 3.0, -1.0, 5.0]) by 0 using np.where.
For x = np.array([2.0, 4.0, 6.0, 8.0, 10.0]) print the standard deviation the NumPy way and the pandas way (the sample version), rounded to 3 places.
Trade-offs
- ↔
Population versus sample statistics.
ddof=0andddof=1differ; for tiny samples the gap is large. - ↔
Reproducibility versus unpredictability. Seeds are ideal for tests and papers and wrong for security.
- ↔
Simple aggregates hide structure. A mean can hide a distribution (the way values are spread out) with two humps, such as a class where half scored 10 and half scored 90. Look at a histogram (Topic 14.9) too.
Done when you can
Done when you can use
sum,mean,std,minandmaxwith and withoutaxis.Done when you can explain
argmaxand rank items withargsort.Done when you can write an
np.wherethat labels or replaces values.Done when you can create a seeded generator and draw floats, integers, normals, choices and shuffles.
Done when you can explain why a seed makes results repeatable but not secure.