Attributes and Instantiation¶
Introduction¶
This is the second tutorial in the Intro Series. Here we will do a deeper dive into attributes and instantiation of models.
We will start with the model code from the prior tutorial which is shown below.
from footings.model import (
model,
step,
def_parameter,
def_intermediate,
def_return,
)
@model(steps=["_add_a_b", "_add_ab_c"])
class AddABC:
a = def_parameter()
b = def_parameter()
c = def_parameter()
ab = def_intermediate()
abc = def_return()
@step(uses=["a", "b"], impacts=["ab"])
def _add_a_b(self):
self.ab = self.a + self.b
@step(uses=["ab", "c"], impacts=["abc"])
def _add_ab_c(self):
self.abc = self.ab + self.c
Inspecting the Model¶
To get a better understanding of the model objected, call getfullargspec
on the AddABC
.
from inspect import getfullargspec
inspection = getfullargspec(AddABC)
inspection
FullArgSpec(args=['self'], varargs=None, varkw=None, defaults=None, kwonlyargs=['a', 'b', 'c'], kwonlydefaults=None, annotations={'return': None})
The first thing one should notice is args
is equal to []
. This means there are no arguments that can be passed in without being assigned to a keyword. Looking at the attribute kwonlyags
we see the 3 parameters - ['a', 'b', 'c']
that were defined using def_parameter
. Neither ab
nor abc
appear in the inspection because they were defined using def_intermediate
and def_return
which excludes them from the __init__
method of the model.
This can be tested by running the following line of code which will return an error.
AddABC(1, 2, 3)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-3-c186c444e43c> in <module>
----> 1 AddABC(1, 2, 3)
TypeError: __init__() takes 1 positional argument but 4 were given
The Footings framework
intentionally make the models key word only objects as many in order to be explicit when models have many parameters.
An Instantiated Model¶
To instantiate the model, we will pass arguments using key words as shown below.
model = AddABC(a=1, b=2, c=3)
Once a model has been instantiated, the parameters appear as attributes under the model.
print(f"attribute a = {model.a}")
print(f"attribute b = {model.b}")
print(f"attribute c = {model.c}")
attribute a = 1
attribute b = 2
attribute c = 3
These attributes are frozen and cannot be modified. The below code will demonstrate this.
model.a = 0
---------------------------------------------------------------------------
FrozenAttributeError Traceback (most recent call last)
<ipython-input-6-b270d9fb6228> in <module>
----> 1 model.a = 0
~/work/footings/footings/.venv/lib/python3.8/site-packages/attr/_make.py in __setattr__(self, name, val)
913 nval = val
914 else:
--> 915 nval = hook(self, a, val)
916
917 _obj_setattr(self, name, nval)
~/work/footings/footings/.venv/lib/python3.8/site-packages/attr/setters.py in frozen(_, __, ___)
33 .. versionadded:: 20.1.0
34 """
---> 35 raise FrozenAttributeError()
36
37
FrozenAttributeError:
In addition, the attributes defined using def_return
and def_intermediate
also appear as attributes under the model object. Though, they have a value of None
as shown below.
print(f"attribute ab = {model.ab}")
print(f"attribute abc = {model.abc}")
attribute ab = None
attribute abc = None
These attributes are not frozen so when the model is run the different steps within the model can modify these attributes. This can be tested using the following code.
model.ab = 0
model.ab
0
Arguments to def_*¶
Returning to the code of our example model -
from footings.model import (
model,
step,
def_parameter,
def_intermediate,
def_return,
)
@model(steps=["_add_a_b", "_add_ab_c"])
class AddABC:
a = def_parameter()
b = def_parameter()
c = def_parameter()
ab = def_intermediate()
abc = def_return()
@step(uses=["a", "b"], impacts=["ab"])
def _add_a_b(self):
self.ab = self.a + self.b
@step(uses=["ab", "c"], impacts=["abc"])
def _add_ab_c(self):
self.abc = self.ab + self.c
When calling the def_*
functions we did not pass any arguments. These functions take a number of optional arguments add the ability to validate data passed to arguments as well as adds to the documentation of the model which will be covered in more detail in the documentation tutorial. To see a list of the available arguments you can see the api section.
Below is an example of how we can add validation to the model when adding arguments to def_parameter
.
from footings.validators import value_min, value_max, in_
@model(steps=["_add_a_b", "_add_ab_c"])
class AddABC:
a = def_parameter(dtype=int, validator=value_min(0))
b = def_parameter(dtype=int, validator=value_max(0))
c = def_parameter(dtype=int, validator=in_([1, 2]))
ab = def_intermediate()
abc = def_return()
@step(uses=["a", "b"], impacts=["ab"])
def _add_a_b(self):
self.ab = self.a + self.b
@step(uses=["ab", "c"], impacts=["abc"])
def _add_ab_c(self):
self.abc = self.ab + self.c
---------------------------------------------------------------------------
ImportError Traceback (most recent call last)
<ipython-input-10-c45eb716b984> in <module>
----> 1 from footings.validators import value_min, value_max, in_
2
3 @model(steps=["_add_a_b", "_add_ab_c"])
4 class AddABC:
5 a = def_parameter(dtype=int, validator=value_min(0))
ImportError: cannot import name 'value_min' from 'footings.validators' (/home/runner/work/footings/footings/src/footings/validators.py)
AddABC(a=-1, b=1, c=3)
AddABC(a=1, b=1, c=3)
AddABC(a=1, b=0, c=3)
Additional def_* functions¶
The footings
library also contains two additional define functions. Both of these will come in handy when building actuarial models.
def_meta
is a way to add metadata to a model. As an example, this might be the run date/time a model is ran.def_sensitivity
is a way to add a default parameter. The name sensitivity is often used in actuarial models to test how sensitive an outcome is to a given parameter. As an example, an actuarial model might have an interest rate parameter and an interest rate sensitivity. The default value for the sensitivity would be 1 but could be changed to 1.1 to test the impact of a 10% increase in interest rates.
Both of these are demonstrated in the code below.
from footings.model import (
model,
step,
def_parameter,
def_sensitivity,
def_meta,
def_intermediate,
def_return,
)
from footings.actuarial_tools import run_date_time
@model(steps=["_calculate"])
class DiscountFactors:
interest_rate = def_parameter()
interest_sensitivity = def_sensitivity(default=1)
discount_factors = def_return()
run_date_time = def_meta(meta=run_date_time)
@step(uses=["interest_rate", "interest_sensitivity"], impacts=["discount_factors"])
def _calculate(self):
rate = self.interest_rate * self.interest_sensitivity
self.discount_factors = [(1-rate)**i for i in range(0, 5)]
As the code below shows, interest_sensitivity
does not need to be set when instantiating the model. It will be assigned a default value of 1.
discount = DiscountFactors(interest_rate=0.05)
print(f"run_date_time = {str(discount.run_date_time)}")
print(f"interest_sensitivity = {str(discount.interest_sensitivity)}")
print(f"output = {str(discount.run())}")
It can optionally be changed though as the code below shows.
discount2 = DiscountFactors(interest_rate=0.05, interest_sensitivity=1.1)
print(f"run_date_time = {str(discount2.run_date_time)}")
print(f"interest_sensitivity = {str(discount2.interest_sensitivity)}")
print(f"output = {str(discount2.run())}")
Closing¶
With this tutorial, we dug deeper into how the Footings framework
defines attributes and how they are represented in the model. When building models, it is recommended to pass in optional arguments to the def_*
functions to add validation.
Below is a summary of the functionality of each def_*
function -
Define Function |
Init |
Default |
Frozen |
Return |
---|---|---|---|---|
def_parameter |
yes |
no |
yes |
no |
def_sensitivity |
yes |
yes |
yes |
no |
def_meta |
no |
no |
yes |
no |
def_intermediate |
no |
no |
no |
no |
def_return |
no |
no |
no |
yes |