Lab 7: NumPy and Matrix Row Reduction

In earlier labs, we wrote functions like scalar_mult(s, v), vector_add(v, w), and dot_product(v, w). In order to avoid having to write these functions every time, people like Travis Oliphant (a BYU alumnus) created a python library we can use called NumPy.

In this lab you will learn how to use NumPy to write functions that perform basic operations related to row-reducing a matrix. You will learn about:

  • importing NumPy

  • vectors and matrices in NumPy

  • accessing NumPy arrays using slicing

NumPy

Importing

To start using NumPy import it using:

>>> import numpy as np

As mentioned in Lab 5, every time we use a function from NumPy, we need to prefix it with np. So calling a function like cos (for cosine) would look like:

>>> np.cos(0)
1.0

Note

np is the commonly understood nickname for NumPy so we don’t have to type out “numpy” every time.

Vectors and Matrices

Recall that in NumPy, we represent matrices and vectors as a special kind of array called a NumPy array. To define a NumPy array, we use the function np.array(). For example, to create the vector

\[\begin{split}\left[\begin{array}1 1 \\ 2 \\ -1\end{array}\right]\end{split}\]

as a NumPy array, we take the Python list [1,2,-1] and plug it into the function np.array().

>>> my_vect=np.array([1,2,-1])

We can add vectors and multiply by scalars in a straightforward way. Think back to vector_add(v, w) and scalar_mult(c, v) from Lab 5.

>>> array1=np.array([1,2,3])
>>> array2=np.array([0,7,4])
>>> array1 + array2  # adding vectors
array([1, 9, 7])
>>> my_vect=np.array([1,2,-1])
>>> 3 * my_vect      # multiplying by scalars
array([3, 6, -3])

Note

While the vectors,

\[\begin{split}\left[\begin{array}1 1 & 2 & -1\end{array}\right] \text{, and} \left[\begin{array}1 1 \\ 2 \\ -1\end{array}\right]\end{split}\]

mean different things when we write them out, NumPy represents row and column vectors in the exact same way:

>>> np.array([1, 2, -1])
array([ 1,  2, -1])

Both row and column vectors will be created with a one dimensional NumPy array.

NumPy makes working with vectors very easy. They even have dot() which replaces our dot_product() function.

>>> array1=np.array([1,2,3])
>>> array2=np.array([4,5,6])
>>> np.dot(array1, array2)
32

Note

Note that if you attempt to add vectors of different lengths or perform the dot product of vectors of different sizes, NumPy will throw an error.

To define matrices in NumPy, we define them as “lists of lists”. Then we can plug that into the function np.array(). For example, to define the matrix

\[\begin{split}\left[ \begin{array}4 1 & 2 & 3 & 4 \\ -5 & -6 & -7 & -8 \\ 1 & 5 & 2 & 3 \end{array} \right]\end{split}\]

we would create a list with three elements. The first element will be the list [1, 2, 3, 4], which we consider the first row of the matrix. The second element in our list will be [-5, -6, -7, -8], representing the second row, and so on.

Note

In NumPy, both arrays and matrices are created using np.array(). np.array() returns a special object called a np.ndarray which will be explained more later.

>>> my_matrix = np.array([[1, 2, 3, 4],[-5, -6, -7, -8],[1, 5, 2, 3]])
>>> print(my_matrix)
[[ 1  2  3  4]
 [-5 -6 -7 -8]
 [ 1  5  2  3]]

Matrix Multiplication with @, np.dot(), and np.matmul()

There are a few different ways to multiply matrices in Python. @ is native to python, while np.dot() and np.matmul() come with NumPy.

>>> import numpy as np
>>> a = np.array([[1, 2, 3],
                  [3, 4, 5]])
>>> b = np.array([[2, 3],
                  [4, 5],
                  [6, 7]])
>>> a @ b
[[28 34]
 [52 64]]
>>> np.dot(a, b)
[[28 34]
 [52 64]]
>>> np.matmul(a, b)
[[28 34]
 [52 64]]

Each of these operations returns the same thing for 2d matrices. Each has different rules for NumPy arrays that are not two dimensional, so be careful of that. You can see more information here.

Accessing NumPy Arrays with Slicing

We can access elements of a NumPy array the same way we access elements in a list, by specifying indices. Recall that Python lists (and NumPy arrays) begin at index 0. So if an element of a list or array has index 3, that really means it’s the 4th element in the list or array.

>>> v=np.array([4,1,-5,3,-2,1,0,9])
>>> print(v[3])
3

If we want more specific indexing, we can use slicing. This is where we specify a range of indices. It looks like:

>>> my_array[start:stop]

We can think of it like \([start,stop)\) in mathematics. So from the array above,

>>> print(v[3:7])
[ 3 -2  1  0]

There is another optional parameter in slicing called step.

>>> my_array[start:stop:step]

step determines how many elements we skip over. So

>>> print(v[3:7:2])
[3 1]

will start at index 3 and select every second element until it reaches (but does not include) 7.

Here are some more examples:

>>> v=np.array([4,1,-5,3,-2,1,0,9])
>>> print(v[2:6])   # [2,6)
[-5 3 -2 1]
>>> print(v[3:])    # [3,end)
[3 -2 1 0 9]
>>> print(v[:4])    # [beginning,4)
[4 1 -5 3]
>>> print(v[::2])   # [beginning,end) stepping every second element
[ 4 -5 -2  0]

We can access the entries in a matrix in a similar way to accessing elements of a list. However for matrices, we need to provide two indices (or ranges of indices), to specify the location of the row(s) and/or column(s) in which we are interested.

>>> my_matrix=np.array([[1, 2, 3, 4],[-5, -6, -7, -8],[1, 5, 2, 3]])
>>> print(my_matrix)
[[ 1  2  3  4]
 [-5 -6 -7 -8]
 [ 1  5  2  3]]
>>> print(my_matrix[1,2])   # row index 1, column index 2
-7
>>> print(my_matrix[2,1:3]) # row index 2, column indices 1 through 3
[5 2]
>>> print(my_matrix[:,3])   # all the rows, column index 3
[4 -8 3]
>>> print(my_matrix[1])     # row index 1 (2nd row)
[-5 -6 -7 -8]

Warning

Remember in python, indexing starts at 0!

Note

my_matrix[1,2] will do the same thing as my_matrix[1][2] for np.ndarrays. For two dimensional python lists, only my_matrix[1][2] is valid.

Other NumPy Tools

When you make a NumPy array, it isn’t an int, str, float, or bool, it is something called an object of type numpy.ndarray (which stands for n-dimensional array). Objects in python are just one more way to represent data. When an object is made, it has attributes that contain different information about the object. We get attributes with the . notation. We will learn more about objects later, but for now you just need to know how to use object attributes. As an example, if our array is named array1, then:

  • array1.ndim will tell you the number of dimensions of the array

  • array1.size will tell you how many elements are in the array

  • array1.shape will give you a tuple with each element represents the number of elements in each dimension of the array (a one dimensional array would be (n,), a two dimensional array would be (n, m) and so on)

NumPy arrays also have functions associated with them. These functions have a special name because they only work on np.ndarrays. These special functions are called methods. We call them in the exact same way we would a normal function. Here are some of the most useful ones:

  • array1.sum() returns the sum of all the elements in the array

  • array1.mean() returns the mean of all the elements in the array

  • array1.max() returns the maximum value of the array

  • array1.min() returns the minimum value of the array

Warning

Attributes are not functions so we don’t call them with ().

The main difference between methods and attributes is that methods are calculated on the fly, while attributes are stored with the object.

NumPy also has built-in functions to create NumPy arrays. These are important to know about, but you don’t need to know all the details right now.

  • np.zeros(shape) creates an array full of 0s

  • np.ones(shape) creates an array full of 1s

  • np.empty(shape) creates an array filled with uninitialized (potentially random) numbers faster than zeros() or ones()

  • np.full(shape, fill_value) creates an array filled with the specified value

  • np.arange(start, stop, step) works just like range(), but it creates an array with all the values

  • np.linspace(start, stop, num) creates an array from start to end (inclusive) of evenly spaced numbers (specified by num)

NumPy also has a set of functions that you access with numpy.linalg. It includes things like matrix multiplication, eigenvalues, the transpose of a matrix, and lots of other useful functions. Again, these are good to know about, but you don’t need to know all the details yet.

Application: Matrix Row Reduction

Now, we will write functions to perform basic matrix operations related to row-reduction. These aren’t directly built into NumPy, but we can use NumPy to make writing them easier! These functions will be very useful for future labs as well.

Requirements

  • The functions you write for this lab should work for matrices of any size.

  • All inputs and outputs for this lab should be NumPy arrays.

Task 1

Write a function row_swap(A, i, j) which takes as input a matrix A, and two indices i and j. Your function should return the matrix obtained from A with rows i and j swapped.

>>> row_swap(np.array([[1, -1, 1], [0, 1, 3], [2, -2, 0]]), 0, 2)
array([[ 2, -2,  0],
       [ 0,  1,  3],
       [ 1, -1,  1]])
>>> row_swap(np.array([[2, -1, 3], [1, 2, 3]]), 0, 1)
array([[ 1,  2,  3],
       [ 2, -1,  3]])

Task 2

Write a function row_mult(A, i, c) which takes as input a matrix A, one index i, and a scalar c. Your function should return the matrix obtained from A with row i multiplied by c.

>>> row_mult(np.array([[1, 1], [2, 3]]), 1, 3)
array([[ 1,  1],
       [ 6,  9]])
>>> row_mult(np.array([[1, 1], [6, 9]]), 0, 0)
array([[ 0,  0],
       [ 6,  9]])

Task 3

Write a function row_add(A, i, j, c) which takes as input a matrix A, two indices i and j, and a scalar c. Your function should return the matrix obtained from A with row i replaced with itself plus c times row j.

>>> row_add(np.array([[0, 1, 1], [1, -1, 3], [1, 3, 2]]), 2, 0, -3)
array([[ 0,  1,  1],
       [ 1, -1,  3],
       [ 1,  0, -1]])
>>> row_add(np.array([[2, 1], [1, -2]]), 0, 1, 0)
array([[ 2,  1],
       [ 1, -2]])

Challenge

  1. Write a function that determines whether or not a matrix is in echelon form.

  2. Write a function that row-reduces a matrix to echelon form. The difficult part of this problem is determining when to swap rows.