Post-fit diagnostics
After fitting, ferx returns a rich ferx_fit object with all the diagnostics needed for model evaluation. This article covers the key diagnostic functions and fields.
For a complete diagnostics walkthrough with plots, see Chapter 5 (Diagnostics) in the ferx book.
Parameter estimates — ferx_estimates()
ferx_estimates() collects all estimated parameters (theta, omega, sigma) into a tidy data frame. Run it after any fit that used covariance = TRUE:
ex <- ferx_example("warfarin")
fit <- ferx_fit(ex$model, ex$data)
ferx_estimates(fit)
# param transform estimate se rse_pct lower_95 upper_95
# TVCL identity 0.134 0.0108 8.1 0.113 0.155
# TVV identity 7.94 0.420 5.3 7.12 8.76
# TVKA identity 1.07 0.160 14.9 0.756 1.38
# ETA_CL variance 0.042 0.012 28.6 0.019 0.066
# ETA_V variance 0.020 0.009 44.9 0.002 0.037
# ETA_KA variance 0.157 0.048 30.6 0.063 0.251
# PROP_ERR proportional 0.011 0.001 9.1 0.009 0.013Columns:
| Column | Description |
|---|---|
param |
Parameter name |
transform |
Scale used by the estimator ("identity", "log", "logit", "variance" for omega) |
estimate |
Point estimate on the estimation scale |
se |
Standard error (requires covariance = TRUE) |
rse_pct |
Relative SE: |SE / estimate| × 100 |
lower_95, upper_95 |
95% CI on the estimation scale |
estimate_natural |
Back-transformed estimate (logit theta only) |
lower_95_natural, upper_95_natural |
Back-transformed CI (logit theta only) |
See Parameter transforms for how the transform column relates to log-normal, logit-normal, and additive ETAs.
Parameter correlation matrix — ferx_cor_matrix()
ferx_cor_matrix() converts the estimated parameter covariance matrix to correlations. A value near ±1 between two parameters signals a structural identifiability problem worth investigating:
ferx_cor_matrix(fit)
# TVCL TVV TVKA ...
# TVCL 1.00 0.12 0.03
# TVV 0.12 1.00 -0.07
# TVKA 0.03 -0.07 1.00High correlations between CL and V often arise in poorly-sampled datasets where absorption and elimination are not separately identifiable.
ETA-covariate correlations — ferx_eta_cov()
ferx_eta_cov() computes Pearson correlations between individual ETA estimates (EBEs) and numeric covariate columns. Identifies covariates worth including in a formal covariate search.
The warfarin dataset has no covariate columns. The following example uses two_cpt_oral_cov, which includes body weight (WT) and creatinine clearance (CRCL):
ex_cov <- ferx_example("two_cpt_oral_cov")
fit_cov <- ferx_fit(ex_cov$model, ex_cov$data)
data_cov <- read.csv(ex_cov$data)
ferx_eta_cov(fit_cov, data_cov)
# eta covariate r p_val flag
# ETA_CL WT 0.38 0.008 [!]
# ETA_CL CRCL 0.41 0.003 [!]
# ETA_V1 WT 0.29 0.047[!] flags correlations with |r| ≥ 0.3 as candidates for formal covariate testing. Pass the data frame (not the path); any numeric column that is constant within a subject and not a standard NONMEM column (TIME, DV, AMT, EVID, MDV, CMT, RATE) is treated as a covariate.
IWRES autocorrelation — check_diagnostics()
check_diagnostics() returns a structured list with an $autocorrelation data frame (Durbin-Watson + lag-1 Pearson r) and a tidy $shrinkage data frame covering all ETA and EPS components:
diag <- check_diagnostics(fit)
diag$autocorrelation
# dw_statistic lag1_r flag
# 1.84 0.0821 no autocorrelation
diag$shrinkage
# param type shrinkage shrinkage_pct
# ETA_CL eta 0.142 14.2
# ETA_V eta 0.283 28.3
# ETA_KA eta 0.452 45.2
# EPS eps 0.094 9.4print(fit) shows the same values in a --- Diagnostics --- block. A message() with ordered remedies is emitted automatically when DW < 1.5 (positive autocorrelation) or DW > 2.5 (negative autocorrelation).
| DW range | Interpretation | Suggested action |
|---|---|---|
| < 1.5 | Positive autocorrelation | Check time ordering; add transit compartment; consider IOV on ka/F; SDE |
| 1.5 – 2.5 | No concern | — |
| > 2.5 | Negative autocorrelation | Over-parameterisation or misspecified error model |
For a full walkthrough with plots see Chapter 5 (Diagnostics) in the ferx book.
Goodness-of-fit plots from fit$sdtab
fit$sdtab is a data frame with one row per observation containing PRED, IPRED, CWRES, IWRES, and additional columns depending on the model:
| Column | Always present | When present |
|---|---|---|
ID, TIME, DV |
Yes | — |
PRED, IPRED, CWRES, IWRES, EBE_OFV |
Yes | — |
CMT |
No | Multi-endpoint models (any CMT != 1 in the data) |
OCC |
No | IOV models |
CENS |
No | When the dataset has a CENS column (BLOQ) |
For multi-endpoint models, split GOF plots by CMT:
library(dplyr)
sdtab <- fit$sdtab |>
mutate(endpoint = factor(CMT, levels = c(2, 3), labels = c("PK", "PD")))sdtab <- fit$sdtabStandard GOF plots with ggplot2:
library(ggplot2)
# DV vs PRED
ggplot(sdtab, aes(PRED, DV)) +
geom_point(alpha = 0.4) +
geom_abline(slope = 1, intercept = 0, colour = "steelblue") +
labs(title = "DV vs PRED", x = "Population prediction", y = "Observed")
# DV vs IPRED
ggplot(sdtab, aes(IPRED, DV)) +
geom_point(alpha = 0.4) +
geom_abline(slope = 1, intercept = 0, colour = "steelblue") +
labs(title = "DV vs IPRED", x = "Individual prediction", y = "Observed")
# CWRES vs TIME
ggplot(sdtab, aes(TIME, CWRES)) +
geom_point(alpha = 0.4) +
geom_hline(yintercept = c(-2, 0, 2),
linetype = c("dashed", "solid", "dashed"), colour = "steelblue") +
labs(title = "CWRES vs TIME")
# CWRES vs PRED
ggplot(sdtab, aes(PRED, CWRES)) +
geom_point(alpha = 0.4) +
geom_hline(yintercept = c(-2, 0, 2),
linetype = c("dashed", "solid", "dashed"), colour = "steelblue") +
labs(title = "CWRES vs PRED")Structured warnings — ferx_warnings()
ferx classifies non-fatal issues from a run into severity levels and categories and makes them available via ferx_warnings(). Critical issues are also surfaced inline in the STATUS line of print(fit).
ferx_warnings(fit)
# severity category message
# warning shrinkage ETA_KA shrinkage = 0.58 (> 0.40): EBEs
# unreliable for this parameter
# info mu_referencing ETA_CL detected as log-normal mu-referenced
# info mu_referencing ETA_V detected as log-normal mu-referencedSeverity levels:
| Severity | Meaning |
|---|---|
critical |
Likely convergence failure or data issue; inspect before using results |
warning |
Non-fatal issue worth investigating (high shrinkage, near-boundary estimate) |
info |
Informational (mu-referencing detection, unused parameter notice) |
Common categories:
| Category | Examples |
|---|---|
shrinkage |
ETA shrinkage > 40%; EPS shrinkage > 30% |
convergence |
Outer optimizer hit maxiter; OFV gradient not converged |
mu_referencing |
Which ETAs were detected as mu-referenced |
unused_parameter |
Theta/omega declared in [parameters] but not used in the model |
boundary |
Estimated parameter is at or near its declared bound |
The raw warning strings are also available on the fit object:
fit$warnings # character vector of all warning messages
fit$warnings_structured # data frame: severity, category, messageRun log — ferx_runlog()
ferx_runlog() produces a NONMEM-style .lst summary in the console — useful for archiving results, quick convergence checks, and sharing with collaborators who expect NONMEM-style output.
ex <- ferx_example("warfarin")
fit <- ferx_fit(ex$model, ex$data, method = "foce", covariance = TRUE)
ferx_runlog(fit)The output for the warfarin example looks like:
============================================================
ferx run log
Model: warfarin
Method: FOCE
ferx: 0.1.5
Date: 2026-06-04 00:54:58
============================================================
============================================================
MODEL FILE
------------------------------------------------------------
# One-compartment oral PK model (warfarin)
[parameters]
theta TVCL(0.134, 0.001, 10.0)
...
============================================================
DATA SUMMARY
------------------------------------------------------------
Subjects: 10
Observations: 110
Time range: 0.5 to 120
============================================================
PARAMETER ESTIMATES
------------------------------------------------------------
PARAMETER INITIAL FINAL SE %RSE
TVCL 0.134 0.132779 0.01433 10.8%
TVV 8.1 7.69273 0.2899 3.8%
TVKA 1 0.756318 0.03491 4.6%
ETA_CL 0.07 0.0284934 0.006363 22.3%
ETA_V 0.02 0.00959794 0.002161 22.5%
ETA_KA 0.4 0.340694 0.07627 22.4%
PROP_ERR 0.01 0.0106401 0.0007838 7.4%
============================================================
ESTIMATION SETTINGS
------------------------------------------------------------
Optimizer: bobyqa
Max iterations: 300
BLOQ method: drop
============================================================
OBJECTIVE FUNCTION
------------------------------------------------------------
OFV = -280.183
AIC = -266.183
BIC = -247.28
Convergence: YES
============================================================
COVARIANCE STEP
------------------------------------------------------------
Condition number: 12.08
Eigenvalues: 2.224 1.023 0.9999 0.9921 0.9246 0.6524 0.1841
============================================================
DIAGNOSTICS
------------------------------------------------------------
ETA shrinkage: ETA_CL=-0.1% ETA_V=0.0% ETA_KA=0.1%
EPS shrinkage: 15.3%
DW statistic: 2.609 (negative autocorrelation)
ETA normality (Shapiro-Wilk):
ETA_CL: W=0.900 p=0.217
ETA_V: W=0.849 p=0.057
ETA_KA: W=0.937 p=0.521
============================================================
FINAL GRADIENT
------------------------------------------------------------
Gradient not available for this optimizer (BOBYQA / BFGS / GN / SAEM).
============================================================
RUNTIME
------------------------------------------------------------
Wall time: 0.0 seconds
Gradient: finite differences (inner) / N/A (outer)
============================================================
Capture to a file:
log_text <- ferx_runlog(fit, verbose = FALSE)
writeLines(log_text, "warfarin_run1.log")Gradient convergence check (for gradient-based optimizers such as L-BFGS):
ferx_runlog(fit, gradient_tol = 0.001) # flag any |grad| > 0.001The FINAL GRADIENT section shows the gradient components at the best-OFV parameter point and warns if any exceed gradient_tol (default 0.01). BOBYQA, GN, and SAEM do not produce a final gradient.
Other ferx_fit fields
fit$ofv # objective function value (-2 log-likelihood)
fit$aic # AIC = OFV + 2p
fit$bic # BIC = OFV + p*ln(N)
fit$converged # TRUE if the outer optimizer converged
fit$method # estimation method used
fit$gradient_used # "dual" (analytic) or "finite_differences"
fit$warnings # non-fatal warnings from the run
fit$ebe_etas # empirical Bayes ETAs per subject (data frame)
fit$individual_estimates # individual PK parameters (CL, V, KA, ...) per subject
fit$eta_normality # Shapiro-Wilk normality test p-value per ETA
fit$shrinkage_eta # ETA shrinkage per random effect
fit$shrinkage_eps # EPS shrinkage (scalar or vector for multi-sigma models)
fit$dw_statistic # pooled Durbin-Watson statistic for IWRES
fit$iwres_lag1_r # pooled lag-1 Pearson r for IWRESShrinkage — values near 1 indicate that the data are not informative about a random effect:
\[\text{ETA shrinkage}_k = 1 - \frac{\text{SD}(\hat{\eta}_k)}{\sqrt{\omega_{kk}}} \qquad \text{EPS shrinkage} = 1 - \text{SD}(\text{IWRES})\]
Convergence diagnostics — ferx_check_init()
Before committing to a long fit, run a 5-iteration pilot to confirm the OFV is decreasing from the initial values:
chk <- ferx_check_init(ex$model, ex$data, method = "focei")
chk$summary
# n_iter ofv_start ofv_end ofv_drop converged
# 5 -180.2 -186.3 6.1 FALSEOptimizer trace — ferx_trace() / ferx_plot_trace()
When optimizer_trace = TRUE is passed to ferx_fit(), ferx writes a per-iteration CSV. Read and plot it:
fit <- ferx_fit(ex$model, ex$data, optimizer_trace = TRUE)
tr <- ferx_trace(fit) # data frame with one row per outer iteration
head(tr[, c("iter", "ofv", "grad_norm")])
ferx_plot_trace(fit) # two-panel plot: OFV + method-specific metric
ferx_plot_trace(fit, log_ofv = TRUE) # OFV on log scale (OFV - min(OFV))ferx_trace() with no argument recalls the trace from the last ferx_fit() call in the current session:
ferx_trace() # same as ferx_trace(fit) after the aboveSee Output Files — Optimizer trace CSV (ferx-core book) for the full column reference.