MF 602

Assignment 5

Objective and Overview

Objective: The objective of this assignment is to practice working with 2-dimensional lists, to think about list data and indices, and to practice computations involving matrices as 2-d lists.

Overview: In this assignment, you will write several functions to manipulate 2-dimensional lists representing matrix data. You will perform basic matrix operations (swapping rows, adding rows, multipling rows by a constant, etc.) as well as some simple linear-algebra operations (transpose, compute the determinant of a matrix, and find the inverse of a matrix).

Preliminaries

In your work on this assignment, make sure to abide by the collaboration policies of the course.

If you have questions while working on this assignment, please post them on Piazza! This is the best way to get a quick response from your classmates and the course staff.


General Guidelines

  • Refer to the class Coding Standards for important style guidelines. The grader will be awarding/deducting points for writing code that comforms to these standards.

  • Include comments at the top of the file that are similar to the ones that we gave you at the top of a1task1.py.

  • Your functions must have the exact names that we have specified, or we won’t be able to test them. Note in particular that the case of the letters matters (all of them should be lowercase), and that you should use an underscore character (_) wherever we have specified one (e.g., in convert_from_inches).

  • Each of your functions should include a docstring that describes what your function does and what its inputs are.

  • If a function takes more than one input, you must keep the inputs in the order that we have specified.

  • You should not use any Python features that we have not discussed in class or read about in the textbook.

  • Unless expressly stated, your functions do not need to handle bad inputs – inputs with a type or value that doesn’t correspond to the description of the inputs provided in the problem.

  • Make sure that your functions return the specified value, rather than printing it. Unless it is expressly stated in the problem, none of these functions should use a print statement.

Important note regarding test cases and Gradescope:

  • You must test each function after you write it. Here are two ways to do so:

    • Run your file after you finish a given function. Doing so will bring you to the Shell, where you can call the function using different inputs and check to see that you obtain the correct outputs.
    • Add test calls to the bottom of your file, inside the if __name__ == '__main__' control struture. For example:

      if __name__ == '__main__':
      
          print("mystery(6,7) returned", mystery(6,7))
      

      These tests will be called every time that you run the file, which will save you from having to enter the tests yourself. We have given you an example of one such test in the starter file.

  • You must not leave any print statements in the global scope. This will cause an error with the Gradescope autograder. Make sure all of your print statements are inside a function scope or insude the if __name__ == '__main__' control struture.


About python library functions

In a future assignment, we will use the numpy numerical computing package to perform these operations, but it’s important and satisfying to learn how to do these operations the long way.

You may NOT use any functions or tools from the numpy library or similar pre-existing linear-algebra toolkits. The goal of this assignment is to practice working with definite loops, 2-dimensional lists, and indexing.

Background Reading: Matrix Algebra

You must be familiar with basic matrix algebra notation and operations. The following three web pages are an excellent resource. If you haven’t taken a matrix algebra class (or even if you have!) please read these pages and try out any examples you need to so that you feel confident about the subject matter:

Introduction

Multiplying matrices

Determinant of a matrix

Inverse of a matrix

Task 1: Implementing a Matrix as a 2-D List

30 points; individual-only

Do this entire task in a file called a5task1.py.

  1. Write the function print_matrix(m, label), that takes two parameters, m which is a 2-dimension list (the matrix) and label (a string), and creates a nicely-formatted printout.

    Example:

    >>> A = [[3,0,2,1],[2,0,-2,3]]
    >>> print_matrix(A, 'A')
    A = 
    [[3.00, 0.00, 2.00, 1.00]
     [2.00, 0.00, -2.00, 3.00]]
    

    Notes:

    • The print_matrix function must not change any values of the matrix. In particular, do not change the values into strings. Rather, you should iterate over each element of the matrix and print out a string representation of that element, without change the matrix’s underlying elements in any way.

    • The printout must have each row on its own line, square brackets as needed to show the begin/end of each row, and commas to separate the columns, as per the example above.

    • The printout of the matrix should always present numeric values as floating-point numbers with 2 digits of precision after the decimal point.

    • Note that the second parameter label is optional. You can create an optional parameter by giving it a default value in the function definition, i.e.,

      def print_matrix(A, label = ''):
      

      When the function is called with only one parameter, the default value (i.e., '') is used for the variable label.

      >>> A = [[3,0,2,1],[2,0,-2,3]]
      >>> print_matrix(A) # no label provided, so don't show a label
      [[3.00, 0.00, 2.00, 1.00]
       [2.00, 0.00, -2.00, 3.00]]
      

      And when the function is called with two parameters the second parameter replaces the default value, e.g.,

      >>> A = [[3,0,2,1],[2,0,-2,3]]
      >>> print_matrix(A, 'A') # label of 'A' is provided, so the printout will show 'A = '
      A = 
      [[3.00, 0.00, 2.00, 1.00]
       [2.00, 0.00, -2.00, 3.00]]
      

    Hints:

    • Use a pair of nested for loops to iterate over all row/column indices of the matrix.

    • The default behavior at the end of a print statement is to add a line break. We can change this behavior!

      To print an individual list element, you should use the following print statement, which includes the necessary formatting string:

      print(f'{matrix[r][c]:7.2f}' , end=', ')
      

      To force a line break at the end of a colunmn, use a regular print() statement.

  2. Write the function zeros(n, m), that creates and returns an n * m matrix containing all zeros.

    Examples:

    >>> print_matrix(zeros(3,2))
    [[0.00, 0.00]
     [0.00, 0.00]
     [0.00, 0.00]]
    
    >>> print_matrix(zeros(2,2))
    [[0.00, 0.00]
     [0.00, 0.00]]
    
    >>> print_matrix(zeros(3,5))
    [[0.00, 0.00, 0.00, 0.00, 0.00]
     [0.00, 0.00, 0.00, 0.00, 0.00]
     [0.00, 0.00, 0.00, 0.00, 0.00]]
    

The function should also use a default parameter for m, such that it is possible to call the function with only one parameter, and obtain a square n*n matrix:

        :::python
        >>> print_matrix(zeros(3))
        [[0.00, 0.00, 0.00]
         [0.00, 0.00, 0.00]
         [0.00, 0.00, 0.00]]
  1. Write the function identity_matrix(n), that creates and returns an n * n identity matrix containing the value of 1 along the diagonal.

    Example:

        >>> I = identity_matrix(3)
        >>> print_matrix(I, 'I')
        I = 
        [[1.00, 0.00, 0.00]
         [0.00, 1.00, 0.00]
         [0.00, 0.00, 1.00]]
    

    Hints:

    • Re-use your zeros(n) function to create the new matrix, then just modify the diagonal elements.
  2. Write the function transpose(M), that creates and returns the transpose of a matrix.

    Example:

    >>> A = [[1,2,3],[4,5,6]]
    >>> print_matrix(A)
    [[1.00, 2.00, 3.00]
     [4.00, 5.00, 6.00]]
    >>> AT = transpose(A)
    >>> print_matrix(AT, 'AT')
    AT = 
    [[1.00, 4.00]
     [2.00, 5.00]
     [3.00, 6.00]]
    

    Hints:

    • Do not modify the parameter matrix M at all. Instead, create a new matrix, fill in the required values, and return this matrix as the result of the function.

    • Your function must work for matrices of any dimensions. Include additional test cases for other dimensions.

  3. Write the function swap_rows(M, src, dest) to perform the elementary row operation that exchanges two rows within the matrix. This function will modify the matrix M such that its row order has changed, but none of the values within the rows have changed.

    Examples:

    >>> A = [[3, 0, 2], [2, 0, -2], [0, 1, 1]]
    >>> print_matrix(A)
    >>> swap_rows(A, 1, 2) # swap rows 1 and 2
    >>> print_matrix(A)
    [[3.00, 0.00, 2.00]
     [0.00, 1.00, 1.00]
     [2.00, 0.00, -2.00]]
    >>> swap_rows(A, 0, 2)
    >>> print_matrix(A)
    [[2.00, 0.00, -2.00]
     [0.00, 1.00, 1.00]
     [3.00, 0.00, 2.00]]
    

    Hints:

    • This function does not return a value! Rather, you will modify the parameter matrix M directly.

    • Your function must verify that the row index row is valid and in-bounds for this matrix.

    • No loops are required in this function! Use assignment statements only. It will be helpful to create a temporary variable to refer to at least one of the rows in this matrix.

  4. Write the function mult_row_scalar(M, row, scalar) to perform the elementary row operation that multiplies all values in the row row by the numerical value scalar.

    Examples:

    >>> A = [[3, 0, 2], [2, 0, -2], [0, 1, 1]]
    >>> print_matrix(A)
    [[3.00, 0.00, 2.00]
     [2.00, 0.00, -2.00]
     [0.00, 1.00, 1.00]]
    
    >>> mult_row_scalar(A, 1, -1) # multiply row 1 by -1
    >>> print_matrix(A)
    [[3.00, 0.00, 2.00]
     [-2.00, 0.00, 2.00]
     [0.00, 1.00, 1.00]]
    
    >>> mult_row_scalar(A, 1, 0.5) # multiply row 1 by 0.5
    >>> print_matrix(A)
    [[3.00, 0.00, 2.00]
     [-1.00, 0.00, 1.00]
     [0.00, 1.00, 1.00]]
    

    Hints:

    • This function does not return a value. Rather, you will modify the parameter matrix M directly.

    • Your function must verify that the row indices src and dest are valid and in-bounds for this matrix. Use the assert statement to check this condition and print an error message if needed.

  5. Write the function add_row_into(M, src, dest), that performs the elementary-row operation to add the src row into the dest row. That is, each element of row src is to be added into the corresponding element of row dest.

    Examples:

    >>> A = [[3, 0, 2], [2, 0, -2], [0, 1, 1]]
    >>> print_matrix(A)
    [[3.00, 0.00, 2.00]
     [2.00, 0.00, -2.00]
     [0.00, 1.00, 1.00]]
    >>> add_row_into(A, 2, 1) # add row 2 into row 1
    >>> print_matrix(A)
    [[3.00, 0.00, 2.00]
     [2.00, 1.00, -1.00]
     [0.00, 1.00, 1.00]]
    >>> add_row_into(A, 2, 1) # add row 2 into row 1
    >>> print_matrix(A)
    [[3.00, 0.00, 2.00]
     [2.00, 2.00, 0.00]
     [0.00, 1.00, 1.00]]
    

    Hints:

    • This function does not return a value. Rather, you will modify the parameter matrix M directly.

    • Your function must verify that the row indices src and dest are valid and in-bounds for this matrix. Use the assert statement to check this condition and print an error message if needed.

Task 2: Matrix Operations

50 points; individual-only

Do this entire task in a file called a5task2.py. You will need to import your work from Task 1 to complete and test this part. Add this import statement to the top of your a5task2.py file:

    from a5task1 import *

Note: if you have any unit-test code (i.e., function calls or print statements that are in the global scope in your a5task1.py file), those statements will run when you import the file. One way to leave that unit test code in place, but prevent it from running when you import the file is to put the unit test code in a special block at the bottom of your a5task1.py file, like this:

    ## unit test code
    if __name__ == '__main__':
        A = [[3,0,2,1],[2,0,-2,3]]
        print_matrix(A, 'A')
    ## end of unit test code

The expression __name__ == '__main__' will evaluate to True only when you run the file, but not when the file is imported.

The following notes apply to all remaining problems:

  • The original matrix must not be modified by your function, but rather you must create and return a new matrix.

  • Your finished function should not print out anything, but merely return the new matrix. You may include print statements in your code while developing the function, but comment them out from the final version.

  1. Write the function add_matrices(A, B), that takes as parameters 2 matrices (2d lists) and returns a new matrix which is the element-wise sum of the matrices A and B.

    Example:

    >>> A = [[1,2,3],[4,5,6]]
    >>> B = [[4,5,6],[1,2,3]]
    >>> S = add_matrices(A, B)
    >>> print_matrix(S, 'S')
    S = 
    [[5.00, 7.00, 9.00]
     [5.00, 7.00, 9.00]]
    
    • Verify that the matrices A and B are compatible for this operation, i.e., the number of rows and columns in each matrix must be the same. For example, you can add a 3x5 matrix with another 3x5 matrix, but you cannot add a 3x5 matrix to a 3x4 matrix because the number of columns does not match.

      Use the assert statement to check this condition and print an error message if needed.

      Example:

      >>> A = [[1,2],[4,5]]
      >>> B = [[4,3,2],[3,2,1]]
      >>> S = add_matrices(A,B)
      Traceback (most recent call last):
        File "<pyshell#37>", line 1, in <module>
          S = add_matrices(A,B)
        File "/Users/azs/code/mf703/assignments/a5task2.py", line 101, in add_matrices
          assert(len(A[0]) == len(B[0])), "incompatible dimensions: %s!" % dim
      AssertionError: incompatible dimensions: cannot add (2,2) with (2,3)!
      
  2. Write the function sub_matrices(A, B), that takes as parameters 2 matrices (2d lists) and returns a new matrix which is the element-wise difference of the matrices A and B.

    Example:

        :::python
        >>> A = [[1,2,3],[4,5,6]]
        >>> B = [[4,5,6],[1,2,3]]
        >>> D = sub_matrices(A, B)
        >>> print_matrix(D, 'D')
        D = 
        [[-3.00, -3.00, -3.00]
         [3.00, 3.00, 3.00]]
    
    • Verify that the matrices A and B are compatible for this operation, i.e., the number of rows and columns in each matrix must be the same. For example, you can add a 3x5 matrix with another 3x5 matrix, but you cannot add a 3x5 matrix to a 3x4 matrix because the number of columns does not match.

      Use the assert statement to check this condition and print an error message if needed.

      Example:

      >>> A = [[1,2],[4,5]]
      >>> B = [[4,3,2],[3,2,1]]
      >>> S = sub_matrices(A,B)
      Traceback (most recent call last):
        File "<pyshell#37>", line 1, in <module>
          S = sub_matrices(A,B)
        File "/Users/azs/code/mf703/assignments/a5task2.py", line 101, in sub_matrices
          assert(len(A[0]) == len(B[0])), "incompatible dimensions: %s!" % dim
      AssertionError: incompatible dimensions: cannot sub (2,2) with (2,3)!
      
  3. Write the function mult_scalar(M, s), that takes as a parameter a 2-dimension list M (the matrix) and a scalar value s (i.e., an int or float), and returns a new matrix containing the values of the original matrix multiplied by the scalar value.

    Example:

        >>> A = [[3,0,2,1],[2,0,-2,3]]
        print_matrix(A)
        [[3.00, 0.00, 2.00, 1.00]
         [2.00, 0.00, -2.00, 3.00]]
        >>> B = mult_scalar(A, 3)
        >>> print_matrix(B)
        [[9.00, 0.00, 6.00, 3.00]
         [6.00, 0.00, -6.00, 9.00]]
    
  4. Write the function dot_product(M, N), that takes as parameters two matrices M and N, and returns a new matrix containing dot product of these matrices.

    Example:

    >>> A = [[1,2,3],[4,5,6]]
    print_matrix(A)
    [[1.00, 2.00, 3.00]
     [4.00, 5.00, 6.00]]
    >>> B = [[3,2],[4,1],[5,0]]
    print_matrix(B)
    [[3.00, 2.00]
     [4.00, 1.00]
     [5.00, 0.00]]
    >>> P = dot_product(A,B)
    >>> print_matrix(P, 'P')
    P = 
    [[26.00, 4.00]
     [62.00, 13.00]]
    

    Notes:

    • Verify that the matrices M and N are compatible for this operation, i.e., the inner dimensions must be the same. For example, you can create a dot product of a 3x5 matrix times a 5x4 matrix and get a 3x4 result. However, you cannot find a dot product of a 3x5 matrix times a 4x3 matrix, because the inner dimensions do not match.

      Use the assert statement to check this condition and print an error message if needed.

      Example:

      >>> A = [[1,2,3],[4,5,6]]
      >>> B = [[4,3,2],[3,2,1]]
      >>> P = dot_product(A,B)
      Traceback (most recent call last):
        File "<pyshell#37>", line 1, in <module>
          P = dot_product(A,B)
        File "/Users/azs/code/fe703/assignments/a5task2.py", line 101, in dot_product
          assert(len(A[0]) == len(B)), "incompatible dimensions: %s!" % dim
      AssertionError: incompatible dimensions: cannot dot-product (2,3) with (2,3)!
      
  5. Write function called create_sub_matrix(M, exclude_row, exclude_col) that returns a sub-matrix of M, with all values that are not in row exclude_row or column exclude_col.

    Example:

    >>> A = [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]]
    >>> print_matrix(A)
    [[1.00, 2.00, 3.00, 4.00]
     [5.00, 6.00, 7.00, 8.00]
     [9.00, 10.00, 11.00, 12.00]
     [13.00, 14.00, 15.00, 16.00]]
    >>>
    >>> print_matrix(create_sub_matrix(A, 0, 0)) # exclude row 0 and column 0
    [[6.00, 7.00, 8.00]
     [10.00, 11.00, 12.00]
     [14.00, 15.00, 16.00]]
    >>>
    >>> print_matrix(create_sub_matrix(A, 2, 3)) # exclude row 2 and column 3
    [[1.00, 2.00, 3.00]
     [5.00, 6.00, 7.00]
     [13.00, 14.00, 15.00]]
    

    The determinant and matrix_of_minors functions that follow will require you to create a sub-matrix that includes all elements of the original matrix, excluding elements from a certain row or column.

  6. Write the function determinant(M), that takes a parameter M that is a (non-singular) matrix, and returns its determinant. The “Laplace expansion” method of calculating a determinant is a simple recursive algorithm.

    Refer to the webpage about determinant determinant of a matrix and using the “Laplace expansion” method to calculate a determinant of a matrix. Your function must use the Laplace expansion method.

    Example:

    >>> A = [[3,4],[8,6]]
    >>> d = determinant(A)
    >>> print(d)
    -14
    >>> B = [[2,-1,0],[3,-5,2], [1,4,-2]]
    >>> d = determinant(B)
    >>> print(d)
    -4
    >>> # a 4x4 matrix example 
    >>> C = [[3, 2, 0, 1], [4, 0, 1, 2], [3, 0, 2, 1], [9, 2, 3, 1]]
    >>> d = determinant(C)
    >>> print(d)
    24
    

    Notes:

    • There is a special case to find the determinant of a 1x1 matrix. The determinant of this matrix is its scalar value, i.e.,

      >>> A = [[3]]
      >>> determinant(A)
      3
      
    • You can only find the determinant of a square and non-singular matrix whose dimensions are at least 2x2. Include assert statements at the top of your function to test for cases in which you cannot find a determinant.

      >>> A = [[3,4],[8,6], [10,7]]
      >>> determinant(A)
      Traceback (most recent call last):
        File "<pyshell#85>", line 1, in <module>
          determinant(A)
        File "/Users/azs/code/mf602/assignments/a5task2.py", line 161, in determinant
          assert(len(A[0]) == len(A)), "incompatible dimensions: %s!" % dim
      AssertionError: incompatible dimensions: (3,2)!
      

    Hints:

    • For a 2x2 matrix, the determinant is simple arithmetic. Test this solution before you try any other dimensions.

    • For a larger (e.g. a 3x3) matrix, you should use the Laplace Expansion method described in the webapge about the determinant of a matrix. It will be necessary to create several sub-matrices and find the determinant of each. You must use the helper function create_sub_matrix(M, exclude_row, exclude_col) (see note above).

      This is a recursive algorithm, and you should use a recursive function call to find the determinant of each sub-matrix. However, you will also need to use a loop to build the sub-matrices (since you do not know in advance how many you need to create).

      Begin by determining how many sub-matrices you need to create. For a 3x3 matrix, you will need to create 3 sub-matrices (use a loop!).

      To build each sub-matrix, you will need a pair of nested loops to add the appropriate elements to that matrix. Then you can use recursion to find the determinant of that sub-matrix.

Computing the inverse of a matrix

We want to calculate the inverse of a matrix, and you will implement 2 functions to do this. Read the following helpful webpage about calculating the inverse of a matrix. You should use the approach described therein.

  1. Write a helper function called matrix_of_minors(M), that takes a matrix and returns the corresponding matrix of minors.

    In your matrix_of_minors(M), function, begin this function by thinking about the result matrix that you want to create. Use two loops to iterate over each element in the desired result matrix. To calculate each element in the result matrix, create the relevant sub-matrix (you should use the helper function create_sub_matrix(M, exclude_row, exclude_col) from above), and call your existing determinant(M) function to calculate its determinant.

    Here is a little test code that might be helpful for you:

        >>> A = [[3,0,2],[2,0,-2],[0,1,1]]
        >>> print_matrix(A, 'A')
        A = 
        [[3.00, 0.00, 2.00]
        [2.00, 0.00, -2.00]
        [0.00, 1.00, 1.00]]
        >>> M = matrix_of_minors(A)
        >>> print_matrix(M)
        [[2.00, 2.00, 2.00]
        [-2.00, 3.00, 3.00]
        [0.00, -10.00, 0.00]]
    
  2. Write the function inverse_matrix(M), that takes a parameter that is a (non-singular) matrix, and returns its inverse.

    Example (1): a 2x2 matrix:

    >>> A = [[7,8],[9,10]]
    >>> print_matrix(A)
    [[7.00, 8.00]
     [9.00, 10.00]]
     >>> determinant(A)
    -2
    >>> AI = inverse_matrix(A)
    >>> print_matrix(AI)
    [[-5.00, 4.00]
     [4.50, -3.50]]
    >>> # check that we have the correct inverse; A * AI should be the identity matrix
    >>> DP = dot_product(A,AI)
    >>> print_matrix(DP, 'DP')
    DP = 
    [[1.00, 0.00]
     [0.00, 1.00]]
    

    Example (2): a 3x3 matrix:

    >>> A = [[3,0,2],[2,0,-2],[0,1,1]]
    >>> print_matrix(A, 'A')
    A = 
    [[3.00, 0.00, 2.00]
     [2.00, 0.00, -2.00]
     [0.00, 1.00, 1.00]]
    >>> AI = inverse_matrix(A)
    >>> print_matrix(AI,'AI')
    AI = 
    [[0.20, 0.20, 0.00]
     [-0.20, 0.30, 1.00]
     [0.20, -0.30, 0.00]]
    >>> # check that we have the correct inverse; A * AI should be the identity matrix
    >>> DP = dot_product(A,AI)
    >>> print_matrix(DP, 'dot_product(A, AI)')
    dot_product(A, AI) = 
    [[1.00, 0.00, 0.00]
     [0.00, 1.00, 0.00]
     [0.00, 0.00, 1.00]]
    

    Example (3): a 4x4 matrix:

    >>> A = [[3,2,0,1],[4,0,1,2],[3,0,2,1],[9,2,3,1]]
    >>> print_matrix(A, 'A')
    A = 
    [[3.00, 2.00, 0.00, 1.00]
     [4.00, 0.00, 1.00, 2.00]
     [3.00, 0.00, 2.00, 1.00]
     [9.00, 2.00, 3.00, 1.00]]
    >>> determinant(A)
    24
    >>> AI = inverse_matrix(A)
    >>> print_matrix(AI, 'AI')
    AI = 
    [[-0.25, 0.25, -0.50, 0.25]
     [0.67, -0.50, 0.50, -0.17]
     [0.17, -0.50, 1.00, -0.17]
     [0.42, 0.25, 0.50, -0.42]]
    >>> # check that we have the correct inverse; A * AI should be the identity matrix
    >>> DP = dot_product(A,AI)
    >>> print_matrix(DP)
    [[1.00, 0.00, 0.00, 0.00]
     [-0.00, 1.00, 0.00, 0.00]
     [-0.00, 0.00, 1.00, 0.00]
     [-0.00, 0.00, 0.00, 1.00]]
    

    Notes:

    • Use the approach described in the web page about the inverse of a matrix, using the matrix of minors, the cofactor matrix, and the adjugate matrix.

    • When calculating the cofactor matrix, use definite loops and think about creating a general way to determine which sign to use (i.e., positive or negative) for each element.

    • You should re-use your code! Call any of your other functions (i.e., zeros(n), determinant(M), mult_scalar(M,s), etc.) as helper functions from within your inverse_matrix(M) or matrix_of_minors(M) functions as needed.

Task 3: Matrix Application: Bond Pricing

20 points; individual-only

Do this entire task in a file called a5task3.py. For this task, you will need to re-use some functions from assignment 4 and 5.

  from a4task1 import discount_factors, bond_cashflows # bond functions
  from a5task1 import *
  from a5task2 import *
  1. Write a new implementation of the bond_price(fv, c, n, m, r) to calculate and return the price of a bond. The parameters are: fv is the future (maturity) value of the bond;
    c is the annual coupon rate expressed as percentage per year;
    n is the number of years until maturity;
    m is the number of coupon payments per year;
    and r, the annualized yield to maturity of the bond

    For example:

    >>> bond_price(1000, 0.08, 5, 2, 0.08) # coupon bond at par
    999.9999999999998
    >>> bond_price(1000, 0.08, 5, 2, 0.09) # coupon bond at discount
    960.4364091144498
    >>> bond_price(1000, 0.00, 5, 2, 0.09) # zero-coupon bond
    643.927682030043
    

    Important Implementation Notes:

    • Your function will use a matrix algebra (no loop with an accumulator pattern or recursion) and requires no if-elif-else logic. The price of the bond is given by the present values of the cashflows of that bond:

      where D is the matrix of discount factors for the bond’s payments, CF is a matrix of the bond’s cashflows, and P is a matrix containing the price of the bond.

    • Re-use your bond_cashflows(fv, c, n,m) function from a3task1 to find the cashflows associated with this bond.

    • Re-use your discount_factors(r, n, m) function from a3task1 to calculate the relevant discount factors.

Important

  • You will need to convert the data types appropriately. Specifically, note that the results of bond_cashflows and discount_factors are 1-dimensional lists, and the return value must be a floating-point number (the bond price in dollars). In between, you will need to have matrices (represented by 2-dimensional lists) to do the computations using your existing linear algebra functions.
  1. The bootstrap method is a technique to find the implied prices of zero-coupon bonds (i.e., implied discount factors) by using the casflows and prices of some coupon-paying bonds at similar maturities.

    Write the function bootstrap(cashflows, prices) to implement the bootstrap method. This function will take parameters cashflows, which is a matrix (2-dimensional list) containing the cashflows for some bonds, and prices which is a column matrix (2-dimensional list) containing the prices of these bonds.

    We can find the prices of the implied zero-coupon bonds (i.e., discount factors) using this equation:

    where P is a matrix containing the price of the bond, CF is the matrix of the bond’s cashflows, and D is the matrix of implied discount factors.

    For example:

    >>> CF = [[105,0,0],[6,106,0],[7,7,107]]
    >>> P = [[99.5], [101.25], [100.35]]
    >>> D = bootstrap(CF, P)
    >>> print_matrix(CF, 'Bond Cashflows')
    Bond Cashflows = 
    [[105.00, 0.00, 0.00]
     [6.00, 106.00, 0.00]
     [7.00, 7.00, 107.00]]
    >>> print_matrix(P, 'Bond Prices')
    Bond Prices = 
    [[99.50]
     [101.25]
     [100.35]]
    >>> print_matrix(D, 'Implied Discount Factors') 
    Implied Discount Factors = 
    [[0.95]
     [0.90]
     [0.82]]
    >>> D
    [[0.9476190476190477], [0.9015498652291104], [0.8168768000940457]]
    

    Notes:

    • Your function must use your matrix algebra functions from above (no loops).

Submitting Your Work

Use the link to GradeScope (left) to submit your work.

Be sure to name your files correctly!

Under the heading for Assignment 5, attach these 3 required files: a5task1.py, a5task2.py, and a5task3.py.

When you upload the files, the autograder will test your program.

Notes:

Warnings about Submissions

  • Make sure to use these exact file names, or Gradescope will not accept your files. If Gradescope reports that a file does not have the correct name, you should rename the file using the name listed in the assignment page.

  • If you make any last-minute changes to one of your Python files (e.g., adding additional comments), you should run the file in Spyder after you make the changes to ensure that it still runs correctly. Even seemingly minor changes can cause your code to become unrunnable.

  • If you submit an unrunnable file, Gradescope will accept your file, but it will not be able to auto-grade it. If time permits, you are strongly encouraged to fix your file and resubmit. Otherwise, your code will fail most if not all of our tests.

Important note regarding test cases and Gradescope:

  • You must test each function after you write it. Here are two ways to do so:

    • Run your file after you finish a given function. Doing so will bring you to the Shell, where you can call the function using different inputs and check to see that you obtain the correct outputs.
    • Add test calls to the bottom of your file, inside the if __name__ == '__main__' control structure. For example:

      if __name__ == '__main__':
      
          print("mystery(6,7) returned", mystery(6,7))
      

      These tests will be called every time that you run the file, which will save you from having to enter the tests yourself. We have given you an example of one such test in the starter file.

  • You must not leave any print statements in the global scope. This will cause an error with the Gradescope autograder. Make sure all of your print statements are inside a function scope or insude the if __name__ == '__main__' control structure.