Numpy Arrays¶

This notebook covers roughly the same ground as this video.

Almost all scientific computation in Python uses the numpy library. There is extensive documentation at numpy.org. To make this library available, we must import it. The standard way to do so is as follows:

In [ ]:
import numpy as np

The most basic thing that this gives us is a collection of standard mathematical functions like $\sin(x)$, $\log(x)$ and so on. Because these are part of numpy, and we have imported numpy as np, we need to refer to these functions as np.sin(), np.log() and so on. For example, it is known that $\sin(\pi/8)=\sqrt{2-\sqrt{2}}/2$, and we can check this numerically as follows: we define $a$ to be $\sin(\pi/8)$, and $b$ to be $\sqrt{2-\sqrt{2}}/2$, then we print $a$, $b$ and $|a-b|$, and we observe that $|a-b|$ is very small.

In [ ]:
a = np.sin(np.pi/8)
b = np.sqrt(2 - np.sqrt(2)) / 2
print (a, b, np.abs(a-b))

Often, we want to deal with vectors. You might think that vectors could be represented as lists. However, if you try to add lists then Python will just join them together, which is not what we want for vector addition. Instead, for vectors and for many other things, we need to use numpy arrays. If you write np.array(x) then Python will look at x and try to find some sensible way to convert it into a numpy array. In particular, this works in an obvious way if x is a list of numbers.

If we use a normal print() statement, we cannot tell the difference between a numpy array and a list. However, if we do print(repr(a)) or just leave a as the last result in the cell, then it will be displayed in a slightly different way which indicates that it is an array.

In [ ]:
a = np.array([9, 8, 7, 6, 5, 4, 3, 2, 1])
print(a)
print(repr(a))
a
In [ ]:
u = np.array([1, 2, 3])
v = np.array([4, 5, 6])
print(f'u         = {u}')
print(f'v         = {v}')
print(f'100 u + v = {100*u + v}')

We can enter a matrix by applying np.array() to a nested list of lists. We can also make matrices filled with zeros or ones using np.zeros() or np.ones(), or identity matrices using np.eye(). Note that the entries in $B$ and $C$ below have decimal points, indicating that they are officially floating point numbers. For $D$ we have added the extra argument dtype=np.int64 to np.eye() to ensure that the entries are officially integers.

In [ ]:
A = np.array([[1, 1, 1, 1], [0, 1, 1, 1], [0, 0, 1, 1]])
B = np.zeros((2, 5))
C = np.ones((3, 2))
D = np.eye(4, dtype=np.int64)
print(f'A = \n {A}\n')
print(f'B = \n {B}\n')
print(f'C = \n {C}\n')
print(f'D = \n {D}\n')

Often we want an array containing equally spaced values. There are two different functions for this, called np.arange() and np.linspace(). The function np.arange(a, b, s) gives values starting with a, increasing in steps of size s, stopping at the last value strictly before b. In particular, this will not include b itself. On the other hand, np.linspace(a, b, n) gives n equally spaced points, starting with a and ending with b. As there are n points including the endpoints, we see that there are n-1 steps, each of size (b - a)/(n - 1). Thus, if we want the list $6,6.1,6.2,\dotsc,6.9,7$ we need np.linspace(6,7,11), not np.linspace(6,7,10) or np.arange(6,7,0.1).

In [ ]:
print(np.linspace(6,7,11))
print(np.linspace(6,7,10))
print(np.arange(6, 7, 0.1))

Every numpy array has a shape, which is a tuple of integers. The matrix $A$ above is a $3\times 4$ matrix, so it has shape (3, 4). The vector u above has length $3$, so the shape is a tuple of length one containing only the number $3$. It is a quirk of Python notation that this must be written as (3,), not just (3) or 3.

In [ ]:
print(f'{A.shape=}')
print(f'{u.shape=}')

Officially, vectors of length 3 are different from matrices of shape $1\times 3$ (which you might call row vectors) or matrices of shape $3\times 1$ (which you might call column vectors). Often, numpy will silently convert between these forms as needed, but sometimes it is necessary to pay attention to the difference between them. The reshape() method can be used to convert an array to a different shape with the same total number of entries.

In [ ]:
v_vec = np.array([7, 8, 9])   # vector of length 3
v_row = v_vec.reshape((1, 3)) # a matrix of shape (1, 3), i.e. a row vector
v_col = v_vec.reshape((3, 1)) # a matrix of shape (3, 1), i.e. a column vector

print(f'v_vec={v_vec}\n')
print(f'v_row={v_row}\n')
print(f'v_col=\n{v_col}')

Note that a vector has only one index, but a matrix has two. It is also possible to have arrays with more than two indices. For example, a colour can be represented by an array of three numbers, giving the brightness of red, green and blue light. A coloured image of $1920\times 1080$ pixels can thus be represented by an array of shape $1920\times 1080\times 3$, giving the colour of each pixel. If Q is an array of this type, we will have Q.ndim=3 (indicating that there are three indices) and Q.shape=(1920, 1080, 3).

Here is a smaller example of an array with three indices:

In [ ]:
Z = np.arange(8).reshape((2, 2, 2))
print(f'Z=\n{Z}\n')
print(f'{Z.ndim=}')
print(f'{Z.shape=}')
In [ ]: