The model this example is drawn from¶
Atal, Fang, Karlsson & Ziebarth (2025, JPE 133(6), doi:10.1086/734781) study German long-term health insurance — “GLTHI”, a guaranteed-renewable contract that locks in a front-loaded premium. Their structural core is a Yaari (1965) life-cycle savings problem, solved by backward induction over annual periods from age 25 to age 94 (roughly seventy periods). Each year an agent:
draws one of seven health risk categories — a first-order Markov chain estimated from insurance-claims data — which drives both medical expenditure and survival into the next period;
receives a deterministic income (one profile per education group);
pays a health-insurance premium and consumes the residual.
The economic question is about the contract: a guaranteed-renewable GLTHI premium (which can only ratchet down as health improves) versus a sequence of short-term contracts repriced every year — and how each insures the reclassification risk of becoming uninsurably sick. The baseline uses time-separable expected utility (CARA, ; a CRRA robustness with a $10,000 consumption floor) and an annual discount factor . The headline finding is that GLTHI delivers welfare “at most 4% lower than the optimal contract” — about 96% of first-best. Epstein–Zin preferences appear only as a robustness check (sec. VI.D.1): separating risk aversion from the intertemporal elasticity of substitution moves that headline gap by at most 0.7%.
What this example keeps, and what it drops¶
This page is a stylized consumer block in that spirit — not a replication. It
keeps the part pylcm models directly: a finite-horizon savings problem with a
discrete health Markov chain, health-dependent mortality into a terminal dead
regime with a bequest, and an uninsurable per-period health cost (the toy analogue
of the paper’s medical-spending risk). It drops the equilibrium insurance layer —
the GLTHI and short-term premium schedules and the optimal contract — which is a
pricing problem around the consumer’s dynamic program, not part of it; so the
paper’s headline welfare gap between contracts is not something this example can
reproduce. It keeps a comparable annual age-25-to-85 horizon but coarsens the seven
health categories to two and replaces the calibrated income, expenditure, and
mortality processes with stylized constants and a simple age-rising hazard.
The one deliberate change from the paper’s baseline is the preference structure:
this example uses Epstein–Zin rather than expected utility, so that risk aversion
and the intertemporal elasticity of substitution are separate parameters. That
separation — the lever the paper pulls in its robustness section — is what pylcm’s
certainty_equivalent seam provides, and the final section shows why it is an
economically meaningful degree of freedom.
The recursion and how it maps onto pylcm¶
The Epstein–Zin recursion separates the elasticity of intertemporal substitution from risk aversion. With current consumption and continuation value :
where is the discount factor, with the elasticity of intertemporal substitution, and is the coefficient of relative risk aversion. When the recursion collapses to expected CRRA utility.
The mapping onto pylcm has four parts:
utilityreturns directly. The aggregator below is a weighted power mean ofutilityand the certainty equivalent, so the value function lives in (positive) consumption units — per-period utility must too.His the outer Kreps–Porteus aggregator. Pass the built-inlcm.H_epstein_zinviafunctions={"H": H_epstein_zin, ...}; it computes((1 − β) · utility^ρ + β · E_next_V^ρ)^(1/ρ), where theE_next_Vargument receives the certainty equivalent already computed by the engine. It is parametrized directly by the IES:params["alive"]["H"]["intertemporal_elasticity_of_substitution"](, with curvature computed inside). The expected-utility counterpartlcm.H_linear(utility + β · E_next_V) is the default when noHis supplied.certainty_equivalent=PowerMean()on theRegimeimplements with the power transform . Its runtime parameter lives atparams["alive"]["certainty_equivalent"]["risk_aversion"].The expectation runs jointly over the health Markov chain and the alive/dead regime transition: . The engine handles the two-level expectation automatically given the stochastic regime transition and the stochastic health transition in
state_transitions.
Pitfalls¶
Positivity. Power transforms require everywhere: with , propagates backward and corrupts the entire solution. Implicit zeros count too — a reachable target regime with no states contributes 0 to the transformed sum, which is coherent only when . Here every value is positive by construction: utility is consumption itself (bounded below by the consumption-grid floor), the bequest is strictly positive at every reachable wealth, and
next_wealthclips to the wealth grid.Scale the bequest. Because
H_epstein_zinis a weighted power mean, the alive value sits at the scale of per-period consumption. A terminal value in wealth-like units — an unscaledsqrt(wealth), say — easily exceeds it, which makes death the good branch of the certainty equivalent, and a more risk-averse agent then runs its wealth down toward the certain bequest. Price the bequest in consumption-equivalent units (herebequest_scale * sqrt(wealth)withbequest_scale = 0.3) so death stays the bad branch.risk_aversion = 1is the geometric-mean (log) limit of the power mean, ;PowerMeanhandles it directly.Solver restriction.
certainty_equivalentis supported only with theGridSearchsolver. Passingcertainty_equivalenttogether with a DC-EGM solver is rejected at model build.
Wealth profiles by risk aversion¶
Solve the annual model over the full age-25-to-85 horizon on fine wealth and consumption grids, then simulate 2,000 subjects for two Epstein–Zin parametrizations that share the intertemporal elasticity of substitution () and differ only in risk aversion ( versus ). Income (1.0) exceeds the consumption floor (0.5), so agents can save; while health is bad they pay an out-of-pocket cost of 1.5 per year, and bad health is persistent — an uninsurable expense risk in the spirit of the paper’s medical spending. Mortality is low when young and rises with age.
import jax.numpy as jnp
import numpy as np
import plotly.graph_objects as go
from lcm import PowerMean
from lcm_examples.epstein_zin import EZRegimeId, HealthStatus, get_model, get_params
n_periods = 61 # annual periods, ages 25 to 85
model_ages = np.arange(25, 25 + n_periods - 1)
# Low mortality when young, rising with age (Gompertz-like); certain death by 85.
hazard = 0.0003 * np.exp((model_ages - 25) / 11.0)
survival_probs = tuple(np.concatenate([np.clip(1.0 - hazard, 0.0, 0.9999), [0.0]]))
model = get_model(
certainty_equivalent=PowerMean(),
n_periods=n_periods,
n_wealth_points=60,
n_consumption_points=60,
)n_subjects = 2_000
labels = {
0.5: "risk tolerant: γ = 0.5, ψ = 2",
5.0: "risk averse: γ = 5, ψ = 2",
}
mean_wealth_by_age = {}
for risk_aversion in labels:
params = get_params(
risk_aversion=risk_aversion,
discount_factor=0.95,
intertemporal_elasticity_of_substitution=2.0,
survival_probs=survival_probs,
income=1.0,
health_cost=1.5,
bequest_scale=0.3,
)
result = model.simulate(
params=params,
initial_conditions={
"age": jnp.full(n_subjects, 25.0),
"wealth": jnp.tile(jnp.linspace(1.0, 10.0, 100), n_subjects // 100),
"health": jnp.full(n_subjects, HealthStatus.good, dtype=jnp.int32),
"regime_id": jnp.full(n_subjects, EZRegimeId.alive, dtype=jnp.int32),
},
period_to_regime_to_V_arr=None,
log_level="off",
seed=42,
)
df = result.to_dataframe()
alive = df[df["regime_name"] == "alive"]
mean_wealth_by_age[risk_aversion] = alive.groupby("age")["wealth"].mean()colors = {0.5: "#999999", 5.0: "#e63946"}
fig = go.Figure()
for risk_aversion, series in mean_wealth_by_age.items():
fig.add_trace(
go.Scatter(
x=series.index,
y=series.to_numpy(),
mode="lines",
name=labels[risk_aversion],
line={"color": colors[risk_aversion], "width": 2.5},
showlegend=False,
)
)
fig.add_annotation(
x=series.index[-1],
y=series.to_numpy()[-1],
text=labels[risk_aversion],
showarrow=False,
xanchor="left",
xshift=6,
font={"color": colors[risk_aversion]},
)
fig.update_layout(
title="Mean wealth among survivors by age",
xaxis_title="Age",
yaxis_title="Mean wealth",
template="simple_white",
margin={"r": 220},
)
fig.show()Both agents start from the same spread of initial wealth (mean 5.5) and draw it down over the first decade toward the level their income of 1.0 can sustain — wealth earns no return, so holding it is costly while buffers are ample. From the mid-30s on, the risk-averse agent () keeps a visibly larger buffer: its mean-wealth line sits above the risk-tolerant one for the rest of life. A persistent bad-health spell drains 1.5 per year against an income of 1.0, and the concave bequest makes dying at low wealth especially bad, so wealth is precautionary insurance. Only risk aversion differs between the two runs — the IES, and with it the willingness to move consumption across time, is identical — so the entire gap is a precautionary response to the health-cost and mortality risk.
Two caveats. First, the precautionary ranking is not automatic: it holds because wealth can effectively self-insure the bad-health spell. Make the spell expensive enough relative to income and feasible buffers and the ranking flips — the risk-averse agent stops buffering the now-uninsurable state and front-loads consumption instead. Second, each point is a mean over survivors, and survival is health-dependent; with this mortality about 88% reach age 65 and just under half survive to the mid-80s, so the oldest ages average over a healthier-than-average, mechanically wealthier, and increasingly small surviving population.
Disentangling risk aversion and the IES¶
Expected utility ties risk aversion and the intertemporal elasticity of substitution
(IES) together: under CRRA the IES is exactly , so one parameter governs
both how much an agent dislikes risk and how willingly it moves consumption across
time. Epstein–Zin breaks that link — here risk aversion lives in the
certainty equivalent and the IES lives in the aggregator H.
The paper uses exactly this separation as a robustness check (sec. VI.D.1) and finds its headline welfare gap between contracts barely moves when the two are unlinked. We cannot reproduce that gap — it belongs to the insurance layer this example drops — but we can show the force underneath it. The sweep below varies and independently and measures the welfare cost of the uninsurable health-expense risk: the fall in the certainty-equivalent lifetime value at entry (already in consumption units under Epstein–Zin) when the bad-health cost is switched from 0 to 1.5. Expected utility can only sit on the locus — the dotted anti-diagonal — so it cannot move through this plane freely.
gammas = [0.5, 1.0, 2.0, 4.0, 8.0]
ies_grid = [0.125, 0.25, 0.5, 1.0, 2.0] # log-2 spaced: EU is the exact anti-diagonal
wealth_grid = np.linspace(0.5, 12.0, 60)
entry_wealth_idx = int(np.argmin(np.abs(wealth_grid - 5.0)))
def value_at_entry(risk_aversion, ies, health_cost):
"""Certainty-equivalent lifetime value V_0 at entry, good health, mid wealth."""
solution = model.solve(
params=get_params(
risk_aversion=risk_aversion,
discount_factor=0.95,
intertemporal_elasticity_of_substitution=ies,
survival_probs=survival_probs,
income=1.0,
health_cost=health_cost,
bequest_scale=0.3,
),
log_level="off",
)
# Engine axis order is (health, wealth); good health is index 1.
return float(np.asarray(solution[0]["alive"])[1, entry_wealth_idx])
welfare_cost = np.array(
[
[
value_at_entry(gamma, psi, 0.0) - value_at_entry(gamma, psi, 1.5)
for psi in ies_grid
]
for gamma in gammas
]
)x_labels = [str(psi) for psi in ies_grid]
y_labels = [str(gamma) for gamma in gammas]
fig = go.Figure(
go.Heatmap(
x=x_labels,
y=y_labels,
z=welfare_cost,
colorscale="Reds",
colorbar={"title": "welfare cost<br>(cons. units)"},
)
)
# Expected utility locks IES = 1/gamma; on the log-2 grid this is the anti-diagonal.
fig.add_trace(
go.Scatter(
x=x_labels[::-1],
y=y_labels,
mode="lines+markers",
line={"color": "#1d3557", "width": 2, "dash": "dot"},
marker={"size": 9, "color": "#1d3557"},
name="expected utility (ψ = 1/γ)",
)
)
fig.update_layout(
title="Welfare cost of the uninsurable health-expense risk",
xaxis_title="intertemporal elasticity of substitution ψ",
yaxis_title="risk aversion γ",
template="simple_white",
legend={"yanchor": "bottom", "y": 1.02, "xanchor": "left", "x": 0},
)
fig.show()Two readings of the same surface:
Down a column (raising risk aversion at a fixed IES), the welfare cost of the risk roughly doubles — from about 0.24 to 0.49 consumption units. The cost is a risk premium, and risk aversion sets it.
Across a row (varying the IES at a fixed risk aversion), the cost moves far less — a few percent at low risk aversion, and even at the highest risk aversion well under what it shifts down the column.
The two parameters are not interchangeable: one is the price of risk, the other is
almost a bystander to it. An expected-utility model is confined to the dotted
anti-diagonal, where raising risk aversion mechanically lowers the IES — so an EU
model matching the top-left cost of 0.49 cannot say whether high risk aversion or a
low IES produced it. Holding one fixed while moving the other — what
certainty_equivalent adds — is what makes the distinction visible, and it is why
the paper could confirm its headline result survives the separation.