Estimation Methods
ModelBuilder.estimation() and the control-stream runner both dispatch into the
native estimator router in openpkpd.estimation.
For the current support posture across all estimators, see
validation_matrix.md.
Choosing a method
Method |
|
When to use |
|---|---|---|
First Order |
|
fast screening, simple models, reduced mixed-endpoint sanity checks |
FOCE |
|
default choice for many PK models |
FOCEI |
|
default likelihood workhorse for proportional/combined error models |
Laplacian |
|
non-Gaussian likelihood approximations and higher-order conditional fits |
SAEM |
|
nonlinear or stochastic-estimation workflows with current secondary validation support |
IMP |
|
importance sampling likelihood refinement where the direct path is stable |
IMPMAP |
|
preferred MAP-style IMP workflow on basin-sensitive models |
Bayesian |
|
posterior summaries or MCMC, with backend-specific support boundaries |
Nonparametric |
|
NPML/NPEM-style support-point estimation on selectively benchmarked workflows |
FO — First Order
Linearises the model around ETA = 0. It is fast, but least accurate when the
model is strongly nonlinear or IIV is large.
.estimation(method="FO", maxeval=500)
FOCE and FOCEI
FOCE linearises around each subject’s conditional mode (their EBE).
.estimation(method="FOCE", maxeval=9999)
FOCEI adds ETA–EPS interaction and is usually the right choice for proportional or combined error models.
.estimation(method="FOCEI", maxeval=9999)
# or:
.estimation(method="FOCE", interaction=True, maxeval=9999)
FOCE/FOCEI also accept optimizer controls that are useful on hard likelihood surfaces:
Argument |
Meaning |
|---|---|
|
number of perturbed restarts; best OFV retained |
|
outer optimizer gradient tolerance |
|
primary outer optimizer, e.g. |
|
optional follow-up polish optimizer, e.g. |
|
evaluation budget for the fallback optimizer |
|
keep the best point visited even if the terminal iterate is worse |
|
rerun FOCEI from structured alternate starts after abnormal termination |
|
OMEGA scaling factors used for structured retries |
Example:
.estimation(
method="FOCEI",
maxeval=200,
n_starts=3,
outer_optimizer="L-BFGS-B",
outer_fallback_optimizer="Powell",
outer_fallback_maxeval=40,
retain_best_iterate=True,
retry_on_abnormal=True,
retry_omega_scales=(0.5, 0.25, 0.1),
)
Laplacian
Laplacian extends FOCE with a Hessian correction term and is the main native approximation for more non-Gaussian observation models.
.estimation(method="LAPLACIAN", maxeval=9999)
SAEM
SAEM is implemented as a stochastic approximation EM workflow using a single-chain Metropolis-Hastings style inner sampler.
.estimation(method="SAEM")
SAEM runs in two phases: a stochastic exploration phase and a smoothing phase.
The number of iterations in each phase is controlled via SAEMMethod directly:
from openpkpd.estimation.saem import SAEMMethod
saem = SAEMMethod(n_iter_phase1=200, n_iter_phase2=100, n_workers=4)
result = saem.estimate(pop_model, params)
Note:
maxevalandn_parallelare not accepted bySAEMMethod. Usen_iter_phase1/n_iter_phase2to control iteration counts andn_workersfor thread-level parallelism in the E-step.
It is useful for harder models, but the current implementation is still less mature than specialized SAEM toolchains. Treat SAEM as a secondary but real production surface: it has external anchors and regression coverage, but not yet FOCE-level empirical breadth.
IMP and IMPMAP
IMP refines the marginal likelihood through importance sampling.
.estimation(method="IMP", maxeval=9999)
IMPMAP runs the same IMP outer objective, but first uses a short FOCEI warm
start to seed the population parameters into a better basin before the IMP
optimization:
.estimation(method="IMPMAP", maxeval=9999)
In practice, prefer IMPMAP over raw IMP when the model is basin-sensitive
or when a direct IMP run tends to stick near the initial THETA values.
Current support posture:
IMPandIMPMAPare secondary validated methodsIMPMAPis the recommended MAP-style path on warfarin PK and similar basin-sensitive workflowsbenchmark budget matters materially; short IMP runs should not be treated as equivalent to full FOCEI-grade validation
Bayesian estimation
BAYES uses the best available backend in this order:
PyMC if
openpkpd[bayes]is installedbuilt-in pure-NumPy NUTS otherwise
.estimation(method="BAYES", n_samples=1000, n_chains=2, tune=500)
You can also request a backend explicitly:
.estimation(method="BAYES", backend="pymc")
.estimation(method="BAYES", backend="nuts")
.estimation(method="BAYES", backend="laplace")
Passing any other backend name now raises a hard error instead of silently falling back to a different path.
Backend guidance:
Backend |
Current role |
Notes |
|---|---|---|
|
best-supported full MCMC backend |
strongest diagnostics and most complete posterior workflow |
|
built-in second-tier MCMC backend |
multi-chain diagnostics are available through |
|
fast approximation fallback |
MAP + Hessian-based Gaussian posterior approximation |
Current support posture:
backend="pymc"is the strongest current Bayesian path when full MCMC credibility mattersbackend="laplace"is a secondary validated weak-prior summary pathbackend="nuts"is an experimental / second-tier native backend today
Important current limitations:
the built-in NUTS backend currently samples THETA only;
OMEGAandSIGMAremain fixed at their starting valuesthe built-in NUTS path now uses the cached symbolic analytical theta-gradient path on the supported analytical PK subset, but still falls back outside that envelope and can be slow on larger ODE-heavy models
NUTS runs now expose sampler and posterior-evaluation diagnostics in
result.diagnostics["nuts"], including log-probability call counts, exact-cache hit/miss counts, warm-start hit counts, FOCE inner/outer call counts, step-size summaries, acceptance statistics, and tree-depth summarieson a bounded synthetic 6-subject oral-PK probe (
n_samples=12,tune=8,n_chains=2), the compiled symbolic path now takes about8.5 s, but the empirical theophylline benchmark still needs a larger budget (24/16) and about28 sto bring maxR-hatdown to roughly1.19, so it should still be treated as a dependency-free second-tier path rather than the default empirical benchmark routethe standalone low-level helper
nuts_estimate()is single-chain only and therefore does not return meaningfulR-hat; use.estimation(method="BAYES", backend="nuts", n_chains>=2)when you want multi-chain diagnosticsLaplace is useful for fast Bayesian summaries, but should not be described as equivalent to full MCMC on difficult posterior geometries
IMPresults expose optimizer-stop and final effective-sample-size details inresult.diagnostics, which is the intended surface for telling apart iteration-budget exhaustion, plateauing, and poor importance-sampling coverage
Nonparametric estimation
OpenPKPD also exposes a nonparametric support-point estimator.
.estimation(method="NONPARAMETRIC", base_method="FOCE", max_iter=100)
This runs a base parametric fit first, then optimizes support-point weights.
Current support note:
the operationally strongest current path is
base_method="FOCEI"on population PK models with a modest number of ETAsthe repository now includes an empirical phenobarbital benchmark against Pharmpy’s bundled
phenodataset and a runnable example inexamples/32_nonparametric_support_points.pythe repository also includes an oral theophylline empirical benchmark so the path is no longer anchored to only one dataset
external validation is still narrower than the FO/FOCEI surface, so treat nonparametric estimation as a secondary but still selectively benchmarked path
Common options
Common arguments are passed through to the selected estimator:
Argument |
Typical use |
|---|---|
|
optimizer / outer-loop budget |
|
FOCEI-style ETA–EPS interaction |
|
FOCE/FOCEI multi-start and gradient controls |
|
FOCE/FOCEI outer-optimizer selection and polish |
|
FOCEI robustness controls for unstable/non-convex fits |
|
Bayesian configuration |
|
nonparametric configuration |
Result objects
Most methods return an EstimationResult:
result.ofv
result.theta_final
result.omega_final
result.sigma_final
result.converged
result.post_hoc_etas
result.ofv_history
result.warnings
result.compute_shrinkage()
print(result.summary())
ETA shrinkage and de-shrinkage
After calling compute_shrinkage(), per-ETA shrinkage fractions are stored in
result.eta_shrinkage:
result.compute_shrinkage()
# result.eta_shrinkage: array of shrinkage fractions, e.g. [0.62]
# Shrinkage > 30% triggers a warning and is flagged in the HTML report.
EBEs (Empirical Bayes Estimates, i.e. post-hoc ETAs) from FOCE are
systematically shrunk toward zero. When shrinkage is high, covariate plots
and ETA distributions based on raw EBEs underrepresent between-subject
variability. The Combes (2013) rescaling correction adjusts each subject’s
ETA vector so that SD(eta_k) = sqrt(omega_kk) exactly:
adjusted_etas = result.compute_deshrinkage_etas()
# Returns dict[subject_id → adjusted_eta_vector]
# adjusted_etas[1] # de-shrunken ETA for subject 1
The correction factor per random effect k is 1 / (1 − shrinkage_k).
Subjects retain their relative ordering; only the dispersion is corrected.
De-shrunken ETAs are appropriate for covariate plots and ETA histograms when
FOCE shrinkage exceeds roughly 30%. SAEM EBEs typically have lower shrinkage
and often need less correction.
Reference: Combes F-P, Retout S, Frey N, Mentré F (2013). Prediction of shrinkage of individual parameters using the Bayesian information matrix in nonlinear mixed-effects models. Pharm Res 30:2355–2367.
method="BAYES" returns a BayesianResult, which adds posterior-specific
fields such as:
result.posterior_samples
result.r_hat
result.n_effective
result.posterior_ci_lo
result.posterior_ci_hi
result.backend_used
method="NONPARAMETRIC" returns a NonparametricResult with support points and
support weights.
Model selection
For nested model comparison, use ΔOFV with a chi-squared reference where the
assumptions are appropriate:
from scipy.stats import chi2
delta_ofv = result_base.ofv - result_full.ofv
p = chi2.sf(delta_ofv, df=1)
Convergence and warnings
If result.converged is False, inspect result.warnings and the OFV history.
Typical causes are poor initials, near-singular OMEGA blocks, too-small
maxeval, or structural non-identifiability.
Parallel execution
FOCE, SAEM, and IMP support parallel subject-level computation via n_parallel.
Method |
Executor |
Parallelised work |
|---|---|---|
FOCE/FOCEI |
|
Per-subject η optimisation (true multi-core) |
SAEM |
|
Per-subject Metropolis–Hastings E-step |
IMP |
|
Per-subject importance sampling |
from openpkpd.estimation import get_estimation_method
# Use 4 worker processes for FOCE inner loop
method = get_estimation_method("FOCE", n_parallel=4)
result = method.estimate(pop_model, params)
# n_parallel=0 auto-detects core count
method = get_estimation_method("SAEM", n_parallel=0)
n_parallel=1 is the default and runs serially. FO, Laplacian, BAYES, and
Nonparametric are inherently serial and ignore n_parallel.
For parallel VPC simulation, see SimulationEngine(n_parallel=N) in the
analysis tools guide.