Object-oriented modeling
================================
Multiple objects of similar types tend to have common definitions
of logic and data.
Modeling these objects manually one by one is not a good idea, because
you would end up having multiple copies of the same definitions, which are hard to maintain and error prone.
modelx supports an inheritance mechanism, which enables you to define the parts common to the multiple objects only once as part of a base object, and model each object by inheriting from the base object and defining only the parts unique to the object.
By making full use of inheritance, you can organize the multiple objects sharing similar features into inheritance trees, minimizing duplicated formulas and keeping your model organized and transparent while maintaining the model's integrity.
Inheritance in object-oriented programming
------------------------------------------
You may have heard about `object-oriented programming (OOP) `_.
OOP is a programming paradigm, and most modern programming languages, such as Python and C++, support OOP.
Such languages include powerful mechanisms, such as `inheritance `_,
for elegantly modeling complex objects.
Inheritance in the OOP languages greatly enhances code reusability and extensibility.
modelx is inspired by OOP, and implements an inheritance mechanism similar to
that of OOP.
However, while most popular object-oriented programming languages
use `class-based inheritance `_,
modelx uses `prototype-bases inheritance `_.
Python for example uses class-based inheritance. Python lets you
define classes and objects are instances of the classes.
Inheritance relationships in Python are defined in terms of classes.
In modelx, there is no class equivalent, and inheritance relationships
are defined between Space objects. A space object inherits from another
space object, in order to use the other object as a prototype.
How Inheritance works in modelx
---------------------------------
An inheritance relationship is established when you define a space(let's name it ``A``)
as a base space of another space(let's name it ``B``).
In this case, ``A`` is called a base space of ``B``, and
``B`` is called a sub space of ``A``.
When ``B`` inherits from ``A``,
copies of all the cells, references and spaces contained in ``A`` are automatically created in ``B``.
This automatic copying is called deriving.
For example, let ``A`` have a child cells ``foo`` and a child reference ``bar``.
As the figure shows, another set of ``foo`` and ``bar`` are derived in ``B``.
The lines between ``A`` and ``B`` with a hollow triangle arrowhead on the side of ``A``
indicates that ``B`` inherits from ``A``.
.. figure:: /images/tutorial/ObjectOrientedExample/SingleInheritance.png
:align: center
Initially, the formula of ``foo`` and the value of ``bar`` in ``B``
are copied from those in ``A``, but you can update ``foo``'s formula and ``bar``'s value
in ``B``. This act of updating derived objects are called overriding.
You can also add a new child object in ``B``, for example a new cells named ``baz``.
.. figure:: /images/tutorial/ObjectOrientedExample/SingleInheritance2.png
:align: center
Adjustable-mortgage Example
----------------------------
Let's learn how to implement inheritance by modeling a simple financial product.
Earlier in this tutorial, a simple fixed-rate mortgage loan is modeled as the ``Fixed`` space.
Let's say we also want to model an `adjustable-rate mortgage loan `_.
For simplicity, we assume the adjustable-rate mortgage has the same loan term and principal as the fixed-rate mortgage's.
Let the loan term be 10, and the principal be 100,000 in this example.
During the first 5 years, the interest rate of the adjustable mortgage is fixed at 2%, but from the 6th year the interest rate is updated
every year till the end of the loan period.
Let's assume the interest rate is expected as follows:
..
| Year |1|2|3|4|5|6|7|8|9|10|
| -----|---|---|---|---|---|---|---|---|---|---|
|Interest Rate | 2% | 2% |2% |2% |2% |4% | 5% |6% | 5% | 4% |
============== ======= ======= ======= ======= ======= ======= ======= ======= ======= =======
Year 1 2 3 4 5 6 7 8 9 10
============== ======= ======= ======= ======= ======= ======= ======= ======= ======= =======
Interest Rate 2% 2% 2% 2% 2% 4% 5% 6% 5% 4%
============== ======= ======= ======= ======= ======= ======= ======= ======= ======= =======
Note that the interest rate applicable after the first 5 years is not known
at the inception of the loan, because the rate is not fixed in advance.
So the interest rate table above is an assumption or a scenario if we
are modeling a loan to be paid off at a future point in time.
We want to model the adjustable mortgage as the ``Adjustable`` space.
Since ``Fixed`` and ``Adjustable`` are both mortgages and expected to have some shared
definitions of formulas and values, we can make use of inheritance.
we create a base space, ``BaseMortgage``, and define cells and references
common to ``Fixed`` and ``Adjustable`` in ``BaseMortgage``.
``Fixed`` and ``Adjustable`` inherit from ``BaseMortgage``, and we override
some of the derived objects inherited from ``BaseMortgage`` to reflect their own features. The diagram below depicts the relationship of the spaces.
.. figure:: /images/tutorial/ObjectOrientedExample/Inheritance.png
:align: center
To identify the commonality between the two types of mortgages, let's review the contents of ``Fixed`` from the earlier example one by one,
and think about whether and how they should be updated.
* ``Term`` is an integer representing the length of the loan term in years. We've assumed above that the fixed and adjustable mortgages have the same term.
* ``Principal`` represents the initial loan balance and is given as an input. We've assumed above that the principals of the fixed and adjustable mortgages are the same amount.
* ``Rate`` is a constant interest rate that applies through the lifetime of the fixed mortgage. Since the interest rate on ``Adjustable`` is adjusted periodically,
``Rate`` for ``Adjustable`` should have a different definition from that of ``Fixed``. The adjustable interest rate can be defined as a ``dict`` indexed with
loan duration.
* ``Payment`` represents the amount of a payment to be made regularly to repay the loan. In the case of ``Fixed``,
``Payment`` is defined as the constant amount calculated from ``Principal``, ``Term``, and ``Rate``. ``Payment``
for ``Adjustable`` needs to be time-dependent, because it is recalculated periodically in response to changes in the interest rate.
We will redefine the formula of ``Payment`` to make it time-dependent and applicable to both ``Fixed`` and ``Adjustable``.
* Instead of directly referring to ``Rate`` from ``Payment``, it's better to refer to ``Rate`` from ``Payment`` indirectly through a new cells with a time index,
because the fixed and adjustable rate can be referenced with the time index in the same fashion. Let's name the cells ``IntRate``.
* ``Balance`` is indexed with the time index ``t``, and represents the remaining balance of the loan at time ``t``.
The formula of ``Balance`` calculates the loan balance at time ``t`` recursively from the previous balance.
The initial balance is input from ``Principal`` and ``Rate`` is referenced for interest accretion.
By replacing ``Rate`` with ``IntRate(t)``, the formula becomes common between ``Fixed`` and ``Adjustable``.
The tables below summarizes how the contents of each space should be defined.
================== ================== ================================== ==================================
Contents ``BaseMortgage`` ``Fixed`` ``Adjustable``
================== ================== ================================== ==================================
``Term`` 10 Inherited from ``BaseMortgage`` Inherited from ``BaseMortgage``
``Principal`` 100000 Inherited from ``BaseMortgage`` Inherited from ``BaseMortgage``
``Rate`` To be defined ``0.03`` a ``dict`` object
``Payment(t)`` Shared formula Inherited from ``BaseMortgage`` Inherited from ``BaseMortgage``
``IntRate(t)`` To be defined Unique formula Unique formula
``Balance(t)`` Shared formula Inherited from ``BaseMortgage`` Inherited from ``BaseMortgage``
================== ================== ================================== ==================================
You may have noticed that instead of creating ``BaseMortgage``,
it is possible to model ``Adjustable`` by inheriting from ``Fixed``.
Although it's technically possible, it's not a good design, because
the adjustable mortgage is not a special form of the fixed mortgage.
Good practice is to make sure that an inheritance relationship should
always represent "is a" relationship.
Modeling Inheritance
--------------------
..
Comment on current directory
Comment on using IPython console, not Spyder GUI
We start from the ``Mortgage`` model from the earlier example, but you may also start from scratch if you prefer::
>>> import modelx as mx
>>> model = mx.read_model("Mortgage")
Let's use the ``Fixed`` space as the base space. Rename it ``BaseMortgage``::
>>> model.Fixed.rename('BaseMortgage')
>>> model.BaseMortgage
Now set 10 to ``Term``, which is a constant shared between the sub spaces::
>>> model.BaseMortgage.Term = 10
Now let's create ``Fixed`` under the model by inheriting from ``BaseMortgage``.
You can do so by passing ``BaseMortgage`` to the ``bases`` parameter of the model's ``new_space`` method::
>>> model.new_space('Fixed', bases=model.BaseMortgage)
In the same way, create ``Adjustable`` by inheriting from ``BaseMortgage``::
>>> model.new_space('Adjustable', bases=model.BaseMortgage)
You can also define an inheritance relationship between existing spaces
using the ``add_bases`` method. Alternatively to calling the ``new_space``
with the ``bases`` parameter, you could also create ``Fixed`` and ``Adjustable``
by calling ``new_space`` without ``bases``, and later calling
``add_bases`` on ``Fixed`` and ``Adjustable`` to set ``BaseMortgage`` as their
base space::
>>> model.new_space('Fixed')
>>> model.new_space('Adjustable')
>>> model.Fixed.add_bases(model.Mortgage)
>>> model.Adjustable.add_bases(model.Mortgage)
Next, we set the interest rates by duration for ``Adjustable`` as a ``dict``.
Note that the index starts from 0, so the key for the Nth rate is (N-1)::
>>> model.Adjustable.Rate = {
... 0: 0.02,
... 1: 0.02,
... 2: 0.02,
... 3: 0.02,
... 4: 0.02,
... 5: 0.04,
... 6: 0.05,
... 7: 0.06,
... 8: 0.05,
... 9: 0.04
... }
You may also assign ``0.03`` to ``Rate`` in ``Fixed``, although the value is inherited::
>>> model.Fixed.Rate = 0.03
To refer to ``Rate`` in the same manner in both ``Fixed`` and ``Adjustable``,
we create a cells ``IntRate`` indexed with ``t``.
First we create ``IntRate`` in ``BaseMortgage`` and define its formula to raise a `NotImplementedError` to indicate that it needs to be defined in the sub spaces.
There are a few ways to define the formula of ``IntRate``.
Here we define it by first defining a Python function and then assigning it
to ``InRate``'s formula::
>>> IntRate = model.BaseMortgage.new_cells('IntRate')
>>> def temp(t): # the name of the function can be anything.
raise NoteImplementedError
>>> IntRate.formula = temp
>>> IntRate.formula
def IntRate(t):
raise NoteImplementedError
Then override ``IntRate`` in ``Fixed`` and ``Adjustable`` to refer to their own ``Rate``::
>>> model.Fixed.IntRate.formula = lambda t: Rate
>>> model.Adjustable.IntRate.formula = lambda t: Rate[t]
>>> model.Adjustable.IntRate[5]
0.04
Next, we are going to define ``Payment`` in ``BaseMortgage`` so that the definition of ``Payment`` in the base space can be inherited and used both in ``Fixed`` and ``Adjustable``
without change.
The formula before update should look like below in ``BaseMortgage`` because
we developed it from ``Fixed`` form the earlier example::
def Payment():
return Principal * Rate * (1+Rate)**Term / ((1+Rate)**Term - 1)
The formula above exactly represents the math expression below, which is a known formula to calculate the amount of level annual payments to pay off in `Term `years
a debt with interest accruing at ``Rate`` a year.
.. math::
Payment = Principal\cdot\frac{Rate(1+Rate)^{Term}}{(1+Rate)^{Term}-1}
To make the formula applicable to ``Adjustable``, we need to apply the following changes.
* Parameterize ``Payment`` with ``t``
* Replace ``Rate`` with ``IntRate(t-1)``
* Replace ``Principal`` with ``Balance(t-1)``
* Replace ``Term`` with ``Term - t + 1``
The expression now looks like below::
Balance(t-1) * IntRate(t-1) * (1 + IntRate(t-1))** (Term - t + 1) / ((1 + IntRate(t-1))** (Term - t + 1) - 1)
The corresponding math expression is as follows:
.. math::
Payment(t) = Balance(t-1)\cdot\frac{IntRate(t)(1+IntRate(t))^{Term-t+1}}{(1+IntRate(t))^{Term-t+1}-1}
You may wonder why ``Payment(t)`` refer to ``Balance(t-1)`` and ``IntRate(t-1)``,
instead of ``Balance(t)`` and ``IntRate(t)``.
You may also wonder why the remaining period is not ``Term - t`` but ``Term - t + 1``.
The figure below illustrates how ``Payment(6)`` is calculated.
``Payment(6)`` is calculated at ``t=5`` such that paying the amount for the rest of
the loan term (5 years) would pays off ``Balance(5)`` with interest accruing at ``IntRate(5)``,
assuming that ``IntRate(5)`` would apply for the rest of the loan period.
.. figure:: /images/tutorial/ObjectOrientedExample/PaymentAt6.png
:align: center
In reality, the interest rate is updated annually, so one year later at ``t=6``,
the ``IntRate(6)`` may be different from ``IntRate(5)``. In that case, ``Payment(7)`` is updated
such that the updated amount would pays off ``Balance(6)`` with interest
accruing at ``IntRate(6)`` for the rest of the loan term.
.. figure:: /images/tutorial/ObjectOrientedExample/PaymentAt7.png
:align: center
Note the ``Payment`` formula above is also valid for ``Fixed``, because
the formula ``Payment`` returns the same value for ``t`` during the loan period if
the interest rate does not change. So we define ``Payment`` in ``BaseMortgage``.
The code below update ``Payment`` in ``BaseMortgage``.
``r`` and ``u`` are defined to make the expression compact::
>>> def temp(t):
... r = IntRate(t-1)
... u = Term - t + 1
... return Balance(t-1) * r * (1 + r)**u / ((1 + r)**u - 1)
>>> model.BaseMort.Payment.formula = temp
We need to update one more cells. ``Balance`` is defined in ``BaseMortgage`` as follows::
>>> model.Mortgage.Balance.formula
def Balance(t):
if t > 0:
return Balance(t-1) * (1+Rate) - Payment
else:
return Principal
The formula should refer to ``IntRate(t-1)`` and ``Payment(t)`` instead of ``Rate``
and ``Payment`` respectively::
>>> def temp(t):
... if t > 0:
... return Balance(t-1) * (1 + IntRate(t-1)) - Payment(t)
... else:
... return Principal
>>> model.BaseMortgage.Balance.formula = temp
Checking the results
---------------------
Now that we have completed making all the necessary changes,
let's check the results.
Below the adjustable payments are output as a ``dict``.
As expected, the payments increase after the first 5 years
because the interest rate at ``t=5`` is higher than before.
The payments then vary every year, reflecting the changes in the interest rate::
>>> {t: model.Adjustable.Payment(t) for t in range(1 ,11)}
{1: 11132.652786531637,
2: 11132.65278653164,
3: 11132.652786531638,
4: 11132.652786531644,
5: 11132.65278653164,
6: 11786.927741021387,
7: 12065.96444749335,
8: 12292.72989621633,
9: 12120.72411143264,
10: 12005.288643704713}
>>> model.Adjustable.Payment.series.plot()
.. figure:: /images/tutorial/ObjectOrientedExample/AdjustablePaymentPlot.png
:align: center
To compare against the adjustable payments,
let's also output and plot the fixed payments.
As you see below, the fixed payments are constant
throughout the loan period, even though the payments
are recalculated every year by the formula shared with ``Adjustable``::
>>> {t: model.Fixed.Payment(t) for t in range(1 ,11)}
{1: 11723.050660515952,
2: 11723.050660515952,
3: 11723.050660515953,
4: 11723.050660515959,
5: 11723.05066051596,
6: 11723.050660515968,
7: 11723.05066051596,
8: 11723.050660515977,
9: 11723.05066051599,
10: 11723.05066051596}
>>> model.Fixed.Payment.series.plot()
.. figure:: /images/tutorial/ObjectOrientedExample/FixedPaymentPlot.png
:align: center
Below is the output of ``Adjustable.Balance``.
You can see that the balance is actually paid off at ``t=0``::
>>> {t: model.Adjustable.Balance(t) for t in range(0 ,11)}
{0: 100000,
1: 90867.34721346837,
2: 81552.0413712061,
3: 72050.42941209857,
4: 62358.78521380889,
5: 52473.30813155344,
6: 42785.31271579419,
7: 32858.613904090555,
8: 22537.40084211966,
9: 11543.546772793003,
10: 1.0913936421275139e-11}
>>> model.Adjustable.Balance.series.plot()
.. figure:: /images/tutorial/ObjectOrientedExample/AdjustableBalancePlot.png
:align: center