Validation#

PolicyEngine closely matches most official statistics on income, consumption and wealth.

Hide code cell source
from policyengine_uk import Microsimulation

sim = Microsimulation()

# Below figures taken from the 2020-26 UKMOD country report.

UKMOD_CASELOADS = {
    "Best Start Grant and Foods": [32, 16, 63, 73, 79, 78, 76],
    "Child Benefit (c)": [12226, 12047, 11368, 11136, 11074, 11036, 10928],
    "Child Benefit": [6828, 6720, 6421, 6280, 6241, 6217, 6154],
    "Child and Working Tax Credits": [1567, 1060, 814, 658, 380, 0, 0],
    "Council Tax Reduction (h)": [5876, 5759, 5257, 5301, 5381, 5424, 5451],
    "Employment and Support Allowance (ib)": [734, 647, 454, 368, 189, 0, 0],
    "Free School Meals": [2940, 2905, 2956, 2978, 3004, 3038, 3092],
    "Healthy Start (food)": [444, 429, 337, 340, 357, 350, 348],
    "Housing Benefit (h)": [2478, 2185, 2068, 1873, 1561, 1192, 1183],
    "Income Support*": [890, 752, 609, 482, 258, 0, 0],
    "Jobseeker's Allowance (cb)": [63, 63, 25, 25, 25, 25, 25],
    "Pension Credit": [1379, 1252, 1357, 1354, 1354, 1357, 1366],
    "School Clothing Grant": [1200, 1162, 1029, 1063, 1105, 1139, 1126],
    "Scottish Carer's Allowance Supplement (i)": [44, 44, 52, 52, 52, 52, 52],
    "Scottish Child Payment (c)": [0, 35, 170, 181, 189, 193, 189],
    "Scottish Child Winter Heating Assistance (c)": [0, 5, 3, 3, 3, 3, 3],
    "Sure Start Maternity Grant": [37, 33, 72, 77, 83, 85, 84],
    "Universal Credit": [4038, 4437, 4337, 4855, 5746, 6572, 6508],
    "Winter Fuel Allowance (h)": [
        11998,
        11435,
        11239,
        11239,
        11239,
        11239,
        11239,
    ],
    "Benefit cap (Housing Benefit)": [47, 47, 36, 22, 14, 2, 2],
    "Benefit cap (Universal Credit)": [169, 166, 85, 77, 98, 108, 102],
    "Income tax (i)": [28797, 29579, 31467, 32495, 32902, 33134, 33396],
    "Basic rate (i)": [22815, 23039, 23776, 24076, 24285, 24296, 24326],
    "Higher rate (i)": [3652, 4110, 5160, 5457, 5562, 5740, 5942],
    "Additional rate (i)": [219, 273, 425, 816, 831, 859, 892],
    "NIC Employees (i)": [22074, 22960, 23024, 22953, 22987, 23088, 23195],
    "NIC Self employed (i)": [1194, 2271, 2613, 2601, 2613, 2696, 2721],
    "NIC Employers (i)": [22504, 23266, 24183, 24380, 24442, 24490, 24534],
}

UKMOD_EXPENDITURE = {
    "Best Start Grant and Foods": [12, 6, 28, 41, 45, 42, 42],
    "Child Benefit": [11098, 10899, 10588, 11357, 11898, 11874, 11742],
    "Child and Working Tax Credits": [8643, 5767, 4703, 4034, 2343, 0, 0],
    "Council Tax Reduction": [7558, 7707, 7503, 7867, 8252, 8600, 8949],
    "Employment and Support Allowance (ib)": [
        5305,
        4579,
        3330,
        2958,
        1672,
        0,
        0,
    ],
    "Free School Meals": [3968, 3909, 4087, 4501, 4757, 4878, 4968],
    "Healthy Start (food)": [126, 167, 133, 134, 140, 136, 136],
    "Housing Benefit": [11004, 10127, 9712, 8969, 7409, 5471, 5431],
    "Income Support*": [3411, 2873, 2452, 2170, 1268, 0, 0],
    "Jobseeker's Allowance (cb)": [233, 234, 99, 109, 115, 116, 116],
    "Pension Credit": [4370, 3831, 4355, 4768, 5043, 5124, 5258],
    "School Clothing Grant": [284, 275, 248, 279, 303, 320, 316],
    "Scottish Carer's Allowance Supplement": [30, 31, 25, 28, 29, 30, 30],
    "Scottish Child Payment": [0, 24, 210, 399, 435, 446, 434],
    "Scottish Child Winter Heating Assistance": [0, 1, 1, 1, 1, 1, 1],
    "Sure Start Maternity Grant": [18, 17, 37, 40, 43, 44, 43],
    "Universal Credit": [38122, 39612, 37137, 43736, 53918, 61863, 61072],
    "Winter Fuel Allowance": [1990, 1916, 1903, 1903, 1903, 1903, 1903],
    "Benefit cap (Housing Benefit)": [190, 308, 116, 95, 46, 1, 1],
    "Benefit cap (Universal Credit)": [687, 646, 260, 230, 337, 362, 319],
    "Income tax": [149405, 164580, 205344, 235819, 240776, 246492, 254345],
    "Basic rate": [60906, 63871, 68330, 71151, 71947, 72366, 73341],
    "Higher rate": [61991, 69490, 89864, 88729, 90784, 93507, 97261],
    "Additional rate": [21054, 25370, 39717, 65776, 67352, 69688, 72670],
    "NIC Employees": [55301, 60554, 65668, 61679, 62486, 63547, 64992],
    "NIC Self-employed": [3773, 4152, 5887, 5493, 5591, 5698, 5827],
    "NIC Employers": [78664, 86786, 103914, 103760, 105282, 107312, 110101],
}

import numpy as np

for variable in UKMOD_EXPENDITURE:
    UKMOD_EXPENDITURE[variable] = np.array(
        [value / 1e3 for value in UKMOD_EXPENDITURE[variable]]
    )

# UKMOD values are from 2020-26

import pandas as pd


def estimate(variable: str):
    return [
        sim.calculate(variable, period=year, map_to="household").sum() / 1e9
        for year in range(2023, 2026)
    ]


from policyengine_uk.data.datasets.frs.calibration.loss import (
    calibration_parameters,
)
from policyengine_core.parameters import get_parameter


def official_estimate(parameter: str):
    return (
        np.array(
            [
                get_parameter(calibration_parameters, parameter)(
                    f"{year}-01-01"
                )
                for year in range(2023, 2026)
            ]
        )
        / 1e9
    )


df = pd.DataFrame()

data = {
    "Income Tax": {
        "Official": official_estimate(
            "programs.income_tax.budgetary_impact.by_country.UNITED_KINGDOM"
        ),
        "PolicyEngine": estimate("income_tax"),
        "UKMOD": UKMOD_EXPENDITURE["Income tax"][3:6],
    },
    "National Insurance": {
        "Official": official_estimate(
            "programs.total_NI.budgetary_impact.UNITED_KINGDOM"
        ),
        "PolicyEngine": estimate("total_NI"),
        "UKMOD": UKMOD_EXPENDITURE["NIC Employees"][3:6]
        + UKMOD_EXPENDITURE["NIC Self-employed"][3:6]
        + UKMOD_EXPENDITURE["NIC Employers"][3:6],
    },
    "Universal Credit": {
        "Official": official_estimate(
            "programs.universal_credit.budgetary_impact.GREAT_BRITAIN"
        ),
        "PolicyEngine": estimate("universal_credit"),
        "UKMOD": UKMOD_EXPENDITURE["Universal Credit"][3:6],
    },
    "Child Benefit": {
        "Official": official_estimate(
            "programs.child_benefit.budgetary_impact.UNITED_KINGDOM"
        ),
        "PolicyEngine": estimate("child_benefit"),
        "UKMOD": UKMOD_EXPENDITURE["Child Benefit"][3:6],
    },
    "Tax Credits": {
        "Official": official_estimate(
            "programs.tax_credits.budgetary_impact.UNITED_KINGDOM"
        ),
        "PolicyEngine": estimate("tax_credits"),
        "UKMOD": UKMOD_EXPENDITURE["Child and Working Tax Credits"][3:6],
    },
    "Housing Benefit": {
        "Official": official_estimate(
            "programs.housing_benefit.budgetary_impact.GREAT_BRITAIN"
        ),
        "PolicyEngine": estimate("housing_benefit"),
        "UKMOD": UKMOD_EXPENDITURE["Housing Benefit"][3:6],
    },
    "Pension Credit": {
        "Official": official_estimate(
            "programs.pension_credit.budgetary_impact.GREAT_BRITAIN"
        ),
        "PolicyEngine": estimate("pension_credit"),
        "UKMOD": UKMOD_EXPENDITURE["Pension Credit"][3:6],
    },
}

# DF columns: year, model, variable, value

for variable in data:
    for model in data[variable]:
        for i, value in enumerate(data[variable][model]):
            df = pd.concat(
                [
                    df,
                    pd.DataFrame(
                        {
                            "year": 2023 + i,
                            "model": model,
                            "variable": variable,
                            "value": value,
                            "error": False,
                            # Text should be: "[model] estimates aggregate [variable] to be [value] in [year]."
                            "text": f"{model} estimates aggregate {variable}<br> to be £{value:.1f} billion <br>in {2023 + i}.",
                        },
                        index=[0],
                    ),
                ]
            )
        for i, value in enumerate(data[variable][model]):
            if model != "Official":
                df = pd.concat(
                    [
                        df,
                        pd.DataFrame(
                            {
                                "year": 2023 + i,
                                "model": model + " (error)",
                                "variable": variable,
                                "value": value - data[variable]["Official"][i],
                                "error": True,
                                # Text should be: "[model]'s estimate of aggregate [variable] is [value] billion [higher/lower] than the official estimate in [year]."
                                "text": f"{model}'s estimate of aggregate {variable}<br> is £{value - data[variable]['Official'][i]:.1f} billion {'higher' if value > data[variable]['Official'][i] else 'lower'} than the official <br>estimate in {2023 + i}.",
                            },
                            index=[0],
                        ),
                    ]
                )

import plotly.express as px
from policyengine_core.charts import format_fig, BLUE_COLOUR_SCALE, GRAY

fig = (
    px.bar(
        df[~df.error][::-1].sort_values(["year", "value"]),
        animation_frame="year",
        y="variable",
        x="value",
        color="model",
        barmode="group",
        orientation="h",
        custom_data=["text"],
        color_discrete_map={
            "Official": GRAY,
            # "Official (error)": GRAY,
            "PolicyEngine": BLUE_COLOUR_SCALE[2],
            # "PolicyEngine (error)": BLUE_COLOR_SCALE[3],
            "UKMOD": BLUE_COLOUR_SCALE[0],
            # "UKMOD (error)": BLUE_COLOR_SCALE[0],
        },
    )
    .update_layout(
        title="Errors against official estimates",
        xaxis_title="Budgetary impact (£bn)",
        yaxis_title="",
        legend_title="",
        xaxis_range=[-10, 300],
    )
    .update_traces(hovertemplate="%{customdata[0]}")
)
fig = format_fig(fig)
fig
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 103
     96 def estimate(variable: str):
     97     return [
     98         sim.calculate(variable, period=year, map_to="household").sum() / 1e9
     99         for year in range(2023, 2026)
    100     ]
--> 103 from policyengine_uk.data.datasets.frs.calibration.loss import (
    104     calibration_parameters,
    105 )
    106 from policyengine_core.parameters import get_parameter
    109 def official_estimate(parameter: str):

ModuleNotFoundError: No module named 'policyengine_uk.data.datasets.frs.calibration.loss'