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