PyHEOR
Python tools for health economic modeling and cost-effectiveness analysis.
Example results
Figures generated by the synthetic examples in this repository. Click an image to view it at full size.
Reproduce these figures with the PSM oncology and multi-strategy comparison examples. CEAC/CEAF show labeled display smoothing; analysis and exports retain raw probabilities.
Install
pip install pyheor
Requires Python 3.9+, NumPy, SciPy, pandas, matplotlib and openpyxl.
Models
| Model | Time | Rewards |
|---|---|---|
MarkovModel |
Explicit fixed cycles | Cohort state and transition rewards |
PSMModel |
Explicit fixed cycles | Survival-based occupancy, optional Terminal bookkeeping |
MicroSimModel |
Explicit fixed cycles | Individual state and transition rewards, heterogeneity and common random numbers |
DESModel |
Continuous, declared time_unit |
State reward rates integrated over time; event rewards at actual times |
Base-case analysis, sensitivity analysis, strategy comparisons, CE planes, CEACs, traces and report exports are available. DES retains base-case and PSA support.
Quick start
State costs and QALYs are per cycle. Discount rates are effective rates per cycle, and the first cycle is undiscounted. Utility weights require an explicit conversion to QALYs.
import pyheor as ph
cycle = ph.Cycle(1, "month")
model = ph.MarkovModel(
states=["Alive", "Dead"], strategies=["SOC", "TRT"],
n_cycles=120, cycle=cycle, method="life-table",
dr_cost=ph.rescale_discount_rate(.03, ph.Cycle(1, "year"), cycle),
dr_qaly=ph.rescale_discount_rate(.03, ph.Cycle(1, "year"), cycle),
)
model.add_param("u_alive", base=.8, dist=ph.Beta(mean=.8, sd=.05))
model.set_transitions("SOC", [[.98, .02], [0, 1]])
model.set_transitions("TRT", [[.985, .015], [0, 1]])
model.set_state_cost("care", {
"SOC": {"Alive": 1000}, "TRT": {"Alive": 1500},
})
model.set_state_qaly("health", {
"Alive": lambda p, k: ph.qaly(p["u_alive"], cycle),
})
model.set_starting_cost("test", 200)
model.set_state_cost("loading", {"Alive": 500}, cycles=1)
model.set_transition_cost("terminal", "Alive", "Dead", 10000)
base = model.run_base_case()
print(base.summary())
print(base.icer())
psa = model.run_psa(n_sim=100, seed=42)
Reward APIs
Costs and QALYs have parallel state, starting, entry, transition and custom interfaces. State rewards accept cycles=1 or a list of cycle numbers. Callbacks receive parameters and optionally a 1-based cycle index and patient attributes. Starting rewards are unweighted time-zero amounts; first-cycle state rewards are weighted by occupancy.
method is beginning, end, or life-table (default). State counts and event flows follow heemod conventions. PSM cannot identify pairwise transition flows, so entry/transition reward registration is unavailable there; use an explicit Terminal state for the documented end-of-life accounting rule.
PSM PSA resamples draws when ordered survival endpoints cross within a strategy, and warns with the rejected-draw count. n_sim counts valid simulations; max_attempts defaults to 10 * n_sim. Base-case runs remain strict. Resampling conditions the parameter distribution on valid curve ordering.
Practical tools
Use these tools through import pyheor as ph. Model engines require explicit units; the helpers perform the conversions you request.
| Tool | Purpose and input convention |
|---|---|
Cycle(length, unit) |
Define a period; .years, .in_unit(unit) and .time(k, unit=...) convert durations and cycle-boundary times |
qaly(utility, duration, unit=None) |
Utility × duration in years; accepts a Cycle or numeric duration with an explicit unit, utility arrays and decrements |
rescale_discount_rate(rate, from_period, to_period) |
Effective-rate conversion: (1 + rate) ** (target duration / source duration) - 1; use two Cycle objects or two numeric periods in the same unit |
rescale_survival(curve, from_unit=..., to_period=...) |
Convert fitted survival time to model periods, including hazards and quantiles |
from_flexsurv(distribution, **parameters) |
Construct a survival distribution from natural-scale R/flexsurv parameters; does not accept optimizer coefficients or convert time units |
ScaledSurvival(curve, factor) |
Direct time scaling: S_new(t) = S_old(t * factor); prefer rescale_survival() when units are known |
ProportionalHazards(curve, hr) |
Apply S_new(t) = S_old(t) ** hr; HR does not change time units |
AcceleratedFailureTime(curve, af) |
Apply S_new(t) = S_old(t / af); af=1.2 extends survival times by 20% |
Beta, Gamma, LogNormal with mean/sd |
Derive sampling parameters from natural-scale moments; LogNormal also accepts meanlog/sdlog |
C |
Fill a transition row's remaining probability; at most one per row, e.g. [ph.C, .02] gives C=.98 |
import pyheor as ph
month = ph.Cycle(1, "month")
year = ph.Cycle(1, "year")
monthly_qaly = ph.qaly(.8, month) # 0.066667 QALY
monthly_cost = 12000 * month.years # Annual cost → monthly: 1000
monthly_dr = ph.rescale_discount_rate(.03, year, month)
three_month_qaly = ph.qaly(.8, 3, unit="month") # 0.2 QALY
month_boundaries = month.time([0, 1, 12], unit="year")
# Fitted in years; after conversion t=1 means one month.
curve = ph.from_flexsurv("weibull", shape=1.3, scale=1.5)
monthly_curve = ph.rescale_survival(
curve, from_unit="year", to_period=month,
)
treated_curve = ph.ProportionalHazards(monthly_curve, hr=.75)
Supported from_flexsurv() inputs:
| Distribution | Parameters |
|---|---|
exp |
rate |
weibull, weibullPH, llogis |
shape, scale |
lnorm |
meanlog, sdlog |
gompertz |
shape, rate |
gengamma |
mu, sigma, Q |
gengamma.orig |
shape, scale, k |
GeneralizedGamma (including from_flexsurv("gengamma", ...)) uses the log-normal limit with meanlog=mu and sdlog=sigma when abs(Q) < 1e-5, to avoid numerical precision loss. This is exact at Q=0 and an approximation for nonzero Q. Excel export uses the same threshold; its positive-Q Gamma upper tails use 1-CDF, which can round very small probabilities to zero.
Parameters follow the respective R distribution definitions; names are not interchangeable across distributions. In particular, weibullPH uses a PH coefficient named scale, which the adapter converts to PyHEOR's Weibull scale.
Duration conventions are 12 months, 52 weeks or 365 days per year, rather than calendar date arithmetic. Multiply annual state cost rates by cycle years; enter one-time costs at their occurrence amounts. Put parameter-dependent QALY, discount-rate and survival conversions inside PSA/OWSA callbacks so they are recomputed for each draw.
Plots
| Plot | Purpose | Entry point |
|---|---|---|
| State traces | State occupancy over time; Markov also supports stacked areas | Markov, PSM, MicroSim: base.plot_trace() |
| Survival curves | Compare survival across strategies | PSM, MicroSim, DES: base.plot_survival() |
| Partitioned survival area | PFS, progressed and dead occupancy | PSM: base.plot_state_area() |
| Individual outcome histograms | Cost, QALY or life-year distributions | MicroSim, DES: base.plot_outcomes_histogram() |
| Model / transition diagrams | State structure and transitions | Markov: base.plot_model_diagram(), base.plot_transition_diagram() |
| Tornado | Rank parameter impacts | owsa.plot_tornado() |
| PSA cost-effectiveness scatter | Incremental costs and QALYs | psa.plot_scatter() |
| CEAC | Cost-effectiveness probability by strategy | psa.plot_ceac() |
| PSA convergence | Inspect simulation stability | Markov, PSM: psa.plot_convergence() |
| Efficiency frontier / NMB curves | Compare strategies and WTP thresholds | cea.plot_frontier(), cea.plot_nmb_curve() |
| CEAF / EVPI curves | Recommendation uncertainty and value of perfect information | PSA-backed cea.plot_ceaf(), cea.plot_evpi() |
Plots return Matplotlib Figure objects for customization and export:
fig = psa.plot_ceac(wtp_range=(0, 100000))
fig.savefig("ceac.png", dpi=150, bbox_inches="tight")
cea = ph.CEAnalysis.from_psa(psa)
cea.plot_ceaf(wtp_range=(0, 100000))
Display smoothing does not change analysis or exported values; empirical survival curves retain steps.
Results and export
base.summary() and base.icer() report totals and incremental analysis. reward_components provides cost/QALY components; cycle_rewards and state_occupancy provide cycle detail. DES also provides event logs and time in state; metadata records units and calculation conventions.
Use ph.export_to_excel(base, "results.xlsx") for result tables, ph.export_excel_model(base, "model.xlsx") for Markov/PSM formula workbooks, and ph.generate_report(model, "report.md") for an analysis report.
Examples
Runnable examples cover Markov, PSM, MicroSim and strategy comparisons. See CHANGELOG for version history.
Development
See development and versioning rules.
pip install -e '.[dev]'
pytest
License: AGPL-3.0-or-later.
Metadata
Release files for pyheor 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pyheor-0.4.1.tar.gz | 152.4 kB | With provenance | Details for pyheor-0.4.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pyheor-0.4.1-py3-none-any.whl | Python 3 | none | any | Details for pyheor-0.4.1-py3-none-any.whl |
Total release size: 268.1 kB
Release files / pyheor-0.4.1.tar.gz
| Download URL | pyheor-0.4.1.tar.gz |
|---|---|
| Size | 152.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e816fadbd350b69994685c0eef62e7d60e4066fcea45b9da96400d5c10af42e4
|
|
BLAKE2b-256 checksum How to use checksums |
eb1feae0df5c0787edeb6b0cfaad354f9f80088b2ac8b1d35f58f7a519e745a0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / pyheor-0.4.1-py3-none-any.whl
| Download URL | pyheor-0.4.1-py3-none-any.whl |
|---|---|
| Size | 115.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
554fb1a75d503776f5506a5faf8508ff84dbcaeb1d08f36486161d8fbaee8af5
|
|
BLAKE2b-256 checksum How to use checksums |
e55dad63f0a6363ed015f7a2ec72e4548914c0347c1c7cac4acfd0aed0aa006b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logSecurity for pyheor 0.4.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for pyheor-0.4.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.