This notebook discusses how to work with vectors and matrices in Numpy.
We will loosely follow LL § 2.3. Another good reference is the Numpy Documentation, especially the
# Standard imports
import numpy as np
import matplotlib.pyplot as plt
import math as m
from mpmath import mp, iv
Vectors are just lists of numbers you can add or subtract. Numpy arrays serve the purpose of vectors.
v = np.array([1, 2, 5])
v
You can get the length of a numpy array, which is the same as the vectors dimension.
len(v)
You can access the entries using square brackets. See below:
for i in range(len(v)):
print("v[{}] = {}".format(i, v[i]))
You can also access entries from the back of the list. The last entry is v[-1]
.
for i in range(-1,-len(v)-1,-1):
print("v[{}] = {}".format(i, v[i]))
To be sure you have a numpy array you can check its type. Technically the arrays we are building are of type numpy.ndarray
and array
is just a convienient abbreviation.
type(v)
Some other functions defined by numpy also return numpy arrays. We have also seen:
x = np.linspace(1, 5, 4)
type(x)
You can also initialize a vector of all zeros.
z = np.zeros(3)
print("z = {}".format(z))
type(z)
It is convientient sometimes to create a vector using zeros
and then set its individual entries.
z = np.zeros(10)
for i in range(10):
z[i] = m.sqrt(i)
print(z)
The most important operations you can do with vectors are addition and scalar multiplication. Both work as you would expect.
print("v = {}".format(v))
0.7 * v
w = np.array([2, 0, -7])
print("v = {}".format(v))
print("w = {}".format(w))
v + w
Another basic operation is the dot product.
v.dot(w)
Note that setting one array equal to another one means the two names point to the same object. This can lead to unexpected issues like the following:
v = np.array([1,2,3])
w = v
w[0] = 5
print("v = {}".format(v))
print("w = {}".format(w))
The correct way to do this is the following:
v = np.array([1,2,3])
w = v.copy()
w[0] = 5
print("v = {}".format(v))
print("w = {}".format(w))
This is discussed in a bit more detail in § 2.3.4 of LL.
The type of number stored by an array v
can be accessed with v.dtype
.
v = np.array([1.1, 2.3])
print(v)
v.dtype
v = np.array([0,1,2])
print(v)
v.dtype
You can override the data type:
v = np.array([0,1,2], dtype=float)
print(v)
v.dtype
v = np.array([0,1,2**64])
print(v)
v.dtype
Storing multiple precision numbers:
mp.dps = 50
v = np.array([mp.tan(1), mp.sin(3)])
print(v)
v.dtype
Storing intervals:
v = np.array([iv.tan(1), iv.pi])
print(v)
v.dtype
mat = np.array([[1,2,3],[4,5,6]])
print(mat)
mat.dtype
You can access the rows using m[row]
.
for row in range(len(mat)):
print("row {} of mat is {}".format(row,mat[row]))
This means you can access the individual entries with m[row][col]
.
row = 0
col = 1
mat[row][col]
Recall an $m \times n$ matrix is one with $m$ rows and $n$ columns. You can initialize a $m \times n$ matrix of all zeros with the following:
z = np.zeros((3,2))
z
Note that above, we pass a pair $(3,2)$ to zeros
, which is why there are double parethesis. The following is the same:
dim = (3,2)
z = np.zeros(dim)
z
You can recover the number of rows and columns using shape
:
z.shape
This means that we can use the following to iterate over the entries of any matrix.
num_rows, num_cols = mat.shape
for row in range(num_rows):
for col in range(num_cols):
print("mat[{}][{}] = {}".format(row, col, mat[row][col]))
You can change the shape of a matrix or a vector using the resize
method.
Below, we take a vector, resize it into a matrix, and then resize it back into a vector.
A = np.array([n for n in range(12)])
print("Initially A = {}.".format(A))
A.resize((3,4))
print("After first resize, A =\n{}.".format(A))
A.resize(12)
print("After second resize, A = {}.".format(A))
Alternately, you can use the reshape method to make a matrix with a new shape that shares the same underlying data. For example:
A = np.array([n for n in range(12)])
B = A.reshape((3,4))
print("A = {}.".format(A))
print("B = \n{}.".format(B))
Because $B$ shares data with $A$, both objects change when you change a value in either one. Observe:
A[11]=999
B[0][0]=888
print("A = {}.".format(A))
print("B = \n{}.".format(B))
So, if you want to preserve the original matrix, you need to make a copy.
A = np.array([n for n in range(12)])
B = A.reshape((3,4)).copy()
A[11]=999
B[0][0]=888
print("A = {}.".format(A))
print("B = \n{}.".format(B))
The expression np.arrange(start, stop, step)
is equivalent to np.array(range(start, stop, step)
and default arguments work in the same way. You can then use the reshape
method to adjust the dimensions.
A = np.arange(1,19,3)
print("First, A = {}.".format(A))
A.resize((2,3))
print("Now A is\n{}.".format(A))
A random $2 \times 1$ matrix with entries in $[0,1)$:
r = np.random.random((2,1))
print(r)
The $3 \times 3$ identity matrix.
I = np.identity(3)
print(I)
Arrays representing matrices can be joined with the function np.concatenate
, but you need to be sure that the dimensions match. By default arrays are joined vertically. For example:
A = np.arange(4).reshape(2,2)
print("A =\n{}".format(A))
B = np.arange(10,14).reshape(2,2)
print("B =\n{}".format(B))
C = np.concatenate((A,B)) # Notice we are passing a pair
C
Concatenate has an optional axis
parameter that allows the matrices to be joined horizontally too. The above is equivalent to:
np.concatenate((A,B), axis=0)
If we change to axis=1
we get horizontal joining.
np.concatenate((A,B), axis=1)
Scalar multiplication:
mat = np.array([[1,2,3],[4,5,6]])
-2*mat
Many numpy functions are applied coordinate-wise:
mat = np.array([[1,2,3],[4,5,6]])
np.sqrt(mat)
This includes addition and multiplication:
mat = np.array([[1,2,3],[4,5,6]])
print(mat+mat)
mat = np.array([[1,2,3],[4,5,6]])
print(mat*mat)
Here is a more elaborate example. Suppose you want a $3 \times 3$ matrix with entries randomly chosen from $\{1, \ldots, 10\}$.
# First get a random matrix with entries in [0,1):
r = np.random.random((3,3))
print(r)
# The map x -> 10*(1-x) will be applied coordinatewise
# It puts entries into the interval $(0,10]$.
r = 10*(1-r)
print(r)
# The ceil function takes a number to the smallest integer
# greater than that number. We apply it coordinatewise.
r = np.ceil(r)
r
# Observe r has data type float:
r.dtype
# if we want integer we can do
r = np.array(r, dtype=int)
r
Note that even operators behave coordinate wise. So, below we get an array off all booleans:
b = (r > 2)
print(b)
You can check whether all the entries are true with the all
method. (This is a logical and
of all the entries.)
b.all()
You can check whether any of the entries are true with the any
method. (This is a logical or
of all the entries.)
b.any()
We can get the total number of trues using the following:
b.sum()
(When we add, True
becomes $1$ and False
becomes $0$.)
Matrix multiplication can be carried out with the @
operator.
m1 = np.array([[3,0],[1,1]])
print(m1, end="\n\n")
m2 = np.array([[1,2],[0,1]])
print(m2, end="\n\n")
m3 = m1 @ m2
print(m3)
It is equivalent to use the dot
method:
m1 = np.array([[3,0],[1,1]])
print(m1, end="\n\n")
m2 = np.array([[1,2],[0,1]])
print(m2, end="\n\n")
m3 = m1.dot(m2)
print(m3)
Recall that you get the transpose of a matrix by switching rows and columns.
mat = np.array([n for n in range(12)])
mat.resize((3,4))
mat
mat.transpose()
mat = np.array([n for n in range(12)])
mat.resize((3,4))
mat
mat[0]
We have seen that a row can be accessed as vectors with mat[row]
. We can also use mat[row,:]
. Here the :
indicates "all."
# First row:
mat[0, :]
So, to get a column, we can do mat[:, col]
.
# Second column:
mat[:, 1]
Note that the above give them as a vectors (1-dimensional arrays). You can also access them as submatrices.
# First row:
mat[[0],:]
# First column:
mat[:,[0]]
mat = np.array([n for n in range(16)])
mat.resize((4,4))
print(mat)
The second method of getting rows and columns is suggestive of a notation for submatrices. The following gets the first two columns as a matrix.
mat[:,[0,1]]
Note that they don't have to be in order. The first, third and fourth rows in reverse order:
mat[[3,2,0],:]
To get the submatrix consiting of those entries in rows $3$, $2$, or $0$ and in columns $0$ or $1$, you can combine the expressions above:
mat[[3,2,0],:][:,[0,1]]
The notation for accessing rows and columns is somewhat similar to the range
command, in that you can give a pair of values start : end
. Recall that in range
this gives a list of values including start
up to but not including end
.
Here is a simple matrix for playing with these things.
mat = np.array([n for n in range(16)])
mat.resize((4,4))
print(mat)
Here we et the submatrix of elements in positions $(m,n)$ where $m,n \in \{1,2\}$. This is the central square in our matrix.
mat[1:3,1:3]
mat[1:3,:]
mat[1:3,:][:, 1:3]
If you omit the first element of a pair, it defaults to zero. So, the following gives the submatrix of entries lying in the first three rows and the first two columns.
mat[:3,:2]
Defaults for the second entry are the total number of rows and the total number of columns, respectively. The following gives elements in rows $2$ and $3$ and in columns $1$, $2$ and $3$.
mat[2:,1:]
You can specify just a single number with no colon to restrict attention to a row or column. This gives a vector consisting of the entries in row $2$ and in columns $1,\ldots,3$.
mat[2, 1:]
You can also use a triple of values to specify start : end : step
for the rows and the columns. The following gives the matrix cosisting of entries in positions $(m,n)$ with $m$ and $n$ even.
print(mat, end="\n\n")
mat[::2,::2]
The entries in odd positions:
mat[1::2,1::2]
Here we reverse the order of rows and columns:
mat[::-1, ::-1]
When you get submatrices, the data is passed by reference. This means assignments to submatrices change the original matrix.
mat = np.array([n for n in range(16)]).reshape((4,4))
print(mat)
Here we set all entries in even positions to $-1$.
mat[::2,::2]=-1
print(mat)
Here we double all entries in odd positions:
mat
mat[1::2,1::2]
mat[1::2,1::2] *= 2
print(mat)
You can also assign a matrix to assign different values to each entry.
mat[::2,::2] = np.array([[-2, -3], [-4, -5]])
print(mat)
mat
This is particularly useful for row reduction. As a first step in row reduction, we would clear the entries in the first column below the $-2$ in the first row by adding multiples of the first row. We can do this with:
mat[1,:] += 2 * mat[0,:]
mat
mat[2,:] += -2 * mat[0,:]
mat[3,:] += 6 * mat[0,:]
print(mat)
Recall that to get columns $1$ and $2$ we can do:
mat
mat[:,[1,2]]
To get them in the opposite order we can do:
mat[:,[2,1]]
So, to swap these two columns you can do mat[:,[1,2]] = mat[:,[2,1]]
:
print(mat, end="\n\n")
mat[:,[1,2]] = mat[:,[2,1]]
print(mat)
mat
mat[2,:] *= 10
mat