nlmixr2 2.0.8 Objectively Surprising
By Matt Fidler and the nlmixr2 Development Team in nlmixr2
October 25, 2022
Last time I blogged promised to talk about a few other things, including:
Likelihood based on each observation (and how to get it)
Standard Errors / Hessians, etc for between subject variabilities or
eta
s (and how to get them)
Hessians for the individual between subject variability is also used for
the focei
calculation. So, if you are impatient, I will give you brief
instructions on where to get each component of the likelihood:
The individual observation’s likelihood contribution is contained in the datasets where the original (left) merged with the fit (right) in any of the following accessor methods:
fit$dataMergeLeft
,fit$dataMergeRight
,fit$dataMergeInner
, orfit$dataMergeFull
. In these dataset an new column is added$nlmixrLlikObs
The individual -Hessian
etas
can be accessed byfit$etaH
orfit$phiH
Other components may be accessed as well:
Syntax | Values returned |
---|---|
$phiH , $etaH |
Hessian matrix |
$phiC , $etaC |
Covariance matrix, standard deviations on diagonals, correlations on off-diagonals |
$phiR , $etaR |
Correlation matrix, standard deviations on diagonals, correlations on off-diagonals |
$phiSE , $etaSE |
standard error of each individual’s eta |
$phiRSE , $etaRSE |
relative standard error of each individual’s eta |
$phiC , $etaC |
Covariance matrix, standard deviations on diagonals, correlations on off-diagonals |
$phiR , $etaR |
Correlation matrix, standard deviations on diagonals, correlations on off-diagonals |
$phiSE ,
$etaSE |
Standard error of each individual’s eta |
$phiRSE ,
$etaRSE |
relative standard error of each individual’s eta |
These all require that the cwres
are in the fit because they come from
the focei
calculations (and are also under the focei
assumption).
Objective function Motivating example
I was working with Bill Denney to prepare past course that features
babelmixr2
. In this course, you can perform a NCA
analysis (using
PKNCA
), then use these values (and possibly calculate a unit
conversion) to create a initial nlmixr2
PK model. This model has with
NCA
derived initial estimates and ranges (and needed unit conversions
too)!
This is exciting to me, as someone who has been wanting this feature in
nonlinear mixed effects modeling packages like nlmixr2
for quite
awhile.
Two nearly identical models
Still, when testing this we came across the following (possibly) surprising situation:
one.compartment <- function() {
ini({
tka <- 0.45 # Log Ka
tcl <- 1 # Log Cl
tv <- 3.45 # Log V
eta.ka ~ 0.6
eta.cl ~ 0.3
eta.v ~ 0.1
add.sd <- 0.7
})
model({
ka <- exp(tka + eta.ka)
cl <- exp(tcl + eta.cl)
v <- exp(tv + eta.v)
d/dt(depot) = -ka * depot
d/dt(center) = ka * depot - cl / v * center
cp = center / v
cp ~ add(add.sd)
})
}
fit1 <- nlmixr(one.compartment, nlmixr2data::theo_sd,
est="focei", control=list(print=0))
## ℹ parameter labels from comments will be replaced by 'label()'
## → loading into symengine environment...
## → pruning branches (`if`/`else`) of full model...
## ✔ done
## → calculate jacobian
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → calculate sensitivities
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → calculate ∂(f)/∂(η)
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → calculate ∂(R²)/∂(η)
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → finding duplicate expressions in inner model...
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → optimizing duplicate expressions in inner model...
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → finding duplicate expressions in EBE model...
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → optimizing duplicate expressions in EBE model...
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → compiling inner model...
## ✔ done
## → finding duplicate expressions in FD model...
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → optimizing duplicate expressions in FD model...
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## → compiling EBE model...
## ✔ done
## → compiling events FD model...
## ✔ done
## calculating covariance matrix
## done
## → Calculating residuals/tables
## ✔ done
## → compress origData in nlmixr2 object, save 5952
## → compress parHist in nlmixr2 object, save 2400
Now multiply the cp
by 1000 and the observations by 1000 for a nearly
identical model (ie, change the scale for different units)
d2 <- nlmixr2data::theo_sd %>%
mutate(DV=ifelse(AMT==0, DV*1000, DV))
# Use model piping to scale `cp`:
one.compartment %>%
model(cp = 1000*center/v) %>%
ini(add.sd=700)->
m2
## ℹ parameter labels from comments will be replaced by 'label()'
## ℹ change initial estimate of `add.sd` to `700`
# Verify the new model
print(m2)
## ── rxode2-based free-form 2-cmt ODE model ──────────────────────────────────────
## ── Initalization: ──
## Fixed Effects ($theta):
## tka tcl tv add.sd
## 0.45 1.00 3.45 700.00
##
## Omega ($omega):
## eta.ka eta.cl eta.v
## eta.ka 0.6 0.0 0.0
## eta.cl 0.0 0.3 0.0
## eta.v 0.0 0.0 0.1
##
## States ($state or $stateDf):
## Compartment Number Compartment Name
## 1 1 depot
## 2 2 center
## ── μ-referencing ($muRefTable): ──
## theta eta level
## 1 tka eta.ka id
## 2 tcl eta.cl id
## 3 tv eta.v id
##
## ── Model (Normalized Syntax): ──
## function() {
## ini({
## tka <- 0.45
## label("Log Ka")
## tcl <- 1
## label("Log Cl")
## tv <- 3.45
## label("Log V")
## add.sd <- c(0, 700)
## eta.ka ~ 0.6
## eta.cl ~ 0.3
## eta.v ~ 0.1
## })
## model({
## ka <- exp(tka + eta.ka)
## cl <- exp(tcl + eta.cl)
## v <- exp(tv + eta.v)
## d/dt(depot) = -ka * depot
## d/dt(center) = ka * depot - cl/v * center
## cp <- 1000 * center/v
## cp ~ add(add.sd)
## })
## }
fit2 <- nlmixr(m2, d2, est="focei", control=list(print=0))
## → loading into symengine environment...
## → pruning branches (`if`/`else`) of full model...
## ✔ done
## → calculate jacobian
## → calculate sensitivities
## → calculate ∂(f)/∂(η)
## → calculate ∂(R²)/∂(η)
## → finding duplicate expressions in inner model...
## → optimizing duplicate expressions in inner model...
## → finding duplicate expressions in EBE model...
## → optimizing duplicate expressions in EBE model...
## → compiling inner model...
## ✔ done
## → finding duplicate expressions in FD model...
## → optimizing duplicate expressions in FD model...
## → compiling EBE model...
## ✔ done
## → compiling events FD model...
## ✔ done
## calculating covariance matrix
## done
## → Calculating residuals/tables
## ✔ done
## → compress origData in nlmixr2 object, save 6400
## → compress parHist in nlmixr2 object, save 2024
Comparing Estimates
As expected the population estimates are similar:
#fit1
print(fixef(fit1))
## tka tcl tv add.sd
## 0.4681803 1.0108966 3.4603969 0.6969408
#fit2
print(fixef(fit2))
## tka tcl tv add.sd
## 0.4632747 1.0116245 3.4602015 695.3004086
Note that the additive error is (unsurprisingly) larger by a factor of about 1000.
Still, the Omega matrices are similar too:
# fit 1
print(fit1$omega)
## eta.ka eta.cl eta.v
## eta.ka 0.3905671 0.00000000 0.00000000
## eta.cl 0.0000000 0.06868965 0.00000000
## eta.v 0.0000000 0.00000000 0.01900547
# fit 2
print(fit2$omega)
## eta.ka eta.cl eta.v
## eta.ka 0.3950723 0.00000000 0.00000000
## eta.cl 0.0000000 0.06806684 0.00000000
## eta.v 0.0000000 0.00000000 0.01905419
And the etas:
# fit 1
print(fit1$etaObf)
## ID eta.ka eta.cl eta.v OBJI
## 1 1 0.08208665 -0.47259882 -0.091897473 12.5606972
## 2 2 0.19410054 0.14488970 0.004567050 17.7860972
## 3 3 0.36685381 0.02921334 0.052141272 0.2980393
## 4 4 -0.28483616 -0.01755511 -0.013311522 10.9727363
## 5 5 -0.04614196 -0.14932588 -0.143715338 29.0206253
## 6 6 -0.38880790 0.37194163 0.193020540 7.7759907
## 7 7 -0.77296044 0.14601460 0.055578295 2.2571264
## 8 8 -0.17120576 0.16513639 0.093028469 6.9589644
## 9 9 1.35263334 0.04662980 -0.001322353 8.5170235
## 10 10 -0.73447486 -0.38123254 -0.171485167 7.9744277
## 11 11 0.74760924 0.28858115 0.135248561 3.4258868
## 12 12 -0.52143688 -0.12502814 -0.201272246 9.2635184
# fit 2
print(fit2$etaObf)
## ID eta.ka eta.cl eta.v OBJI
## 1 1 0.08180853 -0.47296571 -0.092397056 164.5851
## 2 2 0.19429924 0.14418654 0.004310170 169.8321
## 3 3 0.36757318 0.02842365 0.052022866 152.2118
## 4 4 -0.28524042 -0.01807046 -0.013762949 162.9714
## 5 5 -0.04661044 -0.14973343 -0.144315774 181.1468
## 6 6 -0.38822210 0.37075684 0.193368002 159.7219
## 7 7 -0.77379790 0.14547001 0.055117184 154.1658
## 8 8 -0.17103635 0.16431876 0.092932433 158.9221
## 9 9 1.35579162 0.04576087 -0.001438554 160.4134
## 10 10 -0.73590990 -0.38125995 -0.172486227 159.9292
## 11 11 0.74951270 0.28749652 0.135469658 155.3285
## 12 12 -0.52263038 -0.12521875 -0.202224009 161.2270
The ETAs are similar too; You can also see the individual contribution
to the objective functions are quite different (OBJI
). So it should be
no surprise that the objective functions are different:
# fit 1
print(fit1$objf)
## [1] 116.8111
# fit 2
print(fit2$objf)
## [1] 1940.455
What about NONMEM?
You might say, well are these objective functions off? maybe nlmixr2
is broken? (If you see anything surprising of course submit a bug report
if you can).
Well, with the coming babelmixr2
you can run the same models in NONMEM
(with certain caveats we will discuss later), and these objective
functions also are similar NONMEM between nlmixr2
and NONMEM
(which
is unsurprising since we use the NONMEM objective functions in Wang 2007
(1) to validate our likelihood)
This means that nlmixr2
is constitent with NONMEM
and these
objective function differences are due to other factors.
Exploring more with individual observation contribution
One of the new features is the ability to see individual observations
contribution to the likelihood in focei
related methods.
This can help us explore the differences.
In nlmixr2
, you can use the fit$dataMergeInner
to merge the original
data and the fit data. During this merge process it will also add the
column $nlmixrLlikObs
:
dm1 <- fit1$dataMergeInner
dm1ll <- dm1 %>%
select(ID, nlmixrLlikObs) %>%
group_by(ID) %>%
summarize(sllik=sum(nlmixrLlikObs))
dm2 <- fit2$dataMergeInner
dm2ll <- dm2 %>%
group_by(ID) %>%
summarize(sllik=sum(nlmixrLlikObs))
print(dm1ll)
## # A tibble: 12 × 2
## ID sllik
## <fct> <dbl>
## 1 1 0.644
## 2 2 3.23
## 3 3 2.29
## 4 4 0.896
## 5 5 0.124
## 6 6 2.86
## 7 7 1.11
## 8 8 -9.97
## 9 9 -1.94
## 10 10 3.47
## 11 11 -5.26
## 12 12 -0.674
print(dm2ll)
## # A tibble: 12 × 2
## ID sllik
## <fct> <dbl>
## 1 1 -75.3
## 2 2 -72.7
## 3 3 -73.7
## 4 4 -75.1
## 5 5 -75.8
## 6 6 -73.1
## 7 7 -74.9
## 8 8 -86.0
## 9 9 -77.9
## 10 10 -72.5
## 11 11 -81.3
## 12 12 -76.7
# It is clear that there are individual differences in log-likelihood
In the normal (non generalized likelihood) the observation likelihoods are given by \(l_{i, obs}\):
\[l_{i, obs} = -0.5\times\left(\frac{\textsf{IPRED}-\textsf{DV}}{v}\right)^2-0.5*\log(v)\]
Where \(v=\) variance of the estimate at that point. In this case it is \(\textsf{add.sd}^2\)
You can see part of the difference is the relative differences of this term for subjects. Most of this is likely driven by the large (and unsurprising) differences in the variance component.
If you want, you can see which observations give the biggest difference by comparing point by point.
Finishing up the likelihood calculation
A part of the individual Hessians are the other component that is used in the likelihood calculation. With the new tools you can also see what this contribution to each individual’s likelihood is:
hess1 <- merge(fit1$etaObf, dm1ll) %>%
mutate(hessLlik=OBJI-sllik)
hess2 <- merge(fit2$etaObf, dm2ll) %>%
mutate(hessLlik=OBJI-sllik)
# Hess1
print(hess1)
# Hess2
print(hess2)
You can see the individual Hessian contribution is actually large in
this particular likelihood as well. (You can explore their difference
more using $etaH
if you wish)
Conclusion
Well that is everything for now. This illustrates a few things:
How to get individual likelihoods
How to split apart the likelihood contribution from the Normal assumption of the observations and the contribution from the hessian. (Note this works with generalized likelihood too)
Where to get standard errors of
etas
I wish I had known where these came from earlier, but I seem to want to know how things work. For a more in-depth reference you could use the paper by Almquist (2) to dig into the full likelihood math.
References
- Posted on:
- October 25, 2022
- Length:
- 10 minute read, 1965 words
- Categories:
- nlmixr2
- Tags:
- new-version