parameter-transforms.Rmdferx supports three parameterisation styles for individual parameters:
| Style | DSL pattern | Use case |
|---|---|---|
| Log-normal | P = TVP * exp(ETA_P) |
Most PK parameters (CL, V, KA) — positive by construction |
| Logit-normal | P = inv_logit(logit(THETA_P) + ETA_P) |
Fractions/probabilities (F, bioavailability) — bounded (0, 1) |
| Additive | P = TVP + ETA_P |
Parameters with natural additive variability (lag time, threshold) |
The optimiser always works on the unbounded or transformed scale; ferx back-transforms to the natural scale when reporting results.
The standard warfarin model uses log-normal ETAs for CL, V, and KA.
This is the default: each individual parameter equals the typical value
multiplied by exp(ETA), which keeps the parameter positive
and gives a log-normal distribution across subjects.
ex <- ferx_example("warfarin")
ferx_model_inspect(ex$model)
#> Model structure (warfarin.ferx)
#> Structural: 1-cpt oral (TVCL, TVV, TVKA)
#> IIV: ETA_CL, ETA_V, ETA_KA
#> IOV: none
#> Residual: proportionalAfter fitting, print() reports omega as
[log-normal] with a CV%:
fit <- ferx_fit(ex$model, ex$data)
fit
# Omega (between-subject variability):
# ETA_CL [log-normal] var = 0.0423 CV% = 21.1
# ETA_V [log-normal] var = 0.0198 CV% = 14.2
# ETA_KA [log-normal] var = 0.157 CV% = 40.9The CV% is derived from the log-scale variance as
CV% = 100 * sqrt(exp(omega_var) - 1). For small variances
this is approximately 100 * sqrt(omega_var), but the exact
formula is always used.
ferx_estimates() returns the omega variance on the log
scale in estimate, with transform = "variance"
(omega rows always use this label regardless of the ETA parameterisation
style — the log-normal interpretation is conveyed by
print.ferx_fit(), not by the tidy table). SE and 95 % CI
columns are on the same log-variance scale:
ferx_estimates(fit)
# param transform estimate se rse_pct lower_95 upper_95
# OMEGA(ETA_CL) variance 0.0423 0.0121 28.6 0.0186 0.0660
# OMEGA(ETA_V) variance 0.0198 0.0089 44.9 0.0024 0.0372
# OMEGA(ETA_KA) variance 0.1570 0.0480 30.6 0.0629 0.2511
# estimate_natural / lower_95_natural / upper_95_natural are NA for omega rows
# (back-transform is not defined for variance parameters)A common question is: “what omega value corresponds to 30% BSV?”
cv_targets <- c(10, 20, 30, 40, 50, 60, 80, 100)
data.frame(
cv_pct = cv_targets,
omega_approx = round((cv_targets / 100)^2, 4),
omega_exact = round(log(1 + (cv_targets / 100)^2), 4)
)
#> cv_pct omega_approx omega_exact
#> 1 10 0.01 0.0100
#> 2 20 0.04 0.0392
#> 3 30 0.09 0.0862
#> 4 40 0.16 0.1484
#> 5 50 0.25 0.2231
#> 6 60 0.36 0.3075
#> 7 80 0.64 0.4947
#> 8 100 1.00 0.6931Use the exact formula (log(1 + CV²))
for omega variances above 30% — the approximate formula
CV²/100² underestimates the required starting value and can
slow convergence.
Quick rule: for CV% ≤ 30%, omega ≈ (CV/100)² is fine.
For CV% > 30%, use omega = log(1 + (CV/100)²).
When a parameter is a fraction bounded between 0 and 1, use
inv_logit / logit to ensure the individual
values stay in (0, 1):
F = inv_logit(logit(THETA_F) + ETA_F)
THETA_F is specified on the natural (0, 1) scale in
[parameters]; ferx maps it to the logit scale internally so
the optimiser works on an unbounded space. The omega for
ETA_F is the variance on the logit scale.
ex_logit <- ferx_example("warfarin_logit_f")
ferx_model_inspect(ex_logit$model)
#> Model structure (warfarin_logit_f.ferx)
#> Structural: 1-cpt oral (TVCL, TVV, TVKA, THETA_F)
#> IIV: ETA_CL, ETA_V, ETA_F
#> IOV: none
#> Residual: proportionalAfter fitting, print() shows:
THETA_F with [logit scale] and a 95 % CI
back-transformed through inv_logit so the CI is on the (0,
1) scale.ETA_F with [logit], SD_logit,
and a ±1 SD interval on the (0, 1) scale.
fit_logit <- ferx_fit(ex_logit$model, ex_logit$data)
fit_logitferx_estimates() populates
estimate_natural, lower_95_natural, and
upper_95_natural for logit-transformed thetas. The CI is
computed symmetrically on the logit scale and then back-transformed
through inv_logit, giving an asymmetric interval on the (0,
1) scale:
est <- ferx_estimates(fit_logit)
est[est$param == "THETA_F", ]
# param transform estimate se rse_pct lower_95 upper_95 estimate_natural lower_95_natural upper_95_natural
# THETA_F logit 0.847 0.143 16.9 0.567 1.127 0.700 0.638 0.755
# estimate — logit scale (what the optimiser works with)
# estimate_natural — inv_logit(estimate) = probability on (0, 1) scale
# lower/upper_95_natural — asymmetric CI back-transformed to (0, 1)The omega for ETA_F has
transform = "variance" (variance on the logit scale,
symmetric CI — same label as all omega rows):
est[est$param == "OMEGA(ETA_F)", ]
# param transform estimate se rse_pct
# OMEGA(ETA_F) variance 0.0631 0.028 44.4When the BSV on a parameter has an additive rather than multiplicative structure — for example, a lag time where the subject-level deviation is in hours — use a plain sum:
TLAG = TVTLAG + ETA_TLAG
ETA_TLAG follows a normal distribution with mean 0 and
variance given by the corresponding omega element. The individual lag
times are therefore normally distributed around TVTLAG.
ex_add <- ferx_example("warfarin_additive_eta")
ferx_model_inspect(ex_add$model)
#> Model structure (warfarin_additive_eta.ferx)
#> Structural: 1-cpt oral (TVCL, TVV, TVKA, TVTLAG)
#> IIV: ETA_CL, ETA_V, ETA_TLAG
#> IOV: none
#> Residual: proportionalAfter fitting, the ETA_TLAG row in the omega block is
printed with [additive] and an SD in the original units
(hours):
fit_add <- ferx_fit(ex_add$model, ex_add$data)
fit_addferx_estimates() returns
transform = "variance" for that omega element (all omega
rows use this label). The estimate is the variance in the
original units (hours²), and the CI is symmetric — no back-transform is
applied:
ferx_estimates(fit_add)
# param transform estimate se rse_pct lower_95 upper_95
# OMEGA(ETA_TLAG) variance 0.312 0.098 31.4 0.120 0.504
# SD in original units = sqrt(0.312) ≈ 0.56 h
# estimate_natural / lower_95_natural / upper_95_natural are NA (not applicable)All three styles can appear in the same model. The standard two-compartment oral model with a logit bioavailability and an additive lag time would declare:
[individual_parameters]
CL = TVCL * exp(ETA_CL) # log-normal
V1 = TVV1 * exp(ETA_V1) # log-normal
KA = TVKA * exp(ETA_KA) # log-normal
F = inv_logit(logit(THETA_F) + ETA_F) # logit-normal
TLAG = TVTLAG + ETA_TLAG # additive
[structural_model]
pk two_cpt_oral(cl=CL, v1=V1, q=Q, v2=V2, ka=KA, f=F, lagtime=TLAG)
Each ETA is handled independently; the omega matrix contains the corresponding variances on the respective scales (log, logit, or natural units).
Using the bundled two-compartment with covariate example as a proxy for a multi-transform model (it uses log-normal ETAs on all parameters), you can verify the pattern compiles and fits:
ex2 <- ferx_example("two_cpt_oral_cov")
ferx_model_inspect(ex2$model)
fit2 <- ferx_fit(ex2$model, ex2$data, method = "gn", covariance = FALSE)
fit2$thetaA model combining logit and additive ETAs follows exactly the same
path — ferx_fit() detects each ETA’s parameterisation from
the [individual_parameters] expressions and applies the
correct transform automatically.
| Transform | Omega interpretation | Print label |
ferx_estimates() columns |
|---|---|---|---|
| Log-normal | variance on log scale |
[log-normal] + CV% |
transform = "variance", symmetric CI on log-var
scale |
| Logit-normal | variance on logit scale |
[logit] + SD_logit + ±1 SD interval |
transform = "variance"; logit theta gets
estimate_natural etc. |
| Additive | variance in original units |
[additive] + SD |
transform = "variance", symmetric CI in original
units² |