import numpy as npModuleNotFoundError: No module named 'numpy'
Module 3: NumPy
Suppose we need to convert hundreds of temperatures from Celsius to Fahrenheit. With a list, we would normally write a loop. With a NumPy array, the formula can be applied to every value in one expression. This array-based style is the foundation of vector programming.
import numpy as npModuleNotFoundError: No module named 'numpy'
With NumPy, you can easily perform array-with-array arithmetic, or scalar-with-array arithmetic.
# Create two arrays
a = np.array([1, 2, 3, 4])
b = np.array([4, 3, 2, 1])NameError: name 'np' is not defined
# Addition
c = a + b
print(c)NameError: name 'a' is not defined
# Subtraction
c = a - b
print(c)NameError: name 'a' is not defined
# Multiplication element-wise
c = a * b
print(c)NameError: name 'a' is not defined
# Division element-wise
c = a / b
print(c)NameError: name 'a' is not defined
# Power element-wise
c = a ** b
print(c)NameError: name 'a' is not defined
Exercise: How would you create an array containing 50 eights?
Try to solve this exercise in your notebook (using Python code) before opening the solution.
arr = np.full(50, 8)
print(arr)NameError: name 'np' is not defined
Exercise: How would you create an array containing all the powers of 2 from \(2^0\) to \(2^{10}\)?
arr = 2 ** np.arange(0, 11)
print(arr)NameError: name 'np' is not defined
Broadcasting is a powerful mechanism that allows NumPy to work with arrays of different shapes when performing arithmetic operations.
# Create a scalar and an array
scalar = 5
arr = np.array([1, 2, 3, 4])
# Add scalar to array through broadcasting
print(scalar + arr)NameError: name 'np' is not defined
Broadcasting becomes extremely powerful when dealing with multidimensional arrays.
arr1 = np.ones((3, 3))
print(arr1)NameError: name 'np' is not defined
arr2 = np.array([-1, 0, 1])
print(arr2)NameError: name 'np' is not defined
# Use broadcasting
arr3 = arr1 + arr2
print(arr3)NameError: name 'arr1' is not defined
print(arr3[0])NameError: name 'arr3' is not defined
How does broadcasting work?
Example of broadcasting:
A with shape (m, n) and a 1D vector v with shape (n,).A has m rows and n columns, while the vector v has n elements.A = np.array([[1, 2],
[3, 4],
[5, 6]])
print(A.shape)v = np.array([1, 0])
print(v.shape)NameError: name 'np' is not defined
# The last dimension matches! They can be added
result = A + v
print(result)NameError: name 'A' is not defined
In this case, the sum is performed along the columns of A (dimension 1) because v is added to each row of A.
If v had shape (m,) instead, an error would occur.
A = np.array([[1, 2],
[3, 4],
[5, 6]])
print(A.shape)
v = np.array([1, 0, -1])
print(v.shape)
result = A + v
print(result)NameError: name 'np' is not defined
Deactivate AI assistant tools, and try the following exercises.
Exercise: Perform the following operation:
\[5 \cdot A \cdot v\]
Where \[A=\begin{pmatrix} 1 & 2 & 3 & 4\\ 5 & 6 & 7 & 8\\ 9 & 10 & 11 & 12 \end{pmatrix}\] and \(v = [-2, -1, 0, 1]\)
A = np.array([
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
])
v = np.array([-2, -1, 0, 1])
result = 5 * A * v
print(result)NameError: name 'np' is not defined
Exercise: Given any matrix \(A\), multiply the elements of its first column times 1, the second column times 2, the third times 3, and so on.
For instance:
\[A = \begin{pmatrix} 1 & 2 & 3\\ 4 & 5 & 6\\ \end{pmatrix}\]
Would end up being: \[A' = \begin{pmatrix} 1 & 4 & 9\\ 4 & 10 & 18\\ \end{pmatrix}\]
A = np.array([
[1, 2, 3],
[4, 5, 6]
])
weights = np.arange(1, A.shape[1] + 1)
result = A * weights
print(result)NameError: name 'np' is not defined
arr = np.array([1, 2, 3, 4, 5])
# Square root
print(np.sqrt(arr))NameError: name 'np' is not defined
# For higher roots, we can just operate as we do with arrays
print(arr**(1/3))NameError: name 'arr' is not defined
So why use np.sqrt if we can do **(1/2)?
np.sqrt() clearly communicates the intended operation and uses NumPy’s optimized array implementation. The expression arr ** (1 / 2) is also valid. The following optional timing experiment should be run manually rather than whenever the notes are rendered.
arr = np.arange(1, 10000) # We use a very big array
for _ in range(10000):
np.sqrt(arr)
# We are not printing anything, because we do not want to fill the output
# We only want to see how much time this takes!for _ in range(10000):
arr**(1/2)
# We are not printing anything, because we do not want to fill the output
# We only want to see how much time this takes!The same logic applies to exponentiation!
arr = np.array([1, 2, 3, 4, 5])
# Exponentiation
print(np.exp(arr))NameError: name 'np' is not defined
NumPy can do more than just element-wise operations. It also supports matrix multiplication, transposition, and other matrix math.
Matrix multiplication:
\[A\ B = \begin{pmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{pmatrix} \begin{pmatrix} b_{11} & b_{12} \\ b_{21} & b_{22} \end{pmatrix} = \begin{pmatrix} a_{11}b_{11} + a_{12}b_{21} & a_{11}b_{12} + a_{12}b_{22} \\ a_{21}b_{11} + a_{22}b_{21} & a_{21}b_{12} + a_{22}b_{22} \end{pmatrix}\]
# Matrix multiplication (option A)
A = np.array([[1, 2], [3, 4]])
B = np.array([[4, 3], [2, 1]])
C = np.matmul(A, B)
print(C)NameError: name 'np' is not defined
# Matrix multiplication (option B)
C = A @ B
print(C)NameError: name 'A' is not defined
Dot products: For one-dimensional vectors, np.dot() computes the usual dot product.
\[\mathbf{u} \cdot \mathbf{v} = \sum_i u_i v_i\]
For two-dimensional arrays, np.dot(A, B) performs matrix multiplication. Prefer the @ operator when that is what you mean because it makes the operation explicit.
# Vector dot product
u = np.array([1, 2, 3])
v = np.array([4, 5, 6])
print(np.dot(u, v))NameError: name 'np' is not defined
If you want the sum of element-wise products of two matrices—the Frobenius inner product—write it directly:
# Sum of element-wise products
frobenius_product = np.sum(A * B)
print(frobenius_product)NameError: name 'np' is not defined
Both dot and matmul are used for matrix multiplication in NumPy, but they behave differently, especially when dealing with higher-dimensional arrays (i.e., tensors). The primary difference emerges in multi-dimensional array multiplication.
The official documentation says: matmul differs from dot in two important ways.
Cross product: A cross product is defined for vectors with two or three components. With a stack of vectors, NumPy applies the operation to corresponding rows.
For two-dimensional vectors, the result is the scalar \(u_1v_2-u_2v_1\). For three-dimensional vectors, the result is another three-dimensional vector perpendicular to both inputs.
# Create two stacks of 2D vectors
u = np.array([[1, 2], [3, 4]])
v = np.array([[4, 3], [2, 1]])
# One scalar cross product per row
cross_products = np.cross(u, v)
print(cross_products)NameError: name 'np' is not defined
Transposition:
\[A^T = \begin{pmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{pmatrix}^T = \begin{pmatrix} a_{11} & a_{21} \\ a_{12} & a_{22} \end{pmatrix}\]
a = np.array([[1, 2, 3], [4, 5, 6]])
print(a)
# Matrix transposition
b = a.T
print(b)NameError: name 'np' is not defined
Determinants provide information about the matrix’s invertibility, while the inverse of a matrix is crucial for solving systems of linear equations.
Assuming the matrix you created is \(A\), perform the following matrix operation:
\[A B^T / 6\]
Where
\[B=\begin{pmatrix} -1 & 0 & 0 & -1\\ 0 & 1 & 1 & 0\\ 1 & -1 & 1 & -1 \end{pmatrix}\]
A = np.arange(1, 13).reshape(3, 4)
B = np.array([
[-1, 0, 0, -1],
[0, 1, 1, 0],
[1, -1, 1, -1]
])
result = (A @ B.T) / 6
print(result)NameError: name 'np' is not defined
a = np.array([[1, 2], [3, 4]])
det_a = np.linalg.det(a)
print(det_a)NameError: name 'np' is not defined
a = np.array([[1, 2], [3, 4]])
inverse_a = np.linalg.inv(a)
print(inverse_a)NameError: name 'np' is not defined
NumPy operates in radians. If you are using degrees, make sure to convert them first!
angles = np.array([0, 30, 45, 60, 90]) # in degrees
# NumPy operates in radians!
angles_rad = np.radians(angles)
print(angles_rad)NameError: name 'np' is not defined
sines = np.sin(angles_rad)
print(sines)NameError: name 'np' is not defined
cosines = np.cos(angles_rad)
print(cosines)NameError: name 'np' is not defined
tangents = np.tan(angles_rad)
print(tangents)NameError: name 'np' is not defined
The mean is the average of a data set.
arr = np.array([1, 2, 3, 4, 5])
# Mean
print("Mean:", np.mean(arr))NameError: name 'np' is not defined
The median is the middle value when the data set is ordered.
arr = np.array([1, 2, 3, 4, 5])
# Median
print("Median:", np.median(arr))NameError: name 'np' is not defined
The standard deviation measures the dispersion of data from its mean.
arr = np.array([1, 2, 3, 4, 5])
# Standard Deviation
print("Standard Deviation:", np.std(arr))NameError: name 'np' is not defined
Variance is the square of the standard deviation.
arr = np.array([1, 2, 3, 4, 5])
# Variance
print("Variance:", np.var(arr))NameError: name 'np' is not defined
Exercise: Given a set of student grades, compute the mean, median, mode, standard deviation, and variance.
grades = np.array([85, 90, 78, 92, 88, 90])
print("Mean:", np.mean(grades))
print("Median:", np.median(grades))
print("Mode:", 90)
print("Standard Deviation:", np.std(grades))
print("Variance:", np.var(grades))NameError: name 'np' is not defined
grades = np.array([85, 90, 78, 92, 88])
sorted_indices = np.argsort(grades)
print(f"Indices to sort grades: {sorted_indices}")NameError: name 'np' is not defined
max_grade_index = np.argmax(grades)
print(f"Index of highest grade: {max_grade_index}")NameError: name 'np' is not defined
min_grade_index = np.argmin(grades)
print(f"Index of lowest grade: {min_grade_index}")NameError: name 'np' is not defined
arr = np.array([[1, 2], [3, 4]])
print("Array:\n", arr)
total_sum = np.sum(arr)
print(f"Sum: {total_sum}")NameError: name 'np' is not defined
# We sum across dimension 0
col_sum = np.sum(arr, axis=0)
print(f"Sum of columns: {col_sum}")NameError: name 'np' is not defined
# We sum across dimension 1
row_sum = np.sum(arr, axis=1)
print(f"Sum of rows: {row_sum}")NameError: name 'np' is not defined
cumulative_sum = np.cumsum(arr)
print(f"Cumulative Sum: {cumulative_sum}")NameError: name 'np' is not defined
product = np.prod(arr)
print(f"Product: {product}")NameError: name 'np' is not defined
Deactivate AI assistant tools, and try the following exercises.
Exercise: Given a “magic square” matrix \(A\):
\[A = \begin{pmatrix} 23 & 28 & 21\\ 22 & 24 & 26\\ 27 & 20 & 25 \end{pmatrix}\]
Normalize its values (subtract mean and divide by standard deviation). We call the new matrix \(B\).
\[B[i,j] = \frac{A[i,j] - \text{mean}(A)}{\text{std}(A)}\]
arr = np.array([[23, 28, 21], [22, 24, 26], [27, 20, 25]])
b = (arr - np.mean(arr)) / np.std(arr)
print(b)NameError: name 'np' is not defined
Exercise: Compute \(B^T\).
arr = np.array([[23, 28, 21], [22, 24, 26], [27, 20, 25]])
b = (arr - np.mean(arr)) / np.std(arr)
print(b.T)NameError: name 'np' is not defined
Exercise: Compute \(A \cdot B^T\).
arr = np.array([[23, 28, 21], [22, 24, 26], [27, 20, 25]])
b = (arr - np.mean(arr)) / np.std(arr)
print(arr @ b.T)NameError: name 'np' is not defined
Example: Can you find some interesting properties about this magic square matrix \(A\)?
Try to sum its rows, its columns, its diagonal.
arr = np.array([[23, 28, 21], [22, 24, 26], [27, 20, 25]])
print(np.sum(arr, axis=0))
print(np.sum(arr, axis=1))
print(np.trace(arr))NameError: name 'np' is not defined
You will find more exercises here!
1.axis argument controls the direction of an aggregation.* is element-wise multiplication; @ is matrix multiplication.Given arrays with shapes (4, 3) and (3,), explain why addition works. Would addition still work if the second shape were (4,)?
Work through the Session 12 homework exercises available here.