DataLens

Aggregations (sum, mean, and the axis argument)

Summarise arrays as a whole, per row or per column, find where the maximum is, and handle missing values.

Level: Intermediate · 9 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

An aggregation turns many numbers into fewer: a total, an average, a maximum. On a 1-D array that's one number. On a 2-D grid you have a choice: summarise everything, each row, or each column. The axis argument makes that choice, and once it clicks, a whole family of questions becomes one short line.

import numpy as np

stores = ["Jakarta", "Bandung", "Surabaya", "Medan"]
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
units = np.array([
    [120, 135, 150, 160, 155, 170],
    [ 80,  85,  90,  88,  95, 100],
    [ 95,  92, 105, 110, 108, 115],
    [ 40,  45,  43,  50,  52,  55],
])

The whole array

Start with the simplest case, summarising every number in the grid. How many units did all four stores sell over the six months, and what was the smallest and largest monthly figure for any store?

print(units.sum(), units.mean(), units.min(), units.max())
print(units.std().round(1))
2338 97.41666666666667 40 170
37.4

Each method has a function twin: units.sum() is the same as np.sum(units). A few useful ones exist only as functions, like np.median and np.percentile:

print(np.median(units), np.percentile(units, 90))
95.0 153.5

The axis argument

axis says which dimension gets squashed:

print(units.sum(axis=0))   # total per month
print(units.sum(axis=1))   # total per store
[335 357 388 408 410 440]
[890 538 625 285]
Key idea

axis=0 ↓ (down the rows, result per column). axis=1 → (across the columns, result per row). The axis you name is the one that disappears: (4, 6) summed over axis 0 becomes (6,); over axis 1 it becomes (4,).

Every aggregation takes axis:

print(units.mean(axis=1).round(1))   # average month per store
print(units.max(axis=0))             # best store's units in each month
[148.3  89.7 104.2  47.5]
[120 135 150 160 155 170]

Where is the maximum? argmax and argmin

.max() gives the biggest value; .argmax() gives its position. Combined with your label list, that answers "which one?":

print(units.argmax(axis=1))     # position of each store's best month

weakest = units.argmin(axis=1)  # position of each store's weakest month
for store, m in zip(stores, weakest):
    print(f"{store}: weakest month {months[m]}")
[5 5 5 5]
Jakarta: weakest month Jan
Bandung: weakest month Jan
Surabaya: weakest month Feb
Medan: weakest month Jan

Every store peaked in June (position 5), a sign of steady growth. Their weakest months differ. The loop here only formats the output; the calculation itself is one vectorized call. You can even skip the loop with fancy indexing: np.array(months)[weakest].

Shares of a total: keepdims

What share of each store's half-year did each month contribute? Divide each row by its own total. But units.sum(axis=1) has shape (4,), which won't broadcast against (4, 6) row by row (you saw that error last module). keepdims=True keeps the squashed axis as size 1:

totals = units.sum(axis=1, keepdims=True)
print(totals.shape)
share = units / totals
print((share * 100).round(1))
(4, 1)
[[13.5 15.2 16.9 18.  17.4 19.1]
 [14.9 15.8 16.7 16.4 17.7 18.6]
 [15.2 14.7 16.8 17.6 17.3 18.4]
 [14.  15.8 15.1 17.5 18.2 19.3]]

With shape (4, 1), the totals broadcast across each row. Each row now adds up to 100%.

Running totals: cumsum

cumsum gives a running total along an axis, the NumPy version of a cumulative column in Excel:

cumulative = units.cumsum(axis=1)
print(cumulative)
[[120 255 405 565 720 890]
 [ 80 165 255 343 438 538]
 [ 95 187 292 402 510 625]
 [ 40  85 128 178 230 285]]

Combine it with a mask and argmax to answer "in which month did each store pass 400 units?":

passed = cumulative >= 400
print(passed.argmax(axis=1))
[2 4 3 0]

argmax on booleans returns the position of the first True. Careful: if a row has no True at all (Medan never reaches 400 in this window), argmax returns 0, which looks like "January". Check with passed.any(axis=1) before trusting the answer:

print(passed.any(axis=1))
[ True  True  True False]

Missing values: nan-aware functions

Real data has gaps. A customer who didn't leave a rating has no rating, and NumPy represents that with np.nan, which means "not a number". The problem is that nan spreads: any ordinary calculation that touches it comes out as nan too, so one missing rating ruins the average:

ratings = np.array([4.5, 5.0, np.nan, 3.5, 4.0])
print(ratings.mean())
print(np.nanmean(ratings), np.nansum(ratings), np.nanmax(ratings))
nan
4.25 17.0 5.0

np.nanmean, np.nansum, np.nanmax and friends skip missing values, like SQL's AVG and pandas' default .mean().

Why this matters for clean code

units.sum(axis=1) replaces a loop that builds a list of totals, and it says exactly what it computes: totals per store. Name the result for what it is (store_totals, best_month), use keepdims=True when you'll divide by it, and guard argmax on masks with .any(). These small habits prevent the silent wrong answers that are hardest to catch.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Reshaping, stacking, and splitting →