Chapitre 02
nlmixr2 — le modèle structural en R
Racym Berrah, PharmD PMx Explain Relecture scientifique par l'auteur : ORCID
La fonction R à deux blocs : ini({}) pour les nombres, model({}) pour les équations, EDO ou linCmt(), et la ligne cp ~ pour l'observation.
1 activités guidées01 · Pourquoi ce chapitre
Dans Monolix vous écrivez du mlxtran, dans NONMEM un control stream : deux langages qui ne servent qu'à ça, et deux fichiers que R ne sait pas lire. Dans nlmixr2, le modèle est une fonction R. Il tient dans le script, à côté de votre read.csv() ; vous le passez en argument, vous le rangez dans une liste, vous le versionnez avec le reste de l'analyse. La frontière entre le modèle et le code qui l'entoure disparaît.
Cette fonction a une forme imposée : deux blocs, ini({...}) pour les nombres et model({...}) pour les équations. Ce chapitre montre comment on y écrit un 1 compartiment oral — d'abord en équations différentielles, puis en linCmt() — et pourquoi le bloc model ressemble à du R sans tout à fait en être.
02 · Intuition
La vraie question, quand on découvre un outil, est de savoir où il coupe. nlmixr2 ne coupe pas là où Monolix coupe.
ini({...})ne contient que des nombres : où part la recherche, et quel paramètre est aléatoire. Aucune équation n'y a sa place.model({...})contient toutes les équations : la valeur typique, l'effet aléatoire, les EDO, la concentration, et la ligne d'observation.
La conséquence est immédiate et déroute tous ceux qui arrivent de Monolix : cl <- exp(tcl + eta.cl) s'écrit dans model, à trois lignes des d/dt(). La variabilité inter-individuelle — de la statistique pure — vit dans le même bloc que la structure. Il n'y a pas de [INDIVIDUAL] à chercher : la loi lognormale s'écrit à la main. C'est le découpage de NONMEM ($THETA/$OMEGA d'un côté, $PK/$DES de l'autre), transposé en R.
03 · La formule décortiquée
Le 1 compartiment oral tient en deux quantités — le dépôt , le central — et une division :
Version EDO. Le système câblé à la main :
oral1cpt <- function() {
ini({
tka <- log(0.9) # <- : parametre de POPULATION. ka = 0.9 /h, pose sur
tcl <- log(6.0) # l'echelle log pour garantir une valeur positive
tv <- log(45)
eta.cl ~ 0.09 # ~ : effet ALEATOIRE ; 0.09 est une VARIANCE
eta.v ~ 0.04
prop.sd <- 0.15 # <- : erreur residuelle, sur l'echelle ECART-TYPE
})
model({
ka <- exp(tka) # pas d'eta ici : ka est suppose sans IIV
cl <- exp(tcl + eta.cl) # la loi lognormale s'ecrit A LA MAIN
v <- exp(tv + eta.v)
d/dt(depot) = -ka * depot # declarer d/dt CREE le compartiment
d/dt(central) = ka * depot - (cl/v) * central # les EDO portent des QUANTITES
cp = central / v # la division par V est A VOTRE charge
cp ~ prop(prop.sd) # ligne d'OBSERVATION : cp est confronte a DV
})
}
Ligne à ligne :
- Dans
ini,<-déclare un paramètre de population,~déclare un effet aléatoire et sa variance. Il n'y a pas de matrice OMEGA à écrire : elle se déduit des lignes en~. Pour des etas corrélés, on donne le triangle inférieur :eta.cl + eta.v ~ c(0.09, 0.03, 0.04). - Toujours dans
ini,tcl <- c(-Inf, log(6.0), Inf)fixe borne basse, valeur initiale et borne haute ;label("Clairance (L/h)")nomme la ligne dans la table de sortie ;fix()gèle le paramètre. - Dans
model, écrired/dt(depot)suffit à créer le compartimentdepot— il n'y a rien à déclarer ailleurs. Les compartiments sont numérotés dans leur ordre d'apparition, ce qui rend uncmtnumérique fragile : réordonnez deux lignes et le jeu de données ne dose plus au bon endroit. Utilisez les noms dans la colonnecmt. cpn'est pas un mot-clé : c'est le nom que vous donnez à la prédiction.
Version linCmt(). Le bloc ini ne change pas ; seul model maigrit :
model({
ka <- exp(tka)
cl <- exp(tcl + eta.cl)
v <- exp(tv + eta.v)
cp = linCmt() # ka + cl + v en portee -> 1 cpt oral, rend une CONCENTRATION
cp ~ prop(prop.sd)
})
04 · Exemple concret
Le modèle écrit, le reste du script est du R banal :
library(nlmixr2)
dat <- read.csv("pk_oral.csv") # ID TIME AMT DV EVID CMT ; dose 200 mg dans depot
fit <- nlmixr2(oral1cpt, dat, est = "saem", # oral1cpt SANS parentheses
control = saemControl(nBurn = 300, nEm = 400, seed = 42))
fit$parFixed # table des parametres de population, RSE, CV des etas
head(fit) # data.frame : ID TIME DV PRED IPRED CWRES eta.cl eta.v cl v cp ...
Avant de lire quoi que ce soit, vérifiez que la machine fait ce que vous croyez. Pour un sujet typique — dose orale 200 mg, h⁻¹, L, L/h, — la constante d'élimination vaut h⁻¹, d'où un rapport et un pic à
Comme pour ce modèle, on attend mg/L, pour une exposition mg·h/L. Trois nombres calculés en trente secondes, qui disqualifient un modèle mal câblé avant qu'il ne vous coûte une demi-journée de diagnostics.
fit n'est pas un fichier de sortie à ouvrir dans un éditeur : c'est un data.frame augmenté, une ligne par observation, avec PRED, IPRED, CWRES et les etas déjà en colonnes, plus des tables (fit$parFixed, fit$omega) qui partent directement dans ggplot2. C'est là que se paie le choix du modèle-objet.
05 · Piège fréquent
Les fautes de syntaxe, dans nlmixr2, sont bruyantes : un paramètre déclaré dans ini mais absent de model fait échouer l'analyse avant même le premier run. Le piège coûteux est ailleurs, et il est muet.
Le symptôme, lui, ne ressemble pas du tout à la cause. Ce que vous verrez, si vous voyez quelque chose, c'est une étape de covariance qui échoue ou deux paramètres de population aux RSE aberrantes — la signature de leur confusion. Vous partirez chercher un problème de données ou d'identifiabilité. L'erreur est un tilde.
Le même bloc ini cache une seconde asymétrie, purement de lecture celle-là. eta.cl ~ 0.09 donne une variance : l'écart-type vaut 0,3 et le CV lognormal environ 31 %, pas 9 %. Deux lignes plus bas, prop.sd <- 0.15 donne un écart-type : 15 % d'erreur proportionnelle, là où NONMEM vous ferait écrire 0,0225 dans $SIGMA. Même bloc, même syntaxe, deux échelles. D'où le double réflexe : convertir avant de recopier une valeur initiale trouvée dans un run NONMEM, et convertir avant d'annoncer une IIV dans un rapport.
06 · À retenir
- Le modèle est une fonction R à deux blocs :
ini({...})ne contient que des nombres,model({...})que des équations. nlmixr2 lit le corps de la fonction, il ne l'exécute pas — d'oùnlmixr2(oral1cpt, ...), sans parenthèses. - Le découpage est celui de NONMEM, pas celui de Monolix :
cl <- exp(tcl + eta.cl)s'écrit à la main, dans le même bloc que les EDO. Aucun[INDIVIDUAL]à chercher. - Dans
ini,<-= paramètre de population,~= effet aléatoire et sa variance ; l'erreur résiduelle, elle, se déclare en écart-type. d/dt(depot)crée le compartiment par sa seule déclaration, les numéros suivent l'ordre d'apparition — dosez par nom — et la division par est à votre charge.linCmt()ne prend aucun argument : il choisit le modèle d'après les noms des paramètres en portée (ka, cl, v= 1 cpt oral), route la dose et rend une concentration.cp ~ prop(prop.sd)est la ligne d'observation ; le tilde change de sens selon son bloc.- Un
<-mis à la place d'un~dansinisupprime une IIV sans rien casser — et le symptôme désigne un autre coupable.
Point de contrôle
Vérifiez votre compréhension
Q1. Dans le bloc ini, la ligne eta.cl ~ 0.09 déclare...
Q2. linCmt() sélectionne le modèle de PK...
Q3. Dans le bloc model, la ligne cp ~ prop(prop.sd) signifie que...
Exercices d'entraînement
Cas en plusieurs étapes combinant calcul, interprétation et raisonnement.
nlmixr2 : vérifier les échelles
Calcul · Interprétation · Lecture de codeÀ commencer
nlmixr2 : vérifier les échelles
Calcul · Interprétation · Lecture de codeÀ commencerCiter cette page
Berrah R. nlmixr2 — le modèle structural en R. PMx Explain, 2026. https://rberrah.github.io/pharmacometrie/chapitres/nlmixr2-modele-structural/
@misc{berrah2026nlmixr2modelestructural,
author = {Berrah, Racym},
title = {{nlmixr2 — le modèle structural en R}},
howpublished = {PMx Explain},
year = {2026},
url = {https://rberrah.github.io/pharmacometrie/chapitres/nlmixr2-modele-structural/},
note = {Texte actuel sous licence CC BY-NC-SA 4.0}
}