NumPy Math and Broadcasting
1NumPy Math: overview
NumPy maths applies functions and summaries to whole arrays, and broadcasting lets arrays of different shapes work together by stretching the smaller one, so you can standardise, rescale and combine data without writing loops.
Before this lesson: NumPy Basics
2NumPy Math: diagram
3NumPy Math: explained step by step
In NumPy Basics you did arithmetic on whole arrays. This lesson fills in the rest of the number-crunching toolbox: maths functions, summaries along an axis, broadcasting (how arrays of different shapes combine), reshaping, matrix products, and the awkward cases, missing values and number types.
Universal functions (ufuncs)
Functions such as np.sqrt, np.exp, np.log, np.abs and np.round work on every element at once and return a new array of the same shape. NumPy calls them ufuncs. Do not reach for math.sqrt on an array: the math module only understands single numbers.
import numpy as np
areas = np.array([4.0, 9.0, 16.0, 20.25])
print(np.sqrt(areas))
print(np.abs(np.array([-3, 2, -7])))
print(np.round(np.exp(np.array([0.0, 1.0, 2.0])), 2))Summaries and the axis argument
sum, mean, median, std, var, min and max squash an array into one number, and cumsum / cumprod keep the shape and give running totals or running products. On a 2-D array, axis chooses the direction. The axis you name is the one that disappears: axis=0 collapses the rows and leaves one result per column, axis=1 collapses the columns and leaves one result per row.
import numpy as np
scores = np.array([[70, 80, 90, 60],
[60, 65, 70, 55],
[88, 95, 100, 77]])
print(scores.sum(axis=0))
print(scores.sum(axis=1))
print(np.round(scores.std(axis=1), 1))
print(np.cumsum(scores[0]))Broadcasting: arrays of different shapes
When two shapes differ, NumPy stretches the smaller array to fit, without actually copying it. It compares the shapes from the right-hand end, and two dimensions are compatible when they are equal or when one of them is 1. A missing dimension counts as 1.
- A scalar fits anything:
table * 2. - A row of shape
(3,)fits a table of shape(2, 3): the row is applied to every row of the table. - A column of shape
(2, 1)fits the same table: the column is applied across every column.
import numpy as np
table = np.array([[1, 2, 3],
[4, 5, 6]])
row = np.array([10, 20, 30])
col = np.array([[100], [200]])
print(table + row)
print(table + col)
print(table - table.mean(axis=0))If no stretching can make the shapes match, you get ValueError: operands could not be broadcast together with shapes (2,3) (2,). The usual cure is to turn the 1-D array into a column with reshape(2, 1) or [:, None], or to ask a summary to keep its dimension with keepdims=True.
import numpy as np
table = np.ones((2, 3))
try:
table + np.array([1, 2])
except ValueError as err:
print("Error:", err)
print(table + np.array([1, 2]).reshape(2, 1))Changing shape and making ranges
reshape(rows, cols) re-arranges the same values (use -1 for one size and NumPy works it out), .T transposes rows and columns, and ravel() flattens to 1-D. np.arange(start, stop, step) stops before stop; np.linspace(start, stop, count) includes both ends and is safer for decimal steps.
import numpy as np
months = np.arange(1, 13).reshape(4, 3)
print(months)
print(months.T.shape, months.ravel()[:5])
print(np.linspace(0, 1, 5))Matrix products
a * b multiplies element by element. a @ b (the same as np.dot(a, b) for 2-D arrays) is the matrix product: every row of a is multiplied by the column of b and added up. The inner sizes must match: (n, k) @ (k, m) gives (n, m). A typical use is quantities @ prices, which gives one total per order.
Z-scores, clipping and percentiles
To standardise data, subtract the mean and divide by the standard deviation: (x - x.mean()) / x.std(). The result says how many standard deviations each value is from average, which puts columns with different units on the same scale. np.clip(x, low, high) limits values to a range, and np.percentile(x, 90) gives the value that 90% of the data falls below.
Missing values and number types
np.nan marks a missing float. It spreads: any sum or mean that includes a nan is nan. np.isnan() finds them, and np.nanmean, np.nansum and friends skip them. An array also has one fixed dtype, and the dtype decides the maths: / always gives floats, // keeps integers, and small integer types such as int8 silently wrap around when a result does not fit (100 + 100 becomes -56). Convert with astype() before the maths, not after.
import numpy as np
readings = np.array([21.5, np.nan, 22.5, 23.0])
print(readings.mean(), np.nanmean(readings))
print(np.isnan(readings))
small = np.array([100, 120], dtype=np.int8)
print(small + small)
print(small.astype(np.int32) + small)
print(np.array([7, 8]) / 2, np.array([7, 8]) // 2)Quick reference: NumPy Math
- Python NumPy Cheat Sheet — NumPy stores numbers in fixed-type arrays and applies operations to every element at once: build with `np.arr…
4NumPy Math: syntax
import numpy as np
np.sqrt(a), np.exp(a), np.log(a), np.abs(a) # element by element
np.round(a, 2) # round every value
a.sum(), a.mean(), a.std(), a.min(), a.max() # whole array -> 1 number
np.median(a), a.var(), np.percentile(a, 90)
a.sum(axis=0) # down the columns: one result per column
a.sum(axis=1) # across the rows: one result per row
a.sum(axis=1, keepdims=True) # keep it as a column (n, 1)
np.cumsum(a), np.cumprod(a) # running totals / products
a + b # broadcasting: shapes compared from the right
a[:, None] # turn (n,) into a column (n, 1)
a.reshape(2, 3), a.reshape(-1, 3), a.T, a.ravel()
np.arange(0, 10, 2), np.linspace(0, 1, 5)
a @ b # matrix product (same as np.dot)
np.clip(a, low, high)
(a - a.mean()) / a.std() # z-scores
np.isnan(a), np.nanmean(a) # missing values
a.astype(np.float64) # change dtype
5NumPy Math: run the example
Press Run (or Ctrl + Enter). The output (step 6) appears directly below the code. Change anything and run it again.
import numpy as np
# rows = students, columns = three tests
scores = np.array([[72, 85, 60],
[90, 78, 55],
[66, 92, 70],
[81, 64, 95]])
means = scores.mean(axis=0)
stds = scores.std(axis=0)
z = (scores - means) / stds
print("Test averages:", np.round(means, 1))
print("Z-scores:\n", np.round(z, 2))
best = z.mean(axis=1).argmax()
print("Strongest overall: student", best)
print("90th percentile:", round(np.percentile(scores, 90), 1))Expected outputTest averages: [77.2 79.8 70. ]
Z-scores:
[[-0.58 0.51 -0.65]
[ 1.4 -0.17 -0.97]
[-1.24 1.18 0. ]
[ 0.41 -1.52 1.62]]
Strongest overall: student 3
90th percentile: 91.8
7NumPy Math: how the example works
Hover or tap a step to highlight its lines in the example above.
- Line 3-7A 4 x 3 array of marks: each row is a student and each column is a test.
- Line 9
mean(axis=0)goes down the columns and returns 3 numbers, the average of each test. - Line 10
std(axis=0)returns the spread of each test, again 3 numbers. NumPy divides by the number of values (not by n - 1). - Line 11Broadcasting at work:
scoreshas shape(4, 3)whilemeansandstdshave shape(3,). Compared from the right, the 3s match, so each row of the table is shifted and scaled by the same three numbers. The resultzis a new(4, 3)array of z-scores. - Line 12
np.round(means, 1)rounds all three averages at once to keep the printed output short. - Line 13A z-score says how many standard deviations a mark is above (positive) or below (negative) its test's average, so marks from different tests can be compared fairly.
- Line 14-15
z.mean(axis=1)averages each student's z-scores across the three tests, andargmax()returns the position of the highest one: student 3. - Line 16
np.percentile(scores, 90)looks at all 12 marks together: about 90% of them are at or below the answer.round()trims the printed decimals.
8NumPy Math: common mistakes
Using math.sqrt on an array
✗ Wrong
import math
import numpy as np
areas = np.array([4.0, 9.0, 16.0])
print(math.sqrt(areas))✓ Right
import numpy as np
areas = np.array([4.0, 9.0, 16.0])
print(np.sqrt(areas))The math module works on single numbers only (newer NumPy versions word this error "only 0-dimensional arrays can be converted to Python scalars"). Use the NumPy version, np.sqrt, which is applied to every element.
Shapes that cannot be broadcast
✗ Wrong
import numpy as np
sales = np.array([[10, 20, 30, 40],
[5, 15, 25, 35],
[1, 2, 3, 4]])
shop_totals = sales.sum(axis=1)
print(sales / shop_totals)✓ Right
import numpy as np
sales = np.array([[10, 20, 30, 40],
[5, 15, 25, 35],
[1, 2, 3, 4]])
shop_totals = sales.sum(axis=1, keepdims=True)
print(sales / shop_totals)shop_totals has shape (3,), and compared from the right 4 and 3 do not match. Keep the summed axis as a length-1 dimension with keepdims=True (or [:, None]) so the shape is (3, 1) and each total is applied across its row.
Letting one nan ruin an average
✗ Wrong
import numpy as np
readings = np.array([21.5, np.nan, 22.5, 23.0])
print(readings.mean())✓ Right
import numpy as np
readings = np.array([21.5, np.nan, 22.5, 23.0])
print(np.nanmean(readings))Any calculation that touches a nan gives nan. Use the nan-aware versions such as np.nanmean, np.nansum or np.nanmax, or filter the missing values out first with readings[~np.isnan(readings)].
Overflow in a small integer type
✗ Wrong
import numpy as np
stock = np.array([100, 120], dtype=np.int8)
print(stock + stock)✓ Right
import numpy as np
stock = np.array([100, 120], dtype=np.int8)
big = stock.astype(np.int32)
print(big + big)An int8 can only hold -128 to 127, and NumPy does not warn when an array result does not fit. Convert to a bigger type with astype before doing the maths, or pick the dtype carefully when you create the array.
Using * when you wanted a matrix product
✗ Wrong
import numpy as np
quantities = np.array([[2, 1], [0, 3]])
prices = np.array([[1.5, 4.0], [2.0, 3.5]])
print(quantities * prices)✓ Right
import numpy as np
quantities = np.array([[2, 1], [0, 3]])
prices = np.array([[1.5, 4.0], [2.0, 3.5]])
print(quantities @ prices)* multiplies matching positions. For the row-times-column sums of a matrix product, use @ (or np.dot). Check that the inner sizes match: (2, 2) @ (2, 2) is fine.
9NumPy Math: exercises
Write your answer, then press Check. The checks run in your browser.
1. Balance after each year
Write code Easy ✓ CompletedWrite a function yearly_balances(start, rate, years) that returns a NumPy array with the balance at the end of each year, from year 1 to years, rounded to 2 decimal places. A balance grows by start * (1 + rate) ** year. Do it without a loop: build the years with np.arange and let NumPy do the power for all of them at once.
import numpy as np
def yearly_balances(start, rate, years):
pass
print(yearly_balances(1000, 0.10, 3))np.arange(1, years + 1) gives 1, 2, ..., years. A scalar raised to an array gives an array, so (1 + rate) ** year_numbers does every year in one step.import numpy as np
def yearly_balances(start, rate, years):
year_numbers = np.arange(1, years + 1)
return np.round(start * (1 + rate) ** year_numbers, 2)
print(yearly_balances(1000, 0.10, 3))
2. Predict axis, broadcasting and reshape
Predict the output Medium ✓ CompletedRead the code and type exactly what it prints. NumPy prints arrays with spaces instead of commas, and a 2-D array on several lines.
import numpy as np
grid = np.array([[1, 2, 3],
[4, 5, 6]])
print(grid.sum(axis=0))
print(grid.sum(axis=1))
print(grid + np.array([10, 20, 30]))
print(grid.reshape(3, 2)[:, 0])
print(grid.T.shape)[10, 20, 30] is added to both rows. reshape(3, 2) fills row by row: [[1, 2], [3, 4], [5, 6]].[5 7 9]
[ 6 15]
[[11 22 33]
[14 25 36]]
[1 3 5]
(3, 2)
2 more exercises → 6 more examples
Questions learners ask
What is broadcasting in NumPy, in plain words?
Broadcasting is NumPy's rule for combining arrays of different shapes. It lines the shapes up from the right and stretches any dimension of size 1 (or a missing one) to match the other array, without copying the data. That is why a single row can be added to every row of a table. If the shapes cannot be made to match, you get a ValueError that lists both shapes.
Why is np.std different from the standard deviation in pandas or Excel?
np.std divides by n (the population standard deviation, ddof=0), while pandas .std() and Excel's STDEV divide by n - 1 (the sample standard deviation). Pass ddof=1 to np.std to match them: np.std(data, ddof=1). The gap is large for small samples and tiny for big ones.
What is the difference between ravel(), flatten() and reshape(-1)?
All three turn an array into 1-D. flatten() always returns a copy. ravel() and reshape(-1) return a view of the original data whenever they can, so editing the result may change the original. Use flatten() when you plan to modify the flat array and want the original left alone.
Further reading
10Next lesson
Up nextPandas Basicspandas is a library for working with tables: a DataFrame holds rows and named columns like a spreadsheet, and each colu…Related:
Python math and statisticsNumPy BasicsNumPy Indexing and SlicingPandas Basics
← NumPy Indexing and Slicing · All Data Analysis lessons · Practise in the IDE