Skip to contents

Model and source

Hayato 2022 reports one population PK model for intravenous lecanemab and three PK/PD models fitted sequentially on its post hoc individual PK parameters. Following the authors’ structure, the library carries four models:

Model Endpoint
Hayato_2022_lecanemab Serum lecanemab (two-compartment PK)
Hayato_2022_lecanemab_suvr Amyloid PET SUVr (indirect response, Emax stimulation of loss)
Hayato_2022_lecanemab_abeta4240 Plasma Abeta42/40 ratio (indirect response, linear stimulation of production)
Hayato_2022_lecanemab_ptau181 Plasma p-tau181 (indirect response, linear inhibition of production)

Each PK/PD model embeds the PK model with its parameters and IIV fixed, so it can be simulated from a dosing record alone.

mod_names <- c(
  pk = "Hayato_2022_lecanemab",
  suvr = "Hayato_2022_lecanemab_suvr",
  abeta4240 = "Hayato_2022_lecanemab_abeta4240",
  ptau181 = "Hayato_2022_lecanemab_ptau181"
)
mods <- lapply(mod_names, function(nm) rxode2::rxode(readModelDb(nm)))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
  • Citation: Hayato S, Takenaka O, Sreerama Reddy SH, Landry I, Reyderman L, Koyama A, Swanson C, Yasuda S, Hussein Z (2022). Population pharmacokinetic-pharmacodynamic analyses of amyloid positron emission tomography and plasma biomarkers for lecanemab in subjects with early Alzheimer’s disease. CPT Pharmacometrics Syst Pharmacol 11(12):1578-1591. doi:10.1002/psp4.12862.
  • Article: https://doi.org/10.1002/psp4.12862 (open access; supplementary Texts S1-S4 hold the four NONMEM control streams)

Population

The PK dataset holds 9027 serum lecanemab concentrations from 725 subjects in the phase 1 studies 101 (single and multiple ascending doses, mild-to-moderate Alzheimer’s disease) and 104 (multiple ascending doses, MCI due to AD or mild AD) and the phase 2 study 201 (Core: placebo, 2.5 mg/kg biweekly, 5 mg/kg monthly or biweekly, 10 mg/kg monthly or biweekly for 18 months; then a 9-59 month off-treatment gap and an open-label extension at 10 mg/kg biweekly). Supplement Table S2 gives the PK population as mean age 71.0 years (50-93), mean weight 73.7 kg (41.2-124.7), mean albumin 42.9 g/L, 46.9% female, 86.3% White and 6.9% Japanese. The PK/PD datasets are from study 201 only: 374 subjects / 1213 SUVr measurements, 284 subjects / 1254 plasma Abeta42/40 ratios, and 562 subjects / 2021 plasma p-tau181 values, with about 70% APOE4 carriers and about two-thirds MCI due to AD in each.

The same information is available programmatically, e.g. readModelDb("Hayato_2022_lecanemab_suvr")()$population.

Source trace

Every ini() value carries an in-file comment pointing to its source. All estimates are the final NONMEM estimates of Tables 1-3; the functional forms come from the equations printed under those tables and from the supplementary control streams (Model S1 = PK, S2 = SUVr, S3 = Abeta42/40, S4 = p-tau181).

Parameter Value Source
CL, V1, Q, V2 0.0181 L/h, 3.22 L, 0.0349 L/h, 2.19 L Table 1
F for Process B (Process A fixed 1) 0.998 Table 1; Model S1 F1=1; IF (FORM.EQ.1) F1=THETA(11)*EXP(ETA(4))
WT on CL / V1 (power, 73.7 kg) 0.403 / 0.606 Table 1 and equations; Model S1 (WGT/73.7)
ALB on CL (power, 42.9 g/L) -0.243 Table 1 and equation
Female on CL / V1 (ratio) 0.792 / 0.893 Table 1
ADA-positive on CL (ratio) 1.09 Table 1
Japanese on V2 (ratio) 0.455 Table 1
IIV CL, V1, V2, F (CV% = 100 x SD) 38.9, 14.0, 99.5, 34.2 Table 1
Proportional RUV study 101 / 104 / 201 14.0%, 19.7%, 30.3% Table 1; Model S1 $ERROR (W = F + 0.01)
SUVr baseline, Kin 1.34, 0.232 SUVr/year Table 2; Model S2 KIN = THETA(2)/8760, KOUT = KIN/BSL
SUVr Emax, EC50 1.54, 75.0 ug/mL Table 2
APOE4 carrier on SUVr baseline (ratio) 1.04 Table 2
Age on Emax (power, 72 years) 1.58 Table 2; Model S2 (AGE/72)**THETA(5)
IIV SUVr baseline, Emax; correlation 10.9%, 50.3%; R = 0.669 Table 2; Model S2 $OMEGA BLOCK(2)
SUVr proportional RUV 5.01% Table 2
Abeta42/40 baseline, Kout, slope 0.0842, 0.367 /year, 0.00155 per ug/mL Table 3
IIV Abeta42/40 baseline, slope 6.78%, 44.1% Table 3
Abeta42/40 proportional RUV 6.41% Table 3; Model S3 W = IPRED*THETA(4), $SIGMA 1 FIX
p-tau181 baseline, Kout, slope 4.06 pg/mL, 0.468 /year, 0.00313 per ug/mL Table 3
WT on p-tau181 baseline (power, 72.2 kg) -0.300 Table 3; Model S4 (BWGTC/72.2)**THETA(5)
IIV p-tau181 baseline (exponential), slope (additive SD) 35.1%, 0.00151 per ug/mL Table 3; Model S4 SLOPE = THETA(3)+ETA(2)
p-tau181 proportional RUV 19.4% Table 3
PK ODEs n/a Figure 1; Model S1 ADVAN3 TRANS4
d/dt(suvr) = kin - suvr * kout * (1 + emax * Cc / (ec50 + Cc)) n/a Figure 1; Model S2 $DES
d/dt(abeta4240) = kin * (1 + slope * Cc) - kout * abeta4240 n/a Figure 1; Model S3 $DES
d/dt(ptau181) = kin * (1 - slope * (Cc + 0.01)) - kout * ptau181 n/a Figure 1; Model S4 $DES

Event-table helpers

Doses are 10 mg/kg intravenous infusions over 1 hour into central. The reference subject of the Figure 2 forest plot is a 73.7 kg non-Japanese male, albumin 42.9 g/L, ADA-negative, dosed with Process A material; the PD reference subject is 72 years old and an APOE4 non-carrier.

h_per_year <- 8760
h_per_month <- h_per_year / 12

ref_cov <- list(
  WT = 73.7, ALB = 42.9, SEXF = 0, ADA_POS = 0, RACE_JAPANESE = 0,
  FORM_LEC_PROCESSB = 0, STUDY_LEC101 = 0, STUDY_LEC104 = 0,
  AGE = 72, APOE4_CARRIER = 0
)

# One subject: 10 mg/kg infusions every `tau` hours from time 0 until
# `dose_end` (exclusive), observations on the ODE state `obs_cmt`.
make_subject <- function(id, tau, dose_end, obs_times, obs_cmt = "central",
                         label = NA_character_, cov = list()) {
  covs <- utils::modifyList(ref_cov, cov)
  dose_times <- if (dose_end > 0) seq(0, dose_end - 1e-6, by = tau) else numeric(0)
  amt <- 10 * covs$WT
  doses <- if (length(dose_times) > 0) {
    data.frame(
      id = id, time = dose_times, evid = 1L, amt = amt, rate = amt,
      cmt = "central"
    )
  }
  obs <- data.frame(
    id = id, time = sort(unique(obs_times)), evid = 0L, amt = 0, rate = 0,
    cmt = obs_cmt
  )
  ev <- dplyr::bind_rows(doses, obs) |>
    dplyr::arrange(time, dplyr::desc(evid))
  for (nm in names(covs)) ev[[nm]] <- covs[[nm]]
  ev$scenario <- label
  ev
}

PK model

Terminal half-life

The Discussion gives a typical terminal half-life of about 9.5 days. The closed form from the Table 1 micro-constants:

k10 <- 0.0181 / 3.22
k12 <- 0.0349 / 3.22
k21 <- 0.0349 / 2.19
beta <- 0.5 * (k10 + k12 + k21 - sqrt((k10 + k12 + k21)^2 - 4 * k10 * k21))
t_half_days <- log(2) / beta / 24
t_half_days
#> [1] 9.460475
stopifnot(abs(t_half_days - 9.5) < 0.1)

The same quantity estimated by PKNCA from a simulated single 10 mg/kg dose in the reference subject:

sd_times <- c(0, 0.5, 1, 1.5, 2, 4, 8, 24, 48, 96, 168, 336, 504, 672,
              1008, 1344, 1680, 2016)
ev_sd <- make_subject(1L, tau = 1e6, dose_end = 1, obs_times = sd_times,
                      label = "10 mg/kg single dose")
sim_sd <- rxode2::rxSolve(rxode2::zeroRe(mods$pk), ev_sd, keep = "scenario",
                          returnType = "data.frame")
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalvp', 'etalfcentral'
if (is.null(sim_sd$id)) sim_sd$id <- 1L

conc_sd <- sim_sd |>
  dplyr::filter(!is.na(Cc)) |>
  dplyr::select(id, time, Cc, scenario)
dose_sd <- ev_sd |>
  dplyr::filter(evid == 1) |>
  dplyr::select(id, time, amt, scenario)
nca_sd <- PKNCA::pk.nca(PKNCA::PKNCAdata(
  PKNCA::PKNCAconc(conc_sd, Cc ~ time | scenario + id),
  PKNCA::PKNCAdose(dose_sd, amt ~ time | scenario + id),
  intervals = data.frame(start = 0, end = Inf, cmax = TRUE, half.life = TRUE)
))

published_sd <- data.frame(scenario = "10 mg/kg single dose", half.life = 9.5 * 24)
cmp_sd <- nlmixr2lib::ncaComparisonTable(
  simulated = nca_sd,
  reference = published_sd,
  by = "scenario",
  params = "half.life",
  units = c(half.life = "h")
)
knitr::kable(cmp_sd, caption = "Typical terminal half-life: simulated vs the Discussion's ~9.5 days.")
Typical terminal half-life: simulated vs the Discussion’s ~9.5 days.
NCA parameter scenario Reference Simulated % diff
t½ (h) 10 mg/kg single dose 228 227 -0.5%

Steady-state covariate effects (Figure 2)

Figure 2 shows steady-state AUC and Cmax after 10 mg/kg biweekly for the 5th and 95th percentiles of weight and albumin and for each binary covariate, relative to the reference subject. The typical-value model is simulated for one year (26 doses) and PKNCA computes Cmax and AUC over the last dosing interval. Because the dose is weight-based, the weight rows combine the dose change with the CL and V1 effects.

scenarios <- list(
  "Reference" = list(),
  "Weight 51 kg" = list(WT = 51),
  "Weight 99 kg" = list(WT = 99),
  "Albumin 38 g/L" = list(ALB = 38),
  "Albumin 48 g/L" = list(ALB = 48),
  "Female" = list(SEXF = 1),
  "Japanese" = list(RACE_JAPANESE = 1),
  "Process B" = list(FORM_LEC_PROCESSB = 1),
  "ADA-positive" = list(ADA_POS = 1)
)
tau <- 14 * 24
t_last <- 25 * tau
ss_times <- t_last + c(0, 0.5, 1, 1.5, 2, 4, 8, 12, 24, 48, 96, 168, 240, 336)
ev_ss <- dplyr::bind_rows(lapply(seq_along(scenarios), function(i) {
  make_subject(i, tau = tau, dose_end = t_last + 1, obs_times = ss_times,
               label = names(scenarios)[i], cov = scenarios[[i]])
}))
stopifnot(!anyDuplicated(unique(ev_ss[, c("id", "time", "evid")])))

sim_ss <- rxode2::rxSolve(rxode2::zeroRe(mods$pk), ev_ss, keep = "scenario",
                          returnType = "data.frame")
#> Warning: No sigma parameters in the model
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalvp', 'etalfcentral'
#> Warning: multi-subject simulation without without 'omega'

conc_ss <- sim_ss |>
  dplyr::filter(!is.na(Cc)) |>
  dplyr::select(id, time, Cc, scenario)
dose_ss <- ev_ss |>
  dplyr::filter(evid == 1) |>
  dplyr::select(id, time, amt, scenario)
nca_ss <- PKNCA::pk.nca(PKNCA::PKNCAdata(
  PKNCA::PKNCAconc(conc_ss, Cc ~ time | scenario + id),
  PKNCA::PKNCAdose(dose_ss, amt ~ time | scenario + id),
  intervals = data.frame(start = t_last, end = t_last + tau,
                         cmax = TRUE, auclast = TRUE)
))
ss_wide <- as.data.frame(nca_ss) |>
  dplyr::filter(PPTESTCD %in% c("cmax", "auclast")) |>
  dplyr::select(scenario, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES)
ref_row <- ss_wide[ss_wide$scenario == "Reference", ]
ss_ratio <- ss_wide |>
  dplyr::mutate(auclast = auclast / ref_row$auclast, cmax = cmax / ref_row$cmax)
knitr::kable(
  ss_wide |>
    dplyr::rename("Scenario" = scenario, "AUCtau (ug*h/mL)" = auclast, "Cmax (ug/mL)" = cmax),
  digits = 1,
  caption = "Typical steady-state exposure after 10 mg/kg biweekly (last interval of 26 doses)."
)
Typical steady-state exposure after 10 mg/kg biweekly (last interval of 26 doses).
Scenario AUCtau (ug*h/mL) Cmax (ug/mL)
ADA-positive 37536.9 281.1
Albumin 38 g/L 39717.7 286.6
Albumin 48 g/L 42028.3 292.5
Female 51628.2 341.5
Japanese 40802.7 280.6
Process B 40819.0 289.0
Reference 40900.9 289.6
Weight 51 kg 32852.2 246.8
Weight 99 kg 48754.5 329.8

The AUC ratio has a closed form, dose ratio x F / CL ratio, against which the PKNCA ratios are checked tightly; the Figure 2 medians were read off the published plot by the maintainers (reading precision about +/- 0.02).

closed_form_auc <- c(
  "Reference" = 1,
  "Weight 51 kg" = (51 / 73.7)^(1 - 0.403),
  "Weight 99 kg" = (99 / 73.7)^(1 - 0.403),
  "Albumin 38 g/L" = (38 / 42.9)^0.243,
  "Albumin 48 g/L" = (48 / 42.9)^0.243,
  "Female" = 1 / 0.792,
  "Japanese" = 1,
  "Process B" = 0.998,
  "ADA-positive" = 1 / 1.09
)
stopifnot(all(abs(ss_ratio$auclast - closed_form_auc[ss_ratio$scenario]) < 0.005))

# Medians digitised from Figure 2 (circle = AUC, triangle = Cmax).
figure2 <- data.frame(
  scenario = names(scenarios),
  auclast = c(1.00, 0.805, 1.20, 0.97, 1.025, 1.265, 1.00, 1.00, 0.915),
  cmax = c(1.00, 0.855, 1.14, 0.99, 1.01, 1.18, 0.97, 0.995, 0.97)
)
cmp_forest <- nlmixr2lib::ncaComparisonTable(
  simulated = ss_ratio,
  reference = figure2,
  by = "scenario",
  units = c(auclast = "ratio to reference", cmax = "ratio to reference"),
  tolerance_pct = 5
)
knitr::kable(cmp_forest, caption = "Steady-state AUCtau and Cmax relative to the reference subject: simulated vs Figure 2 (digitised medians). * differs by > 5%.")
Steady-state AUCtau and Cmax relative to the reference subject: simulated vs Figure 2 (digitised medians). * differs by > 5%.
NCA parameter scenario Reference Simulated % diff
Cmax (ratio to reference) Reference 1 1 +0.0%
Cmax (ratio to reference) Weight 51 kg 0.855 0.852 -0.3%
Cmax (ratio to reference) Weight 99 kg 1.14 1.14 -0.1%
Cmax (ratio to reference) Albumin 38 g/L 0.99 0.99 -0.0%
Cmax (ratio to reference) Albumin 48 g/L 1.01 1.01 +0.0%
Cmax (ratio to reference) Female 1.18 1.18 -0.1%
Cmax (ratio to reference) Japanese 0.97 0.969 -0.1%
Cmax (ratio to reference) Process B 0.995 0.998 +0.3%
Cmax (ratio to reference) ADA-positive 0.97 0.97 +0.1%
AUClast (ratio to reference) Reference 1 1 +0.0%
AUClast (ratio to reference) Weight 51 kg 0.805 0.803 -0.2%
AUClast (ratio to reference) Weight 99 kg 1.2 1.19 -0.7%
AUClast (ratio to reference) Albumin 38 g/L 0.97 0.971 +0.1%
AUClast (ratio to reference) Albumin 48 g/L 1.02 1.03 +0.3%
AUClast (ratio to reference) Female 1.26 1.26 -0.2%
AUClast (ratio to reference) Japanese 1 0.998 -0.2%
AUClast (ratio to reference) Process B 1 0.998 -0.2%
AUClast (ratio to reference) ADA-positive 0.915 0.918 +0.3%
stopifnot(all(abs(ss_ratio$auclast - figure2$auclast[match(ss_ratio$scenario, figure2$scenario)]) < 0.04))
stopifnot(all(abs(ss_ratio$cmax - figure2$cmax[match(ss_ratio$scenario, figure2$scenario)]) < 0.04))

Amyloid PET SUVr model

Re-accumulation half-life

Kout = Kin / baseline = 0.232 / 1.34 = 0.173 per year, the value quoted in the Discussion, so the recovery half-life after treatment stops is about 4 years:

kout_suvr <- 0.232 / 1.34
c(kout_per_year = kout_suvr, recovery_half_life_years = log(2) / kout_suvr)
#>            kout_per_year recovery_half_life_years 
#>                0.1731343                4.0035225
stopifnot(abs(kout_suvr - 0.173) < 0.001, abs(log(2) / kout_suvr - 4) < 0.05)

Typical profiles (Figures 4 and 5)

Figure 4 compares 10 mg/kg biweekly with 10 mg/kg monthly for 18 months, with the baseline SUVr set to 1.38 (the study 201 median). Figure 5 continues 10 mg/kg biweekly for 15 years or stops it at 18 months. The same four regimens are simulated for all three biomarkers from the typical-value models; the SUVr baseline is overridden to 1.38 as in the figures.

t18 <- 18 * h_per_month
t15y <- 15 * h_per_year
obs_typ <- sort(unique(c(seq(0, t15y, by = 14 * 24), t18, t15y)))
regimens <- list(
  "10 mg/kg biweekly, 18 months" = list(tau = 14 * 24, dose_end = t18),
  "10 mg/kg monthly, 18 months" = list(tau = 28 * 24, dose_end = t18),
  "10 mg/kg biweekly, continuous" = list(tau = 14 * 24, dose_end = t15y)
)
sim_typical <- function(endpoint, ui) {
  ev <- dplyr::bind_rows(lapply(seq_along(regimens), function(i) {
    make_subject(i, tau = regimens[[i]]$tau, dose_end = regimens[[i]]$dose_end,
                 obs_times = obs_typ, obs_cmt = endpoint, label = names(regimens)[i])
  }))
  sim <- rxode2::rxSolve(rxode2::zeroRe(ui), ev, keep = "scenario",
                         returnType = "data.frame")
  data.frame(endpoint = endpoint, scenario = sim$scenario, time = sim$time,
             value = sim[[endpoint]])
}
typ <- dplyr::bind_rows(
  sim_typical("suvr", rxode2::ini(mods$suvr, lrbase = log(1.38))),
  sim_typical("abeta4240", mods$abeta4240),
  sim_typical("ptau181", mods$ptau181)
)
#> ℹ change initial estimate of `lrbase` to `0.322083499169113`
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalvp', 'etalfcentral', 'etalrbase', 'etalemax'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalvp', 'etalfcentral', 'etalrbase', 'etalslope'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalvp', 'etalfcentral', 'etalrbase', 'etaslope'
#> Warning: multi-subject simulation without without 'omega'
typ |>
  dplyr::filter(time <= t18, scenario != "10 mg/kg biweekly, continuous") |>
  ggplot(aes(time / h_per_month, value, colour = scenario)) +
  geom_line() +
  geom_hline(data = data.frame(endpoint = "suvr", y = 1.17),
             aes(yintercept = y), linetype = "dashed") +
  facet_wrap(~endpoint, scales = "free_y") +
  labs(x = "Time (months)", y = NULL, colour = NULL,
       caption = "Replicates Figure 4 of Hayato 2022 (typical values; dashed line: SUVr 1.17).") +
  theme(legend.position = "bottom")

typ |>
  dplyr::filter(scenario != "10 mg/kg monthly, 18 months") |>
  ggplot(aes(time / h_per_year, value, colour = scenario)) +
  geom_line() +
  geom_vline(xintercept = 1.5, linetype = "dotted") +
  facet_wrap(~endpoint, scales = "free_y") +
  labs(x = "Time (years)", y = NULL, colour = NULL,
       caption = "Replicates Figure 5 of Hayato 2022 (typical values).") +
  theme(legend.position = "bottom")

The values at 18 months and at 15 years, against values read off Figures 4 and 5 by the maintainers. The published curves are medians over 1000 draws of the PD parameter uncertainty, so a close but not exact match is expected.

pick <- function(ep, sc, t) {
  v <- typ$value[typ$endpoint == ep & typ$scenario == sc & abs(typ$time - t) < 1e-6]
  if (length(v) != 1L) stop("no unique row for ", ep, " / ", sc, " at ", t)
  v
}
checks <- tibble::tribble(
  ~endpoint, ~scenario, ~time_h, ~figure, ~published,
  "suvr", "10 mg/kg biweekly, 18 months", t18, "Figure 4", 1.14,
  "suvr", "10 mg/kg monthly, 18 months", t18, "Figure 4", 1.215,
  "abeta4240", "10 mg/kg biweekly, 18 months", t18, "Figure 4", 0.0904,
  "abeta4240", "10 mg/kg monthly, 18 months", t18, "Figure 4", 0.0873,
  "ptau181", "10 mg/kg biweekly, 18 months", t18, "Figure 4", 3.33,
  "ptau181", "10 mg/kg monthly, 18 months", t18, "Figure 4", 3.68,
  "suvr", "10 mg/kg biweekly, continuous", t15y, "Figure 5", 0.725,
  "suvr", "10 mg/kg biweekly, 18 months", t15y, "Figure 5", 1.355,
  "abeta4240", "10 mg/kg biweekly, continuous", t15y, "Figure 5", 0.0987,
  "ptau181", "10 mg/kg biweekly, continuous", t15y, "Figure 5", 2.62
)
checks$simulated <- mapply(pick, checks$endpoint, checks$scenario, checks$time_h)
checks$pct_diff <- 100 * (checks$simulated / checks$published - 1)
checks |>
  dplyr::mutate(time = ifelse(time_h == t18, "18 months", "15 years")) |>
  dplyr::select(figure, endpoint, scenario, time, published, simulated, pct_diff) |>
  dplyr::rename("Figure" = figure, "Endpoint" = endpoint, "Regimen" = scenario,
                "Time" = time, "Published (digitised)" = published,
                "Simulated" = simulated, "% diff" = pct_diff) |>
  knitr::kable(digits = c(0, 0, 0, 0, 4, 4, 1),
               caption = "Typical biomarker values: simulated vs Figures 4 and 5.")
Typical biomarker values: simulated vs Figures 4 and 5.
Figure Endpoint Regimen Time Published (digitised) Simulated % diff
Figure 4 suvr 10 mg/kg biweekly, 18 months 18 months 1.1400 1.1283 -1.0
Figure 4 suvr 10 mg/kg monthly, 18 months 18 months 1.2150 1.2034 -1.0
Figure 4 abeta4240 10 mg/kg biweekly, 18 months 18 months 0.0904 0.0908 0.5
Figure 4 abeta4240 10 mg/kg monthly, 18 months 18 months 0.0873 0.0876 0.3
Figure 4 ptau181 10 mg/kg biweekly, 18 months 18 months 3.3300 3.2688 -1.8
Figure 4 ptau181 10 mg/kg monthly, 18 months 18 months 3.6800 3.6467 -0.9
Figure 5 suvr 10 mg/kg biweekly, continuous 15 years 0.7250 0.7257 0.1
Figure 5 suvr 10 mg/kg biweekly, 18 months 15 years 1.3550 1.3527 -0.2
Figure 5 abeta4240 10 mg/kg biweekly, continuous 15 years 0.0987 0.0999 1.3
Figure 5 ptau181 10 mg/kg biweekly, continuous 15 years 2.6200 2.5068 -4.3
stopifnot(all(abs(checks$pct_diff) < 6))

The largest difference is the p-tau181 plateau under 15 years of continuous dosing (about 4% below the figure); the 18-month values, which the trial data inform directly, agree within 2%.

Age effect on the SUVr reduction (Figure S5)

The Results state that, after 18 months of 10 mg/kg biweekly, an 84-year-old has a 24% larger and a 57-year-old a 29% smaller SUVr reduction than a 72-year-old reference APOE4 non-carrier.

ages <- c(72, 84, 57)
ev_age <- dplyr::bind_rows(lapply(seq_along(ages), function(i) {
  make_subject(i, tau = 14 * 24, dose_end = t18, obs_times = c(0, t18),
               obs_cmt = "suvr", label = paste0("Age ", ages[i]),
               cov = list(AGE = ages[i]))
}))
sim_age <- rxode2::rxSolve(rxode2::zeroRe(mods$suvr), ev_age, keep = "scenario",
                           returnType = "data.frame")
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalvp', 'etalfcentral', 'etalrbase', 'etalemax'
#> Warning: multi-subject simulation without without 'omega'
age_tab <- sim_age |>
  dplyr::group_by(scenario) |>
  dplyr::summarise(change = suvr[time == t18] - suvr[time == 0], .groups = "drop") |>
  dplyr::mutate(relative = change / change[scenario == "Age 72"])
age_tab |>
  dplyr::rename("Subject" = scenario, "SUVr change at 18 months" = change,
                "Relative to age 72" = relative) |>
  knitr::kable(digits = 3,
             caption = "SUVr change from baseline at 18 months relative to a 72-year-old (published: 1.24 at 84 years, 0.71 at 57 years).")
SUVr change from baseline at 18 months relative to a 72-year-old (published: 1.24 at 84 years, 0.71 at 57 years).
Subject SUVr change at 18 months Relative to age 72
Age 57 -0.179 0.715
Age 72 -0.250 1.000
Age 84 -0.310 1.238
stopifnot(
  abs(age_tab$relative[age_tab$scenario == "Age 84"] - 1.24) < 0.01,
  abs(age_tab$relative[age_tab$scenario == "Age 57"] - 0.71) < 0.01
)

Amyloid negativity at 18 months

The paper predicts that 64.6% of subjects reach amyloid negativity (SUVr < 1.17) after 18 months of 10 mg/kg biweekly and 36.7% after 10 mg/kg monthly, from model-predicted individual profiles “considering interindividual variabilities of PD parameters”. The simulation below follows the setting of Figure 4: typical PK, baseline SUVr 1.38, and the Emax interindividual variability, with ages drawn from the SUVr analysis set (mean 71.5, SD 8.1, 50-89 years) and body weight from the same set for the mg/kg dose.

set.seed(2022)
rxode2::rxSetSeed(2022)
n_arm <- 200
rtrunc <- function(n, mean, sd, lo, hi) pmin(pmax(rnorm(n, mean, sd), lo), hi)
make_arm <- function(n, tau, id_offset, label) {
  age <- rtrunc(n, 71.5, 8.1, 50, 89)
  wt <- rtrunc(n, 74.3, 15.1, 40, 119)
  dplyr::bind_rows(lapply(seq_len(n), function(i) {
    make_subject(id_offset + i, tau = tau, dose_end = t18,
                 obs_times = c(0, t18), obs_cmt = "suvr", label = label,
                 cov = list(AGE = age[i], WT = wt[i]))
  }))
}
ev_neg <- dplyr::bind_rows(
  make_arm(n_arm, 14 * 24, 0L, "10 mg/kg biweekly"),
  make_arm(n_arm, 28 * 24, 1000L, "10 mg/kg monthly")
)
stopifnot(!anyDuplicated(unique(ev_neg[, c("id", "time", "evid")])))

# Baseline fixed at 1.38 for every subject (baseline IIV switched off) and PK
# at typical values; Emax keeps its estimated IIV.
mod_neg <- mods$suvr |>
  rxode2::ini(
    lrbase = log(1.38),
    etalrbase + etalemax ~ c(1e-12, 0, 0.253009),
    etalcl ~ fix(0), etalvc ~ fix(0), etalvp ~ fix(0), etalfcentral ~ fix(0)
  )
#> ℹ change initial estimate of `lrbase` to `0.322083499169113`
#> ℹ some correlations may have been dropped for the variables: `etalrbase`, `etalemax`
#> ℹ the piping should specify the needed covariances directly
#> ℹ change initial estimate of `etalrbase` to `1e-12`
#> ℹ change initial estimate of `etalemax` to `0.253009`
#> Warning: trying to fix 'etalcl', but already fixed
#> ℹ change initial estimate of `etalcl` to `0`
#> Warning: trying to fix 'etalvc', but already fixed
#> ℹ change initial estimate of `etalvc` to `0`
#> Warning: trying to fix 'etalvp', but already fixed
#> ℹ change initial estimate of `etalvp` to `0`
#> Warning: trying to fix 'etalfcentral', but already fixed
#> ℹ change initial estimate of `etalfcentral` to `0`
sim_neg <- rxode2::rxSolve(mod_neg, ev_neg, keep = "scenario",
                           returnType = "data.frame")
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalvp', 'etalfcentral'
neg_tab <- sim_neg |>
  dplyr::filter(time == t18) |>
  dplyr::group_by(scenario) |>
  dplyr::summarise(pct_negative = 100 * mean(suvr < 1.17),
                   median_suvr = median(suvr), .groups = "drop") |>
  dplyr::mutate(published = c(64.6, 36.7)[match(scenario, c("10 mg/kg biweekly", "10 mg/kg monthly"))])
neg_tab |>
  dplyr::rename("Regimen" = scenario, "Simulated % SUVr < 1.17" = pct_negative,
                "Median SUVr at 18 months" = median_suvr,
                "Published %" = published) |>
  knitr::kable(digits = c(0, 1, 3, 1),
               caption = "Predicted percentage of subjects amyloid-negative after 18 months.")
Predicted percentage of subjects amyloid-negative after 18 months.
Regimen Simulated % SUVr < 1.17 Median SUVr at 18 months Published %
10 mg/kg biweekly 62.5 1.143 64.6
10 mg/kg monthly 30.0 1.215 36.7
# A 200-subject arm has a binomial SD of about 3.5 percentage points; 12
# points is about 3.5 SD, wide enough for any cohort draw and narrow enough
# to catch a mis-transcribed Emax, EC50 or Kin.
stopifnot(
  all(abs(neg_tab$pct_negative - neg_tab$published) < 12),
  neg_tab$pct_negative[neg_tab$scenario == "10 mg/kg biweekly"] -
    neg_tab$pct_negative[neg_tab$scenario == "10 mg/kg monthly"] > 10
)

Plasma Abeta42/40 ratio and p-tau181 models

Recovery half-lives and the weight effect

The Discussion gives recovery half-lives of about 1.9 years for the Abeta42/40 ratio and 1.5 years for p-tau181 (ln 2 / Kout), and the Results give an 11.7% higher p-tau181 baseline at 50 kg and an 8.2% lower one at 96 kg than at 72 kg.

hl <- c(abeta4240 = log(2) / 0.367, ptau181 = log(2) / 0.468)
wt_effect <- 100 * ((c(50, 96) / 72.2)^-0.300 - 1)
hl
#> abeta4240   ptau181 
#>  1.888684  1.481084
wt_effect
#> [1] 11.652939 -8.192156
stopifnot(
  abs(hl[["abeta4240"]] - 1.9) < 0.05, abs(hl[["ptau181"]] - 1.5) < 0.05,
  abs(wt_effect[1] - 11.7) < 0.1, abs(wt_effect[2] + 8.2) < 0.1
)

The typical 18-month and long-term values for both plasma biomarkers are in the Figure 4 / Figure 5 table above.

Stochastic simulation of the study 201 Core

A population simulation of the 18-month Core phase (placebo vs 10 mg/kg biweekly, 100 subjects per arm, full PK and PD variability) shows the spread the PK/PD models produce; compare the Core portion (0-18 months) of the prediction-corrected VPCs in Figure 3.

set.seed(201)
rxode2::rxSetSeed(201)
n_vpc <- 100
obs_core <- seq(0, t18, by = 4 * 7 * 24)
make_core <- function(endpoint, n, dose_end, id_offset, label) {
  wt <- rtrunc(n, 72.5, 14.6, 40, 119)
  alb <- rtrunc(n, 42.9, 2.9, 35, 53)
  age <- rtrunc(n, 71.5, 8.1, 50, 89)
  sexf <- rbinom(n, 1, 0.5)
  apoe <- rbinom(n, 1, 0.7)
  jpn <- rbinom(n, 1, 0.06)
  dplyr::bind_rows(lapply(seq_len(n), function(i) {
    make_subject(id_offset + i, tau = 14 * 24, dose_end = dose_end,
                 obs_times = obs_core, obs_cmt = endpoint, label = label,
                 cov = list(WT = wt[i], ALB = alb[i], AGE = age[i], SEXF = sexf[i],
                            APOE4_CARRIER = apoe[i], RACE_JAPANESE = jpn[i]))
  }))
}
vpc <- dplyr::bind_rows(lapply(c("suvr", "abeta4240", "ptau181"), function(ep) {
  ev <- dplyr::bind_rows(
    make_core(ep, n_vpc, 0, 0L, "Placebo"),
    make_core(ep, n_vpc, t18, 1000L, "10 mg/kg biweekly")
  )
  sim <- rxode2::rxSolve(mods[[ep]], ev, keep = "scenario", returnType = "data.frame")
  data.frame(endpoint = ep, scenario = sim$scenario, id = sim$id, time = sim$time,
             value = sim$ipredSim)
}))
vpc |>
  dplyr::group_by(endpoint, scenario, time) |>
  dplyr::summarise(q05 = quantile(value, 0.05), q50 = median(value),
                   q95 = quantile(value, 0.95), .groups = "drop") |>
  ggplot(aes(time / h_per_month, q50, colour = scenario, fill = scenario)) +
  geom_ribbon(aes(ymin = q05, ymax = q95), alpha = 0.15, colour = NA) +
  geom_line() +
  facet_wrap(~endpoint, scales = "free_y", ncol = 1) +
  labs(x = "Time (months)", y = NULL, colour = NULL, fill = NULL,
       caption = "Median and 90% interval of individual predictions; compare the Core phase of Figure 3 of Hayato 2022.") +
  theme(legend.position = "bottom")


# Placebo profiles stay at their individual baselines (the untreated turnover
# is at steady state; the p-tau181 0.01 ug/mL offset moves it by 0.003%).
plac <- vpc |>
  dplyr::filter(scenario == "Placebo") |>
  dplyr::group_by(endpoint, id) |>
  dplyr::summarise(drift = max(abs(value / value[time == 0] - 1)), .groups = "drop")
stopifnot(all(plac$drift < 1e-3))

Assumptions and deviations

  • APOE4 coding of the SUVr baseline effect. The Table 2 footnote reads “APOE = 0 (APOE4 carrier) or 1 (non-carrier)”, which would make non-carriers 4% higher. The Table 2 row label (“APOE4 carrier ~ baseline”), the Results (“APOE4 carrier subjects have higher baseline SUVr”) and the Discussion (“APOE4 carriers have slightly higher baseline SUVr than non-carriers”) all make carriers the 1-level, so the maintainers treat the footnote as an inversion and apply the 1.04 ratio to APOE4_CARRIER = 1.
  • Baseline SUVr / Emax correlation. Table 2 gives R = 0.669 (bootstrap median 0.674); the Results text quotes 0.608. The table value is used.
  • p-tau181 baseline equation. The footnote under Table 3 prints the typical baseline as “0.00313 x (BW/72.2)^-0.300”, repeating the slope estimate; Table 3 and Model S4 give the baseline theta as 4.06 pg/mL, which is used.
  • Weight normalisation of the PK model. The control stream and the Table 1 equations normalise weight to 73.7 kg; the Figure 2 caption names a 73.4 kg reference subject. 73.7 kg is used.
  • p-tau181 concentration offset. Model S4 $DES drives the inhibition with C1A + 0.01 (ug/mL); Figure 1 prints the equation without the offset. The control-stream form is used; it shifts the untreated steady state by 0.003%.
  • Diagnosis and ADA on the p-tau181 baseline. Model S4 carries both as THETA = 1 FIX after backward elimination (Table S10); they have no effect and are not encoded.
  • PK residual error. Model S1 $ERROR is W = F + 0.01; Y = W + W*ERR, so the residual SD is sigma x (prediction + 0.01 ug/mL). This is encoded as a combined1() error with the additive SD equal to 0.01 ug/mL times the study-specific proportional SD. The study is selected with STUDY_LEC101 and STUDY_LEC104 (both 0 = study 201).
  • Sequential fitting. The paper fitted each PK/PD model on the post hoc individual PK parameters of the PK model. The library PK/PD models instead embed the population PK model with its parameters and IIV fixed, so a simulation draws PK and PD variability together. Residual error on serum concentration is carried only by Hayato_2022_lecanemab.
  • Year length. All three PK/PD control streams convert per-year rate constants with 8760 h per year (365 days); the models do the same.
  • Amyloid-negativity simulation. The paper does not state the cohort behind its 64.6% / 36.7% figures beyond “interindividual variabilities of PD parameters”. The maintainers reproduced them with the Figure 4 setting (baseline 1.38, typical PK) and the Emax IIV; with the baseline IIV and the model’s APOE4-dependent baseline switched on as well, the same cohort gives roughly 70% and 47%.
  • Virtual cohorts. Covariates are drawn from truncated normal distributions matching the means and SDs of Table S2; sex, APOE4 status and Japanese race are Bernoulli with the Table S2 proportions. ADA status is set negative and Process A material is assumed (Core phase).
  • Digitised values. The Figure 2, 4 and 5 comparison values were read off the published figures by the maintainers.
  • No correction notice for this article was found on Europe PMC (checked 2026-10-10).