Net income after taxes and benefits

This shows a range of Ontario households.

Hide code cell source

from policyengine_canada import Microsimulation, Simulation
from policyengine_core.reforms import Reform
from policyengine_canada.model_api import *
import pandas as pd

YEAR = 2023


def make_hh(adults, children, child_age):
    people = dict(head=dict(age=30))
    members = ["head"]
    if adults == 2:
        people["spouse"] = dict(age=30)
        members += ["spouse"]
    for i in range(children):
        people[f"child{i}"] = dict(age=child_age)
        members += [f"child{i}"]
    return dict(
        people=people,
        households=dict(household=dict(members=members, province="ONTARIO")),
        # Same impact for households with $250k+ income, so show up to $300k.
        # $1k increments.
        axes=[[dict(name="employment_income", count=301, min=0, max=300_000)]],
    )


l = []
for adults in [1, 2]:
    for children in [0, 1, 2, 3]:
        for child_age in [1]:
            hh = make_hh(adults, children, child_age)
            baseline_hh = Simulation(situation=hh)
            l.append(
                pd.DataFrame(
                    dict(
                        adults=adults,
                        children=children,
                        child_age=child_age,
                        # Reshape combined array to get head's varied earnings.
                        employment_income=baseline_hh.calculate(
                            "employment_income", YEAR
                        )[0 :: (adults + children)],
                        household_net_income=baseline_hh.calculate(
                            "household_net_income", YEAR
                        ),
                        mtr=baseline_hh.calculate("marginal_tax_rate", YEAR)[
                            0 :: (adults + children)
                        ],
                    )
                )
            )

df = pd.concat(l)
df
/opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[1], line 43
     31             hh = make_hh(adults, children, child_age)
     32             baseline_hh = Simulation(situation=hh)
     33             l.append(
     34                 pd.DataFrame(
     35                     dict(
     36                         adults=adults,
     37                         children=children,
     38                         child_age=child_age,
     39                         # Reshape combined array to get head's varied earnings.
     40                         employment_income=baseline_hh.calculate(
     41                             "employment_income", YEAR
     42                         )[0 :: (adults + children)],
---> 43                         household_net_income=baseline_hh.calculate(
     44                             "household_net_income", YEAR
     45                         ),
     46                         mtr=baseline_hh.calculate("marginal_tax_rate", YEAR)[
     47                             0 :: (adults + children)
     48                         ],
     49                     )
     50                 )
     51             )
     53 df = pd.concat(l)
     54 df

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:491, in Simulation.calculate(self, variable_name, period, map_to, decode_enums)
    488 np.random.seed(hash(variable_name + str(period)) % 1000000)
    490 try:
--> 491     result = self._calculate(variable_name, period)
    492     if isinstance(result, EnumArray) and decode_enums:
    493         result = result.decode_to_str()

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:721, in Simulation._calculate(self, variable_name, period)
    719 try:
    720     self._check_for_cycle(variable.name, period)
--> 721     array = self._run_formula(variable, population, period)
    723     # If no result, use the default value and cache it
    724     if array is None:
    725         # Check if the variable has a previously defined value

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:946, in Simulation._run_formula(self, variable, population, period)
    944 for added_variable in adds_list:
    945     if added_variable in self.tax_benefit_system.variables:
--> 946         values = values + self.calculate(
    947             added_variable, period, map_to=variable.entity.key
    948         )
    949     else:
    950         try:

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:491, in Simulation.calculate(self, variable_name, period, map_to, decode_enums)
    488 np.random.seed(hash(variable_name + str(period)) % 1000000)
    490 try:
--> 491     result = self._calculate(variable_name, period)
    492     if isinstance(result, EnumArray) and decode_enums:
    493         result = result.decode_to_str()

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:721, in Simulation._calculate(self, variable_name, period)
    719 try:
    720     self._check_for_cycle(variable.name, period)
--> 721     array = self._run_formula(variable, population, period)
    723     # If no result, use the default value and cache it
    724     if array is None:
    725         # Check if the variable has a previously defined value

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:946, in Simulation._run_formula(self, variable, population, period)
    944 for added_variable in adds_list:
    945     if added_variable in self.tax_benefit_system.variables:
--> 946         values = values + self.calculate(
    947             added_variable, period, map_to=variable.entity.key
    948         )
    949     else:
    950         try:

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:491, in Simulation.calculate(self, variable_name, period, map_to, decode_enums)
    488 np.random.seed(hash(variable_name + str(period)) % 1000000)
    490 try:
--> 491     result = self._calculate(variable_name, period)
    492     if isinstance(result, EnumArray) and decode_enums:
    493         result = result.decode_to_str()

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:721, in Simulation._calculate(self, variable_name, period)
    719 try:
    720     self._check_for_cycle(variable.name, period)
--> 721     array = self._run_formula(variable, population, period)
    723     # If no result, use the default value and cache it
    724     if array is None:
    725         # Check if the variable has a previously defined value

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:1011, in Simulation._run_formula(self, variable, population, period)
   1009     array = formula(population, period)
   1010 else:
-> 1011     array = formula(population, period, parameters_at)
   1013 return array

File ~/work/policyengine-canada/policyengine-canada/policyengine_canada/variables/gov/cra/tax/income/credits/climate_action/climate_action_incentive.py:13, in climate_action_incentive.formula(household, period, parameters)
     12 def formula(household, period, parameters):
---> 13     amount = household("climate_action_incentive_pre_rural", period)
     14     rural = household("is_rural", period)
     15     rural_percent_bonus = parameters(
     16         period
     17     ).gov.cra.tax.income.credits.climate_action_incentive.rural

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/populations/group_population.py:38, in GroupPopulation.__call__(self, variable_name, period, options)
     36     return self.sum(self.members(variable_name, period, options))
     37 else:
---> 38     return super().__call__(variable_name, period, options)

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/populations/population.py:142, in Population.__call__(self, variable_name, period, options)
    138     return self.simulation.calculate_divide(
    139         variable_name, period, **calculate_kwargs
    140     )
    141 else:
--> 142     return self.simulation.calculate(
    143         variable_name, period, **calculate_kwargs
    144     )

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:491, in Simulation.calculate(self, variable_name, period, map_to, decode_enums)
    488 np.random.seed(hash(variable_name + str(period)) % 1000000)
    490 try:
--> 491     result = self._calculate(variable_name, period)
    492     if isinstance(result, EnumArray) and decode_enums:
    493         result = result.decode_to_str()

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:721, in Simulation._calculate(self, variable_name, period)
    719 try:
    720     self._check_for_cycle(variable.name, period)
--> 721     array = self._run_formula(variable, population, period)
    723     # If no result, use the default value and cache it
    724     if array is None:
    725         # Check if the variable has a previously defined value

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:946, in Simulation._run_formula(self, variable, population, period)
    944 for added_variable in adds_list:
    945     if added_variable in self.tax_benefit_system.variables:
--> 946         values = values + self.calculate(
    947             added_variable, period, map_to=variable.entity.key
    948         )
    949     else:
    950         try:

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:491, in Simulation.calculate(self, variable_name, period, map_to, decode_enums)
    488 np.random.seed(hash(variable_name + str(period)) % 1000000)
    490 try:
--> 491     result = self._calculate(variable_name, period)
    492     if isinstance(result, EnumArray) and decode_enums:
    493         result = result.decode_to_str()

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:721, in Simulation._calculate(self, variable_name, period)
    719 try:
    720     self._check_for_cycle(variable.name, period)
--> 721     array = self._run_formula(variable, population, period)
    723     # If no result, use the default value and cache it
    724     if array is None:
    725         # Check if the variable has a previously defined value

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:1011, in Simulation._run_formula(self, variable, population, period)
   1009     array = formula(population, period)
   1010 else:
-> 1011     array = formula(population, period, parameters_at)
   1013 return array

File ~/work/policyengine-canada/policyengine-canada/policyengine_canada/variables/gov/cra/tax/income/credits/climate_action/climate_action_incentive_person.py:14, in climate_action_incentive_person.formula(person, period, parameters)
     12 def formula(person, period, parameters):
     13     province = person.household("province_code_str", period)
---> 14     category = person("climate_action_incentive_category", period)
     15     amounts = parameters(
     16         period
     17     ).gov.cra.tax.income.credits.climate_action_incentive.amount
     18     return amounts[category][province]

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/populations/population.py:142, in Population.__call__(self, variable_name, period, options)
    138     return self.simulation.calculate_divide(
    139         variable_name, period, **calculate_kwargs
    140     )
    141 else:
--> 142     return self.simulation.calculate(
    143         variable_name, period, **calculate_kwargs
    144     )

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:491, in Simulation.calculate(self, variable_name, period, map_to, decode_enums)
    488 np.random.seed(hash(variable_name + str(period)) % 1000000)
    490 try:
--> 491     result = self._calculate(variable_name, period)
    492     if isinstance(result, EnumArray) and decode_enums:
    493         result = result.decode_to_str()

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:785, in Simulation._calculate(self, variable_name, period)
    777             array = np.array(
    778                 [
    779                     item.index if isinstance(item, Enum) else item
    780                     for item in array
    781                 ]
    782             )
    783             array = EnumArray(array, variable.possible_values)
--> 785     array = self._cast_formula_result(array, variable)
    786     holder.put_in_cache(array, period, self.branch_name)
    788 except SpiralError:

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/simulations/simulation.py:1059, in Simulation._cast_formula_result(self, value, variable)
   1057 def _cast_formula_result(self, value: Any, variable: str) -> ArrayLike:
   1058     if variable.value_type == Enum and not isinstance(value, EnumArray):
-> 1059         return variable.possible_values.encode(value)
   1061     if not isinstance(value, np.ndarray):
   1062         population = self.get_variable_population(variable.name)

File /opt/hostedtoolcache/Python/3.14.3/x64/lib/python3.14/site-packages/policyengine_core/enums/enum.py:100, in Enum.encode(cls, array)
     98         invalid_values = np.unique(array[invalid_mask])
     99         valid_names = [item.name for item in cls]
--> 100         raise ValueError(
    101             f"Invalid value(s) {invalid_values.tolist()} for enum "
    102             f"{cls.__name__}. Valid values are: {valid_names}"
    103         )
    104 elif array.dtype.kind in {"i", "u"}:
    105     # Integer array - already indices
    106     indices = array

ValueError: Invalid value(s) ['0'] for enum ClimateActionIncentiveCategory. Valid values are: ['HEAD', 'SPOUSE', 'ELDEST_CHILD_IN_SINGLE_PARENT_HOUSEHOLD', 'OTHER_CHILD']

Hide code cell source

from policyengine_canada import Simulation

sim = Simulation(
    situation=dict(
        people=dict(
            person=dict(
                age=30,
                employment_income=20_000,
            )
        )
    )
)

sim.calculate("household_net_income")
array([18395.], dtype=float32)

Here’s an example of using axes to calculate how variables relate to each other. Income tax is a progressive schedule (for an example), and people over 65 are exempt. The chart below plots income tax by income and age together.

import plotly.express as px

px.line(
    df,
    "employment_income",
    "household_net_income",
    color="children",
    facet_col="adults",
    title="Household net income by employment income",
)
px.line(
    df,
    "employment_income",
    "mtr",
    color="children",
    facet_col="adults",
    title="Marginal tax rate by employment income",
)