sum, mean, max - and the one argument that decides what they mean.
Overview
The default collapses everything
Aggregation functions reduce an array to a smaller result. sum adds all elements. mean computes the average. min and max find the extremes. By default, each of these operates on the entire array and returns a single number, regardless of the array's shape.
That default is often not what you want. A 2-D array of sales by city and month needs a sum per city, not one grand total. The axis parameter tells the aggregation which dimension to collapse. axis=0 collapses rows, giving one result per column. axis=1 collapses columns, giving one result per row.
The axis convention trips people up because collapsing along axis 0 gives a result with as many entries as there are columns, not as many as there are rows. The way to remember it is that the axis number is the dimension that disappears. axis=0 removes the row dimension. The columns survive.
keepdims preserves the collapsed dimension as a size-one axis instead of removing it. That is useful when you want to broadcast the result back against the original array. A column mean with keepdims=True has shape (1, n) and can be subtracted from the original (m, n) array directly. Without keepdims, the result has shape (n,), and the broadcast rules treat it as a row, not a column.
NumPy provides both function and method forms for most aggregations. np.sum(a) and a.sum() do the same thing. The method form is shorter. The function form accepts array-like inputs that are not yet arrays. Use whichever reads better in context.
Some aggregations have nan-aware variants. np.nansum ignores NaN values instead of propagating them to the result. np.nanmean does the same for the mean. The regular versions return NaN if any element is NaN, which is correct but often not what you want when the NaN represents missing data.
This module covers the common aggregation functions, the axis parameter, keepdims, and the nan-aware variants. The examples use 2-D arrays so that the axis behaviour is visible. Understanding axis here carries over directly to pandas, where the same parameter does the same thing.
a.sum() with no args adds each element, what the shape, and returns a single value.
That value is a NumPy scalar rather than a Python int or float. It behaves like one almost all over. Wrap it in int() or float() when you need a genuine Python number, for JSON or for a format string that cares.
Worth knowing
With no axis, a reduction collapses the whole array to a single value.
axis names the axis that disappears. axis=0 collapses the rows and gives one value per column.
keepdims=True leaves the reduced axis as length 1, which is what lets the result broadcast back against the original.
A tuple of axes reduces several at once — axis=(1,2) on a stack gives one value per frame.
argmax on a multi-dimensional array returns a flat position; np.unravel_index turns it into coordinates.
Ties go to the first occurrence, and cumsum keeps the shape rather than reducing it.
Aggregations and axis
sum, mean and max - and the one arg that decides what they mean.
Without an axis, everything reduces
The default collapses the whole array to one number, whatever its shape.
example_01.pyNumPy
import numpy as np
a = np.arange(12).reshape(3, 4)
print(a)
print()
print("sum :", int(a.sum()))
print("mean :", float(a.mean()))
print("min :", int(a.min()), " max:", int(a.max()))
print("std :", round(float(a.std()), 4))
print("prod :", int(np.arange(1, 6).prod()), "<- 5!")
print()
print("the result is a 0-d array, not a Python number:")
print(" type :", type(a.sum()).__name__, " shape:", a.sum().shape)
Output
axis names the axis that DISAPPEARS
That is the whole rule, and it is the opposite of how most people first read it.
example_02.pyNumPy
import numpy as np
a = np.arange(12).reshape(3, 4)
print("a has shape", a.shape)
print(a)
print()
print("a.sum(axis=0) ->", a.sum(axis=0).shape, ":", a.sum(axis=0))
print(" axis 0 (the rows) is gone; one value per COLUMN")
print()
print("a.sum(axis=1) ->", a.sum(axis=1).shape, ":", a.sum(axis=1))
print(" axis 1 (the columns) is gone; one value per ROW")
print()
print("Read it as: which axis am I collapsing?")
Output
keepdims, and why it exists
Keeping the reduced axis as length 1 is what lets the result broadcast back against the original.
example_03.pyNumPy
import numpy as np
a = np.arange(12).reshape(3, 4).astype(float)
row_means = a.mean(axis=1)
print("row means :", row_means, "shape", row_means.shape)
try:
a - row_means
except ValueError as e:
print("a - row_means :", type(e).__name__, "- 4 against 3")
kept = a.mean(axis=1, keepdims=True)
print()
print("with keepdims :", kept.shape)
print("a - kept works :", (a - kept).shape)
print(np.round(a - kept, 2))
print()
print("Centring by column needs no keepdims, because (4,) already aligns:")
print(" ", (a - a.mean(axis=0)).shape)
Output
Three dimensions, and a tuple of axes
The rule is the same however many axes there are, and you can collapse several at once.
example_04.pyNumPy
import numpy as np
a = np.arange(24).reshape(2, 3, 4)
print("shape", a.shape)
print()
for ax in [0, 1, 2, (0, 1), (1, 2), None]:
r = a.sum(axis=ax)
print("sum(axis=%-7s) -> shape %s" % (str(ax), r.shape))
print()
print("Think of an image stack (frames, rows, cols):")
print(" mean over frames -> axis=0 ->", a.mean(axis=0).shape)
print(" mean per frame -> axis=(1,2) ->", a.mean(axis=(1, 2)).shape)
Output
argmin and argmax give positions
And on more than one dimension they give a flat position, which you unpack with unravel_index.
example_05.pyNumPy
import numpy as np
a = np.array([[3, 9, 2],
[7, 1, 8]])
print(a)
print()
print("max value :", int(a.max()))
print("argmax (flat) :", int(a.argmax()))
print("as a position :", np.unravel_index(a.argmax(), a.shape))
print()
print("per row argmax(axis=1):", a.argmax(axis=1))
print("per col argmax(axis=0):", a.argmax(axis=0))
print()
print("argmax returns the FIRST maximum when there are ties:")
print(" ", np.array([5, 9, 9]).argmax())
Output
Cumulative and boolean reductions
Running totals keep the shape; any and all reduce a mask along an axis just like sum.
example_06.pyNumPy
import numpy as np
a = np.array([[1, 2, 3],
[4, 5, 6]])
print("cumsum flat :", a.cumsum())
print("cumsum axis=1:"); print(a.cumsum(axis=1))
print("cumprod axis=0:"); print(a.cumprod(axis=0))
print()
mask = a > 3
print("mask:"); print(mask)
print("any per column:", mask.any(axis=0))
print("all per row :", mask.all(axis=1))
print("count per row :", mask.sum(axis=1))
print()
print("np.diff is the inverse of cumsum:")
print(" ", np.diff(np.array([1, 3, 6, 10])))
Output
axis names what disappears
This is the rule, and it's the opposite of how most people read it at first.
axis=0 does not mean "along the rows" in the sense of giving one answer per row. It means *collapse axis 0*. Axis 0 is the row axis. The rows vanish and you're left with one value per column.
axis=1 merges the columns and leaves one value per row.
Reading it as "which axis am I dropping?" makes each case predictable, including higher dims where intuition runs out. The shape rule is simple. The result has the input shape with that axis deleted.
A handy check when unsure: print a.shape and a.sum(axis=k).shape and confirm the missing entry is the one you meant.
keepdims
Reductions drop the axis, and that is often what you want. Until you try to use the result against the first array.
Row means of a (3, 4) array give shape (3,). Subtracting that from the (3, 4) array fails. Broadcasting aligns from the right and checks 4 with 3.
keepdims=True gives (3, 1) instead. This Broadcasts down the rows right.
Note the skew: centring by column needs no keepdims. mean(axis=0) gives (4,) which already aligns with the last axis. Centring by row needs it. That skew is a direct result of right-aligned broadcasting, and it's why keepdims exists.
A square array hides the error. Both alignments are legal. So this is worth getting right on principle rather than by testing.
Several axes at once
axis takes a tuple. On a (frames, rows, cols) stack, axis=(1, 2) reduces each frame to one number. axis=0 means across frames to give one image.
That covers most of what people reach for loops to do with image or batch data.
argmin and argmax
These return spots rather than values.
On a 1-D array the spot is an index. On a multi-dimensional array with no axis, the spot is into the *flattened* array. This Is rarely right handy. np.unravel_index(a.argmax(), a.shape) converts it into coords.
With an axis, you get one spot per left slice. This Is often what you want: a.argmax(axis=1) gives the column of the largest value in each row.
Ties resolve to the first case. That matters when the max isn't unique and a thing downstream assumes it's.
Cumulative operations
cumsum and cumprod aren't reductions: they keep the shape and fill in running totals. Without an axis they operate on the flattened array. This Is at times what you want and often not. Pass axis on purpose.
np.diff is the inverse of cumsum. In a row differences, one element shorter.
Boolean reductions
any and all take axis just like sum. A boolean array is numeric and these are just reductions over it.
mask.any(axis=0) answers "does any row meet this, per column". mask.sum(axis=1) counts matches per row. Between them and argmax, most "find the first row where..." questions have a one-line answer.
The common mistakes
**Reading axis=0 as "per row".** It gives per column. The axis named is the one removed.
**Losing keepdims when centring by row.** It raises on a rectangular array and silently misbehaves on a square one.
**Using a flat argmax as if it were coords.** Unravel it.
Reducing without an axis by accident.a.mean() on a 2-D array gives one number. If you wanted per-column means, the missing arg is the whole bug and the result still looks like a plausible number.
The accumulator dtype
a.sum() on an int array builds up in that array's int type, and that type can overflow.
Summing a million int32 values each around ten thousand overflows silently and gives a minus answer. The array is fine. The accumulator isn't.
a.sum(dtype=np.int64) fixes it by building up in a wider type than the data. The same arg works on mean, prod and the rest.
NumPy already does something like this by default for the narrowest types. Summing int8 builds up in the platform int. But it doesn't promote int32 or int64. The risk is real for just the widths people really use.
For floats the concern is other: not overflow but precision loss. Summing many float32 values builds up rounding error. a.sum(dtype=np.float64) reads the narrow array and builds up in the wide type. This Is the standard compromise between memory and score.
NumPy uses pairwise summation inside rather than a naive running total. The error grows far more slowly than a hand-written loop would. It's still worth being deliberate when summing millions of float32 values.
Weighted means and percentiles
np.mean isn't a synonym for np.mean. It takes a weights arg, which mean doesn't.
np.mean(x, weights=w) finds the weighted mean. returned=True also gives back the sum of the weights. This Is what you need to combine means from a few groups right.
np.median is the 50th percentile, and np.percentile(a, q) generalises it. Both accept an axis. Per-column medians are one call.
Percentiles involve blending between the two nearest ranks when the requested quantile doesn't fall on an element. The method arg selects among a few rules. Other statistical packages default differently. Which is the usual reason when NumPy's answer disagrees slightly with another tool's.
np.quantile is the same function taking fractions rather than percentages.
All of these have nan-aware variants: nanmedian, nanpercentile, nanquantile.
reduce and accumulate as the general form
each reduction has a ufunc underneath, and the ufunc's reduce method is the general case.
np.add.reduce(a, axis=0) is a.sum(axis=0). np.max.reduce is a.max(). np.logical_and.reduce is a.all().
This matters when you want a reduction NumPy doesn't name. A running max has no cummax function. np.max.build up(a) is just that. Is the standard way to find a drawdown baseline or a high-water mark.
np.ufunc.reduceat performs segmented reductions — a reduction over slices defined by a list of start indices — which is the closest thing NumPy has to a group-by on sorted data.
Reductions that return more than a number
np.ptp gives the peak-to-peak range, max - min, in one pass.
np.histogram reduces to counts per bin and returns the bin edges alongside them.
np.bincount counts cases of small non-minus ints. Is much faster than unique(return_counts=True) for that case because it indexes right rather than sorting. Its weights arg turns it into a group-sum: np.bincount(group_ids, weights=values) sums values per group in one call. This Is the fastest group-by NumPy offers.
Empty arrays and the identity element
np.sum([]) is 0.0. np.prod([]) is 1.0. Both return the identity element for the step. This Is in math the right answer.
np.max([]) raises, because there's no identity for a max.
np.mean([]) returns nan with a warning, since it cuts by zero.
This matters in code that reduces over groups where some group might be empty. A sum silently gives zero. This May or may not be the meaning you want. A max raises. This At least tells you. Filtering out empty groups before reducing, or using the initial arg that max and min accept, makes the intent clear.
The habits worth forming
Pass axis by name. The default of "reduce all" is right often enough to hide a missing arg. Wrong often enough to matter.
Pass keepdims=True when the result will be used against the first array.
Pass dtype when summing ints that could be large, or many float32 values.
Check the shape of the result rather than given it. One printed .shape resolves most confusion about which axis went where. It's faster than thinking about it.
Reductions on boolean arrays
Because booleans promote to ints, each numeric reduction works on a mask and means a thing handy.
mask.sum() counts. mask.mean() gives the proportion. The fraction of elements meeting the rule. This Is often just the summary you want and avoids a division you'd else write by hand.
mask.any() and mask.all() are the logical reductions, and take axis like all else.
mask.argmax() finds the first True, since True is 1 and ties go to the first case. It returns 0 when nothing is True. This Is the same from a match at spot zero — check mask.any() first.
Mixing these covers most "how many rows meet" and "which is the first row where" questions in one line each.
Reductions with a condition
Modern ufunc reductions accept where. This Restricts the reduction to selected elements without building an mid array.
a.sum(where=mask) sums only the matching elements. a.mean(where=mask) means them. Checked with a[mask].sum(), it avoids allocating the extracted subset. This Matters on large arrays inside a loop.
initial supplies a starting value. This Is what makes max work on a possibly-empty pick: a.max(initial=0, where=mask) returns 0 rather than raising when nothing matches.
Those two args together cover the awkward cases in group-wise reductions. Here Some groups may be empty and the plain functions either raise or return an identity that means a thing other.
Reductions across a stack of arrays
A frequent question is how to reduce over a list of arrays rather than over the axes of one.
The answer is to stack them and reduce along the new axis:
result = np.stack(arrays).mean(axis=0)
That is the elementwise mean across all of them. max(axis=0) gives the elementwise max, and so on.
For two arrays there are direct functions. np.max(a, b) and np.min(a, b) are elementwise and broadcast. And they're worth distinguishing from np.max. This Reduces a single array. The names differ by one letter and the steps are fully other. This Is a recurring source of confusion.
np.max.reduce(arrays) generalises the pairwise version to any number, without the mid stack.
The summary
Pass axis by name, and read it as "the axis that disappears".
Pass keepdims=True when the result will be used against the first.
Pass dtype when summing many ints or many float32 values.
Use the nan* variants when missing values are possible. Know that they make a choice on your behalf about what missing means.
Use where and initial for gated reductions rather than extracting a subset first.
And when the result shape isn't what you expected, print a.shape and the result's shape together. One compare resolves nearly each axis confusion faster than thinking about it does.
Questions people ask about Aggregations and axis
Which axis do I pass to get column sums? Pass axis=0. The axis you name is the one that disappears, so axis=0 collapses the rows and leaves one number per column.
Why does my result have the wrong shape after a sum? The summed axis is removed from the shape. Pass keepdims=True if you want a length-1 axis left in place so the result still broadcasts against the original.
What does axis=None do? It reduces over every element and returns a single scalar. That is the default for sum, mean, max and the other reductions.
How do I get the index of the maximum instead of the value? Use argmax, which takes the same axis argument. Without an axis it indexes into the flattened array, so use np.unravel_index to turn it back into coordinates.
Why does np.mean of an array with a NaN return NaN? NaN propagates through arithmetic, so one NaN poisons the whole reduction. Use nanmean, nansum and friends if you want the missing values skipped.
Does sum work over several axes at once? Yes. Pass a tuple such as axis=(0, 1) and both axes are collapsed in one pass.
A closing note on Aggregations and axis
Reductions are simple until an axis is involved. Then almost each confusion comes from one place. Reading axis=0 as "along the rows" rather than "collapse the rows".
The axis named is the axis removed. Holding onto that phrasing makes the result shape predictable in any number of dims. Turns keepdims, tuple axes and higher-dimensional reductions from special cases into results of the same rule.
Check yourself
0 of 4
Answer without scrolling back up.
On a (3,4) array, what does `a.sum(axis=0)` give?
axis names the axis that disappears. Axis 0 is the rows, so they collapse and you get one value per column.
Why does `a - a.mean(axis=1)` fail on a (3,4) array?
Right-aligned broadcasting. `keepdims=True` gives (3,1), which stretches correctly - while centring by column needs no keepdims at all.
What does `argmax` return on a 2-D array with no axis?
Use `np.unravel_index(a.argmax(), a.shape)` to turn it into coordinates. Ties go to the first occurrence.
What does `axis=(1,2)` do on a (frames, rows, cols) array?
A tuple reduces several axes at once, which covers most of what people reach for loops to do with image or batch data.
Cheat sheet
Aggregations and axis
Run sum, mean and max on a NumPy array and change the axis argument. See how one parameter decides whether the result is a row, column or scalar.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.