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
1 change: 1 addition & 0 deletions changelog.d/2654.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The aggregate impact report now records why it withholds a standard reform's costing and target change in a year. For each withheld year, `withheld_because` names every condition that holds (`suppressed_component_changed`, `small_component_left`, `small_balance_change`, `small_target_change` or `no_baseline`) and the component or variable it holds for, in `result.json` and in the long-format report that qualification reads. The pull request comment lists the reasons under the reform costings. No reason gives a count or an amount.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Reform withholding as a yes or no, frozen at da31e8698 without commentary.

Keep this independent of the production tool: it is the differential oracle
for ``measure.withheld_reasons`` (#2654), which must withhold exactly the
years this rule withholds.
"""

from typing import Mapping, Optional

import numpy as np

MIN_CELL = 10
CHANGE_GBP = 0.005


def small(count: int) -> bool:
return 0 < count < MIN_CELL


def array(sim, variable: str, period, map_to: Optional[str] = None) -> np.ndarray:
try:
result = sim.calculate(variable, period, map_to=map_to, unweighted=True)
except TypeError:
result = sim.calculate(variable, period, map_to=map_to)
return np.asarray(getattr(result, "values", result))


def entity_of(sim, variable: str) -> str:
return sim.tax_benefit_system.variables[variable].entity.key


def to_household(sim, values: np.ndarray, entity: str) -> np.ndarray:
if entity == "household":
return np.asarray(values, dtype=np.float64)
return np.asarray(
sim.map_result(np.asarray(values, dtype=np.float64), entity, "household"),
dtype=np.float64,
)


def _weighted_records(mask, weight) -> int:
return int(np.count_nonzero(np.asarray(mask) & (np.asarray(weight) > 0)))


def reform_withheld(
reformed, year: int, baseline: Optional[Mapping], components
) -> bool:
if baseline is None:
return True
for variable, before in baseline["hidden"].items():
if not np.array_equal(
array(reformed, variable, year).astype(np.float64), before
):
return True
weights = {}

def weight(variable):
entity = entity_of(reformed, variable)
if entity not in weights:
weights[entity] = array(reformed, f"{entity}_weight", year).astype(
np.float64
)
return weights[entity]

for variable in components:
if variable in baseline["hidden"]:
continue
values = array(reformed, variable, year).astype(np.float64)
if small(_weighted_records(values != 0, weight(variable))):
return True
balance = to_household(
reformed,
array(reformed, "gov_balance", year).astype(np.float64),
entity_of(reformed, "gov_balance"),
)
changed = np.abs(balance - baseline["balance"]) > CHANGE_GBP
if small(_weighted_records(changed, baseline["household_weight"])):
return True
for variable, before in baseline["targets"].items():
after = array(reformed, variable, year).astype(np.float64)
if small(
_weighted_records(np.abs(after - before) > CHANGE_GBP, weight(variable))
):
return True
return False
Loading
Loading