Notes to accompany version 0.2.0 of my re-implementation of FSIC. To all intents and purposes these changes are backward compatible with version 0.1.0 from an earlier post. The only change which isn’t strictly backward compatible is to the handling of the status and iterations attributes ([4]) below). However, there’s rarely any reason for the user to modify these directly. Version 0.2.0 changes are essentially backward compatible.

  1. Support for tab completion in interactive settings
  2. Use copy() to duplicate model instances
  3. Features to ease manual edits to model definitions
  4. Treatment of status and iterations attributes now consistent with that of the economic variables

The examples below use a simplified five-equation version of Godley and Lavoie’s (2007) Model SIM, abstracting away the mechanisms that equalise demand and supply as well as the treatment of the labour market. See the first part of one of my earlier posts for details of the original model.

Setup

Model

In this five-equation model, consumption in time is a function of disposable income and past accumulated wealth . The propensities to consume out of each are and , respectively:

Disposable income is national income, (here, implicitly equal to household wage income), less taxes :

National income is consumption plus government expenditure, . The economy is closed, and with no investment:

Taxes are levied as a fixed proportion of income:

Households accumulate savings (wealth) over time from the difference between ingoings (income) and outgoings (expenditure):

Implementation

In FSIC, generate the accompanying class definition as follows (see my earlier post for more details about the syntax and workflow):

import numpy as np  # Import for later
import fsic
script = '''
C = {alpha_1} * YD + {alpha_2} * H[-1]
YD = Y - T
Y = C + G
T = {theta} * Y
H = H[-1] + YD - C
'''

symbols = fsic.parse_model(script)
SIM = fsic.build_model(symbols)

Features

Having constructed an example model, we can take a look at the changes in version 0.2.0. As mentioned above, all but one of these changes is backward compatible. The last one isn’t strictly so but the use cases in which this matters are obscure enough for it not to be a problem in most situations.

1. Support for tab completion in interactive settings

Object attributes that point to variable names (e.g. model.C, model.G) are generated dynamically on instantiation. A new custom __dir__() magic method in the base class for models includes the variable names (as well as attributes like span, names, status and iterations) when passing a model object to dir().

model = SIM(range(1945, 2010 + 1))

print(list(filter(lambda x: not x.startswith('_'),
                  dir(model))))
['C', 'CHECK', 'CODE', 'ENDOGENOUS', 'ERRORS',
 'EXOGENOUS', 'G', 'H', 'LAGS', 'LEADS',
 'NAMES', 'PARAMETERS', 'T', 'Y', 'YD',
 'alpha_1', 'alpha_2', 'copy', 'iterations', 'names',
 'solve', 'solve_period', 'solve_t', 'span', 'status', 
 'theta', 'values']
# In IPython, Jupyter etc, type `model.` (as below; currently commented out)
# and then press tab to see the completion options

#model.

2. Use copy() to duplicate model instances

Having set up a new model instance with data and parameters, we might want to run different scenarios as separate instances. Rather than generate the model from scratch each time, use copy() to create duplicate instances with the same state (values). The class also supports the copy() and deepcopy() functions from the standard library copy module.

# Create an initial model instance and copy to a second instance
model = SIM(range(1945, 2010 + 1),
            alpha_1=0.6, alpha_2=0.4, theta=0.2)

model2 = model.copy()

# Set different values for `G` and solve
model.G = 20
model2.G = 25

model.solve()
model2.solve()

# Print the results for output (Y) from the first five periods of each run:
#  - first row lists results for G = 20
#  - second row lists results for G = 25
np.vstack([model.Y, model2.Y])[:, :5]
array([[ 0.        , 38.46153068, 47.92898039, 55.93990487, 62.71838627],
       [ 0.        , 48.07691335, 59.91123828, 69.92488908, 78.39797772]])

3. Features to ease manual edits to model definitions

FSIC makes it easier to define models and generate valid Python code with supporting features (embedded in the base class) for economic modelling. The code so generated defines a derived class that can be edited further, for example, to:

  1. change the economic relationships in the model
  2. add/remove variables, including those used to check for solution convergence each period
  3. add other behaviour e.g. new controls with keyword arguments

As below, the original code is accessible either by:

  1. inspecting the CODE attribute of a class definition created with build_model()
  2. using build_model_definition() to return the code as a string (and avoid the exec() call that makes the class available during a Python session)
# Either of these work to print the class definition

# 1. Inspect the `CODE` attribute of a class definition created with
#    `build_model()` (previously created with `fsic.build_model()`)
#print(SIM.CODE)

# 2. Use `build_model_definition()` to return the code as a string (and avoid
#    the `exec()` call that makes the class available during a Python session)
print(fsic.build_model_definition(symbols))
class Model(BaseModel):
    ENDOGENOUS = ['C', 'YD', 'H', 'Y', 'T']
    EXOGENOUS = ['G']
    
    PARAMETERS = ['alpha_1', 'alpha_2', 'theta']
    ERRORS = []
    
    NAMES = ENDOGENOUS + EXOGENOUS + PARAMETERS + ERRORS
    CHECK = ENDOGENOUS
    
    LAGS = 1
    LEADS = 0
    
    def _evaluate(self, t):
        self._C[t] = self._alpha_1[t] * self._YD[t] + self._alpha_2[t] * self._H[t-1]
        self._YD[t] = self._Y[t] - self._T[t]
        self._H[t] = self._H[t-1] + self._YD[t] - self._C[t]
        self._Y[t] = self._C[t] + self._G[t]
        self._T[t] = self._theta[t] * self._Y[t]

The code above is valid Python code and thus editable to, for example:

  1. remove extraneous variable lists like the ERRORS attribute
    • delete the ERRORS attribute (equally, you could define new lists of variables)
    • edit the (new) NAMES attribute definition to exclude ERRORS (it’s just a list: you can write in lists of strings directly if you want)
    • NAMES is important because it’s what the model uses to dynamically generate variables
  2. (arbitrarily) reduce the list of variables to be checked for convergence during solution
    • CHECK is the other new attribute and defines the names of the variables to test for convergence in solution each period
  3. extend the bottom-level _evaluate() function signature with a keyword argument to apply exogenous adjustments to household consumption expenditure (with a new line of code in the function body)

The new code is below. Alternatively, see the diff between the code above and below here.

from fsic import BaseModel

class CustomSIM(BaseModel):
    ENDOGENOUS = ['C', 'YD', 'H', 'Y', 'T']
    EXOGENOUS = ['G']

    PARAMETERS = ['alpha_1', 'alpha_2', 'theta']

    # Remove extraneous (empty) `ERRORS` attribute
    NAMES = ENDOGENOUS + EXOGENOUS + PARAMETERS

    # (Arbitrarily) reduce the variables to be checked for convergence during
    # solution
    CHECK = ['C', 'H', 'Y']

    LAGS = 1
    LEADS = 0

    # Extend function signature with a new keyword argument to apply exogenous
    # changes in household consumption expenditure
    # Not required, but note use of keyword-only argument in the function
    # signature:
    #     https://www.python.org/dev/peps/pep-3102/
    def _evaluate(self, t, *, exogenous_change_in_consumption=0):
        self._C[t] = self._alpha_1[t] * self._YD[t] + self._alpha_2[t] * self._H[t-1]

        # Apply exogenous changes in household consumption expenditure
        self._C[t] += exogenous_change_in_consumption

        self._YD[t] = self._Y[t] - self._T[t]
        self._H[t] = self._H[t-1] + self._YD[t] - self._C[t]
        self._Y[t] = self._C[t] + self._G[t]
        self._T[t] = self._theta[t] * self._Y[t]
custom_model = CustomSIM(range(1945, 2010 + 1),
                         alpha_1=0.6, alpha_2=0.4, theta=0.2)
custom_model.G = 20

custom_model.solve()
custom_model2 = custom_model.copy()

# Re-solve from 1980 onwards with an exogenous increase (decrease) in household
# saving (consumption)
custom_model2.solve(start=1980, exogenous_change_in_consumption=-5)
# Output results for 1979-83 from each run:
#  - first row lists results from the baseline
#  - second row lists results with the increase in saving
np.vstack([custom_model['Y', 1979:1983],
           custom_model2['Y', 1979:1983]])
array([[99.7516991 , 99.78989923, 99.82222242, 99.84957282, 99.87271546],
       [99.7516991 , 90.17453275, 91.68614743, 92.96520534, 94.04749008]])

4. Treatment of status and iterations attributes is now consistent with that of the economic variables

Now, as with the economic variables in a model, there’s protection around the status and iterations attributes to preserve:

  • their type as NumPy arrays and, by extension, their shape
  • the dtype (variable type) of the arrays
model = SIM(range(1945, 2010 + 1),
            alpha_1=0.6, alpha_2=0.4, theta=0.2)

print(model.status[:5])
print(model.iterations[:5])
['-' '-' '-' '-' '-']
[-1 -1 -1 -1 -1]
model.status = '.'
model.iterations[::2] = 0

print(model.status[:5])
print(model.iterations[:5])
['.' '.' '.' '.' '.']
[ 0 -1  0 -1  0]

See here for this post as a Jupyter notebook along with supporting Python code.

References

Godley, W., Lavoie, M. (2007) Monetary economics: An integrated approach to credit, money, income, production and wealth, Palgrave Macmillan