Basic modeling example ======================== For the second example, we are going to model a fixed-rate mortgage loan, one of the basic types of amortized mortgage loans. In a fixed-rate mortgage, the borrower repays the same amount periodically for a certain term, such as 20 years or 30 years. To learn more about mortgage loans, Wikipedia has a great article _ detailing various types of mortgage loans. For this exercise, we assume the payments are made annually. We model the annual payment amount using a well-known formula, for a given principal, interest rate and term. We also model the outstanding loan balance at each year throughout the payment term. Through this second exercise, we are going to learn many new techniques, such as: * How to create a Model and Space explicitly, * How to set the Formula of an existing Cells, * What namespaces the Formulas of Cells are evaluated in, * What are *References* and how to define them, * How to interpret error messages, * How to change the values of References, * How to parameterize Spaces to create *ItemSpaces*. For your reference, mortgage loans can also be modeled without using modelx. If you want to know how to model mortgage loans using Python and Pandas, check out great articles _ on the Practical Business Python _ site. Creating a Model and a Space explicitly --------------------------------------- We start by creating a new *MxConsole* for this exercise. Right-click on the tab of the existing *MxConsole* or the default Console, and select *New MxConsole*. .. figure:: /images/tutorial/Mortgage/OpenNewMxConsole2.png :align: center MxConsole Tab Context Menu A new *MxConsole* tab opens and after a few second, a new IPython session becomes ready to read your input in the *MxConsole*. In the previous example, before creating a Cells, you did not explicitly create the parent Space or the Model for the Cells, but modelx automatically created them for you when the Cells was created. This time, we start by creating a Model and a Space explicitly, and name them Mortgage and Fixed. Right-click in the blank *MxExplorer*, and select *New Model*. Enter Mortgage in the *Model Name* box in the dialog box. As you type Mortgage, the *Import As* box is also filled with Mortgage. This makes the Model accessible in the IPython in the *MxConsole* as a variable named Morgage. Click *OK*. .. figure:: /images/tutorial/Mortgage/NewModelDialogMortgage.png :align: center New Model Dialog Box Now you see *Current Model - Mortgage* is shown in the *Model* box at the top right corner of the *MxExplorer*. .. figure:: /images/tutorial/Mortgage/ModelSelectorMortgage.png :align: center Model Selector Next we are going to create a new Space named Fixed in Mortgage, which stands for fixed-rate mortgage. Right-click in the blank *MxExplorer*, and select *New Space*. In the dialog box, you can see that *Mortgage* is selected in the *Parent* box. As there is no Space, only the Model can be the parent of the Space to be created. Enter *Fixed* in the *Space Name* box. The *Import As* box should be filled with *Fixed* automatically. The *Base Spaces* box is for inheriting other Spaces. We don't cover the *Inheritance* concept in this exercise, so leave it blank here and click *OK*. .. figure:: /images/tutorial/Mortgage/NewSpaceDialogFixed.png :align: center New Space Dialog Box Now you should have the *Fixed* Space item in the *MxExplorer*. .. figure:: /images/tutorial/Mortgage/MxExplorerFixed.png :align: center MxExplorer Creating Cells and defining their Formulas ------------------------------------------ The annual payment for a fixed-rate mortgage can be calculated by a well-known formula and can be expressed as follows: .. math:: Payment = Principal\cdot\frac{Rate(1+Rate)^{Term}}{(1+Rate)^{Term}-1} where :math:Principal is the principal amount borrowed, :math:Rate is the fixed annual interest rate on the outstanding loan balance and :math:Term is the length of the loan period in years. This formula can be expressed in a Python function as follows:: def Payment(): return Principal * Rate * (1+Rate)**Term / ((1+Rate)**Term - 1) In Python ** in math expressions is the power operator, so the expressions (1+Rate)**Term above calculate (1+Rate) to the power Term. Let's create a Cells named Payment and define its formula by the function above. This time, let's create it following steps different from the first exercise: We create Payment as an empty Cells, and assign the formula after it's created. Right-click on the MxExplorer, and select *New Cells* from the dialog box. Then enter Payment in the *Cells Name* box. As is always the case, leave the *Import As* check box checked to import Payment into IPython in the *MxConsole*. Click *OK* .. figure:: /images/tutorial/Mortgage/NewCellsDialogPayment.png :align: center New Cells Dialog Box You can see the *Payment* Cells created under the *Fixed* Space in *MxExplorer*. Select the *Payment* Cells, right-click and select *Show Properties* item. The properties of the *Payment* Cells are shown in the Properties tab on the right side of the *MxExplorer*. The expression lambda: None is set to the *Formula* property as the default formula. Enter the Payment function above in the *Formula* pane. .. figure:: /images/tutorial/Mortgage/MxExplorerFixedPayment.png :align: center MxExplorer The other item to calculate is the outstanding loan balance. Let :math:Balance(t) be the loan balance at time :math:t. :math:Balance(t) can be expressed as the following recursive formula: .. math:: &Balance(t)=Balance(t-1)\cdot(1+Rate)-Payment\qquad&(0 0: return Balance(t-1) * (1+Rate) - Payment else: return Principle You may have noticed that the code above has a typo Principle, but let's leave it as is to observe an error caused by the typo later. Right-click on the MxExplorer, and select *New Cells* from the dialog box. Then enter Balance in the *Cells Name* box. Leave the *Import As* check box checked to import Balance into IPython in the *MxConsole*. Click *OK* .. figure:: /images/tutorial/Mortgage/NewCellsDialogBalance.png :align: center New Cells Dialog Box In the same way as you did for Payment, Open show the properties of Balance and put the function above in the *Formula* Pane. .. figure:: /images/tutorial/Mortgage/MxExplorerFixedBalanceWrongFormula.png :align: center MxExplorer Reading error messages ---------------------- The Payment Formula refers to names such as Principal, Rate and Term. We haven't define those names yet, so calculating Payment should raise an error. Type Fixed.Payement() in the *MxConsole* and you should get the following error message: .. code-block:: none FormulaError: Error raised during formula execution NameError: name 'Principal' is not defined Formula traceback: 0: Mortgage.FixedRate.Payment(), line 3 Formula source: def Payment(): return Principal * Rate * (1+Rate)**Term / ((1+Rate)**Term - 1) The error message consists of 3 blocks of text. The first block shows the type and message of the original error. The original error in this case is :obj:NameError, as the name Principal is not defined. The second block is Formula traceback. It shows the stack of Formula calls, as pairs of Cells and arguments, with the Formula you called on top, and the Formula call that raises the error at the bottom. In the case above, since the error is raised in the first Formula call, it only shows one Formula call, Payment(). The last block shows the Formula that raised the error. Creating References ------------------- The Payment Formula refers to the names Principal, Rate and Term so we need to define those names. Let's assume the principal is \$100,000, the interest rate is 3% and the payment term is 30 years. You would think defining those names in the *MxConsole* as follows would work:: >>> Principal = 100000 >>> Rate = 0.03 >>> Term = 30 But actually it doesn't. This is because, by the commands above you just define those names in the IPython's global namespace. However, the Payment Formula is evaluated in the namespace associated with its parent Space, Fixed. In order for the Payment Formula to be able to refer to those names, you need to define *References* in the Fixed Space as below:: >>> Fixed.Principal = 100000 >>> Fixed.Rate = 0.03 >>> Fixed.Term = 30 You just created 3 *Reference* objects in the Fixed Space. A *Reference* object binds a name in its parent's namespace to an arbitrary object. Now you see that the 3 items are created in the *MxExplorer*. In the *Type* field, the types of *Principal* and *Term* are *Ref/int*, meaning that they are Reference objects, and the type of the associated values is :obj:int. In the same way, the type field of *Rate* shows *Ref/float*, which means that it is a Reference object, and the type of its value is :obj:float. .. figure:: /images/tutorial/Mortgage/MxExplorerFixedReferences.png :align: center MxExplorer Getting calculated results -------------------------- Now that you have defined all the References referenced by the Payment, calling the Formula should succeed:: >>> Payment() 5101.925932025255 To check the value is calculated correctly, we can make use of pmt_ function from numpy-financial_ package:: >>> import numpy_financial as npf >>> npf.pmt(0.03, 30, 100000) -5101.925932025255 You see that the absolute value of the returned value matches the Payment value. .. note:: pmt_ function has been in numpy_ package, and it is still available in numpy_, but it is deprecated and moved to a separate package numpy-financial_. If you don't have numpy-financial_ installed, pmt_ function may be available in numpy_. .. _pmt: https://numpy.org/numpy-financial/latest/pmt.html .. _numpy: https://numpy.org/ .. _numpy-financial: https://numpy.org/numpy-financial/ Next try getting the loan balance at year 30: >>> Balance(30) You should get the following error, as there is a typo in the formula. .. code-block:: none FormulaError: Error raised during formula execution NameError: name 'Principle' is not defined Formula traceback: 0: Mortgage.FixedRate.Balance(t=30), line 4 ... 28: Mortgage.FixedRate.Balance(t=2), line 4 29: Mortgage.FixedRate.Balance(t=1), line 4 30: Mortgage.FixedRate.Balance(t=0), line 6 Formula source: def Balance(t): if t > 0: return Balance(t-1) * (1+Rate) - Payment() else: return Principle The error message tells you that a :obj:NameError is raised in Mortgage.FixedRate.Balance(t=0) at line 6, because the name Principle is not found in the namespace in which Mortgage.FixedRate.Balance(t=0) is executed. Correct the typo by going to *MxExplorer* and changing Principle to Principal in the *Formula* pane. .. figure:: /images/tutorial/Mortgage/MxExplorerBalance.png :align: center MxExplorer Calculate the balance again:: >> Balance(30) 1.2096279533579946e-10 The result is the reciprocal of 1.2 to the 10th power, which is effectively zero. It looks like the balance at each annual step till the year 30 is calculated correctly. You can check the values of the balance by dict(Balance) or Balance.frame, and also you can output a graph of the balance by:: >>> Balance.frame.plot() You should get a line graph of the balance in Spyder's *Plots* widget, and see that the line smoothly decreases till the year 30 where the balance becomes fully repaid. .. figure:: /images/tutorial/Mortgage/BalanceGraph.png :align: center Mortgage Loan Balance Changing Reference values ------------------------- So far, we considered only one combination of principals, payment terms and interest rates. Usually, you want to explore other patterns as well. For example, you may want to know the annual payment amount when the payment term is 20 years. To change Term from 30 to 20, assign 20 to Terms as follows:: >>> Fixed.Term = 20 The above changes the payment term to 20 years, and the values of Payment and Balance Cells are cleared because their calculations are dependent on Fixed.Term, except for Balance(0), which only depends on Principal. You can check how many values the Cells have by the :func:len built-in function:: >>> len(Payment) 0 >>> len(Balance) 1 To get the annual payment amount, simply call Payment:: >>> Payment() 6721.570759685908 The same applies to the interest rate. If you want to know what the payment is when the interest rate is 4%, assign 0.04 to Rate:: >>> Fixed.Rate = 0.04 >>> Payment() 7358.175032862885 When assigning a value to a Reference, be aware that you need to specify its parent Space, such as Fixed.Term = 20 and Fixed.Rate = 0.04 as explained in the previous section. Statements like Term = 20 and Rate = 0.04 will not work, because they are interpreted as just defining variables in the IPython's global namespace. Parameterizing the Space ------------------------ One drawback of changing Reference values to get results for various combinations of input is that, you can have results for only one combination of input at a time. If you update a Reference value, then the result for the previous value disappears. This is inconvenient if you want to use results from different combinations of input for subsequent calculations. Space parameterization is a very powerful feature to quickly and naturally extend a Space written in terms of one combination of input into a parameterized Space. The parameterized Space supports the subscription operator([]) and the call operator(()). By passing arguments to the parameters through either of the operators, child Spaces of the ItemSpace type are dynamically created in the parameterized Space. The ItemSpaces are read-only Spaces and they inherit child Spaces, Cells and References from the parent Space, but the values of References that have the same names as the parameters are overridden by the arguments. Using this feature, you can get results for any combinations of Term and Rate and maintain the results for all the combinations. To parameterize the Fixed Space by Term and Rate, assign a tuple of the Reference names to Fixed's parameters property as follows:: >>> Fixed.parameters = ("Term", "Rate") You can optionally give default values. For example, to give a default value of 30 to Term and 0.03 to Rate, execute the following assignment:: >>> Fixed.parameters = ("Term=30", "Rate=0.03") Now the Fixed Space is parameterized by Term and Rate. By adding arguments to the Fixed Space as a subscription or call operators, a new child Space is created under the Fixed Space:: >>> Fixed[20, 0.03] The ItemSpace has the same Cells and References as the parent Space, except for the values of Term and Rate, which are set to the arguments:: >>> Fixed[20, 0.03].Term 20 >>> Fixed[20, 0.04].Rate 0.04 Let's try to calculate Payment for various combinations of Term and Rate:: >>> Fixed[20, 0.03].Payment() 6721.570759685908 >>> Fixed[30, 0.03].Payment() 5101.925932025255 >>> Fixed[20, 0.04].Payment() 7358.175032862885 >>> Fixed[30, 0.04].Payment() 5783.009913366131 You can use () in place of [] in the code above. Since Term and Rate have default values, expressions like below yields the same ItemSpaces as above:: >>> Fixed[20].Payment() 6721.570759685908 >>> Fixed().Payment() # Or Fixed[()].Payment() 5101.925932025255 >>> Fixed(Rate=0.04).Payment() 7358.175032862885 >>> Fixed[30].Payment() 5783.009913366131 In MxExplorer, you see that the ItemSpaces are created under the Fixed Space. .. figure:: /images/tutorial/Mortgage/ItemSpaces.png :align: center ItemSpaces in MxExplorer Open one of the ItemSpaces and you see that the Cells and References in the ItemSpace are the same as the parent Space, except for Term and Rate, whose values are set to the arguments of the ItemSpace. .. figure:: /images/tutorial/Mortgage/ItemSpaces2.png :align: center ItemSpaces in MxExplorer Instead of manually specifying the arguments of the ItemSpaces, you can take full advantage of Python's iterator and comprehension expressions. For example, suppose you want to compare the annual payment amounts for all the possible combinations of payment terms and interest rates, where the payment terms range from 20 years stepping up by 5 years to 35 years, and the interest rates from 2% to 4% by 1%. For this task, you can use the product _ iterator, available from the Python standard library. The code below shows how to get the desired results as a :obj:dict with tuples of Term and Rate as keys and Payment as values:: >>> from itertools import product >>> {(term, rate): Fixed[term, rate/100].Payment() for term, rate in product(range(20, 36, 5), range(2, 5))} {(20, 2): 6115.671812529034, (20, 3): 6721.570759685908, (20, 4): 7358.175032862885, (25, 2): 5122.043841739468, (25, 3): 5742.787103912777, (25, 4): 6401.196278645458, (30, 2): 4464.992229340292, (30, 3): 5101.925932025255, (30, 4): 5783.009913366131, (35, 2): 4000.2209190750104, (35, 3): 4653.929156959947, (35, 4): 5357.732236826054} The code above use a form of expressions called dict comprehensions _. If you're not familiar with the expression, you can simply use for statement:: >>> result = {} >>> for term, rate in product(range(20, 36, 5), range(2, 5)): result[(term, rate)] = Fixed[term, rate/100].Payment() >>> result {(20, 2): 6115.671812529034, (20, 3): 6721.570759685908, (20, 4): 7358.175032862885, (25, 2): 5122.043841739468, (25, 3): 5742.787103912777, (25, 4): 6401.196278645458, (30, 2): 4464.992229340292, (30, 3): 5101.925932025255, (30, 4): 5783.009913366131, (35, 2): 4000.2209190750104, (35, 3): 4653.929156959947, (35, 4): 5357.732236826054} Saving the work --------------- You can save the Model in the same way we did in the fist exercise. From the context menu in *MxExplorer*, select *Write Model* and follow the same steps as the first example. Note that the ItemSpaces in the Model are not saved, as they are dynamically created when you get them through the subscription or call operations for the first time. So, when you read the saved Model, the ItemSpaces do not exists, but they appear as you try to get them by the subscription or call operations, such as Fixed[20, 0.02].