Slice arrays, reduce along an axis, and reshape without copying. The axis argument is the concept that trips everyone up — get it right and NumPy clicks.
Why: you constantly pull out rows, columns, or elements that match a condition — NumPy indexing does it without loops. When: use slices for ranges, integer arrays for specific positions, and boolean masks for conditions. Where: a basic slice is a view (shares memory); fancy/boolean indexing returns a copy.
x = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
print(x[2:5]) # [2 3 4] — slice
print(x[[1, 3, 8]]) # [1 3 8] — fancy indexing
print(x[x % 2 == 0]) # [0 2 4 6 8] — boolean mask
m = np.arange(12).reshape(3, 4)
print(m[1, 2]) # 6 — row 1, col 2
print(m[:, 0]) # [0 4 8] — first column of every rowWhy: reductions like sum and mean take an axis that says WHICH dimension to collapse — axis=0 collapses rows (giving a per-column result), axis=1 collapses columns (per-row). When: this is the number-one point of confusion; think "the axis you name is the one that disappears." Where: no axis means reduce the whole array to a scalar.
m = [[1, 2, 3],
[4, 5, 6]] # shape (2, 3)
m.sum() -> 21 (everything, a scalar)
m.sum(axis=0) -> [5 7 9] (collapse rows -> one value per column)
m.sum(axis=1) -> [6 15] (collapse cols -> one value per row)
Rule: "axis=k removes dimension k." axis=0 is down the columns.Why: models expect specific shapes (a batch of vectors, an image tensor), so you reshape data to fit — without copying the underlying numbers. When: use reshape to reorganize and -1 to let NumPy infer one dimension. Where: reshape needs the total element count to stay the same.
x = np.arange(12)
print(x.reshape(3, 4)) # 3 rows, 4 cols
print(x.reshape(2, -1)) # 2 rows, NumPy infers 6 cols
# Add an axis (common when a model wants a batch dimension):
v = np.array([1, 2, 3]) # shape (3,)
print(v.reshape(1, -1).shape) # (1, 3) — one row
print(v[:, None].shape) # (3, 1) — one column