NumPy Array Arithmetic

Module 3: NumPy

NumPy Array Arithmetic

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 np
ModuleNotFoundError: No module named 'numpy'

Element-Wise Operations

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

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

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 in NumPy

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?

  • NumPy compares the shapes of the arrays element-wise, starting from the trailing dimensions.
  • If the dimensions are equal, or one of them is 1, they are considered compatible.
  • NumPy then “stretches” the smaller array along the dimension with size 1 to match the larger array.

Example of broadcasting:

  • Let’s say you have a 2D array (matrix) A with shape (m, n) and a 1D vector v with shape (n,).
  • The array 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

Practice

Deactivate AI assistant tools, and try the following exercises.

Exercise

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

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

Mathematical Functions

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

Geometry Functions

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.

  • Multiplication by scalars is not allowed.
  • Stacks of matrices are broadcast together as if the matrices were elements.

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.

Exercise

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

Linear Algebra Functions

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

Trigonometric Functions

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

Statistical Functions

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

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

Sorting and Searching

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

Aggregation Functions

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

Practice

Deactivate AI assistant tools, and try the following exercises.

Exercise

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

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

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

Further Practice

You will find more exercises here!


Summary and Reflection

  • Arithmetic operators act element by element on compatible arrays.
  • Broadcasting compares shapes from right to left and allows dimensions that match or equal 1.
  • The 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,)?


Homework and Review

Work through the Session 12 homework exercises available here.

Homework