Skip to content

Intra-decile income_change formula doubles all percentage changes #3282

@PavelMakarchuk

Description

@PavelMakarchuk

Bug

The intra_decile_impact and intra_wealth_decile_impact functions in policyengine_api/endpoints/economy/compare.py (lines 324-331, 386-393) double all household percentage income changes due to a variable name error.

The code (line 326-331)

absolute_change = (reform_income - baseline_income).values
capped_baseline_income = np.maximum(baseline_income.values, 1)
capped_reform_income = (
    np.maximum(reform_income.values, 1) + absolute_change      # <-- bug: reform_income should be baseline_income
)
income_change = (
    capped_reform_income - capped_baseline_income
) / capped_baseline_income

Why it's wrong

For the common case (both baseline and reform income >= 1):

absolute_change = R - B
capped_baseline = B
capped_reform   = R + (R - B) = 2R - B

income_change = (2R - B - B) / B = 2(R - B) / B

That's 2x the actual percentage change. A household going from $50,000 to $52,500 (5% gain) registers as 10%.

The fix

Line 327 should use baseline_income.values instead of reform_income.values:

capped_reform_income = (
    np.maximum(baseline_income.values, 1) + absolute_change
)

This gives (R - B) / max(B, 1) — the standard percentage change with a floor of 1 to avoid division by zero. Same fix needed on line 389 for intra_wealth_decile_impact.

Or more simply, replace lines 324-331 with:

absolute_change = (reform_income - baseline_income).values
capped_baseline_income = np.maximum(baseline_income.values, 1)
income_change = absolute_change / capped_baseline_income

Origin

Introduced in commit 20292bf7 (Dec 28, 2022, PR #38, branch intra-decile-fix). The commit was titled "Fix bug in intra-decile chart" and was addressing negative income edge cases. The original formula np.maximum(reform_income, 1) / np.maximum(baseline_income, 1) - 1 was correct for the common case but mishandled households where both incomes were below 1. The rewrite accidentally used reform_income where it meant baseline_income.

Impact

  • All intra-decile winners/losers charts on policyengine.org overstate the proportion of people with large gains or losses (>5%) and understate small changes (<5%) and "No change"
  • Affects both income decile and wealth decile breakdowns
  • The same formula was copied into PolicyEngine/state-legislative-tracker (scripts/compute_impacts.py)
  • Not visually obvious because proportions still sum to 100% — reforms just appear more dramatic than they are

Note for v2 API

This calculation has not yet been ported to policyengine-api-v2. When porting, use the corrected formula.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions