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:
axis=0squashes the rows (goes down each column): one result per column, here per month.axis=1squashes the columns (goes across each row): one result per row, here per store.
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]
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
.sum(),.mean(),.min(),.max(),.std(),np.median,np.percentilesummarise arrays.axis=0gives one result per column;axis=1one per row. The named axis disappears..argmax()/.argmin()give the position of the extreme value.keepdims=Truekeeps the shape broadcast-ready, e.g. for shares of a row total..cumsum(axis=...)gives running totals;argmaxon a mask finds the firstTrue(check.any()).np.nanmean,np.nansum... ignorenan.