In this task, you will become familiar with the **Sympy** library for linear algebra. A library is a collection of functions, methods, and objects that allow us to create and manipulate vectors and matrices. Sympy is the library that contains these items to assist us in doing so.

**Getting Started**
Libraries need to be loaded and are not standard when opening Python. This is due to the nature of memory and allocation of computer resources. If Python were to automatically load every package in existence, your computer would likely crash. We import a library by using the *import* command. The *"as"* portion allows us to simplify the name of the library so that, when we call it, we can use sp instead of spelling out numpy each time. sp is just a name. We could, in theory, call it abc, but then it would be harder to remember.

```
import sympy as sp
# The commands below are not important to understand - they make the matrices display nicely in the lines that follow.
from sympy.interactive.printing import init_printing
init_printing(use_unicode=False, wrap_line=False, no_global=True)
from sympy.matrices import *
```

We are now ready to use the Sympy library features. We wish to create a matrix that contains two rows: the first one will contain elements 1 and 2 and the second will contain elements 3 and 4. Since creating a matrix requires calling the Matrix() function, and since this function is native to the Sympy library, we need to tell Python to look for it in the Sympy library. To do this, we simply type sp.Matrix(). If we omit sp, then we will get an error, since the function cannot be found in the standard Python libraries. Take careful note of the use of brackets: the outer brackets start the matrix. Each row is created by []. Within those brackets, we type the elements, separated by commas. We separate each row block by commas. So, one row will be [1, 2] and the other will be [3, 4]. To put this in a matrix, we would need [ [1,2], [3,4] ], as shown below. We store this matrix in a variable called 𝑋X so that we can perform manipulations on the matrix. We also create matrices called 𝑌Y and 𝑍Z so that we can perform operations on them.

Example

```
X = sp.Matrix( [ [1,2],[3,4] ] )
X # Show X
```

Output:

[1 2

3 4]

```
Y = sp.Matrix( [ [5,6],[7,8] ] )
Z = sp.Matrix( [ [1], [7] ] )
```

**Matrix Features and Operations**
First, we can check to see the size of a matrix by using the **.shape** *attribute*. If we type X.shape, we will get back its size. Notice that we called this an attribute instead of a function. This is because we are not performing any operations on the matrix. Instead, we wish only to get its attribute - namely, its size.

```
print(X.shape)
print(Y.shape)
print(Z.shape)
```

Output:

```
(2, 2)
(2, 2)
(2, 1)
```

The **tuples** (ordered pairs) above are useful to see, but we may need to know, for instance, the number of rows that a matrix has so that we can execute some task accordingly (knowing how many rows to add or remove from a matrix, for example). You can access the components of a tuple via **indexing**. An ordered pair has two indices. The first is for the first element. For example, if you have an ordered pair (7,5), that represents the shape of a 7 x 5 matrix, 𝑋X, and you want to know how many rows this matrix has (it has 7), you can access the 7 by calling *X.shape[0]* (recall that Python starts with an index of 0). To access the 5, we would type *X.shape[1],*. Both of these calls will return a single value.

```
B = sp.Matrix( [ [1,7,3,5], [2,4,6,10] ] )
print(B.shape)
```

Output:

`(2, 4)`

`B.shape[0] # Returns 2, the number of rows in matrix B`

Output: 2

`B.shape[1] # Returns 4, the number of columns in matrix B`

Output: 4

**Performing matrix arithmetic **

Performing matrix arithmetic is quite simple. Below, we scale 𝑍Z by a factor of 3 and then add 𝑋X and 𝑌Y and then attempt to add 𝑌Y and 𝑍Z (this will return a strange matrix, which is not consistent with what we know about linear algebra - addition of matrices requires matrices of equal size).

```
#Mulitply Matrix
3*Z
```

Output:

[3

21]

```
#Adding Two Matrix
X+Y
```

Output:

[6 8

10 12]

**Transpose a Matrix or Vector**

Sometimes it becomes handy to **transpose** a matrix or vector. For a vector, this simply means taking a column vector and turning it into a row (or vice versa). For a matrix, this means we take the first row and turn it into the first column of the new matrix, the second row into the second column, etc. To do this we use the **T** attribute.

```
X = sp.Matrix([[1,2],[3,4]])
X
X.T
```

We can also create matrices filled with desired values. There are two useful functions: zeros() and ones().

**zeros()** takes two dimensions as an argument and outputs a matrix of that size (rows, columns) filled with zeros.

**ones()** takes the same type of argument, but fills the matrix with ones.

```
sp.zeros(3,3)
sp.ones(4,3)
```

What if we want to fill the matrix with, say, 2.4's? We can create a ones matrix and multiply it by the scalar 2.4.

`sp.ones(4,3)*2.4`

From time to time, it will be handy to create a matrix filled with random values between 0 and 1. Run the cell below a few times to see it generate different 3 x 2 matrices.

`sp.randMatrix(3,2)`

**Turning Vectors into a Matrix**

As you will see later on in this course, it will be handy to take a set of vectors and to then form a matrix out of them. In this example, we first define three vectors, each of which is a single column of three values (a 3 x 1):

```
x1 = sp.Matrix([[1],[2],[3]])
x2 = sp.Matrix([[4],[5],[6]])
x3 = sp.Matrix([[7],[8],[9]])
x1, x2, x3
```

If we want these vectors to form individual columns of a new matrix, we want to stack them horizontally. The function row_join() will attach matrices to some initial matrix (x1) another matrix. We can have multiple row_join() calls in one line.

`x1.row_join(x2).row_join(x3)`

If we want to save this as a new matrix, we simply assign it to a new variable.

```
X = x1.row_join(x2).row_join(x3)
X
```

Less obviously, we might wish for these vectors to get stacked vertically, so they form one long vector (9 x 1). We use the col_join() function to do this.

`x1.col_join(x2).col_join(x3)`

There are many instances in which we wish to extract a particular row, column, or individual element of a matrix. We can use a similar process as when we wanted to extract a single element from an ordered pair. A matrix has two indices: one for the rows and another for the column. If we have a matrix, 𝑋X, and we want the element in the third row and fourth column, we would type *X[2,3]*. Why 2 and 3? Recall that the first row is row 0 and that the first column is column 0. The third row would be row value 2 and the fourth column would be the column value 3.

```
X=sp.Matrix([ [1,2,3,5],[3,4,-1,2],[0,2,1,-6] ])
X
```

`print( X[2,3] ) # Returns the bottom-right element of this matrix`

Output:

-6

To access an entire row, say row 3, we need to tell Python that we would like *all* columns for that particular row. To do this, we use a colon.

`X[2,:] # Returns "all" columns for row 3`

Output:

[0 2 1 −6]

Similar logic holds for returning column 4:

`X[:,3] # Returns "all" rows for column 4`

output:

[5

2

−6]

**Gaussian Elimination**

Suppose we have the system of equations below that we want to solve:

Suppose we have the system of equations below that we want to solve:

𝑥+𝑦=10 2𝑥−𝑦=14

We can create the matrix representation of this system and then run the rref() (stands for **reduced-row echelon form**).

```
A = sp.Matrix( [ [1, 1, 10], [2, -1, 14] ])
A
```

Output:

[1 1 10

2 -1 14]

`A.rref() # The tuple (0,1) is not of importance to us`

Output:

We see that x=8 and y=2 solves this system.

**Changing the Rows and Columns of a Matrix**

Sometimes we want to add or delete rows or columns from a matrix. First off, we present a shorthand way to access a certain row or column.

```
X = sp.Matrix( [ [1, 2, 3], [4, 5, 6] ])
X
```

Output:

[1 2 3

4 5 6]

Simply using the .row() or .col() functions on a matrix will return the desired row. For instance, to get the 2nd row and 1st column of 𝑋X:

`X.row(1)`

Output:

[4 5 6]

`X.col(0)`

Output:

[1

4]

To delete a row or column, we use the .row_del() and .col_del() functions on a matrix, respectively.

```
Y = sp.Matrix( [ [1,2,3], [4,5,6], [7,8,9] ] )
Y
```

Output:

[1 2 3

4 5 6

7 8 9]

```
Y.col_del(1)
Y
```

Output:

[1 3

4 6

7 9]

**Problem 1**

The function below is to calculate the total prices for various initial prices given in a matrix called P. Imagine P represents a table of prices for, maybe various products (rows) and their various attributes (columns). We wish to first apply a discount, given by the matrix D, a matrix that is the same size as 𝑃P but that contains the discount for the corresponding product price in P. Finally, a tax rate of t% is to be applied to these discounted rates. The output should be a matrix, T, containing the total prices after discount and tax. Note that t is a percentage and so it will need to be converted into decimal form.

**Solution:**

```
def total_price(P,D,t):
T1 = (P-D)
T2 = T1 * (t/100)
T = T1 +T2
return T
```

```
P = sp.Matrix([[45,27,48],[39,44,27]])
D = sp.Matrix([[5,4,8],[2,3,4]])
```

Test:

```
t = 8.25
print(total_price(P,D,t).evalf(4))
```

Output: Matrix([[43.30, 24.90, 43.30], [40.05, 44.38, 24.90]])

**Problem 1.) Code Test**
If your total_price() function is working properly, you should get the corresponding outputs below for each of the input calls shown. Note that you can copy/paste the input into a new code box and run it on each set of inputs.
###################################################
P = sp.Matrix([[45,27,48],[39,44,27]])
D = sp.Matrix([[5,4,8],[2,3,4]])
t = 8.25
print(total_price(P,D,t).evalf(4)) # .evalf(4) rounds values to 4 significant digits
OUTPUT: Matrix([[43.30, 24.90, 43.30], [40.05, 44.38, 24.90]])
###################################################
P = sp.Matrix([[17,20,17],[8,41,36]])
D = sp.Matrix([[1,2.5,4.1],[3.1,7.6,4.9]])
t = 7.5
print(total_price(P,D,t).evalf(4))
OUTPUT: Matrix([[17.20, 18.81, 13.87], [5.268, 35.90, 33.43]])
###################################################
P = sp.Matrix([[8,7],[3, 2],[10, 12]])
D = sp.Matrix([[1,2.1],[.5, .1],[1, 1.2]])
t = 8
print(total_price(P,D,t).evalf(4))
OUTPUT: Matrix([[7.560, 5.292], [2.700, 2.052], [9.720, 11.66]])
###################################################

**Problem 2**

It is sometimes the case that a matrix of data is not oriented properly. That is to say, maybe you want the rows to be columns. Suppose you have an m x p matrix, A, that you need to add to a data matrix, D. However, suppose that D is p x m, and so it needs to be transposed before you can apply A to it. Complete the function below, so that the two input matrices get added correctly to form a final matrix, F, that is m x p.

```
def add_to_data(A,D):
D = D.T
F = A + D
return F
```

```
A = sp.Matrix( [ [1,3,4],[2,8,-4] ] )
D = sp.Matrix( [ [4,5],[3,7],[2,1] ] )
#D.T
print(add_to_data(A,D))
```

Output: Matrix([[5, 6, 6], [7, 15, -3]])

```
A = sp.Matrix( [ [2,4,1],[1.2,3.7,-4.6], [1,2,3], [3,4,3] ] )
D = sp.Matrix( [ [4,5,3,2],[3,7,1,7],[2,1,-3,1] ] )
print(add_to_data(A,D).evalf(2))
```

Output: Matrix([[6.0, 7.0, 3.0], [6.2, 11., -3.6], [4.0, 3.0, 0], [5.0, 11., 4.0]])

**Problem 2.) Code Test**

If your add_to_data() function is working properly, you should get the corresponding outputs below for each of the input calls shown. Note that you can copy/paste the input into a new code box and run it on each set of inputs. ################################################### A = sp.Matrix( [ [1,3,4],[2,8,-4] ] ) D = sp.Matrix( [ [4,5],[3,7],[2,1] ] ) print(add_to_data(A,D)) OUTPUT: Matrix([[5, 6, 6], [7, 15, -3]]) ################################################### A = sp.Matrix( [ [2,4,1],[1.2,3.7,-4.6], [1,2,3], [3,4,3] ] ) D = sp.Matrix( [ [4,5,3,2],[3,7,1,7],[2,1,-3,1] ] ) print(add_to_data(A,D).evalf(2)) OUTPUT: Matrix([[6.0, 7.0, 3.0], [6.2, 11., -3.6], [4.0, 3.0, 0], [5.0, 11., 4.0]]) ###################################################

**Problem 3**

Here you will write a function that accepts a matrix, X, that is m x p and a constant, c. In some applications it is necessary to construct a matrix filled with a constant that is the same size as another matrix, X, for example. The function should determine the size of X, and should output a matrix, B, that is the same size as X and that is filled with the constant, c, that is passed to the function.

```
def fill_matrix(X, c):
B = sp.ones(X.shape[0],X.shape[1])
B = B * c
return B
```

```
X = sp.Matrix( [ [1,3,4],[2,8,-4] ])
print(fill_matrix( X , 3))
```

Output:

`Matrix([[3, 3, 3], [3, 3, 3]])`

**Problem 3.) Code Test**
If your fill_matrix() function is working properly, you should get the corresponding outputs below for each of the input calls shown. Note that you can copy/paste the input into a new code box and run it on each set of inputs.
###################################################
X = sp.Matrix( [ [1,3,4],[2,8,-4] ])
print(fill_matrix( X , 3))
OUTPUT: Matrix([[3, 3, 3], [3, 3, 3]])
###################################################
X = sp.Matrix( [ [2,0],[4,1] ] )
print(fill_matrix( X , 1.2).evalf(2))
OUTPUT: Matrix([[1.2, 1.2], [1.2, 1.2]])
###################################################
X = sp.Matrix( sp.ones(8,6) )
print(fill_matrix( X , 7).evalf(1))
OUTPUT: Matrix([[7., 7., 7., 7., 7., 7.], [7., 7., 7., 7., 7., 7.], [7., 7., 7., 7., 7., 7.], [7., 7., 7., 7., 7., 7.], [7., 7., 7., 7., 7., 7.], [7., 7., 7., 7., 7., 7.], [7., 7., 7., 7., 7., 7.], [7., 7., 7., 7., 7., 7.]])
###################################################

**Problem 4**

Suppose you have a matrix filled with data. Furthermore, suppose one of the columns, say column j, contains some ID's that you would like to swap with the first column, which contains some attribute for those ID's. The function below will perform this swap of columns. Note that you cannot simply replace the first column without first temporarily storing the first column into some new variable (otherwise you just overwrite the first column and create a repeat of column j). Once you have the first column stored, you can replace it with column j and you can then take this temporary variable and replace column j with it. Your function will accept the matrix, X and the column number, j, and will return the new matrix, S, after the swap.

NOTE OF CAUTION: Whenever you store a column of a matrix into a new variable, that variable *always* updates to store the current column of X. So, for example, suppose the first column of a matrix is

[3

4

5]

Suppose you store this in a variable called temp_col=

[3

4

5]

Later you change that column in the X matrix to

[1

2

7]

The value of temp_col is *also* updated to be

[1

2

7]

! This is a strange occurrence, but from a programming standpoint it makes sense: the value of temp_col is *pointing* to the first column of X. Since X changed, so does the value stored in temp_col. To get arround this, you can create a *copy* of a column. If you take X.col(0) and convert it to a matrix, by writing

`temp_col = sp.Matrix( X.col(0) )`

, this tells Python to create a *copy* of the first column of X and to store it into a variable called temp_col. In short, avoid frustration by creating a copy of a column instead of having it *reference* the column of a matrix.

```
def swap(X, j):
first_col = X.col(0)
S = X.col_insert(j, first_col)
S.col_del(0)
j_col = S.col(j)
S.col_del(j)
S = S.col_insert(0, j_col)
return S
```

```
X = sp.Matrix( [ [3,4,5],[1,2,3] ] )
j = 2
swap(X,j)
```

Output:

[5 4 3

3 2 1]

```
X = sp.Matrix( [ [2,1],[3,-2],[4,6],[9,3] ] )
j = 1
swap(X,j)
```

OUTPUT: Matrix([[1, 2], [-2, 3], [6, 4], [3, 9]])

```
X = sp.Matrix( [ [2,1,3],[3,-2,0],[4,6,4],[9,3,7],[3,2,-1] ] )
j = 2
swap(X,j)
```

OUTPUT: Matrix([[3, 1, 2], [0, -2, 3], [4, 6, 4], [7, 3, 9], [-1, 2, 3]])

**Problem 4.) Code Test**
If your swap() function is working properly, you should get the corresponding outputs below for each of the input calls shown. Note that you can copy/paste the input into a new code box and run it on each set of inputs.
###################################################
X = sp.Matrix( [ [3,4,5],[1,2,3] ] )
j = 2
swap(X,j)
OUTPUT: Matrix([[5, 4, 3], [3, 2, 1]])
###################################################
X = sp.Matrix( [ [2,1],[3,-2],[4,6],[9,3] ] )
j = 1
swap(X,j)
OUTPUT: Matrix([[1, 2], [-2, 3], [6, 4], [3, 9]])
###################################################
X = sp.Matrix( [ [2,1,3],[3,-2,0],[4,6,4],[9,3,7],[3,2,-1] ] )
j = 2
swap(X,j)
OUTPUT: Matrix([[3, 1, 2], [0, -2, 3], [4, 6, 4], [7, 3, 9], [-1, 2, 3]])
###################################################

**Problem 5**

In a similar fashion to Problem 2.) above, suppose that you need to operate on matrices A (which is m×p) and D, but suppose that D is p x n, where n<m. That is, the data matrix is missing one or more columns. Suppose you know that it is okay to add columns filled with zeros to make D into a p x m matrix. The function below should do that and should return the new version of D (which is transposed into an m×p) with the correct number of zero columns appended.

**TIP:** This problem can be challenging if you start coding without mapping out what you need to do first. Test your steps on a pair of matrices (perhaps the one in the Code Test below) that satisfy the criteria for A and D. Write out the matrix sizes and describe in short sentences what you need to do first, second, etc. Only then should you focus on writing the code for this function. Note that there are several ways this problem can be solved.

```
def create_col(A,D):
D_new = D.T
if D_new.shape[0] < A.shape[0]:
no_row = A.shape[0] - D_new.shape[0]
zero_row = sp.zeros(no_row ,D_new.shape[1])
D_new = D_new.row_insert(D_new.shape[0] + 1,zero_row)
return D_new
return D_new
```

```
A = sp.Matrix( [ [1,2,3],[4,5,6] ] )
D = sp.Matrix( [ [3],[1],[3] ] )
print(create_col(A,D))
```

Output: Matrix([[3, 1, 3], [0, 0, 0]])

```
A = sp.Matrix( [ [3,1,2,3],[5,4,5,6],[-1,3.2,1,7] ] )
D = sp.Matrix( [ [3,1],[-1,-4],[1,2],[3,4] ] )
print(create_col(A,D))
```

OUTPUT: Matrix([[3, -1, 1, 3], [1, -4, 2, 4], [0, 0, 0, 0]])

**Problem 5.) Code Test**
If your create_col() function is working properly, you should get the corresponding outputs below for each of the input calls shown. Note that you can copy/paste the input into a new code box and run it on each set of inputs.
###################################################
A = sp.Matrix( [ [1,2,3],[4,5,6] ] )
D = sp.Matrix( [ [3],[1],[3] ] )
print(create_col(A,D))
OUTPUT: Matrix([[3, 1, 3], [0, 0, 0]])
###################################################
A = sp.Matrix( [ [3,1,2,3],[5,4,5,6],[-1,3.2,1,7] ] )
D = sp.Matrix( [ [3,1],[-1,-4],[1,2],[3,4] ] )
print(create_col(A,D))
OUTPUT: Matrix([[3, -1, 1, 3], [1, -4, 2, 4], [0, 0, 0, 0]])
###################################################
A = 2.1*sp.ones(5,3)
D = 6.7*sp.ones(3,2)
print(create_col(A,D).evalf(2))
OUTPUT: Matrix([[6.7, 6.7, 6.7], [6.7, 6.7, 6.7], [0, 0, 0], [0, 0, 0], [0, 0, 0]])
###################################################

## Comments