7  Introduction to NumPy

7.1 Introduction

Python’s built-in data types like lists and tuples are not particularly well-suited for mathematical operations. We will show three examples of computations that we often need to do, and we will see that using lists involves quite a lot of coding to get the tasks done. We will then see that NumPy can do these tasks very efficiently.

7.2 Three Example Problems

Example 1

For the first example, suppose we have a list x of numbers and we want to double each of the elements. We can’t use 2 * x because as we learned in Chapter 5 that just repeats the list twice. We have to do something like:

x = [2, 4, 8]
y = []
for i in x:
    y.append(2 * i)
y
[4, 8, 16]

We create an empty list y. We then loop over the elements of x and append two times the element to y. This is very clunky. A better way of doing this is using a list comprehension:

x = [2, 4, 8]
[2 * i for i in x]
[4, 8, 16]

But this is still a bit clunky. We would prefer a method that can just do 2 * x and get the same output.

Example 2

Another example computation that we often need to do is if we have two lists of numbers x and y with the same number of elements and and we want to multiply them by each other element-by-element. Expressed in mathematical notation, suppose we have two vectors of numbers \boldsymbol{x} and \boldsymbol{y}: \begin{split} \boldsymbol{x}&=\left[x_1, x_2, \dots, x_n\right] \\ \boldsymbol{y}&=\left[y_1, y_2, \dots, y_n\right] \\ \end{split} and we want to calculate \boldsymbol{z} from this which is:

\boldsymbol{z}=\left[x_1 \times y_2, x_2 \times y_2, \dots, x_n \times y_n\right] We can’t do x * y. That would return an error. But we could do this using a for loop:

x = [2, 4, 8]
y = [3, 2, 2]
z = []
for i in range(len(x)):
    z.append(x[i] * y[i])
z
[6, 8, 16]

Because we want to loop over the elements of both x and y, we have to loop over the indices 0, 1, 2. We could have written for i in [0, 1, 2], but range(len(x)) does this for us automatically (which is very useful if we have a long list). To see better what range is doing we can do:

list(range(5))
[0, 1, 2, 3, 4]

We can see it creates a list from 0 up to but not including the argument.

We can improve on this code slightly by using the zip() function, which combines several iterables into one iterable.

x = [2, 4, 8]
y = [3, 2, 2]
z = []
for i, j in zip(x, y):
    z.append(i * j)
z
[6, 8, 16]

Similarly we can use zip() to do the task with a list comprehension:

x = [2, 4, 8]
y = [3, 2, 2]
[i * j for i, j in zip(x, y)]
[6, 8, 16]

Even though we have now shortened the command down to one line, this last solution is still pretty clunky and also quite complicated. We would prefer an operation where we can just do x * y.

Example 3

For the last example, suppose we want to find the median of a list of numbers. Recall that if the length of the list of numbers is odd, then the median is just the number in the middle when we sort the numbers. If the length of the list of numbers is even, then the median is the average of the two numbers closest to the middle when we sort the numbers.

We could create our own function to do this:

def median(x):
    y = sorted(x)
    if len(y) % 2 == 0:
        return (y[len(y) // 2 - 1] + y[len(y) // 2]) / 2
    else:
        return y[len(y) // 2]

The function first sorts the list. The sorted() function gives the sorted list as the output. We do this instead of x.sort() because otherwise the function would sort our input list globally which we may not want it to do. The if len(y) % 2 == 0: checks if the length of the list is even. If it is even it takes the average of the element with index len(y) // 2 - 1 (just left of the middle) and the element with index len(y) // 2 (just right of the middle). We use // to ensure the division returns an integer instead of a float. If the length of the list is odd it returns the element with index len(y) // 2. Because len(y) / 2 is not an integer when len(y) is odd we use // to round down.

Let’s test it out:

median([2, 6, 4])
4
median([2, 6, 4, 3])
3.5

We get the expected ouptut. However, this is quite complicated. We wouldn’t want to have to code this function every time we wanted to do something as common as finding the median.

We will see that functions from the module numpy can solve each of these problems (and a lot more) very easily.

7.3 Importing the NumPy Module

We can import the numpy module using import numpy like with other modules. However it is conventional to load NumPy the following way:

import numpy as np

This way we can use the functions from NumPy with the shorter np instead of having to type numpy out in full every time. Doing this shortcut is okay because so many people do it that it’s easy for people to read. You can load other modules with shortcuts in a similar way, but you should follow the normal conventions when you can.

NumPy works with arrays. An array is like a list but all elements must be of the same type (such as all floats). We can create an array using NumPy’s array function. Because we can shorten numpy to np, we can create an array with the function np.array() like this:

import numpy as np
x = np.array([2, 4, 8])
x
array([2, 4, 8])

We will now show the power of NumPy by doing all the previous examples with very little code.

7.4 Solving the Example Problems with NumPy

Example 1

To double every number in array:

x = np.array([2, 4, 8])
2 * x
array([ 4,  8, 16])

Example 2

To multiply the elements of two arrays element-by-element:

x = np.array([2, 4, 8])
y = np.array([3, 2, 2])
x * y
array([ 6,  8, 16])

Example 3

To get the median of an array:

x = np.array([2, 6, 4])
np.median(x)
4.0
x = [2, 6, 4, 3]
np.median(x)
3.5

The np.median() function also works if we just provide a list instead of an np.array:

np.median([2, 6, 4, 3])
3.5

These are just a few examples of how NumPy can simplify coding drastically.

For many programming tasks we need to do, it’s very often many people had to do the same thing before. This means there is often a module available that can do the task. Of course we learn a lot from coding functions from scratch, but in order to complete a task quickly and efficiently it is usually better to use the modules made for the task.

7.5 Matrix Operations

NumPy can also do matrix operations very easily. For example, suppose we had two 3 \times 3 matrices \boldsymbol{A} and \boldsymbol{B}:

\boldsymbol{A} = \left(\begin{matrix} 1 & 2 & 3 \\ 2 & 3 & 1 \\ 3 & 1 & 3 \end{matrix}\right) \qquad \boldsymbol{B} = \left(\begin{matrix} 2 & 1 & 2 \\ 3 & 2 & 1 \\ 1 & 3 & 1 \\ \end{matrix}\right) and wanted to calculate their product \boldsymbol{C}=\boldsymbol{AB}.

Manually, we could calculate each row i and column j of \boldsymbol{C} with \sum_{k=1}^3 a_{ik} b_{kj}. For example, row 2 and column 1 of \boldsymbol{C} would be: \begin{split} c_{21}&=\sum_{k=1}^3 a_{2k} b_{k1}\\&= a_{21} b_{11} + a_{22} b_{22} + a_{23} b_{33}\\ &= 2\times2 + 3 \times 3 + 1 \times 1\\&=4+9+1\\&=14 \end{split} But doing this for all 9 elements would take a long time, and we could easily make a mistake along the way. Let’s use Python to calculate it instead.

If we were to try and do this with only built-in Python commands, it would still be rather complicated. We could define the matrices \boldsymbol{A} and \boldsymbol{B} using nested lists, where each list contains 3 lists representing the rows:

A = [
    [1, 2, 3],
    [2, 3, 1],
    [3, 1, 3]
]

B = [
    [2, 1, 2],
    [3, 2, 1],
    [1, 3, 1]
]

To calculate \boldsymbol{C}=\boldsymbol{AB} then we follow the same approach as the manual way. We loop through each row i and each column j of the matrix and calculate: c_{ij} = \sum_{k=1}^3 a_{ik} b_{kj}. We do this by starting with a zero matrix and progressively fill it up.

C = [
    [0, 0, 0],
    [0, 0, 0],
    [0, 0, 0]
]

for i in range(3):
    for j in range(3):
        for k in range(3):
            C[i][j] += A[i][k] * B[k][j]

for row in C:
    print(row)
[11, 14, 7]
[14, 11, 8]
[12, 14, 10]

This is an example of a triple-nested loop: a loop inside a loop inside a loop.

Using NumPy to do the multiplication is much easier. We can just use the np.dot() function:

import numpy as np
A = np.array(A)
B = np.array(B)
np.dot(A, B)
array([[11, 14,  7],
       [14, 11,  8],
       [12, 14, 10]])

NumPy can also do many other matrix operations, such as:

  • transposing with the command np.transpose()
  • inversion with the command np.linalg.inv().

You can therefore use Python to help you with the Mathematics course that you are taking alongside this one!