openfisca / openfisca-core

OpenFisca core engine. See other repositories for countries-specific code & data.
https://openfisca.org
GNU Affero General Public License v3.0
165 stars 74 forks source link

Parametrised formulas #1193

Open bonjourmauko opened 9 months ago

bonjourmauko commented 9 months ago

Hi there!

I really enjoy OpenFisca, but I recently encountered an issue.

Here is what I did:

I tried to model this variable:

Abatement rate means the rate at which a rate of benefit (for example, specified in Schedule 4) must, under the appropriate income test, be reduced on account of income.

Income Test 1 means that the applicable rate of benefit must be reduced— (a) by 30 cents for every $1 of the total income of the beneficiary and the beneficiary’s spouse or partner that is more than $160 a week but not more than $250 a week; and (b) by 70 cents for every $1 of that income that is more than $250 a week

Here is what I expected to happen:

Because the calculation if income is specific (and different) to each social benefit, it does not refer to a specific variable to a range of possible variables, from which we need to use the one that corresponds to the benefit we're calculating.

That breaks a fundamental (implicit?) assumption in OpenFisca: the law does not contain abstract formulations. In software terms, we're not supposed to define variables whose application result in the creation of another variable, because, supposedly, the law doesn't. A good practice has emerged that can be resumed as: Model as a rule-maker, not as a developer.

However, in the text above, income is unknowable as along as benefit is unknowable, and benefit is only knowable at run time. Therefore, I expected to be able to do something like this:

class abatement_rate(variables.Variable):
    ...

    def formula_2018_11_26(people, period, params, *args, **kwargs):
        income = kwargs["income"]
        ...

Or:

class abatement_rate(variables.MetaVariable):
    income = lambda variable: variable.income

    def formula_2018_11_26(cls, people, period, params):
        income = people(cls.income, period)
        ...

Etc.

Here is what actually happened:

We can't. For now, I had to do this:

@dataclasses.dataclass(frozen=True)
class AbatementRate:
    """Abatement rate applicable to income-tested benefits.
    Income test means that the applicable rate of a benefit must be reduced by
    a certain amount for each unit of income defined by the policy-maker, above
    a certain floor but below a certain ceiling.
    Abatement rate is the application of an income test to a specific benefit,
    taking into account that benefit's own definition of total income; that is,
    "the rate at which a rate of benefit [...] must, under the appropriate
    income test, be reduced on account of income."
    """

    #: Applicable rate of benefit to be reduced.
    applicable_rate: Vector

    #: Total income of the beneficiary and the beneficiary’s spouse or partner.
    total_income: Vector

    def __call__(self, income_test: Callable[[Vector], Vector]) -> Vector:
        """Apply an income test to a benefit's applicable rate."""

        # numpy.floor is required for income tests as i.e. "35c for every $1".
        floor = numpy.floor(self.total_income)

        # The abatement rate regardless of benefit rate.
        abatement_rate = income_test(floor)

        # The abatement rate capped at the applicable benefit rate.
        return numpy.minimum(abatement_rate, self.applicable_rate)

Here is data (or links to it) that can help you reproduce this issue:

https://www.legislation.govt.nz/act/public/2018/0032/latest/whole.html#whole https://github.com/digitalaotearoa/openfisca-aotearoa/pull/59 https://github.com/openfisca/openfisca-core/blob/f7d979d461e38278c99874f9cf4e4f8beecb20bf/openfisca_core/simulations/simulation.py#L309-L328

Context

I identify more as a:

Discussion

While we should not create code abstracttions that do not exist in the law, it seems logical that we were able to model the abstractions that do exist, which today we can't (the current implementation won't be registered as a Variable).