import numpy as np
import pandas as pd
from heormodel.params import Beta, Gamma, Normal, ParameterSet
from heormodel.run import SeedManager, as_outcomes
WTP = 30_000.0 # sits between the two frontier ratios, so the decision is uncertain
N = 2_000
seed_manager = SeedManager(20260704)
params = ParameterSet(
{
"p_response": Beta.from_mean_se(0.35, 0.05),
"rr_drug": Normal(0.75, 0.08),
"c_drug": Gamma.from_mean_se(12_000, 1_500),
"c_monitoring": Gamma.from_mean_se(2_000, 400),
"u_gain": Beta.from_mean_se(0.12, 0.03),
},
correlation={("p_response", "u_gain"): 0.3},
)
draws = params.sample(N, seed=seed_manager.generator())
base_qaly = 8.0
effect_drug = base_qaly + draws["u_gain"] * draws["p_response"] / draws["rr_drug"] * 10
rows = pd.concat([
pd.DataFrame({"intervention": "Standard care", "iteration": draws.index,
"cost": 40_000.0, "qaly": base_qaly}),
pd.DataFrame({"intervention": "New drug", "iteration": draws.index,
"cost": 40_000.0 + draws["c_drug"], "qaly": effect_drug}),
pd.DataFrame({"intervention": "Drug + monitoring", "iteration": draws.index,
"cost": 40_000.0 + draws["c_drug"] + draws["c_monitoring"],
"qaly": effect_drug + 0.15 * draws["p_response"]}),
])Bring your own outputs
This tutorial shows how to take a costs-and-effects table computed outside heormodel, a spreadsheet export or a legacy simulator’s output, and run it through the complete analysis: cost-effectiveness, value of information, plots, and a reproducibility record, without building a model engine. This page walks through examples/byoo_example.py step by step. Install heormodel first: see the overview.
Simulating an external results table
Three interventions compete for a chronic disease: standard of care, a new drug, and drug plus monitoring. A real analysis would load this table from a spreadsheet or a legacy simulator’s output; here it is synthesized from sampled parameters instead, so that the value-of-information results computed later can be traced back to the parameters that drive them.
Converting the table to the standard structure
as_outcomes accepts a DataFrame or a CSV path with intervention, iteration, cost, and effect columns, and returns Outcomes, the structure every analysis function below uses. Converting once here means icer_table, the acceptability curves, the value-of-information estimators, and the plots all take the same Outcomes, regardless of where the numbers originally came from.
outcomes = as_outcomes(rows)
outcomes.summary().round(2)| cost | qaly | |
|---|---|---|
| intervention | ||
| Standard care | 40000.00 | 8.00 |
| New drug | 51985.32 | 8.57 |
| Drug + monitoring | 53981.58 | 8.62 |
Running the cost-effectiveness analysis
icer_table orders interventions by cost and reports incremental cost-effectiveness ratios along the efficiency frontier.
from heormodel.cea import ceac, ceaf, icer_table
icer_table(outcomes).round(3)| cost | effect | inc_cost | inc_effect | icer | status | |
|---|---|---|---|---|---|---|
| intervention | ||||||
| Standard care | 40000.000 | 8.000 | NaN | NaN | NaN | ND |
| New drug | 51985.317 | 8.572 | 11985.317 | 0.572 | 20970.857 | ND |
| Drug + monitoring | 53981.578 | 8.624 | 1996.261 | 0.052 | 38074.078 | ND |
The status column marks dominated (D) and extended-dominated (ED) interventions; here all three are non-dominated (ND), and the willingness-to-pay threshold of 30,000 sits between the two frontier incremental cost-effectiveness ratios, so the decision is genuinely uncertain. ceac and ceaf turn that uncertainty into acceptability curves across a grid of thresholds, reused below in the plots and the value-of-information ranking.
grid = np.linspace(0, 80_000, 51)
ceac_df = ceac(outcomes, grid)
ceaf_df = ceaf(outcomes, grid)Ranking parameters by their value of information
The expected value of perfect information (EVPI) puts a monetary value, at the willingness-to-pay threshold, on resolving every remaining source of uncertainty at once. evppi_ranking breaks that value down by parameter instead, so a research budget can target whichever one drives the decision the most; it needs the draw matrix as well as the outcomes, linked to it by the shared iteration index.
from heormodel.voi import evpi, evppi_ranking
print(f"EVPI at WTP {WTP:,.0f}: {evpi(outcomes, WTP):,.1f}")
evppi_ranking(outcomes, draws, WTP).round(1)EVPI at WTP 30,000: 551.3
u_gain 309.9
p_response 94.1
c_monitoring 23.6
c_drug 0.5
rr_drug 0.4
Name: evppi, dtype: float64
Plotting the frontier, acceptability, and tornado
The cost-effectiveness plane scatters every iteration’s incremental cost and effect against a comparator, so the spread of the cloud shows the uncertainty that the table’s point estimates alone cannot.
from heormodel.report import plot_ce_plane, plot_ceac, plot_frontier, plot_tornado, tornado_data
plot_ce_plane(outcomes, comparator="Standard care", wtp=WTP);
The acceptability curves plot the same probabilities computed above as one line per intervention against the threshold, making the crossover in the decision visible at a glance.
plot_ceac(ceac_df, ceaf_df=ceaf_df);
A tornado plot instead breaks down the uncertainty in one comparison, new drug against standard care, by parameter, ranking each by how much it moves the incremental net benefit at the threshold.
td = tornado_data(outcomes, draws, WTP, intervention="New drug", comparator="Standard care")
plot_tornado(td);
Recording the run for reproducibility
capture_run snapshots the seed, the parameter specifications, and the outcome shape into a record that serializes to JSON and renders as a run report, so a later reviewer can check what produced these numbers without rerunning the analysis.
from heormodel.report import capture_run
record = capture_run(seed=seed_manager, params=params, outcomes=outcomes,
note="Bring-your-own-outputs tutorial.")
print(record.to_markdown("heormodel example run report"))# heormodel example run report
- **Created:** 2026-07-11T20:19:47+00:00
- **Note:** Bring-your-own-outputs tutorial.
- **Root seed entropy:** 20260704
- **Iterations:** 2000
- **Interventions:** Standard care, New drug, Drug + monitoring
## Parameters
| Parameter | Distribution |
|---|---|
| p_response | `Beta(alpha=31.5, beta=58.5)` |
| rr_drug | `Normal(mean_=0.75, sd_=0.08)` |
| c_drug | `Gamma(shape=64, scale=187.5)` |
| c_monitoring | `Gamma(shape=25, scale=80)` |
| u_gain | `Beta(alpha=13.96, beta=102.373)` |
## Software versions
| Package | Version |
|---|---|
| python | 3.12.3 |
| heormodel | 0.7.1 |
| numpy | 2.5.0 |
| scipy | 1.18.0 |
| pandas | 3.0.3 |
| joblib | 1.5.3 |
| scikit-learn | 1.9.0 |
Next: the Markov cohort model builds the standard state-transition model and runs its probabilistic sensitivity analysis through the same analysis functions.