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]``.