NumPy arrays
Do math on whole columns at once, the foundation of every ML library.
Why it matters
Every machine learning library is built on arrays of numbers. A dataset is a 2D array. An image is a 3D array. A model's weights are arrays. And an embedding, the way models capture the meaning of text, is a 1D array of a few thousand decimals. NumPy is where this way of working comes from. PyTorch, which you'll use for deep learning, copies its style almost exactly. Learn to think in whole arrays here, and the math in Phase 2 becomes code you can run.
What you'll be able to do
- Create arrays and read their shape and dtype
- Do math on whole arrays at once instead of looping
- Select data with indexes, slices and boolean masks
- Summarize along an axis, and predict how broadcasting combines shapes
What it covers
- Lists can't do math on themselves
- Shape and dtype
- Indexing, slicing and masks
- Summaries and axes
- Broadcasting
- Worked example: a week of campaign data
- Two slips that NumPy makes easy
- Shapes must line up
Lists can't do math on themselves
Multiply a Python list by 2 and you don't get doubled numbers. You get the list twice:
clicks = [5470, 2050, 3800] print(clicks * 2) print([c * 2 for c in clicks])
To do math on every item of a list, you need a loop or a comprehension. NumPy's array does math on every element directly. Think of a spreadsheet column: type one formula, and it applies to every row at once.
import numpy as np clicks = np.array([5470, 2050, 3800]) impressions = np.array([182340, 410200, 950000]) print(clicks * 2) print(clicks / impressions)
How it works:
import numpy as npis the standard way to load it. Every tutorial and AI assistant writes it.np.array(...)turns a list into an array.clicks * 2doubles each element.clicks / impressionsdivides element by element: first with first, second with second.
This is called vectorized math. You write the operation once, and NumPy applies it to every element in fast, pre-built code. On large data, that's often tens to hundreds of times faster than a Python loop.
Shape and dtype
import numpy as np spend = np.array([3120.5, 1890, 6400, 120]) table = np.array([ [5470, 301], [2050, 88], [3800, 41], ]) print(spend.dtype, spend.shape) print(table.dtype, table.shape, table.ndim)
Two attributes describe every array:
- dtype is the type of every element. Unlike a list, an array holds one type throughout.
spendbecamefloat64(decimals), even though 1890 was written as a whole number.tableholds whole numbers:int32here in the browser,int64on most computers. - shape gives the size along each axis (direction).
(4,)is a 1D array of 4 values.(3, 2)is 3 rows by 2 columns. Shapes are written as tuples: fixed-size groups of values in parentheses.ndimis the number of axes.
Most bugs in ML code are shape bugs. So printing .shape is the first thing to do when something looks wrong.
NumPy can also create arrays for you:
import numpy as np print(np.zeros(3)) print(np.arange(0, 10, 2)) print(np.linspace(0, 1, 5)) rng = np.random.default_rng(seed=42) print(rng.integers(1, 7, size=5))
arangeworks likerange.linspacegives evenly spaced values between two ends.- The random generator with a seed gives the same "random" numbers every run, so experiments can be repeated.
Units sold (tee, hoodie, mug) on two days, times each product's price. Every cell is combined with the cell in the same position. No loop needed.
Indexing, slicing and masks
A 1D array indexes and slices like a list. A 2D array takes a row and a column, separated by a comma. : means "all of them":
import numpy as np table = np.array([ [5470, 301], # clicks, conversions for campaign 0 [2050, 88], # campaign 1 [3800, 41], # campaign 2 ]) print(table[0]) # first row print(table[0, 1]) # row 0, column 1 print(table[:, 0]) # every row, column 0: all the clicks print(table[1:, 1]) # rows 1 onward, column 1
The most useful trick is the boolean mask. It works like a stencil laid over the data. A comparison on an array gives an array of True and False. Indexing with that array keeps only the True positions:
import numpy as np spend = np.array([3120.5, 1890, 6400, 120, 0, 2390.75]) revenue = np.array([15840, 5236, 2870, 6600, 0, 3290]) profitable = revenue > spend print(profitable) print(spend[profitable]) print("Spend on profitable campaigns:", spend[profitable].sum()) big_and_profitable = (revenue > spend) & (spend > 2000) print(big_and_profitable)
Combine conditions with & (and) and | (or), with each condition in parentheses. Python's and and or don't work on arrays, because they expect a single True or False.
import numpy as np a = np.array([4, 9, 1, 7]) print(a[a > 5])
Show the answer
[9 7]
a > 5 is a mask; indexing with it keeps 9 and 7. Arrays print without commas, which tells you it's an array and not a list.
Summaries and axes
Arrays have methods for summaries: sum, mean, min, max, std, and argmax (the position of the largest value). On a 2D array, axis says which direction to sum up:
import numpy as np # rows: 3 campaigns. columns: clicks on Mon, Tue, Wed daily = np.array([ [120, 135, 150], [80, 60, 70], [45, 50, 40], ]) print(daily.sum()) # everything print(daily.sum(axis=0)) # down the rows: one total per day print(daily.sum(axis=1)) # across the columns: one total per campaign print(daily.mean(axis=1).round(1)) print(daily.sum(axis=1).argmax())
The axis you name is the one that disappears:
axis=0collapses the rows, leaving one value per column.axis=1collapses the columns, leaving one value per row.
The last line prints 0: campaign 0 had the most clicks, 405 in total.
import numpy as np t = np.array([[1, 2], [3, 4]]) print(t.sum(axis=0))
Show the answer
[4 6]
axis=0 collapses the rows, leaving one total per column: 1 + 3 = 4 and 2 + 4 = 6.
Broadcasting
NumPy can combine arrays of different shapes when it can stretch the smaller one to fit. This is called broadcasting. A single number stretches to every element:
import numpy as np prices = np.array([28, 64, 18]) # shape (3,) print(prices * 1.2) # 1.2 is applied to every element units = np.array([ [3, 1, 4], # day 1: tee, hoodie, mug [2, 5, 0], # day 2 ]) # shape (2, 3) print(units * prices) # prices is stretched down both rows
The rule: line the shapes up from the right. Each pair of sizes must be equal, or one of them must be 1. A missing size counts as 1.
(2, 3)and(3,)line up as 3 with 3, sopricesis reused for every row.(2, 3)and(2,)line up as 3 with 2, which fails:
import numpy as np units = np.array([[3, 1, 4], [2, 5, 0]]) day_discount = np.array([0.9, 0.8]) print(units * day_discount)
To apply one discount per day (per row), make the discounts a column with shape (2, 1). Now 3 lines up with 1, and the column is stretched across:
import numpy as np units = np.array([[3, 1, 4], [2, 5, 0]]) day_discount = np.array([0.9, 0.8]) print(units * day_discount.reshape(2, 1))
units has shape (2, 3). Which of these can you multiply it by without an error?Show the answer
An array of shape (3,): one value per column
Line the shapes up from the right: 3 with 3 works. 3 with 2 or 4 fails. For one value per row, reshape to (2, 1) first.
Worked example: a week of campaign data
Three campaigns, seven days of spend and revenue. Find each campaign's weekly ROAS, the best day overall, and how many days Search beat a ROAS of 5, all without a single loop over the numbers.
import numpy as np campaigns = ["Search", "Display", "Email"] # rows: campaigns. columns: Mon to Sun spend = np.array([ [450, 470, 430, 460, 480, 300, 280], [270, 265, 280, 270, 275, 260, 270], [20, 15, 25, 20, 20, 10, 10], ]) revenue = np.array([ [2300, 2500, 2100, 2400, 2600, 1500, 1300], [740, 700, 800, 760, 750, 720, 766], [1000, 900, 1200, 950, 1100, 700, 750], ]) weekly_roas = revenue.sum(axis=1) / spend.sum(axis=1) daily_revenue = revenue.sum(axis=0) search_daily_roas = revenue[0] / spend[0] for name, roas in zip(campaigns, weekly_roas.round(2)): print(name, roas) print("Best day (0 = Monday):", daily_revenue.argmax()) print("Days Search beat ROAS 5:", (search_daily_roas > 5).sum())
It prints a weekly ROAS of 5.12 for Search, 2.77 for Display and 55.0 for Email. Then the best day is 4 (Friday), and Search beat a ROAS of 5 on 4 days.
sum(axis=1)totals each campaign's week. Dividing two arrays of 3 gives three ROAS figures at once.sum(axis=0)totals each day across campaigns, andargmaxfinds the best one.revenue[0] / spend[0]takes the Search row from both arrays and divides day by day.(search_daily_roas > 5).sum()counts the True values, because True counts as 1 and False as 0.
The for loop only formats the output. All the math is vectorized.
Two slips that NumPy makes easy
To combine conditions on arrays, use & and | with brackets: (a > 1) & (a < 5). The words and and or expect one true-or-false answer, not a whole array, so they raise an error.
For axes, remember that the axis you name is the one that disappears. On a table of rows and columns, axis=0 collapses the rows, which gives one answer per column.
Shapes must line up
Broadcasting only works when the shapes are compatible, so check .shape whenever you get an error here.
Practice what you just read
- 11 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
You multiply an array of shape (4, 3) by an array of shape (3,). What's the shape of the result?