This tutorial shows how to build one Sick-Sicker-style model twice, as a MarkovModel cohort trace and as a MicrosimModel individual simulation, from the same transition rates and rewards, in order to cross-validate the two engines against each other and show what a microsimulation represents that a cohort model averages away. It builds on the cohort state-transition tutorial and the microsimulation engine tutorial. The full script is examples/markov_vs_microsim.py.
Specifying one model for two engines
The model is progressive, with four states, Healthy, Sick, Sicker, and Dead, over 40 annual cycles. Onset, progression, and death are competing annual hazards; each cycle’s transition probabilities come from the same hazards function for both engines, so any difference between the two engines’ results is Monte Carlo noise, not a coding difference between two separately written models. Frailty z multiplies the progression and mortality hazards, and the cohort runs at z = 1, the population average.
With a memoryless, homogeneous population, the microsimulation mean should approach the cohort trace as the population grows; if it did not, one of the two models would have a coding error. The check below runs the microsimulation at increasing population sizes and compares each to the cohort trace computed above.
rows = [dict(n=n, **run_psa(microsim(n, 0.0), draws, seed=1, sequential=True) .outcomes.summary().loc["Standard of care"]) for n in (2_000, 10_000, 40_000)]pd.DataFrame(rows).set_index("n").round(3)
cost
qaly
n
2000
80088.470
11.780
10000
78186.563
11.763
40000
78949.204
11.769
The cohort trace gives 78,614 dollars and 11.740 quality-adjusted life-years. At 40,000 individuals the microsimulation lands within half a percent of both, and the gap shrinks further as the population grows. The two engines agree because they are, mathematically, the same model.
Adding heterogeneous frailty
This section assigns each individual a frailty z, drawn from a Gamma distribution with mean 1 and variance 0.5, that multiplies the progression and mortality hazards. The mean hazard across the population is unchanged, so the cohort model, which tracks only the average person, produces the same result as before. The microsimulation does not, because it tracks each individual rather than the average.
het = run_psa(microsim(40_000, 0.5), draws, seed=1, sequential=True).outcomes.summary()het.round(3)
cost
qaly
intervention
Standard of care
80445.793
12.665
Quality-adjusted life-years rise by about 8% and cost by about 2% against the cohort trace, even though the mean rates are identical. The reason is frailty selection: individuals with higher frailty die sooner, so the survivors are increasingly drawn from the lower-frailty part of the population, and this survivor group lives longer on average than a cohort model, which tracks a single average-frailty person throughout, would predict. This gap is a property of the model, not a bug, and it is why a microsimulation is the right model type to use when risk is heterogeneous.
Representing history with duration groups
The same machinery also carries history, which the frailty example above did not need. This section makes mortality in the sick states rise 8% for every year already spent sick. A cohort model can only represent that by adding a tunnel state for each year already spent sick, since a Markov transition cannot depend on how long an individual has been in a state; the microsimulation instead tracks a duration_groups counter per individual and reads it directly in the transition function, without restructuring the state space.
The trade is plain. The cohort model is faster and exact for its assumptions. The microsimulation costs iterations but represents heterogeneity and history the cohort averages away. Neither is more correct in general; they answer under different assumptions.
Next: the discrete-event simulation engine adds a scarce, shared resource that couples entities together, a dependence neither the cohort nor the independent microsimulation carries.