import numpy as np
import pandas as pd
from heormodel.models import MicrosimModel
from heormodel.run import run_psa
STATES = ("H", "S1", "S2", "D")
def population(rng, n):
return pd.DataFrame({"x": rng.uniform(0.95, 1.05, n)})
def transition_probabilities(p, intervention, state, attrs, rng):
n = len(state); probs = np.zeros((n, 4))
mult = 1.0 + (attrs["dur"].to_numpy() + 1.0) * p["rp_S1S2"]
r_HD = -np.log(1 - p["p_HD"])
p_S1D = 1 - np.exp(-p["rr_S1"] * r_HD * mult)
p_S2D = 1 - np.exp(-p["rr_S2"] * r_HD * mult)
h = state == 0
probs[h] = np.array([1 - p["p_HS1"] - p["p_HD"], p["p_HS1"], 0.0, p["p_HD"]])
s1 = state == 1
probs[s1, 0], probs[s1, 2], probs[s1, 3] = p["p_S1H"], p["p_S1S2"], p_S1D[s1]
probs[s1, 1] = 1 - p["p_S1H"] - p["p_S1S2"] - p_S1D[s1]
s2 = state == 2
probs[s2, 2], probs[s2, 3] = 1 - p_S2D[s2], p_S2D[s2]
probs[state == 3, 3] = 1.0
return probs
def state_rewards(p, intervention, state, attrs):
n = len(state); cost = np.zeros(n); util = np.zeros(n)
dur, x, tx = attrs["dur"].to_numpy(), attrs["x"].to_numpy(), intervention == "Treatment"
tx_cost = p["c_Trt"] if tx else 0.0
util[state == 0] = p["u_H"]; cost[state == 0] = p["c_H"]
s1 = state == 1
cost[s1] = p["c_S1"] + tx_cost
util[s1] = x[s1] * (p["u_Trt"] - dur[s1] * p["ru_S1S2"]) if tx else p["u_S1"]
s2 = state == 2
cost[s2] = p["c_S2"] + tx_cost; util[s2] = p["u_S2"]
return cost, utilMicrosimulation replication
This tutorial shows how to reproduce the Sick-Sicker microsimulation of Krijkamp and others (2018) with MicrosimModel, building on the microsimulation engine tutorial. Full script: examples/mdm_microsim.py.
Modeling duration-dependent risk and a treatment effect modifier
Two features rule out a cohort average: mortality rises with the number of consecutive years spent in the Sick and Sicker states, and a per-individual effect modifier scales the treatment utility. The duration clock keeps running across the Sick to Sicker progression, so the engine tracks it with duration_groups, a counter over a set of states that time_in_state (one exact state) cannot express. The counter is 0 on the first sick cycle; mortality reads it as dur + 1 and utility reads it directly.
Running the base case and comparing interventions
The base-case parameters drive both arms through common random numbers, with 50,000 individuals rather than the source’s 100,000, which trades some Monte Carlo precision for a faster run.
from heormodel.cea import icer_table
base = dict(p_HD=0.005, p_HS1=0.15, p_S1H=0.5, p_S1S2=0.105, rr_S1=3.0, rr_S2=10.0,
rp_S1S2=0.2, c_H=2000.0, c_S1=4000.0, c_S2=15000.0, c_Trt=12000.0,
u_H=1.0, u_S1=0.75, u_S2=0.5, u_Trt=0.95, ru_S1S2=0.03)
engine = MicrosimModel.discrete(
states=STATES, transition_probabilities=transition_probabilities,
state_rewards=state_rewards, population=population,
n_individuals=50_000,
interventions=["No Treatment", "Treatment"],
n_cycles=30, discount_rate=0.03,
cycle_correction="none", duration_groups={"dur": ("S1", "S2")})
outcomes = run_psa(
engine, pd.DataFrame([base], index=pd.RangeIndex(1, name="iteration")),
seed=1, sequential=True,
).outcomes
icer_table(outcomes).round(2)| cost | effect | inc_cost | inc_effect | icer | status | |
|---|---|---|---|---|---|---|
| intervention | ||||||
| No Treatment | 62691.63 | 15.34 | NaN | NaN | NaN | ND |
| Treatment | 117468.28 | 15.85 | 54776.65 | 0.51 | 107824.62 | ND |
The published Table 3 reports no-treatment 62,667 dollars and 15.28 quality-adjusted life-years (QALYs), treatment 117,455 dollars and 15.79 QALYs, and an incremental cost-effectiveness ratio of 107,986 dollars per QALY. The sampler differs from the source, so the totals agree within Monte Carlo error rather than to the dollar and converge as the population grows.
Reference: Krijkamp EM, Alarid-Escudero F, Enns EA, Jalal HJ, Hunink MGM, Pechlivanoglou P. Microsimulation modeling for health decision sciences using R: a tutorial. Medical Decision Making. 2018;38(3):400-422.