Skip to content

replacement_rate_adjust is ignored by every pension system except US-Style Social Security #1200

Description

@marcelolafleur

replacement_rate_adjust is a (T+S, J) multiplier on the pension replacement rate — the natural way to model a country that automatically cuts benefits as it ages.

It only works under one of the four pension systems. Under the other three it is accepted, stored, and ignored.

The test. Same parameter, same call, two systems:

out = pensions.pension_amount(0.04, 1.0, n, 1.0, 0.1, -1, 0, False, "SS", e, 1e5, p)
US-Style Social Security   adj=1.0 -> 3.60000   adj=0.5 -> 1.80000   scales exactly
Defined Benefits           adj=1.0 -> 0.92659   adj=0.5 -> 0.92659   no effect

Why. In ogcore/pensions.py the parameter is read in six places, all inside SS_amount() (lines 159–199), which serves only "US-Style Social Security". DB_amount() (line 204), NDC_amount() (332) and PS_amount() (429) contain no reference to it.

Why it matters. Nothing warns. A calibration that sets it under Defined Benefits looks calibrated and is not — the solved steady state is byte-identical to one that never set it (pension/Y = 0.093040, theta = 0.0038901, both ways). This is the same trap as alpha_db defaulting to 0.0, where switching to Defined Benefits without setting it pays zero pensions. A parameter that silently does nothing is worse than a missing one, because it produces a confident wrong answer.

A concrete case. Japan's pension system has a macroeconomic slide that automatically reduces benefits to stay solvent — the MHLW 2024 actuarial review projects the replacement rate falling from 61.2% in FY2024 to 50.4% in FY2057. Without a working adjustment, the DB block pays a fixed replacement rate, so the pension bill rises mechanically with the old-age ratio: 12.6% of GDP against an actual 9.3% today. The calibration then has to choose between matching today's spending and matching the long run, when Japan's actual institution does both. Any country with a legislated benefit glide or a notional-account balancing mechanism hits this.

Suggested fix. Move the adjustment out of SS_amount and into pension_amount, which already receives t and method. Every system then honours it, and US-Style results are unchanged because the steady state keeps taking the terminal value:

def pension_amount(r, w, n, Y, theta, t, j, shift, method, e, factor, p):
    if p.pension_system == "US-Style Social Security":
        pension = SS_amount(...)      # with its private copy removed
    elif ...

    adj = p.replacement_rate_adjust
    idx = -1 if method == "SS" else t
    scale = adj[idx, :] if j is None else adj[idx, j]
    return pension * scale

happy to open a PR.

cc @jdebacker @rickecon

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