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
12 changes: 10 additions & 2 deletions src/policyengine/tax_benefit_models/uk/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
TaxBenefitModelVersion,
Variable,
)
from policyengine.utils.parameter_labels import (
build_scale_lookup,
generate_label_for_parameter,
)

from .datasets import PolicyEngineUKDataset, UKYearData

Expand Down Expand Up @@ -146,17 +150,21 @@ def __init__(self, **kwargs: dict):

from policyengine_core.parameters import Parameter as CoreParameter

scale_lookup = build_scale_lookup(system)

for param_node in system.parameters.get_descendants():
if isinstance(param_node, CoreParameter):
parameter = Parameter(
id=self.id + "-" + param_node.name,
name=param_node.name,
label=param_node.metadata.get("label", param_node.name),
label=generate_label_for_parameter(
param_node, system, scale_lookup
),
tax_benefit_model_version=self,
description=param_node.description,
data_type=type(param_node(2025)),
unit=param_node.metadata.get("unit"),
_core_param=param_node, # Store for lazy value loading
_core_param=param_node,
)
self.add_parameter(parameter)

Expand Down
12 changes: 10 additions & 2 deletions src/policyengine/tax_benefit_models/us/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
TaxBenefitModelVersion,
Variable,
)
from policyengine.utils.parameter_labels import (
build_scale_lookup,
generate_label_for_parameter,
)

from .datasets import PolicyEngineUSDataset, USYearData

Expand Down Expand Up @@ -134,17 +138,21 @@ def __init__(self, **kwargs: dict):

from policyengine_core.parameters import Parameter as CoreParameter

scale_lookup = build_scale_lookup(system)

for param_node in system.parameters.get_descendants():
if isinstance(param_node, CoreParameter):
parameter = Parameter(
id=self.id + "-" + param_node.name,
name=param_node.name,
label=param_node.metadata.get("label"),
label=generate_label_for_parameter(
param_node, system, scale_lookup
),
tax_benefit_model_version=self,
description=param_node.description,
data_type=type(param_node(2025)),
unit=param_node.metadata.get("unit"),
_core_param=param_node, # Store for lazy value loading
_core_param=param_node,
)
self.add_parameter(parameter)

Expand Down
4 changes: 4 additions & 0 deletions src/policyengine/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from .dates import parse_safe_date as parse_safe_date
from .parameter_labels import build_scale_lookup as build_scale_lookup
from .parameter_labels import (
generate_label_for_parameter as generate_label_for_parameter,
)
from .plotting import COLORS as COLORS
from .plotting import format_fig as format_fig
107 changes: 107 additions & 0 deletions src/policyengine/utils/parameter_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Utilities for generating human-readable labels for tax-benefit parameters."""

import re


def generate_label_for_parameter(param_node, system, scale_lookup):
"""
Generate a label for a parameter that doesn't have one.

For breakdown parameters: Uses parent label + enum value
For bracket parameters: Uses scale label + bracket info

Args:
param_node: The CoreParameter object
system: The tax-benefit system (has variables and parameters)
scale_lookup: Dict mapping scale names to ParameterScale objects

Returns:
str or None: Generated label, or None if cannot generate
"""
if param_node.metadata.get("label"):
return param_node.metadata.get("label")

param_name = param_node.name

if "[" in param_name:
return _generate_bracket_label(param_name, scale_lookup)

if param_node.parent and param_node.parent.metadata.get("breakdown"):
return _generate_breakdown_label(param_node, system)

return None


def _generate_breakdown_label(param_node, system):
"""Generate label for a breakdown parameter using enum values."""
parent = param_node.parent
parent_label = parent.metadata.get("label")
breakdown_vars = parent.metadata.get("breakdown", [])

if not parent_label:
return None

child_key = param_node.name.split(".")[-1]

for var_name in breakdown_vars:
var = system.variables.get(var_name)
if var and hasattr(var, "possible_values") and var.possible_values:
enum_class = var.possible_values
try:
enum_value = enum_class[child_key].value
return f"{parent_label} ({enum_value})"
except (KeyError, AttributeError):
continue

return f"{parent_label} ({child_key})"


def _generate_bracket_label(param_name, scale_lookup):
"""Generate label for a bracket parameter."""
match = re.match(r"^(.+)\[(\d+)\]\.(\w+)$", param_name)
if not match:
return None

scale_name = match.group(1)
bracket_index = int(match.group(2))
field_name = match.group(3)

scale = scale_lookup.get(scale_name)
if not scale:
return None

scale_label = scale.metadata.get("label")
scale_type = scale.metadata.get("type", "")

if not scale_label:
return None

bracket_num = bracket_index + 1

if scale_type in ("marginal_rate", "marginal_amount"):
bracket_desc = f"bracket {bracket_num}"
elif scale_type == "single_amount":
bracket_desc = f"tier {bracket_num}"
else:
bracket_desc = f"bracket {bracket_num}"

return f"{scale_label} ({bracket_desc} {field_name})"


def build_scale_lookup(system):
"""
Build a lookup dict mapping scale names to ParameterScale objects.

Args:
system: The tax-benefit system

Returns:
dict: Mapping of scale name -> ParameterScale object
"""
from policyengine_core.parameters import ParameterScale

return {
p.name: p
for p in system.parameters.get_descendants()
if isinstance(p, ParameterScale)
}
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""PolicyEngine test suite."""
1 change: 1 addition & 0 deletions tests/fixtures/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Test fixtures for PolicyEngine tests."""
159 changes: 159 additions & 0 deletions tests/fixtures/parameter_labels_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Fixtures for parameter_labels utility tests."""

from enum import Enum
from typing import Any
from unittest.mock import MagicMock


class MockFilingStatus(Enum):
"""Mock filing status enum for testing breakdown labels."""

SINGLE = "Single"
JOINT = "Joint"
HEAD_OF_HOUSEHOLD = "Head of household"


class MockStateCode(Enum):
"""Mock state code enum where key equals value."""

CA = "CA"
TX = "TX"
NY = "NY"


def create_mock_parameter(
name: str,
label: str | None = None,
parent: Any = None,
) -> MagicMock:
"""Create a mock CoreParameter object."""
param = MagicMock()
param.name = name
param.metadata = {"label": label} if label else {}
param.parent = parent
return param


def create_mock_parent_node(
name: str,
label: str | None = None,
breakdown: list[str] | None = None,
) -> MagicMock:
"""Create a mock parent ParameterNode with optional breakdown metadata."""
parent = MagicMock()
parent.name = name
parent.metadata = {}
if label:
parent.metadata["label"] = label
if breakdown:
parent.metadata["breakdown"] = breakdown
return parent


def create_mock_scale(
name: str,
label: str | None = None,
scale_type: str | None = None,
) -> MagicMock:
"""Create a mock ParameterScale object."""
scale = MagicMock()
scale.name = name
scale.metadata = {}
if label:
scale.metadata["label"] = label
if scale_type:
scale.metadata["type"] = scale_type
return scale


def create_mock_variable(
name: str,
possible_values: type[Enum] | None = None,
) -> MagicMock:
"""Create a mock Variable object with optional enum values."""
var = MagicMock()
var.name = name
if possible_values:
var.possible_values = possible_values
else:
var.possible_values = None
return var


def create_mock_system(
variables: dict[str, MagicMock] | None = None,
scales: list[MagicMock] | None = None,
) -> MagicMock:
"""Create a mock tax-benefit system."""
system = MagicMock()
system.variables = variables or {}

descendants = list(scales) if scales else []
system.parameters.get_descendants.return_value = descendants

return system


# Pre-built fixtures for common test scenarios

PARAM_WITH_EXPLICIT_LABEL = create_mock_parameter(
name="gov.tax.rate",
label="Tax rate",
)

PARAM_WITHOUT_LABEL_NO_PARENT = create_mock_parameter(
name="gov.tax.rate",
label=None,
parent=None,
)

PARENT_WITH_BREAKDOWN_AND_LABEL = create_mock_parent_node(
name="gov.exemptions.personal",
label="Personal exemption amount",
breakdown=["filing_status"],
)

PARENT_WITH_BREAKDOWN_NO_LABEL = create_mock_parent_node(
name="gov.exemptions.personal",
label=None,
breakdown=["filing_status"],
)

PARENT_WITHOUT_BREAKDOWN = create_mock_parent_node(
name="gov.exemptions.personal",
label="Personal exemption amount",
breakdown=None,
)

SCALE_WITH_LABEL_MARGINAL = create_mock_scale(
name="gov.tax.rates",
label="Income tax rate",
scale_type="marginal_rate",
)

SCALE_WITH_LABEL_SINGLE_AMOUNT = create_mock_scale(
name="gov.tax.amounts",
label="Tax amount",
scale_type="single_amount",
)

SCALE_WITHOUT_LABEL = create_mock_scale(
name="gov.tax.rates",
label=None,
scale_type="marginal_rate",
)

VARIABLE_WITH_FILING_STATUS_ENUM = create_mock_variable(
name="filing_status",
possible_values=MockFilingStatus,
)

VARIABLE_WITH_STATE_CODE_ENUM = create_mock_variable(
name="state_code",
possible_values=MockStateCode,
)

VARIABLE_WITHOUT_ENUM = create_mock_variable(
name="age",
possible_values=None,
)
1 change: 0 additions & 1 deletion tests/test_household_impact.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for calculate_household_impact functions."""


from policyengine.tax_benefit_models.uk import (
UKHouseholdInput,
UKHouseholdOutput,
Expand Down
Loading