NumPy Arrays
NumPy is a third-party library for numeric arrays and the math operations that go with them. It fills a gap that plain Python lists leave open: fast, element-wise math over a whole sequence of numbers at once.
The Using Libraries page covers importing a library in general.
NumPy is almost always imported under the alias np.
The NumPy documentation has a Quickstart guide.
Why NumPy?
A plain Python list does not support element-wise math. Multiplying a list by a number repeats its contents rather than scaling each value:
prices = [10, 20, 30]
print(prices * 2) # this DUPLICATES the list, it doesn't scale it!
import numpy as np
prices_arr = np.array(prices)
print(prices_arr * 2) # this is what you actually wanted
[10, 20, 30, 10, 20, 30]
[20 40 60]
Converting the list to a NumPy array with np.array() changes what * means: instead of repeating the sequence, it multiplies every element.
Creating Arrays
NumPy provides several functions for creating arrays beyond converting an existing list:
import numpy as np
a = np.array([1, 2, 3, 4, 5])
z = np.zeros(5)
o = np.ones((2, 3)) # a 2x3 array of ones
r = np.arange(0, 10, 2) # like range(), but returns an array
lin = np.linspace(0, 10, 5) # 5 evenly spaced points from 0 to 10
print(a)
print(z)
print(o)
print(r)
print(lin)
[1 2 3 4 5]
[0. 0. 0. 0. 0.]
[[1. 1. 1.]
[1. 1. 1.]]
[0 2 4 6 8]
[ 0. 2.5 5. 7.5 10. ]
np.zeros and np.ones create arrays filled with a single value, np.arange works like range() but returns an array, and np.linspace produces a fixed number of evenly spaced points between two bounds.
Array Shape and dtype
Every array has a shape, describing its dimensions, and a dtype, describing the type of value it stores:
import numpy as np
a = np.array([1, 2, 3, 4, 5])
o = np.ones((2, 3))
print(a.shape)
print(o.shape)
print(a.dtype)
print(np.array([1, 2, 3], dtype=float).dtype)
(5,)
(2, 3)
int64
float64
Unlike a plain Python list, every element of a NumPy array shares the same dtype.
Passing dtype=float (or another type) when creating an array forces that type, regardless of what the input values look like.
Reshaping an Array
reshape returns the same data arranged into a different shape, without changing the number of elements:
import numpy as np
flat = np.arange(6)
print(flat)
grid = flat.reshape(2, 3)
print(grid)
print(grid.shape)
[0 1 2 3 4 5]
[[0 1 2]
[3 4 5]]
(2, 3)
The new shape’s dimensions must multiply out to the same total count as the original.
Reshaping 6 elements into (2, 3) works because $2 \times 3 = 6$, but reshaping into (2, 4) would raise an error.
Element-wise Arithmetic
Arithmetic between two arrays of the same shape operates element by element, with no loop needed:
import numpy as np
x = np.array([1, 2, 3])
y = np.array([10, 20, 30])
print(x + y)
print(x * y)
print(y / x)
print(x ** 2)
print(np.sqrt(y))
[11 22 33]
[10 40 90]
[10. 10. 10.]
[1 4 9]
[3.16227766 4.47213595 5.47722558]
This is the main advantage NumPy has over a list comprehension for numeric work: x ** 2 is both shorter and faster than [xi ** 2 for xi in x].
Broadcasting
Arithmetic between an array and a single number applies to every element, with no loop and no need to build a second array of the same shape:
import numpy as np
x = np.array([1, 2, 3])
print(x + 100)
print(x * 2)
[101 102 103]
[2 4 6]
This is called broadcasting: NumPy treats the single number as if it were stretched to match the array’s shape. The same idea extends to arithmetic between arrays of compatible, but not identical, shapes, though the shapes used in this course are usually the same size already.
Indexing, Slicing, and Boolean Masking
Basic indexing and slicing on a NumPy array work the same as on a list.
Boolean masking is new: comparing an array to a value produces an array of True/False, which can then be used to select only the matching elements.
import numpy as np
h = np.array([200, 400, 800, 1500, 20000, 35786, 100000])
print(h[0])
print(h[-1])
print(h[1:4])
mask = h < 2000
print(mask) # an array of True/False
print(h[mask]) # only the elements where mask is True
# You'll usually write this as one line:
print(h[h < 2000])
print(h[(h >= 2000) & (h < 35786)]) # use & (not 'and') for arrays!
200
100000
[ 400 800 1500]
[ True True True True False False False]
[ 200 400 800 1500]
[ 200 400 800 1500]
[20000]
h < 2000 produces a mask the same length as h, and h[mask] keeps only the elements where the mask is True.
Combining conditions on arrays uses & (and) and | (or), not Python’s and/or, which don’t work element-wise.
Example: Filtering Noisy Sensor Readings
Question
A gravimeter logs local gravity readings in m/s2, but a loose connection occasionally produces a garbage value far outside the physically reasonable range. Given the readings below, keep only the values between 9.0 and 10.5 m/s2, then compute their average.
Solution
A boolean mask built from both bounds at once selects only the valid readings, which np.mean then averages.
import numpy as np
readings = np.array([9.79, 9.81, 9.80, 15.02, 9.78, 9.82, -1.00])
valid = readings[(readings > 9.0) & (readings < 10.5)]
print(valid)
print(np.mean(valid))
[9.79 9.81 9.8 9.78 9.82]
9.8
Useful Reductions
A reduction collapses an array down to a single summary value.
NumPy provides several as both functions (np.mean(v)) and array methods (v.mean()):
import numpy as np
v = np.array([7.2, 7.5, 7.6, 7.7, 7.4])
print(np.mean(v))
print(np.std(v))
print(np.max(v))
print(np.min(v))
print(np.argmax(v)) # INDEX of the maximum value
print(np.sum(v))
7.4799999999999995
0.17204650534085245
7.7
7.2
3
37.4
np.argmax (and np.argmin) return the index of the extreme value, not the value itself, which is useful for finding where something happened rather than just what the peak value was.
Concatenating Arrays
np.concatenate joins a list of arrays end to end into a single array:
import numpy as np
first_batch = np.array([7.2, 7.5])
second_batch = np.array([7.6, 7.7, 7.4])
all_readings = np.concatenate([first_batch, second_batch])
print(all_readings)
[7.2 7.5 7.6 7.7 7.4]
This is useful for combining readings collected in separate batches, such as data logged before and after a pause, into one array for analysis.
2D Arrays and Matrix Operations
A NumPy array can have more than one dimension, which is how matrices are represented:
import numpy as np
M = np.array([[1, 2], [3, 4]])
print(M)
print(M.shape)
print(M.T) # transpose
print(M @ M) # matrix multiplication (NOT M * M!)
print(M * M) # element-wise multiplication, different from @
print(np.linalg.det(M))
print(np.linalg.norm(np.array([3, 4]))) # vector magnitude
[[1 2]
[3 4]]
(2, 2)
[[1 3]
[2 4]]
[[ 7 10]
[15 22]]
[[ 1 4]
[ 9 16]]
-2.0000000000000004
5.0
@ performs true matrix multiplication, while * still multiplies element by element, even for a 2D array.
Mixing these up is a common source of bugs.
np.linalg provides other linear algebra operations, including det for the determinant and norm for a vector’s magnitude.
Reading Questions
- What does
[1, 2, 3] * 2produce for a plain Python list, versus a NumPy array? - What is the difference between
np.zeros(5)andnp.arange(5)? - What does an array’s
shapedescribe? - What is a boolean mask, and how is it used to filter an array?
- Why must
&be used instead ofandwhen combining two conditions on NumPy arrays? - What is the difference between
np.argmax(v)andnp.max(v)? - What is the difference between
M @ MandM * Mfor a 2D arrayM? - What condition must a shape satisfy to be a valid
reshapetarget for a given array? - What does broadcasting mean when adding a single number to a NumPy array?
- What does
np.concatenatedo, and how is it different from adding two arrays together with+?