Skip to content
Draft
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/base-year-uprating.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Uprating metadata can name a statutory base: with `base: {period, value}`, uprating computes each projected value as the base amount plus the increase the index implies over the base period, rounding that increase rather than the uprated amount.
73 changes: 73 additions & 0 deletions docs/usage/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,79 @@ parameter.metadata["reference"] = [*parameter.metadata["reference"], new_referen

The same holds within one tree: a dated value written in dictionary form (`value:`, with or without `metadata:`) holds its parameter's metadata values, and `propagate_parameter_metadata` gives a node's descendants the node's values.

## Uprating

A parameter's `uprating` metadata extends its value history with another parameter, the index, such as a price index. When a tax-benefit system loads its parameters, `uprate_parameters` adds a value at each instant where the index has a value after the parameter's last explicit value. Uprating never changes an explicit value, so a new explicit value always replaces a projection.

### Chained mode

By default, uprating scales the last explicit value, at instant `L`, by the index's change since then:

```none
value(t) = value(L) × index(t) / index(L)
```

```yaml
metadata:
uprating:
parameter: gov.example.price_index
rounding:
type: downwards # or nearest, or upwards
interval: 50
```

`rounding` rounds the uprated amount to a multiple of `interval`. A bare float, such as `rounding: 0.01`, rounds to the nearest multiple. Chained mode also takes `start_instant`, to project from an instant other than the last explicit value; `at_defined_interval`, to uprate on a fixed cadence; and `parameter: self`, to extend the parameter's own growth.

### Base mode

Some statutes index a fixed base amount and round the increase rather than the increased amount. 26 U.S.C. 63(c)(7)(B)(ii), for example, increases the $23,625 head-of-household standard deduction by the cost-of-living adjustment since 2024 and rounds that increase down to a multiple of $50. A `base` block opts a parameter into this rule:

```yaml
metadata:
uprating:
parameter: gov.example.price_index
base:
period: 2025
value: 23_625
rounding:
type: downwards
interval: 50
```

For each instant `t` where the index has a value after the last explicit value, and no earlier than `base.period`, base mode computes

```none
value(t) = base + round(base × (index(t) / index(base.period) − 1))
```

applying `rounding`'s type and interval to the increase. Without `rounding`, `value(t) = base × index(t) / index(base.period)`.

- `base.period` names the base in the index's own periods, since base mode reads the index there. Where the index's value at period `Y` measures calendar year `Y − 1`, as in the example above, a statute's base year `Y − 1` is `period: Y`.
- `base.value` defaults to the parameter's own value at `base.period`. When the parameter has a value there, a stated `base.value` must equal it, which catches a period one year off. State it when the parameter has no value at the base period, such as a 1997 base for a parameter whose history starts later; elsewhere, leaving it out lets the base follow a reform (see below).
- Base mode takes only `parameter`, `base` and `rounding`. It raises an error for `start_instant`, `at_defined_interval`, `parameter: self` and any other key, rather than ignore them.
- Before rounding, base mode counts an increase within 10⁻⁹ intervals of a whole number of intervals as that whole number, so floating-point error cannot drop an exact multiple to the one below. A 40% rise on $1,400 is exactly $560, which floats compute as 559.9999999999998. For amounts large next to the interval, where float error is larger, the tolerance widens to about 3.6 × 10⁻¹⁵ of the amounts, a few units in their last place.
- An index below its base-period level gives a value below the base; base mode sets no floor. An infinite base, such as a scale's `-.inf` lowest threshold, stays infinite.

### Base mode and reforms

Base mode's projections depend on the explicit values only through the base amount, not on the values just before them. That matters where reforms apply before uprating, as in a `TaxBenefitSystem` that loads its parameters and receives a `reform`: it applies the reform, then uprates. Take the head-of-household example and a reform that sets $30,000 from some date on:

- From 2026 on, the reform sets 2026 to $30,000, and the base is still $23,625, so 2027 follows the statute again. Chained mode would instead project 2027 from $30,000.
- From 2020 on, the reform also sets the base period's value. With `base.value` left out, $30,000 becomes the base and the projections index it. A stated `base.value` of $23,625 no longer matches, and uprating raises an error that says to leave it out.

A reform that should hold its value in later years can give it a range covering them, such as `2026-01-01.2100-12-31`. Country packages that apply reforms after uprating, as PolicyEngine US does, see projected values as before: a reform replaces them from its start.

### How the modes differ

Without rounding, the modes agree up to floating-point error whenever the explicit values follow the index from the base: if `value(L) = base × index(L) / index(base)`, chained mode's `value(L) × index(t) / index(L)` equals base mode's `base × index(t) / index(base)`.

With rounding, they round different quantities. Chained mode rounds the whole amount, so every value it projects is a multiple of the interval. Base mode rounds the increase, so its values are the base plus a multiple of the interval, and a base off the interval's grid stays off it. Rev. Proc. 2026-35 prints two 2027 amounts of this kind, which chained mode cannot produce:

- Section 3.15 sets the head-of-household standard deduction at $24,925: the $23,625 base plus $1,300, a multiple of $50. $24,925 is not.
- Section 3.29 sets the long-term care premium limit for ages 41 to 50 at $945: the $375 base in 26 U.S.C. 213(d)(10)(A) plus $570, an increase that section 213(d)(10)(B)(i) rounds to the nearest $10. $945 is not a multiple of $10.

Chained mode also carries rounding forward when it starts from a published, rounded value. Take a $2,000 base, an increase rounded down to $100, and an index 4.9% above its base level in year 1 and 10% above it in year 2. The year-1 amount is $2,000, since $98 rounds down to $0. Base mode gives $2,200 for year 2. Chained mode from the year-1 amount gives $2,000 × 1.10 / 1.049 = $2,097.24, which rounds down to $2,000.

### Metadata for parameters

```{eval-rst}
Expand Down
33 changes: 28 additions & 5 deletions policyengine_core/data_structures/parameter_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ class UpratingRoundingConfig(dict):
"""The interval to round to."""


class UpratingBase(dict):
"""
The statutory base amount that base-mode uprating projects from.
"""

period: Union[int, str]
"""
The period of the base amount, such as 2025 or 1997-01-01, in the uprating index's own periods. Base mode measures each increase from the index's value here.
"""

value: float
"""
The base amount. Optional: it defaults to the parameter's value at ``period``, and when the parameter has a value there, a stated ``value`` must equal it.
"""


class UpratingSchema(dict):
"""
A schema for uprating a parameter node.
Expand All @@ -35,12 +51,17 @@ class UpratingSchema(dict):

start_instant: int
"""
The instant to add uprated values after.
The instant to add uprated values after. Chained mode only.
"""

type: UpratingRoundingConfig
rounding: Union[UpratingRoundingConfig, float]
"""
The rounding configuration to use when uprating.
The rounding to apply. Chained mode rounds the uprated amount; base mode rounds the increase over the base amount. A bare number rounds to the nearest multiple of it.
"""

base: UpratingBase
"""
Opts in to base mode: each value after the last explicit one is the base amount plus the increase the index implies since the base period, with ``rounding`` applied to that increase. Base mode takes only ``parameter``, ``base`` and ``rounding``.
"""


Expand Down Expand Up @@ -68,9 +89,11 @@ class ParameterMetadata:
uprating: Union[UpratingIndex, UpratingSchema]
"""The schema for uprating this parameter.

The process for uprating parameter $X$ based on parameter $Y$ is as follows:
By default (chained mode), the process for uprating parameter $X$ based on parameter $Y$ is as follows:

1. Look at $X$ and $Y$ at the same time, and move forward in history.

2. If $Y$ increases by $z$%, then increase $X$ by $z$%.

With ``base`` (base mode), each value after the last explicit one is a fixed base amount plus the increase $Y$ implies since the base period. See "Uprating" in the parameters guide.
"""
Loading
Loading