Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ def fake_analysis() -> SimpleNamespace:
{
"decile": 2,
"absolute_change": 20.0,
"relative_change": 0.02,
"relative_change": 2.0,
},
{
"decile": 1,
"absolute_change": 10.0,
"relative_change": 0.01,
"relative_change": 1.0,
},
]
),
Expand All @@ -62,7 +62,7 @@ def fake_analysis() -> SimpleNamespace:
{
"decile": 1,
"absolute_change": 30.0,
"relative_change": 0.03,
"relative_change": 3.0,
}
]
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def build_decile(analysis: object) -> DecileOutput:
def build_decile_output(collection: object) -> DecileOutput:
"""Key absolute and relative changes by decile number (``"1"``..``"10"``).

Rows with a decile of 0 or below are dropped; missing values become 0.0.
policyengine.py reports ``relative_change`` as a percentage; ``relative`` is
a fraction, so it is divided by 100. Rows with a decile of 0 or below are
dropped; missing values become 0.0.
"""
if isinstance(collection, DecileOutput):
return collection
Expand All @@ -49,7 +51,7 @@ def build_decile_output(collection: object) -> DecileOutput:
continue
key = str(decile)
average[key] = _number(row.get("absolute_change"))
relative[key] = _number(row.get("relative_change"))
relative[key] = _number(row.get("relative_change")) / 100
return DecileOutput(average=average, relative=relative)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
REFORM_POVERTY_BY_AGE,
REFORM_POVERTY_BY_GENDER,
REFORM_POVERTY_BY_RACE,
FakeCollection,
FakeModelOutput,
fake_analysis,
)
Expand Down Expand Up @@ -61,6 +62,9 @@
from policyengine_simulation_executor.simulation_output_builder import (
SimulationOutputBuilder,
)
from policyengine_simulation_executor.simulation_output_distribution import (
build_decile_output,
)
from policyengine_simulation_executor.simulation_output_geographic import (
build_congressional_district_impact,
build_congressional_district_impact_output,
Expand Down Expand Up @@ -953,6 +957,23 @@ def serialize(runtime):
)


def test_decile_relative_is_a_fraction_of_the_baseline_mean():
# policyengine.py reports relative_change as a percentage (1.1254 is
# 1.1254%); the API's relative field is a fraction (0.011254).
output = build_decile_output(
FakeCollection(
[
{"decile": 1, "absolute_change": 180.7, "relative_change": 1.1254},
{"decile": 10, "absolute_change": 1410.3, "relative_change": 0.8227},
{"decile": 2, "absolute_change": 0.0, "relative_change": None},
]
)
)

assert output.average == {"1": 180.7, "2": 0.0, "10": 1410.3}
assert output.relative == pytest.approx({"1": 0.011254, "2": 0.0, "10": 0.008227})


def test_builder_maps_uk_wealth_outputs_and_omits_us_only_race(monkeypatch):
output = _build_schema_output(monkeypatch, country="uk").model_dump(mode="json")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Decile outputs built from policyengine.py's own decile calculation."""

from __future__ import annotations

from types import SimpleNamespace

import pytest
from fixtures.weighted_frames import weighted_entity_frame
from policyengine.core import Simulation
from policyengine.outputs.decile_impact import calculate_decile_impacts
from policyengine_simulation_executor.simulation_output_distribution import (
build_decile_output,
)

BASELINE_INCOMES = [10_000.0, 20_000.0, 30_000.0, 40_000.0]


class _InMemorySimulation(Simulation):
"""A simulation whose output is already in memory, so nothing runs."""

def ensure(self) -> None:
return None


def _simulation(incomes: list[float]) -> Simulation:
household = weighted_entity_frame(
{
"household_net_income": incomes,
"household_count_people": [1.0, 2.0, 1.0, 3.0],
"household_wealth_decile": [1, 1, 2, 2],
"household_weight": [1.0, 2.0, 3.0, 4.0],
}
)
return _InMemorySimulation.model_construct(
output_dataset=SimpleNamespace(data=SimpleNamespace(household=household)),
tax_benefit_model_version=None,
)


@pytest.mark.parametrize("decile_variable", [None, "household_wealth_decile"])
def test_decile_relative_is_the_fraction_policyengine_computes(decile_variable):
# Every household gains 1%, so each decile's relative change is 0.01.
# policyengine.py's relative_change is a percentage (1.0 here); the
# API's relative field is a fraction. Running policyengine.py's own
# calculation, not hand-written rows, catches a change of its units.
impacts = calculate_decile_impacts(
baseline_simulation=_simulation(BASELINE_INCOMES),
reform_simulation=_simulation([income * 1.01 for income in BASELINE_INCOMES]),
decile_variable=decile_variable,
entity="household",
quantiles=2,
)

output = build_decile_output(impacts)

rows = impacts.dataframe.set_index("decile")
assert set(output.relative) == {"1", "2"}
for decile, relative in output.relative.items():
row = rows.loc[int(decile)]
assert relative == pytest.approx(row["absolute_change"] / row["baseline_mean"])
assert relative == pytest.approx(0.01)
assert output.average[decile] == pytest.approx(row["absolute_change"])
Loading