NumPy02/18
Starting sandbox…
Beginner·4 graded tests

Axes and Reductions

Axes and Reductions

Core Principle: Reductions aggregate values along a designated dimension (collapsing axis kk), where axis=0 reduces across rows to summarize columns and axis=1 reduces across columns to summarize rows.

The rule that actually works

Forget "rows" and "columns" for a moment. The axis you name is the axis that disappears.

[1]python
out1ms
np.int64(66)

Translating back: for a 2-D array, axis=0 collapses down the rows and gives you one value per column; axis=1 collapses across the columns and gives one value per row.

Say it in terms of the output, not the input. "I want one value per gene, and genes are rows, so the output length is n_genes, so I collapse axis 1."

keepdims

[2]python
out1ms
array([[-1.5, -0.5,  0.5,  1.5],
       [-1.5, -0.5,  0.5,  1.5],
       [-1.5, -0.5,  0.5,  1.5]])

Use keepdims=True whenever the result feeds back into an operation with the original array. It removes the need for [:, None] and eliminates a whole class of Broadcasting errors.

The reduction catalogue

a.sum(); a.prod(); a.mean(); a.std(); a.var()
a.min(); a.max(); a.ptp()               # ptp = peak-to-peak (max - min)
a.argmin(); a.argmax()                  # index of the extreme
a.any(); a.all()
np.median(a, axis=1)
np.percentile(a, [25, 50, 75], axis=1)
np.quantile(a, 0.95, axis=1)
np.count_nonzero(a, axis=1)

a.cumsum(axis=1); a.cumprod(axis=1)     # not reductions: shape preserved
np.diff(a, axis=1)                       # shape shrinks by 1 along that axis

NaN-aware versions skip missing values instead of propagating them:

[3]python
out1ms
(<function nanmin at 0x1063a43b0>, <function nanmax at 0x1063a4530>, <function nanpercentile at 0x1063a4f30>, <function nanargmax at 0x1063a47f0>)

See Missing Data in NumPy.

The ddof trap

[4]python
out1ms
np.float64(3.605551275463989)

NumPy defaults to population (ddof=0). pandas defaults to sample (ddof=1). So np.std(x) and pd.Series(x).std() give different answers, and neither library is wrong. In statistics you almost always want ddof=1. Be explicit in both.

argmin / argmax and flat indices

[5]python
out1ms
array([3, 3, 3])

Ties go to the first occurrence. argmax on a boolean array finds the first True: but returns 0 when there is no True at all, so always guard with mask.any().

Bioinformatics: the two directions

With expr of shape (n_genes, n_samples):

expr.mean(axis=1)        # per-gene mean expression      → (n_genes,)
expr.sum(axis=0)         # per-sample library size       → (n_samples,)
expr.std(axis=1, ddof=1) # per-gene variability          → (n_genes,)
(expr > 0).sum(axis=1)   # in how many samples is each gene detected?
(expr > 0).sum(axis=0)   # how many genes detected per sample?
expr.argmax(axis=1)      # which sample expresses each gene most?

The sanity check: print the shape of every reduction result and confirm it equals the number of things you expect. (20000,) = genes. (500,) = samples. If you get 500 when you wanted 20000, you have the axis backwards.

This matters because with a square-ish matrix, or with a downstream step that broadcasts either way, the wrong axis produces a plausible result. Per-sample means and per-gene means are both smooth, reasonable-looking numbers. There is no error message.

Reductions along more than two axes

[6]python
out1ms
array([[0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.]], shape=(256, 256))

Segmented and grouped reductions

When groups are not whole axes:

np.add.reduceat(values, segment_starts)      # sum within contiguous segments
np.bincount(group_codes, weights=values)     # sum by integer group id
np.bincount(group_codes)                      # count by group

For labelled groups, this is where you switch to pandas groupby. See Split-Apply-Combine.

Common mistakes

  • axis=0 vs axis=1 reversed. Silent, plausible, wrong. Always check the output shape.
  • Forgetting keepdims=True and then fighting broadcasting.
  • ddof mismatch between NumPy and pandas in the same analysis.
  • mean() on data containing NaN: the result is NaN, and one bad cell poisons the whole row. Use nanmean, but know that it silently changes your denominator.
  • argmax on an all-False mask returning 0, which looks like a valid index.
  • sum() on int32 counts overflowing. Pass dtype=np.int64.
  • Assuming axis=-1 is "columns". It is the last axis, which is columns only for 2-D. It is the right choice when you want "along the innermost dimension" regardless of rank.

See also

Broadcasting · ufuncs · Missing Data in NumPy · ndarray · Split-Apply-Combine · groupby · Applied - Expression Matrices with NumPy

Exercise

Write group_profile(matrix, labels). The matrix holds one gene per row and one sample per column; labels gives the group each column belongs to. Return ["genes=0"] when the matrix has no rows, which is the case a reduction over an empty axis would otherwise turn into a warning and a NaN.

Otherwise the first line is groups=<names joined by commas> in sorted order. Then emit one line per gene, row<i> <group> <mean>, naming the group whose columns give that gene its highest mean and that mean rounded to three decimals; where two groups tie, the earlier name in sorted order wins. Follow those with peak=row<i>,<group>, locating the single largest entry of the gene-by-group mean table by converting the flat argmax into a pair of indices, and finally spread=<max minus min> over that same table, rounded to three decimals.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdinctrl ctrl treat treat 10 12 40 44 8 8 8 8 30 28 5 7 groups=ctrl,treat row0 treat 42.0 row1 ctrl 8.0 row2 ctrl 29.0 peak=row0,treat spread=36.0
2stdina b a b a 1 100 3 100 5 9 1 9 1 9 groups=a,b row0 b 100.0 row1 a 9.0 peak=row0,b spread=99.0
3stdinonly 5 7 groups=only row0 only 5.0 row1 only 7.0 peak=row1,only spread=2.0
4stdinctrl treat genes=0

Hints · 2

01Hint

Reduce each group's columns with matrix[:, mask].mean(axis=1), then np.stack the results into a genes x groups table.

02Hint

np.unravel_index turns the flat index from .argmax() into row and column indices for a 2-D table.