Python Tutorial
NumPy Indexing, Broadcasting and Array Operations
Once data is in an array, you need to pick out parts of it, combine arrays, summarise them and change their shape. This lesson covers indexing and slicing in one and two dimensions, boolean masks (the NumPy way to filter), fancy indexing, aggregation along rows or columns with axis, broadcasting rules, reshaping and stacking, and the difference between views and copies.
Indexing, Slicing and Masks
One-dimensional arrays index like lists: a[0], a[-1], a[2:5], a[::2]. Two-dimensional arrays take a row and a column: m[1, 2], a whole row m[0], a column m[:, 1], a block m[:2, 1:]. A comparison such as a > 50 produces a boolean array — a mask — and a[a > 50] selects matching elements. Combine conditions with &, | and ~ (with parentheses), not and/or. Fancy indexing uses a list of positions: a[[0, 3, 4]].
Aggregations and axis
sum, mean, median, std, min, max, argmin/argmax (position of the extreme), cumsum and percentile summarise arrays. On a 2-D array, axis=0 collapses the rows and gives one result per column; axis=1 collapses the columns and gives one result per row. np.where(condition, x, y) chooses values element by element, np.clip limits values to a range, and np.unique(..., return_counts=True) counts distinct values.
Broadcasting, Reshaping and Views
Broadcasting lets NumPy combine arrays of different shapes: dimensions are compared from the right, and each pair must be equal or one of them must be 1 (which is stretched). So a (3, 4) matrix minus a (4,) row subtracts that row from every row. reshape changes shape without changing data (-1 means "work it out"); ravel/flatten make it 1-D; .T transposes; concatenate, vstack and hstack join arrays. Slices are views that share memory with the original — changing a view changes the original — so call .copy() when you need independent data. Boolean and fancy indexing always return copies.
Examples
Indexing, slicing, boolean masks and fancy indexing
import numpy as np
temps = np.array([21, 25, 31, 28, 35, 19, 24])
print(temps[0], temps[-1], temps[2:5], temps[::3])
print(temps > 27) # a boolean mask
print(temps[temps > 27]) # filter with the mask
print(temps[(temps > 20) & (temps < 30)]) # use & and |, with parentheses
print((temps > 30).sum(), "hot days") # True counts as 1
print(temps[[0, 2, 4]]) # fancy indexing
scores = np.array([[78, 92, 65],
[88, 71, 94],
[59, 85, 90]])
print(scores[1, 2], scores[0], scores[:, 1])
print(scores[:2, 1:])
scores[scores < 60] = 60 # assign through a mask
print(scores[2])
21 24 [31 28 35] [21 28 24]
[False False True True True False False]
[31 28 35]
[21 25 28 24]
2 hot days
[21 31 35]
94 [78 92 65] [92 71 85]
[[92 65]
[71 94]]
[60 85 90]
Aggregations with axis, where, clip and unique
import numpy as np
# rows = 3 students, columns = 4 subjects
scores = np.array([[78, 92, 65, 80],
[88, 71, 94, 60],
[59, 85, 90, 72]])
print("overall mean:", scores.mean().round(2))
print("per subject (axis=0):", scores.mean(axis=0).round(1))
print("per student (axis=1):", scores.sum(axis=1))
print("best subject per student:", scores.argmax(axis=1))
print("median, std:", np.median(scores), scores.std().round(2))
print("90th percentile:", np.percentile(scores, 90))
print(np.where(scores >= 75, "pass", "retry")[0])
print(np.clip(scores[1], 65, 90))
print(np.cumsum([100, 250, -50, 400]))
grades = np.array(["A", "B", "A", "C", "B", "A"])
values, counts = np.unique(grades, return_counts=True)
print(dict(zip(values.tolist(), counts.tolist())))
overall mean: 77.83
per subject (axis=0): [75. 82.7 83. 70.7]
per student (axis=1): [315 313 306]
best subject per student: [1 2 2]
median, std: 79.0 11.86
90th percentile: 91.8
['pass' 'pass' 'retry' 'pass']
[88 71 90 65]
[100 350 300 700]
{'A': 3, 'B': 2, 'C': 1}
Broadcasting, reshape, stacking and views vs copies
import numpy as np
sales = np.array([[10, 20, 30, 40],
[15, 25, 35, 45],
[12, 22, 32, 42]])
column_means = sales.mean(axis=0) # shape (4,)
print(sales - column_means) # (3, 4) - (4,) -> broadcast over rows
print((sales / sales.sum(axis=1, keepdims=True)).round(2)) # (3, 4) / (3, 1): share of each row
a = np.arange(12)
print(a.reshape(3, 4))
print(a.reshape(2, -1).shape, a.reshape(3, 4).T.shape)
print(np.vstack([[1, 2], [3, 4]]), np.hstack([[1, 2], [3, 4]]))
original = np.array([1, 2, 3, 4, 5])
view = original[1:4]
view[0] = 99 # changes the original too!
print(original)
safe = original[1:4].copy()
safe[0] = -1
print(original, safe)
[[-2.33333333 -2.33333333 -2.33333333 -2.33333333]
[ 2.66666667 2.66666667 2.66666667 2.66666667]
[-0.33333333 -0.33333333 -0.33333333 -0.33333333]]
[[0.1 0.2 0.3 0.4 ]
[0.12 0.21 0.29 0.38]
[0.11 0.2 0.3 0.39]]
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
(2, 6) (4, 3)
[[1 2]
[3 4]] [1 2 3 4]
[ 1 99 3 4 5]
[ 1 99 3 4 5] [-1 3 4]
Common Mistakes
- Using and/or instead of & and | with arrays, or forgetting parentheses around each condition.
- Mixing up axis=0 (per column) and axis=1 (per row).
- Modifying a slice and being surprised the original array changed — slices are views.
- Broadcasting shape errors from arrays whose trailing dimensions do not match; use keepdims=True or reshape.
- Writing loops to count or filter when a mask and .sum() do it in one step.
Key Points to Remember
- Index with a[i], m[row, col], slices and : for whole rows/columns.
- Boolean masks filter and assign; fancy indexing picks arbitrary positions.
- axis=0 aggregates down columns, axis=1 across rows.
- Broadcasting stretches size-1 dimensions to combine different shapes.
- reshape/T/stack change layout; slices are views, so copy() when needed.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local project environment for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.