Creating a Nonmem Parameter Table

Tim Bergsma

2023-09-20

The impact of a great model depends a lot on how well it is communicated. A parameter table can help; it brings together critical details of the model in a systematic way. However, creating parameter tables can be tedious, especially for modeling systems like NONMEM where information is distributed among numerous text files. Here we describe a systematic approach for generating parameter tables in NONMEM.

Sources

NONMEM results end up in many places.

Some of the most important information may not be captured anywhere! You may know that THETA1 is “apparent oral clearance” in your model, and that the units are liters. But to NONMEM it is just THETA1.

When you are making parameter tables by hand, no problem: your memory supplies all the integration, as well as missing details. But manual table generation can be tedious, time-consuming, and error-prone. If, rather, we want to automate the generation of parameter tables, we’re going to have to be more systematic about where and how the data sources are stored.

Conventions

The most fundamental storage convention is the concept of a project. This is a path – preferably a relative path, to a directory where your models live.

A second useful convention is the habit of keeping all the files related to a specific model in a project subdirectory with a unique name: i.e., the name of the model. While not strictly necessary, it really helps with organization and keeps NONMEM models from interfering with eachother while running.

I mentioned relative paths above. Relative to what? A third useful convention is to write all file paths relative to the file in which you are writing them. So if I have a model in home/project/1001/, and I write a script home/script/analysis.R that processes model ouptut, the path to the model directory will be written as ../project/1001.

Challenges

Okay, cut to the chase: we want an R function that creates a NONMEM parameter table. The challenges to be solved are as follows.

Solution

The package nonmemica provides the function partab that creates a parameter table for a single NONMEM model. partab knows where the models live, because you explicitly pass a project argument or (if you are lazy like me) you set an option near the top of your script.

Install and load nonmemica like this:

install.packages('nonmemica')
library(nonmemica)

Specify the project directory like this:

partab(1001, project='../model')

Or this:

options(project = '../model')
partab(1001)

A Word about Object Orientation

Users should not have to worry much about object orientation, but knowing something about it can be very helpful.

Briefly, object orientation means that a function does different things to different object types. Preparing a meal is very different from preparing a tax return. And formatting a data.frame is very different from formatting a POSIXlt. Try

methods(format)

to see all ways things get formatted in R. Typically we just say format and R finds the right method, depending on what it is we are formatting.

partab is no different. If you say partab(1001), the software sees that you are trying to create a parameter table from a number, which doesn’t really make sense. So it assumes 1001 is a model name, converts it to character, and goes looking for the function partab.character(). In fact, most of the interesting arguments to partab are actually documented under partab.character for this reason.

A Word about Piping

You’ve probably noticed … R usage is undergoing a revolution. The magrittr package recently gave us a “forward pipe operator” that fundamentally changes how R can be used. Even if you don’t use piping, an awareness will help you understand the occasional example. For instance, the following have the same meaning.

Traditional:

partab(1001)

Piped:

1001 %>% partab

Essentially, the left-hand-side is the first argument to the function on the right-hand-side, and the result can be used as input to yet-another-function in an ongoing chain. That means we can express functionality in a natural order, rather than reverse-nested order. This:

2 %>% sqrt %>% signif(3)

is easier to write and easier to understand (in my opinion) than this:

signif(sqrt(2),digits=3)

but the two are equivalent.

Finding Estimates

partab gets estimates and standard errors from *.xml. Make sure your NONMEM execution procedure calls for this file to be created and returned. It should appear in the model directory as a sibling of *.lst. If your xml file is in an unusual place, you can specify the path explicitly using the xmlfile argument (othewise partab will guess).

partab(1001, xmlfile='..model/1001.xml')

Finding Bootstraps

partab gets bootstrap estimates from bootstrap_dirx/bootstrap_results.csv (in the model directory). This is created using PsN. partab will try to find the latest file if there are more than one. You can specify the path explicitly using the bootcsv argument.

partab(1001, bootcsv='..model/1001/bootstrap_results.csv')

Finding Metadata

Let’s face it. NONMEM does not have integrated metadata. It knows the final estimate of THETA1, but it does not know what THETA1 means in human terms. A really useful parameter table should give a symbolic interpretation of THETA1, such as CL/F, units (if any), and hopefully a short definition.

The bad news is: you have to supply these yourself. The good new is: if you do it systematically, you can save a lot of effort.

partab looks in two places for metadata. First, it looks in the control stream for the items mentioned in the fields argument. It will guess where the control stream is, but you can supply the ctlfile argument. You can also modify the fields argument to change what it seeks.

partab(1001, ctlfile = '../models/1001.ctl',fields = c('symbol','label','unit')).

partab hopes to find each parameter on a line by itself, with semicolon-delimited fields trailing on the same line.

$THETA 
(0,10,50)     ; CL/F; clearance;       L/h
(0,10,100)    ; Vc/F; central volume;  L
(0,0.2,5)     ; Ka;   absorption rate; 1/h

By the way, the same conventions apply to tabled items. You can specify in your control stream how you think about table output.

$TABLE NOPRINT FILE=mod2.tab ONEHEADER 
ID            ; ID;    subject identifier;
TIME          ; TIME;  time;                       h
IPRE          ; IPRED; individual prediction;      ng/mL
CL            ; CLI;   posthoc systemic clearance; L/h
ETA1          ; BSV_CL; clearance variability;

Of course, these won’t show up in a parameter table, but you’ll see them in a list of item definitions if you do this:

library(magrittr)
library(nonmemica)
options(project = system.file('project/model',package='nonmemica'))
1001 %>% definitions %>% head
##       item symbol                        label unit
## 1 theta_01   CL/F                    clearance  L/h
## 2 theta_02   Vc/F               central volume    L
## 3 theta_03     Ka     absorption rate constant  1/h
## 4 theta_04    Q/F intercompartmental clearance  L/h
## 5 theta_05   Vp/F            peripheral volume    L
## 6 theta_06  WT_CL   weight effect on clearance <NA>

If you like, you can write out the defintions and edit them. You can change the file location or stick with the default. If you didn’t like exactly what was in the control stream, you can ignore it by passing a zero-length argument for ctlfile.

1001 %>% definitions(write=T)
1001 %>% partab(ctlfile = NULL)

The metafile needs to stay comma-separated. White space will be stripped, and blanks, spaces, or dots will be understood as NA.

Customizing Your Parameter Table

By default, the parameter table looks like this (but if bootstraps can’t be found, the ci columm will be missing).

partab(1001)
##      parameter estimate prse                 ci   symbol
## 1     theta_01     9.51 9.67       (7.24, 11.2)     CL/F
## 2     theta_02     22.8 9.54       (19.7, 25.4)     Vc/F
## 3     theta_03   0.0714 7.35   (0.0646, 0.0803)       Ka
## 4     theta_04     3.47 15.4       (3.02, 4.59)      Q/F
## 5     theta_05      113   21        (90.5, 380)     Vp/F
## 6     theta_06     1.19 28.3        (0.88, 1.3)    WT_CL
## 7     theta_07     1.02   11       (0.53, 1.73)  MALE_CL
## 8  omega_01_01    0.214 22.8     (0.151, 0.319)   IIV_CL
## 9  omega_02_01    0.121 26.4                        CL_V
## 10 omega_02_02   0.0945 33.2      (0.07, 0.179)   IIV_Vc
## 11 omega_03_01  -0.0116  173                       CL_Ka
## 12 omega_03_02  -0.0372 36.1                       Vc_Ka
## 13 omega_03_03   0.0466 34.7    (0.0475, 0.146)   IIV_Ka
## 14 sigma_01_01   0.0492 10.9   (-0.039, 0.0165) ERR_PROP
## 15 sigma_02_02    0.202 33.5 (-0.0512, -0.0097)  ERR_ADD
##                                            label unit
## 1                                      clearance  L/h
## 2                                 central volume    L
## 3                       absorption rate constant  1/h
## 4                   intercompartmental clearance  L/h
## 5                              peripheral volume    L
## 6                     weight effect on clearance <NA>
## 7                       male effect on clearance <NA>
## 8       interindividual variability on clearance <NA>
## 9    interindividual clearance-volume covariance <NA>
## 10 interindividual variability on central volume <NA>
## 11       interindividual clearance-Ka covariance <NA>
## 12          interindividual volume-Ka covariance <NA>
## 13             interindividual variability on Ka <NA>
## 14                            proportional error <NA>
## 15                                additive error <NA>

If you want to modify the table, save it first.

x <- partab(1001)

For better debugging information, set verbose = TRUE.

partab(1001, verbose=TRUE)

partab uses global options project and nested to guess where the run directory is, and uses that to guess where the source files are (metafile, xmlfile,ctlfile,bootcsv). You can supply any of these directly.

By default, partab grabs the 5th and 95th percentile of bootstraps as lo and hi, but you can specify other levels using the like-named arguments. partab formats these and other numerics as character(unless you say format = F), first limiting to the number of significant digits you specify (default digits = 3). If ci = TRUE (the default), lo and hi will be combined into a single interval using the default values for open, sep, and close. Also, you can get absolute standard errors by setting relative = FALSE. Let’s compare the default table above with something a bit more “raw”.

1001 %>% partab(
  format = F, 
  ci = F, 
  relative = F, 
  digits = NULL
)
##      parameter     estimate           se          lo            hi   symbol
## 1     theta_01   9.50751181  0.919790154  7.23607500  11.165510000     CL/F
## 2     theta_02  22.79071262  2.175293028 19.68059000  25.360120000     Vc/F
## 3     theta_03   0.07143288  0.005249901  0.06458277   0.080336250       Ka
## 4     theta_04   3.47447234  0.535642513  3.01994200   4.586354000      Q/F
## 5     theta_05 113.26550777 23.729595389 90.54828000 380.010800000     Vp/F
## 6     theta_06   1.19214554  0.337188000  0.88016420   1.301607000    WT_CL
## 7     theta_07   1.02437681  0.112446588  0.52951980   1.733120000  MALE_CL
## 8  omega_01_01   0.21385145  0.048857713  0.15135260   0.319490800   IIV_CL
## 9  omega_02_01   0.12075418  0.031922082          NA            NA     CL_V
## 10 omega_02_02   0.09453026  0.031343114  0.06995132   0.179007400   IIV_Vc
## 11 omega_03_01  -0.01162268  0.020065690          NA            NA    CL_Ka
## 12 omega_03_02  -0.03720323  0.013425269          NA            NA    Vc_Ka
## 13 omega_03_03   0.04656332  0.016175082  0.04754577   0.145969600   IIV_Ka
## 14 sigma_01_01   0.04916786  0.005383747 -0.03904045   0.016537490 ERR_PROP
## 15 sigma_02_02   0.20181742  0.067636559 -0.05122678  -0.009697708  ERR_ADD
##                                            label unit
## 1                                      clearance  L/h
## 2                                 central volume    L
## 3                       absorption rate constant  1/h
## 4                   intercompartmental clearance  L/h
## 5                              peripheral volume    L
## 6                     weight effect on clearance <NA>
## 7                       male effect on clearance <NA>
## 8       interindividual variability on clearance <NA>
## 9    interindividual clearance-volume covariance <NA>
## 10 interindividual variability on central volume <NA>
## 11       interindividual clearance-Ka covariance <NA>
## 12          interindividual volume-Ka covariance <NA>
## 13             interindividual variability on Ka <NA>
## 14                            proportional error <NA>
## 15                                additive error <NA>

You may want to do other things as well, such as converting error estimates to standard deviation or CV%. In my experience, it is difficult to do this in an automated way, as it depends on hard-to-guess properties, such as whether your distributions are log normal, and whether error has been coded as a sigma or a theta. If your goal is routinely to present random effects next to the corresponding parameter estimate, consider using the same symbol for each, and then “unstacking” the data with something like dplyr:spread(x, parameter, symbol).

Rendering Your Parameter Table

To some extent, the science of creating a parameter table is a separate issue from the aesthetics. How should you render the table for viewing by your audience? nonmemica is deliberately agnostic on this question, since there is no one right answer, and very many good ones. In an rmarkdown environment, consider knitr::kable.

library(knitr)
1001 %>% partab %>% kable
parameter estimate prse ci symbol label unit
theta_01 9.51 9.67 (7.24, 11.2) CL/F clearance L/h
theta_02 22.8 9.54 (19.7, 25.4) Vc/F central volume L
theta_03 0.0714 7.35 (0.0646, 0.0803) Ka absorption rate constant 1/h
theta_04 3.47 15.4 (3.02, 4.59) Q/F intercompartmental clearance L/h
theta_05 113 21 (90.5, 380) Vp/F peripheral volume L
theta_06 1.19 28.3 (0.88, 1.3) WT_CL weight effect on clearance NA
theta_07 1.02 11 (0.53, 1.73) MALE_CL male effect on clearance NA
omega_01_01 0.214 22.8 (0.151, 0.319) IIV_CL interindividual variability on clearance NA
omega_02_01 0.121 26.4 CL_V interindividual clearance-volume covariance NA
omega_02_02 0.0945 33.2 (0.07, 0.179) IIV_Vc interindividual variability on central volume NA
omega_03_01 -0.0116 173 CL_Ka interindividual clearance-Ka covariance NA
omega_03_02 -0.0372 36.1 Vc_Ka interindividual volume-Ka covariance NA
omega_03_03 0.0466 34.7 (0.0475, 0.146) IIV_Ka interindividual variability on Ka NA
sigma_01_01 0.0492 10.9 (-0.039, 0.0165) ERR_PROP proportional error NA
sigma_02_02 0.202 33.5 (-0.0512, -0.0097) ERR_ADD additive error NA

You can also try pander::pander.

library(pander)
1001 %>% partab %>% pander(justify='right')
Table continues below
parameter estimate prse ci symbol
theta_01 9.51 9.67 (7.24, 11.2) CL/F
theta_02 22.8 9.54 (19.7, 25.4) Vc/F
theta_03 0.0714 7.35 (0.0646, 0.0803) Ka
theta_04 3.47 15.4 (3.02, 4.59) Q/F
theta_05 113 21 (90.5, 380) Vp/F
theta_06 1.19 28.3 (0.88, 1.3) WT_CL
theta_07 1.02 11 (0.53, 1.73) MALE_CL
omega_01_01 0.214 22.8 (0.151, 0.319) IIV_CL
omega_02_01 0.121 26.4 CL_V
omega_02_02 0.0945 33.2 (0.07, 0.179) IIV_Vc
omega_03_01 -0.0116 173 CL_Ka
omega_03_02 -0.0372 36.1 Vc_Ka
omega_03_03 0.0466 34.7 (0.0475, 0.146) IIV_Ka
sigma_01_01 0.0492 10.9 (-0.039, 0.0165) ERR_PROP
sigma_02_02 0.202 33.5 (-0.0512, -0.0097) ERR_ADD
label unit
clearance L/h
central volume L
absorption rate constant 1/h
intercompartmental clearance L/h
peripheral volume L
weight effect on clearance NA
male effect on clearance NA
interindividual variability on clearance NA
interindividual clearance-volume covariance NA
interindividual variability on central volume NA
interindividual clearance-Ka covariance NA
interindividual volume-Ka covariance NA
interindividual variability on Ka NA
proportional error NA
additive error NA

Getting Help

If you have the nonmemica package installed, you can find this document using vignette('parameter-table', package = 'nonmemica'). Of course, the usual R help is available as well. Contact the mantainer for bug fixes and feature requests.

Conclusion

Making a NONMEM parameter table can be tedious. The nonmemica package provides partab, a flexible way of assembling the main sources of information and presenting them nicely. A good parameter table should have rich metadata: symbols, labels, and units at least. partab supports several options for encoding metadata systematically.