Validation against independent R implementations and PROC MIXED
Source:vignettes/validation.Rmd
validation.RmdPurpose and evidentiary standard
This vignette reports the numerical comparisons used to validate
lmmix. Every comparison is restricted to a model class for
which the two implementations use the same fixed-effects specification,
covariance model, and estimation criterion. A small difference is
meaningful only after those definitions have been aligned.
Validation is organized in four layers:
- comparisons with independent R implementations for overlapping models;
- structural checks for combined random-effect and residual-correlation models that no single reference R function covers;
- comparisons with published output tables from four official PROC MIXED examples;
- regression comparisons with stored PROC MIXED targets for one fully specified combined model.
The numerical tables below are produced by executable R code when the vignette is built. The official PROC MIXED values are transcribed from the published SAS/STAT output tables. The repository also stores a runnable SAS program, but it does not contain a SAS execution transcript. These sections are therefore documentation-based comparisons, not a new independent SAS run.
Comparison map
| lmmix model | reference function | quantities compared | tolerance |
|---|---|---|---|
| Random-intercept REML | nlme::lme() |
fixed effects and log-likelihood: 1e-6;
variance components: 1e-4
|
absolute |
| Random-slope REML | nlme::lme() |
fixed effects and log-likelihood: 1e-5;
covariance parameters: 1e-4
|
absolute |
| Random-intercept ML | nlme::lme() |
fixed effects, log-likelihood | 1e-6 |
| Marginal CS and AR(1) REML | nlme::gls() |
fixed effects, residual variance, correlation, log-likelihood | 1e-5 |
| Marginal AR(1) REML | mmrm::mmrm() |
fixed effects and log-likelihood: 1e-5;
residual covariance matrix: 1e-4
|
absolute |
| Random-intercept REML | lmerTest::lmer() |
estimates and standard errors: 1e-5;
coefficient df: 5e-3
|
absolute |
| Type III Satterthwaite tests | lmerTest::lmer() |
numerator df: 1e-6; denominator df and F
statistics: 5e-3
|
absolute |
| Marginal AR(1) Kenward-Roger | mmrm::mmrm() |
fixed effects: 1e-5; standard errors:
3e-3; denominator df: 5e-3
|
absolute |
| Crossed random intercepts | lme4::lmer() |
log-likelihood: 1e-5; covariance
parameters: 1e-4
|
absolute |
| Nested fixed-effect models | lme4::anova() |
likelihood-ratio statistic and p-value:
1e-5
|
absolute |
| Official split-plot example 79.1 | PROC MIXED published output | covariance parameters, REML criterion, type III tests | published precision |
| Official repeated-measures example 79.2 | PROC MIXED published output | UN and CS covariance, ML criterion, fixed effects, type III statistics | published precision |
| Official random-coefficients example 79.5 | PROC MIXED published output | random covariance, REML criterion, fixed effects, BLUPs, type III test | published precision |
| Official line-source example 79.6 | PROC MIXED published output | three random variances, TOEP(4) residual
covariance, REML criterion |
published precision |
| Random center plus AR(1) residual | PROC MIXED stored targets | covariance parameters, fixed effects, type III tests, LS-means, differences | target-specific precision |
nlme provides independent likelihood comparisons for
random-effects and structured marginal models (Pinheiro and Bates 2000; Pinheiro et al. 2025).
lmerTest provides an independent Satterthwaite
implementation for models fitted by lme4 (Kuznetsova et al.
2017). mmrm provides a second independent
comparison for the marginal repeated-measures model (Sabanes Bove et al.
2026).
Shared longitudinal data
The R comparisons use the Orthodont data distributed
with nlme. Occasion is created explicitly so
that AR(1) visits have a declared order.
library(lmmix)
orthodont <- as.data.frame(nlme::Orthodont)
orthodont$Occasion <- factor(ave(
seq_len(nrow(orthodont)),
orthodont$Subject,
FUN = seq_along
))
tibble::tibble(
n.obs = nrow(orthodont),
n.subjects = nlevels(orthodont$Subject),
n.occasions = nlevels(orthodont$Occasion)
) |>
show_table(digits = 0)| n obs | n subjects | n occasions |
|---|---|---|
| 108 | 27 | 4 |
Random-intercept REML comparison with nlme::lme()
The first comparison uses the same fixed formula, random intercept, and REML criterion in both implementations.
fit.ri <- lmm(
orthodont,
distance ~ age + Sex,
random = ~ 1 | Subject,
method = "REML"
)
ref.ri <- nlme::lme(
distance ~ age + Sex,
random = ~ 1 | Subject,
data = orthodont,
method = "REML"
)
comparison_table(
term = names(fixef(fit.ri)),
lmmix = fixef(fit.ri),
reference = nlme::fixef(ref.ri)
) |>
show_table()| term | lmmix | reference | abs diff |
|---|---|---|---|
| (Intercept) | 17.706713 | 17.706713 | 0 |
| age | 0.660185 | 0.660185 | 0 |
| SexFemale | -2.321023 | -2.321023 | 0 |
ref.ri.cov <- as.matrix(nlme::getVarCov(
ref.ri,
type = "random.effects"
))
ref.ri.variance <- c(
ref.ri.cov[1L, 1L],
ref.ri$sigma^2
)
comparison_table(
term = c("random.intercept.var", "residual.var"),
lmmix = VarCorr(fit.ri)$estimate,
reference = ref.ri.variance
) |>
show_table()| term | lmmix | reference | abs diff |
|---|---|---|---|
| random.intercept.var | 3.266790 | 3.266784 | 6.0e-06 |
| residual.var | 2.049417 | 2.049456 | 3.9e-05 |
comparison_table(
term = "logLik",
lmmix = logLik(fit.ri),
reference = logLik(ref.ri)
) |>
show_table()| term | lmmix | reference | abs diff |
|---|---|---|---|
| logLik | -218.7563 | -218.7563 | 0 |
Random-slope REML comparison with nlme::lme()
This comparison adds a correlated random slope for age. It checks two random variances, their correlation, the residual variance, the fixed effects, and the REML log-likelihood.
fit.rs <- lmm(
orthodont,
distance ~ age + Sex,
random = ~ 1 + age | Subject,
method = "REML"
)
ref.rs <- nlme::lme(
distance ~ age + Sex,
random = ~ age | Subject,
data = orthodont,
method = "REML"
)
comparison_table(
term = names(fixef(fit.rs)),
lmmix = fixef(fit.rs),
reference = nlme::fixef(ref.rs)
) |>
show_table()| term | lmmix | reference | abs diff |
|---|---|---|---|
| (Intercept) | 17.635201 | 17.635200 | 1e-06 |
| age | 0.660185 | 0.660185 | 0e+00 |
| SexFemale | -2.145493 | -2.145492 | 1e-06 |
ref.rs.cov <- as.matrix(nlme::getVarCov(
ref.rs,
type = "random.effects"
))
ref.rs.parameters <- c(
ref.rs.cov[1L, 1L],
ref.rs.cov[2L, 2L],
ref.rs.cov[1L, 2L] / sqrt(ref.rs.cov[1L, 1L] * ref.rs.cov[2L, 2L]),
ref.rs$sigma^2
)
comparison_table(
term = c(
"random.intercept.var",
"random.age.var",
"random.intercept.age.cor",
"residual.var"
),
lmmix = VarCorr(fit.rs)$estimate,
reference = ref.rs.parameters
) |>
show_table()| term | lmmix | reference | abs diff |
|---|---|---|---|
| random.intercept.var | 7.823340 | 7.823336 | 4e-06 |
| random.age.var | 0.051269 | 0.051269 | 0e+00 |
| random.intercept.age.cor | -0.765844 | -0.765847 | 3e-06 |
| residual.var | 1.716198 | 1.716204 | 7e-06 |
comparison_table(
term = "logLik",
lmmix = logLik(fit.rs),
reference = logLik(ref.rs)
) |>
show_table()| term | lmmix | reference | abs diff |
|---|---|---|---|
| logLik | -217.6169 | -217.6169 | 0 |
Maximum-likelihood comparison with nlme::lme()
The ML comparison verifies that agreement is not specific to the REML criterion.
fit.ml <- lmm(
orthodont,
distance ~ age + Sex,
random = ~ 1 | Subject,
method = "ML"
)
ref.ml <- nlme::lme(
distance ~ age + Sex,
random = ~ 1 | Subject,
data = orthodont,
method = "ML"
)
rbind(
comparison_table(
term = names(fixef(fit.ml)),
lmmix = fixef(fit.ml),
reference = nlme::fixef(ref.ml)
),
comparison_table(
term = "logLik",
lmmix = logLik(fit.ml),
reference = logLik(ref.ml)
)
) |>
show_table()| term | lmmix | reference | abs diff |
|---|---|---|---|
| (Intercept) | 17.706713 | 17.706713 | 0 |
| age | 0.660185 | 0.660185 | 0 |
| SexFemale | -2.321023 | -2.321023 | 0 |
| logLik | -217.428243 | -217.428243 | 0 |
Marginal CS and AR(1) comparisons with nlme::gls()
These models contain no random effect. The repeated-measures covariance is compound symmetric or AR(1), and both implementations optimize a REML criterion.
marginal.fits <- list()
marginal.refs <- list()
for (structure in c("cs", "ar1")) {
marginal.fits[[structure]] <- lmm(
orthodont,
distance ~ age + Sex,
repeated = ~ Occasion | Subject,
structure = structure,
method = "REML"
)
correlation <- if (structure == "cs") {
nlme::corCompSymm(form = ~ 1 | Subject)
} else {
nlme::corAR1(form = ~ 1 | Subject)
}
marginal.refs[[structure]] <- nlme::gls(
distance ~ age + Sex,
data = orthodont,
correlation = correlation,
method = "REML"
)
}
marginal.fixed <- do.call(rbind, lapply(c("cs", "ar1"), function(structure) {
fit <- marginal.fits[[structure]]
ref <- marginal.refs[[structure]]
table <- comparison_table(
term = names(fixef(fit)),
lmmix = fixef(fit),
reference = stats::coef(ref)
)
tibble::add_column(table, structure = structure, .before = 1)
}))
show_table(marginal.fixed)| structure | term | lmmix | reference | abs diff |
|---|---|---|---|---|
| cs | (Intercept) | 17.706713 | 17.706713 | 0 |
| cs | age | 0.660185 | 0.660185 | 0 |
| cs | SexFemale | -2.321023 | -2.321023 | 0 |
| ar1 | (Intercept) | 17.878709 | 17.878709 | 0 |
| ar1 | age | 0.652960 | 0.652960 | 0 |
| ar1 | SexFemale | -2.418714 | -2.418714 | 0 |
marginal.covariance <- do.call(
rbind,
lapply(c("cs", "ar1"), function(structure) {
fit <- marginal.fits[[structure]]
ref <- marginal.refs[[structure]]
reference.parameters <- c(
ref$sigma^2,
as.numeric(stats::coef(
ref$modelStruct$corStruct,
unconstrained = FALSE
))
)
table <- comparison_table(
term = c("residual.var", "residual.cor"),
lmmix = VarCorr(fit)$estimate,
reference = reference.parameters
)
tibble::add_column(table, structure = structure, .before = 1)
})
)
show_table(marginal.covariance)| structure | term | lmmix | reference | abs diff |
|---|---|---|---|---|
| cs | residual.var | 5.316233 | 5.316240 | 7e-06 |
| cs | residual.cor | 0.614490 | 0.614491 | 1e-06 |
| ar1 | residual.var | 5.296881 | 5.296881 | 0e+00 |
| ar1 | residual.cor | 0.625867 | 0.625867 | 0e+00 |
tibble::tibble(
structure = c("cs", "ar1"),
lmmix = vapply(
marginal.fits,
function(x) as.numeric(logLik(x)),
numeric(1)
),
reference = vapply(
marginal.refs,
function(x) as.numeric(logLik(x)),
numeric(1)
)
) |>
transform(abs.diff = abs(lmmix - reference)) |>
show_table()| structure | lmmix | reference | abs diff |
|---|---|---|---|
| cs | -218.7563 | -218.7563 | 0 |
| ar1 | -222.7241 | -222.7241 | 0 |
Marginal AR(1) comparison with mmrm::mmrm()
The mmrm comparison provides a second independent
implementation of the marginal AR(1) model. It checks the complete
residual covariance matrix rather than only the variance and correlation
parameters.
ref.mmrm <- mmrm::mmrm(
distance ~ age + Sex,
data = orthodont,
covariance = mmrm::cov_struct(
type = "ar1",
visits = "Occasion",
subject = "Subject"
),
reml = TRUE
)
fit.mmrm <- marginal.fits[["ar1"]]
comparison_table(
term = names(fixef(fit.mmrm)),
lmmix = fixef(fit.mmrm),
reference = stats::coef(ref.mmrm)
) |>
show_table()| term | lmmix | reference | abs diff |
|---|---|---|---|
| (Intercept) | 17.878709 | 17.878710 | 1e-06 |
| age | 0.652960 | 0.652960 | 0e+00 |
| SexFemale | -2.418714 | -2.418715 | 1e-06 |
tibble::tibble(
quantity = c("logLik", "residual.covariance.max.abs.diff"),
lmmix = c(as.numeric(logLik(fit.mmrm)), NA_real_),
reference = c(as.numeric(logLik(ref.mmrm)), NA_real_),
abs.diff = c(
abs(as.numeric(logLik(fit.mmrm)) - as.numeric(logLik(ref.mmrm))),
max(abs(
unname(fit.mmrm$covariance$residual_base) -
unname(mmrm::component(ref.mmrm, "varcor"))
))
)
) |>
show_table()| quantity | lmmix | reference | abs diff |
|---|---|---|---|
| logLik | -222.7241 | -222.7241 | 0.0e+00 |
| residual.covariance.max.abs.diff | NA | NA | 4.3e-05 |
Satterthwaite comparison with lmerTest::lmer()
The covariance model in this section is a random intercept with independent residuals, which is supported by both packages. The coefficient comparison checks estimates, standard errors, and one-dimensional Satterthwaite degrees of freedom.
ref.lmer <- lmerTest::lmer(
distance ~ age + Sex + (1 | Subject),
data = orthodont,
REML = TRUE
)
lmmix.coef <- generics::tidy(fit.ri)
lmer.coef <- summary(ref.lmer)$coefficients
tibble::tibble(
term = lmmix.coef$term,
lmmix.estimate = lmmix.coef$estimate,
lmerTest.estimate = lmer.coef[, "Estimate"],
estimate.abs.diff = abs(lmmix.estimate - lmerTest.estimate),
lmmix.std.error = lmmix.coef$std.error,
lmerTest.std.error = lmer.coef[, "Std. Error"],
std.error.abs.diff = abs(lmmix.std.error - lmerTest.std.error),
lmmix.df = lmmix.coef$df,
lmerTest.df = lmer.coef[, "df"],
df.abs.diff = abs(lmmix.df - lmerTest.df)
) |>
show_table()| term | lmmix estimate | lmerTest estimate | estimate abs diff | lmmix std error | lmerTest std error | std error abs diff | lmmix df | lmerTest df | df abs diff |
|---|---|---|---|---|---|---|---|---|---|
| (Intercept) | 17.706713 | 17.706713 | 0 | 0.833917 | 0.833922 | 5e-06 | 99.35300 | 99.35237 | 0.000639 |
| age | 0.660185 | 0.660185 | 0 | 0.061605 | 0.061606 | 1e-06 | 80.00151 | 80.00000 | 0.001514 |
| SexFemale | -2.321023 | -2.321023 | 0 | 0.761416 | 0.761417 | 0e+00 | 25.00002 | 25.00000 | 0.000020 |
The type III comparison uses the same fixed-effect terms and the same Satterthwaite denominator-df method.
lmmix.type3 <- anova(fit.ri)
lmer.type3 <- anova(ref.lmer, type = 3)
tibble::tibble(
term = lmmix.type3$term,
lmmix.num.df = lmmix.type3$num.df,
lmerTest.num.df = lmer.type3[, "NumDF"],
num.df.abs.diff = abs(lmmix.num.df - lmerTest.num.df),
lmmix.den.df = lmmix.type3$den.df,
lmerTest.den.df = lmer.type3[, "DenDF"],
den.df.abs.diff = abs(lmmix.den.df - lmerTest.den.df),
lmmix.statistic = lmmix.type3$statistic,
lmerTest.statistic = lmer.type3[, "F value"],
statistic.abs.diff = abs(lmmix.statistic - lmerTest.statistic)
) |>
show_table()| term | lmmix num df | lmerTest num df | num df abs diff | lmmix den df | lmerTest den df | den df abs diff | lmmix statistic | lmerTest statistic | statistic abs diff |
|---|---|---|---|---|---|---|---|---|---|
| age | 1 | 1 | 0 | 80.00151 | 80 | 0.001514 | 114.840466 | 114.838287 | 0.002179 |
| Sex | 1 | 1 | 0 | 25.00002 | 25 | 0.000020 | 9.292108 | 9.292099 | 0.000009 |
Kenward-Roger comparison with mmrm::mmrm()
The Kenward-Roger comparison uses a marginal AR(1) model, which both packages fit by REML. It checks fixed effects, adjusted standard errors, and denominator degrees of freedom.
fit.kr <- lmm(
orthodont,
distance ~ age + Sex,
repeated = ~ Occasion | Subject,
structure = "ar1",
ddf = "kenward-roger"
)
mmrm.formula <- stats::as.formula(
"distance ~ age + Sex + ar1(Occasion | Subject)",
env = asNamespace("mmrm")
)
ref.kr <- mmrm::mmrm(
mmrm.formula,
data = orthodont,
method = "Kenward-Roger"
)
fit.kr.table <- generics::tidy(fit.kr)
ref.kr.table <- coef(summary(ref.kr))
tibble::tibble(
term = fit.kr.table$term,
estimate.abs.diff = abs(fit.kr.table$estimate - coef(ref.kr)),
std.error.abs.diff = abs(
fit.kr.table$std.error - ref.kr.table[, "Std. Error"]
),
df.abs.diff = abs(fit.kr.table$df - ref.kr.table[, "df"])
) |>
show_table()| term | estimate abs diff | std error abs diff | df abs diff |
|---|---|---|---|
| (Intercept) | 1e-06 | 0.001800 | 0.001019 |
| age | 0e+00 | 0.000215 | 0.001574 |
| SexFemale | 1e-06 | 0.001449 | 0.000018 |
Crossed random effects and model comparison
The crossed-intercept comparison uses two independent grouping factors. The likelihood-ratio comparison uses nested fixed-effect models and the same ML covariance specification in both packages.
set.seed(42)
crossed <- expand.grid(
site = factor(seq_len(8)),
observer = factor(seq_len(6)),
replicate = seq_len(3)
)
site.effect <- stats::rnorm(8, sd = 0.8)
observer.effect <- stats::rnorm(6, sd = 0.5)
crossed$response <- 2 +
site.effect[crossed$site] +
observer.effect[crossed$observer] +
stats::rnorm(nrow(crossed), sd = 0.4)
fit.crossed <- lmm(
crossed,
response ~ 1,
random = list(site = ~ 1 | site, observer = ~ 1 | observer)
)
ref.crossed <- lme4::lmer(
response ~ 1 + (1 | site) + (1 | observer),
data = crossed,
REML = TRUE
)
tibble::tibble(
quantity = c(
"logLik", "site variance", "observer variance", "residual variance"
),
lmmix = c(as.numeric(logLik(fit.crossed)), VarCorr(fit.crossed)$estimate),
reference = c(
as.numeric(logLik(ref.crossed)),
as.data.frame(lme4::VarCorr(ref.crossed))$vcov
),
abs.diff = abs(lmmix - reference)
) |>
show_table()| quantity | lmmix | reference | abs diff |
|---|---|---|---|
| logLik | -96.991425 | -96.991425 | 0e+00 |
| site variance | 0.306957 | 0.306963 | 6e-06 |
| observer variance | 0.539502 | 0.539504 | 2e-06 |
| residual variance | 0.157861 | 0.157861 | 0e+00 |
fit.reduced <- lmm(
orthodont,
distance ~ age,
random = ~ 1 | Subject
)
fit.full <- lmm(
orthodont,
distance ~ age + Sex,
random = ~ 1 | Subject
)
lmmix.lrt <- suppressMessages(anova(fit.reduced, fit.full))
ref.reduced <- lme4::lmer(
distance ~ age + (1 | Subject),
data = orthodont,
REML = FALSE
)
ref.full <- lme4::lmer(
distance ~ age + Sex + (1 | Subject),
data = orthodont,
REML = FALSE
)
reference.lrt <- anova(ref.reduced, ref.full, refit = FALSE)
tibble::tibble(
quantity = c("likelihood-ratio statistic", "p-value"),
lmmix = c(lmmix.lrt$Chisq[2], lmmix.lrt$p.value[2]),
reference = c(reference.lrt$Chisq[2], reference.lrt$`Pr(>Chisq)`[2]),
abs.diff = abs(lmmix - reference)
) |>
show_table()| quantity | lmmix | reference | abs diff |
|---|---|---|---|
| likelihood-ratio statistic | 8.533057 | 8.533057 | 0 |
| p-value | 0.003488 | 0.003488 | 0 |
Combined covariance structures
The included multicentre data reproduce the
multilocation repeated-measures example in Section 28.3 of Milliken and Johnson (2009). They contain 153 rows,
including 28 missing responses. The complete-case models therefore use
125 observations.
No single external R function used above fits all of the following
components in one object: a random center effect, a structured
within-subject residual covariance, and the lmmix
Satterthwaite inference layer. For this combined case, all five residual
structures are checked for optimizer convergence, a positive-definite
likelihood Hessian, and a positive-definite marginal covariance
matrix.
combined.fits <- lapply(
c("id", "cs", "ar1", "toep", "un"),
function(structure) {
lmm(
multicentre,
Y ~ Drug * Time,
random = ~ 1 | Center,
repeated = ~ Time | Center:Drug:Subject,
structure = structure,
method = "REML",
ddf = "satterthwaite"
)
}
)
names(combined.fits) <- c("id", "cs", "ar1", "toep", "un")
tibble::tibble(
structure = names(combined.fits),
convergence = vapply(
combined.fits,
function(x) x$convergence$code,
integer(1)
),
hessian.positive.definite = vapply(
combined.fits,
function(x) x$convergence$hessian_positive_definite,
logical(1)
),
min.marginal.eigenvalue = vapply(
combined.fits,
function(x) {
min(eigen(
x$covariance$v,
symmetric = TRUE,
only.values = TRUE
)$values)
},
numeric(1)
)
) |>
show_table()| structure | convergence | hessian positive definite | min marginal eigenvalue |
|---|---|---|---|
| id | 0 | TRUE | 9.458960 |
| cs | 0 | TRUE | 0.618274 |
| ar1 | 0 | TRUE | 0.473359 |
| toep | 0 | TRUE | 0.467053 |
| un | 0 | TRUE | 0.383388 |
Official PROC MIXED examples
The official SAS/STAT 14.3 documentation supplies complete data,
model statements, and published output tables for several PROC MIXED
examples (SAS Institute
Inc. 2017). Four examples match the current
lmmix model space exactly. The package includes their data,
a runnable SAS program, and a machine-readable CSV of the published
numerical targets.
| SAS example |
lmmix coverage |
status |
|---|---|---|
| 79.1 Split-Plot Design | two independent random-intercept terms, REML, type III tests | exact overlap |
| 79.2 Repeated Measures | ML with UN and CS residual covariance | exact likelihood and covariance overlap |
| 79.5 Random Coefficients | correlated random intercept and slope, REML, BLUPs | exact overlap |
| 79.6 Line-Source Sprinkler | three random terms and banded TOEP(4)
residual covariance |
exact covariance and likelihood overlap |
| 79.4 Known G and R | externally fixed multivariate G and R matrices | outside the current API |
| 79.7 and 79.8 Influence | deletion diagnostics | influence diagnostics are not implemented |
The source tables use SAS’s displayed precision. Comparisons therefore use the number of decimals printed in the documentation. The executable files are available through:
system.file(
"validation",
"sas",
package = "lmmix"
)
#> [1] "/home/runner/work/_temp/Library/lmmix/validation/sas"Example 79.1: split-plot design
The split-plot example has random block and block-by-A intercepts. The Satterthwaite denominator degrees of freedom coincide with the containment values published by SAS for this balanced design.
official.split.fit <- lmm(
sas_split_plot,
Y ~ A * B,
random = list(
Block = ~ 1 | Block,
A_Block = ~ 1 | Block:A
),
method = "REML",
ddf = "satterthwaite"
)
split.cov.target <- official_sas_target("split_plot", "covariance")
split.cov.fit <- VarCorr(official.split.fit)$estimate
tibble::tibble(
quantity = split.cov.target$term,
lmmix = split.cov.fit,
SAS.target = split.cov.target$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| quantity | lmmix | SAS target | abs diff |
|---|---|---|---|
| Block | 62.395978 | 62.3958 | 0.000178 |
| A*Block | 15.381958 | 15.3819 | 0.000058 |
| Residual | 9.361148 | 9.3611 | 0.000048 |
tibble::tibble(
quantity = "-2 restricted log likelihood",
lmmix = deviance(official.split.fit),
SAS.target = official_sas_target(
"split_plot",
"deviance"
)$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| quantity | lmmix | SAS target | abs diff |
|---|---|---|---|
| -2 restricted log likelihood | 119.7618 | 119.7618 | 0 |
split.test.target <- official_sas_target("split_plot", "type3")
split.test.fit <- anova(official.split.fit)
tibble::tibble(
term = split.test.fit$term,
lmmix.den.df = split.test.fit$den.df,
SAS.den.df = split.test.target$den_df,
lmmix.F = split.test.fit$statistic,
SAS.F = split.test.target$statistic,
lmmix.p.value = split.test.fit$p.value,
SAS.p.value = split.test.target$p_value
) |>
show_table()| term | lmmix den df | SAS den df | lmmix F | SAS F | lmmix p value | SAS p value |
|---|---|---|---|---|---|---|
| A | 5.999994 | 6 | 4.069568 | 4.07 | 0.076416 | 0.0764 |
| B | 8.999967 | 9 | 19.388647 | 19.39 | 0.001712 | 0.0017 |
| A:B | 8.999967 | 9 | 4.019272 | 4.02 | 0.056578 | 0.0566 |
Example 79.2: repeated measures
The Potthoff-Roy growth data are fitted by ML with the same fixed effects and the same subject-level covariance blocks used in the SAS example. First, the unstructured model reproduces all ten entries of the published covariance matrix.
official.growth.un.fit <- lmm(
sas_growth,
y ~ Gender * Age,
repeated = ~ Age | Person,
structure = "un",
method = "ML"
)
growth.un.target <- official_sas_target("growth_un", "covariance")
growth.un.matrix <- official.growth.un.fit$covariance$residual_base
growth.un.fit <- c(
growth.un.matrix[1, 1],
growth.un.matrix[2, 1], growth.un.matrix[2, 2],
growth.un.matrix[3, 1], growth.un.matrix[3, 2],
growth.un.matrix[3, 3],
growth.un.matrix[4, 1], growth.un.matrix[4, 2],
growth.un.matrix[4, 3], growth.un.matrix[4, 4]
)
tibble::tibble(
covariance.parameter = growth.un.target$term,
lmmix = growth.un.fit,
SAS.target = growth.un.target$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| covariance parameter | lmmix | SAS target | abs diff |
|---|---|---|---|
| UN(1,1) | 5.119365 | 5.1192 | 0.000165 |
| UN(2,1) | 2.440758 | 2.4409 | 0.000142 |
| UN(2,2) | 3.927855 | 3.9279 | 0.000045 |
| UN(3,1) | 3.610416 | 3.6105 | 0.000084 |
| UN(3,2) | 2.717559 | 2.7175 | 0.000059 |
| UN(3,3) | 5.979744 | 5.9798 | 0.000056 |
| UN(4,1) | 2.522030 | 2.5222 | 0.000170 |
| UN(4,2) | 3.062336 | 3.0624 | 0.000064 |
| UN(4,3) | 3.823402 | 3.8235 | 0.000098 |
| UN(4,4) | 4.617993 | 4.6180 | 0.000007 |
tibble::tibble(
quantity = "-2 log likelihood",
lmmix = deviance(official.growth.un.fit),
SAS.target = official_sas_target(
"growth_un",
"deviance"
)$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| quantity | lmmix | SAS target | abs diff |
|---|---|---|---|
| -2 log likelihood | 419.477 | 419.477 | 0 |
SAS uses a last-level reference parameterization, whereas R uses a
first-level treatment contrast by default. The following estimable
transformation puts the lmmix coefficients on the published
SAS scale before comparison.
growth.sas.transformation <- rbind(
Intercept = c(1, 1, 0, 0),
"Gender F" = c(0, -1, 0, 0),
Age = c(0, 0, 1, 1),
"Age*Gender F" = c(0, 0, 0, -1)
)
growth_sas_solution <- function(fit) {
variance <- growth.sas.transformation %*%
vcov(fit) %*%
t(growth.sas.transformation)
tibble::tibble(
term = rownames(growth.sas.transformation),
estimate = as.vector(growth.sas.transformation %*% fixef(fit)),
std.error = sqrt(diag(variance))
)
}
growth.un.solution <- growth_sas_solution(official.growth.un.fit)
growth.un.fixed.target <- official_sas_target("growth_un", "fixed")
tibble::tibble(
term = growth.un.solution$term,
lmmix.estimate = growth.un.solution$estimate,
SAS.estimate = growth.un.fixed.target$estimate,
estimate.abs.diff = abs(lmmix.estimate - SAS.estimate),
lmmix.std.error = growth.un.solution$std.error,
SAS.std.error = growth.un.fixed.target$std_error,
std.error.abs.diff = abs(lmmix.std.error - SAS.std.error)
) |>
show_table()| term | lmmix estimate | SAS estimate | estimate abs diff | lmmix std error | SAS std error | std error abs diff |
|---|---|---|---|---|---|---|
| Intercept | 15.842191 | 15.8423 | 0.000109 | 0.935623 | 0.93560 | 2.3e-05 |
| Gender F | 1.583192 | 1.5831 | 0.000092 | 1.465839 | 1.46580 | 3.9e-05 |
| Age | 0.826813 | 0.8268 | 0.000013 | 0.079117 | 0.07911 | 7.0e-06 |
| Age*Gender F | -0.350450 | -0.3504 | 0.000050 | 0.123952 | 0.12390 | 5.2e-05 |
The compound-symmetry model reproduces the common covariance, diagonal enhancement, fixed-effects solution, and likelihood criterion.
official.growth.cs.fit <- lmm(
sas_growth,
y ~ Gender * Age,
repeated = ~ Age | Person,
structure = "cs",
method = "ML"
)
growth.cs.matrix <- official.growth.cs.fit$covariance$residual_base
growth.cs.fit <- c(
growth.cs.matrix[1, 2],
growth.cs.matrix[1, 1] - growth.cs.matrix[1, 2]
)
growth.cs.target <- official_sas_target("growth_cs", "covariance")
tibble::tibble(
covariance.parameter = growth.cs.target$term,
lmmix = growth.cs.fit,
SAS.target = growth.cs.target$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| covariance parameter | lmmix | SAS target | abs diff |
|---|---|---|---|
| CS | 3.030536 | 3.0306 | 6.4e-05 |
| Residual | 1.874597 | 1.8746 | 3.0e-06 |
growth.cs.solution <- growth_sas_solution(official.growth.cs.fit)
growth.cs.fixed.target <- official_sas_target("growth_cs", "fixed")
tibble::tibble(
term = growth.cs.solution$term,
lmmix.estimate = growth.cs.solution$estimate,
SAS.estimate = growth.cs.fixed.target$estimate,
estimate.abs.diff = abs(lmmix.estimate - SAS.estimate),
lmmix.std.error = growth.cs.solution$std.error,
SAS.std.error = growth.cs.fixed.target$std_error,
std.error.abs.diff = abs(lmmix.std.error - SAS.std.error)
) |>
show_table()| term | lmmix estimate | SAS estimate | estimate abs diff | lmmix std error | SAS std error | std error abs diff |
|---|---|---|---|---|---|---|
| Intercept | 16.340625 | 16.3406 | 2.5e-05 | 0.963084 | 0.96310 | 1.6e-05 |
| Gender F | 1.032102 | 1.0321 | 2.0e-06 | 1.508863 | 1.50890 | 3.7e-05 |
| Age | 0.784375 | 0.7844 | 2.5e-05 | 0.076538 | 0.07654 | 2.0e-06 |
| Age*Gender F | -0.304830 | -0.3048 | 3.0e-05 | 0.119913 | 0.11990 | 1.3e-05 |
PROC MIXED uses the between-within denominator-df method in this
example. lmmix does not currently expose that method, so
the validation compares the likelihood, covariance, fixed-effect
estimates, standard errors, and F statistics, but does not claim
equality of denominator degrees of freedom.
growth.type3 <- rbind(
cbind(
model = "UN",
as.data.frame(anova(official.growth.un.fit))
),
cbind(
model = "CS",
as.data.frame(anova(official.growth.cs.fit))
)
)
growth.type3.target <- rbind(
official_sas_target("growth_un", "type3"),
official_sas_target("growth_cs", "type3")
)
tibble::tibble(
model = growth.type3$model,
term = growth.type3$term,
lmmix.F = growth.type3$statistic,
SAS.F = growth.type3.target$statistic,
abs.diff = abs(lmmix.F - SAS.F)
) |>
show_table()| model | term | lmmix F | SAS F | abs diff |
|---|---|---|---|---|
| UN | Gender | 1.166527 | 1.17 | 0.003473 |
| UN | Age | 110.534895 | 110.54 | 0.005105 |
| UN | Gender:Age | 7.993630 | 7.99 | 0.003630 |
| CS | Gender | 0.467892 | 0.47 | 0.002108 |
| CS | Age | 111.099057 | 111.10 | 0.000943 |
| CS | Gender:Age | 6.462269 | 6.46 | 0.002269 |
Example 79.5: random coefficients
The pharmaceutical stability example fits a correlated random intercept and slope for each batch. It validates the random covariance matrix, residual variance, fixed effects, BLUPs, REML criterion, and the Month test.
official.random.fit <- lmm(
sas_random_coefficients,
Y ~ Month,
random = ~ 1 + Month | Batch,
method = "REML",
ddf = "satterthwaite"
)
random.cov.target <- official_sas_target(
"random_coefficients",
"covariance"
)
random.cov.fit <- c(
official.random.fit$covariance$g$Batch[1, 1],
official.random.fit$covariance$g$Batch[2, 1],
official.random.fit$covariance$g$Batch[2, 2],
official.random.fit$covariance$residual_base[1, 1]
)
tibble::tibble(
covariance.parameter = random.cov.target$term,
lmmix = random.cov.fit,
SAS.target = random.cov.target$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| covariance parameter | lmmix | SAS target | abs diff |
|---|---|---|---|
| UN(1,1) | 0.976738 | 0.97680 | 6.2e-05 |
| UN(2,1) | -0.104473 | -0.10450 | 2.7e-05 |
| UN(2,2) | 0.037166 | 0.03717 | 4.0e-06 |
| Residual | 3.293237 | 3.29320 | 3.7e-05 |
random.fixed.target <- official_sas_target(
"random_coefficients",
"fixed"
)
tibble::tibble(
term = names(fixef(official.random.fit)),
lmmix.estimate = fixef(official.random.fit),
SAS.estimate = random.fixed.target$estimate,
lmmix.std.error = sqrt(diag(vcov(official.random.fit))),
SAS.std.error = random.fixed.target$std_error
) |>
show_table()| term | lmmix estimate | SAS estimate | lmmix std error | SAS std error |
|---|---|---|---|---|
| (Intercept) | 102.703803 | 102.7000 | 0.645594 | 0.6456 |
| Month | -0.525945 | -0.5259 | 0.119373 | 0.1194 |
random.blup.target <- official_sas_target(
"random_coefficients",
"random"
)
random.blup <- ranef(official.random.fit)[[1L]]
tibble::tibble(
term = random.blup.target$term,
lmmix = as.vector(t(as.matrix(
random.blup[c("(Intercept)", "Month")]
))),
SAS.target = random.blup.target$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| term | lmmix | SAS target | abs diff |
|---|---|---|---|
| Intercept Batch 1 | -1.001034 | -1.00100 | 3.4e-05 |
| Month Batch 1 | 0.128680 | 0.12870 | 2.0e-05 |
| Intercept Batch 2 | 0.393400 | 0.39340 | 0.0e+00 |
| Month Batch 2 | -0.205992 | -0.20600 | 8.0e-06 |
| Intercept Batch 3 | 0.607634 | 0.60760 | 3.4e-05 |
| Month Batch 3 | 0.077312 | 0.07731 | 2.0e-06 |
tibble::tibble(
quantity = c("-2 restricted log likelihood", "Month F statistic"),
lmmix = c(
deviance(official.random.fit),
anova(official.random.fit)$statistic
),
SAS.target = c(
official_sas_target(
"random_coefficients",
"deviance"
)$estimate,
official_sas_target(
"random_coefficients",
"type3"
)$statistic
),
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| quantity | lmmix | SAS target | abs diff |
|---|---|---|---|
| -2 restricted log likelihood | 350.32814 | 350.3281 | 0.000000 |
| Month F statistic | 19.41176 | 19.4100 | 0.001765 |
Example 79.6: line-source sprinkler irrigation
The line-source example exercises the combined model directly. It
contains three independent random-intercept terms and a four-band
Toeplitz residual covariance within each block-by-cultivar series. In
structure = "toep(4)", the main diagonal and the first
three covariance bands are estimated, while all longer-lag covariances
are fixed to zero.
official.line.fit <- lmm(
sas_line_source,
Y ~ (Cult + Dir + Irrig)^2,
random = list(
Block = ~ 1 | Block,
Block.Dir = ~ 1 | Block:Dir,
Block.Irrig = ~ 1 | Block:Irrig
),
repeated = ~ Sbplt | Block:Cult,
structure = "toep(4)",
method = "REML",
ddf = "residual"
)
line.target <- official_sas_target("line_source", "covariance")
line.residual <- official.line.fit$covariance$residual_base
line.fit <- c(
official.line.fit$covariance$g$Block[1, 1],
official.line.fit$covariance$g$Block.Dir[1, 1],
official.line.fit$covariance$g$Block.Irrig[1, 1],
line.residual[1, 2],
line.residual[1, 3],
line.residual[1, 4],
line.residual[1, 1]
)
tibble::tibble(
covariance.parameter = line.target$term,
lmmix = line.fit,
SAS.target = line.target$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| covariance parameter | lmmix | SAS target | abs diff |
|---|---|---|---|
| Block | 0.219405 | 0.219400 | 5.0e-06 |
| Block*Dir | 0.017678 | 0.017680 | 2.0e-06 |
| Block*Irrig | 0.035388 | 0.035390 | 2.0e-06 |
| TOEP(2) | 0.007982 | 0.007986 | 4.0e-06 |
| TOEP(3) | 0.001454 | 0.001452 | 2.0e-06 |
| TOEP(4) | -0.092537 | -0.092530 | 7.0e-06 |
| Residual | 0.285018 | 0.285000 | 1.8e-05 |
tibble::tibble(
quantity = "-2 restricted log likelihood",
lmmix = deviance(official.line.fit),
SAS.target = official_sas_target(
"line_source",
"deviance"
)$estimate,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| quantity | lmmix | SAS target | abs diff |
|---|---|---|---|
| -2 restricted log likelihood | 183.238 | 183.238 | 0 |
The published fixed-effect tests use the containment denominator-df
method. Because lmmix does not implement containment
degrees of freedom, this comparison is deliberately restricted to the
covariance parameters and the restricted likelihood criterion.
Stored PROC MIXED comparison
The stored targets correspond to the following specification. The
RANDOM and REPEATED statements represent distinct covariance sources,
and DDFM=SATTERTHWAITE requests approximate denominator
degrees of freedom as documented by SAS Institute
Inc. (2017).
proc mixed data=multicentre method=reml;
class Center Drug Subject Time;
model Y = Drug Time Drug*Time / ddfm=satterth solution;
random Center;
repeated Time / type=ar(1) subject=Subject(Center*Drug);
lsmeans Drug Time / diff;
run;
The exact lmmix fit used in the comparison is:
sas.fit <- combined.fits[["ar1"]]
sas.fit
#> Linear mixed model fit by REML
#> Formula: Y ~ Drug * Time
#> Random (Center): ~1 | Center
#> Repeated: ~Time | Center:Drug:Subject (AR1)
#> Log-likelihood: -245.313
#> Convergence code: 0Covariance parameters and likelihood criterion
sas.covariance.target <- c(5.1737, 10.6702, 0.9351)
lmmix.covariance <- VarCorr(sas.fit)
tibble::tibble(
parameter = paste(
lmmix.covariance$group,
lmmix.covariance$term,
lmmix.covariance$component,
sep = "."
),
lmmix = lmmix.covariance$estimate,
SAS.target = sas.covariance.target,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| parameter | lmmix | SAS target | abs diff |
|---|---|---|---|
| Center.(Intercept).var | 5.173828 | 5.1737 | 0.000128 |
| Residual.ar1.var | 10.670170 | 10.6702 | 0.000030 |
| Residual.ar1.cor | 0.935120 | 0.9351 | 0.000020 |
tibble::tibble(
quantity = "deviance",
lmmix = deviance(sas.fit),
SAS.target = 490.62570532,
abs.diff = abs(lmmix - SAS.target)
) |>
show_table()| quantity | lmmix | SAS target | abs diff |
|---|---|---|---|
| deviance | 490.6257 | 490.6257 | 0 |
Fixed effects
sas.fixed.target <- c(
12.0524590,
4.7647059,
3.9411765,
0.9210892,
2.3922237,
-0.1427475,
-0.4735052,
0.8233152,
0.3342713
)
sas.fixed.table <- comparison_table(
term = names(fixef(sas.fit)),
lmmix = fixef(sas.fit),
reference = sas.fixed.target
)
names(sas.fixed.table) <- c("term", "lmmix", "SAS.target", "abs.diff")
show_table(sas.fixed.table)| term | lmmix | SAS target | abs diff |
|---|---|---|---|
| (Intercept) | 12.052381 | 12.052459 | 7.8e-05 |
| Drug2 | 4.764706 | 4.764706 | 0.0e+00 |
| Drug3 | 3.941176 | 3.941176 | 0.0e+00 |
| Time2 | 0.921092 | 0.921089 | 3.0e-06 |
| Time3 | 2.392232 | 2.392224 | 8.0e-06 |
| Drug2:Time2 | -0.142749 | -0.142747 | 2.0e-06 |
| Drug3:Time2 | -0.473514 | -0.473505 | 9.0e-06 |
| Drug2:Time3 | 0.823316 | 0.823315 | 1.0e-06 |
| Drug3:Time3 | 0.334252 | 0.334271 | 2.0e-05 |
Type III tests
The stored type III targets contain F statistics rounded to two
decimal places and the interaction p-value. The other two SAS p-values
were not available in the stored target set and are therefore reported
as not.stored rather than reconstructed.
lmmix.sas.type3 <- anova(sas.fit)
tibble::tibble(
term = lmmix.sas.type3$term,
lmmix.num.df = lmmix.sas.type3$num.df,
lmmix.den.df = lmmix.sas.type3$den.df,
lmmix.statistic = lmmix.sas.type3$statistic,
SAS.statistic = c(11.43, 59.27, 1.35),
statistic.abs.diff = abs(lmmix.statistic - SAS.statistic),
lmmix.p.value = lmmix.sas.type3$p.value,
SAS.p.value = c("not.stored", "not.stored", "0.2597")
) |>
show_table()| term | lmmix num df | lmmix den df | lmmix statistic | SAS statistic | statistic abs diff | lmmix p value | SAS p value |
|---|---|---|---|---|---|---|---|
| Drug | 2 | 46.35956 | 11.428034 | 11.43 | 0.001966 | 0.000092 | not.stored |
| Time | 2 | 68.45549 | 59.267365 | 59.27 | 0.002635 | 0.000000 | not.stored |
| Drug:Time | 4 | 68.39047 | 1.352082 | 1.35 | 0.002082 | 0.259736 | 0.2597 |
LS-means
The following table compares every stored Drug and Time marginal mean, standard error, and denominator df.
drug.means <- lsmeans(sas.fit, ~Drug)
time.means <- lsmeans(sas.fit, ~Time)
lmmix.means <- rbind(
tibble::tibble(
factor = "Drug",
level = as.character(drug.means$Drug),
estimate = drug.means$estimate,
std.error = drug.means$std.error,
df = drug.means$df
),
tibble::tibble(
factor = "Time",
level = as.character(time.means$Time),
estimate = time.means$estimate,
std.error = time.means$std.error,
df = time.means$df
)
)
sas.mean.estimate <- c(
13.1568, 18.1484, 17.0516,
14.9543, 15.6700, 17.7324
)
sas.mean.std.error <- c(
1.5290, 1.5324, 1.5327,
1.3961, 1.3985, 1.4032
)
sas.mean.df <- c(2.98, 3.01, 3.01, 2.08, 2.09, 2.12)
tibble::tibble(
factor = lmmix.means$factor,
level = lmmix.means$level,
lmmix.estimate = lmmix.means$estimate,
SAS.estimate = sas.mean.estimate,
estimate.abs.diff = abs(lmmix.estimate - SAS.estimate),
lmmix.std.error = lmmix.means$std.error,
SAS.std.error = sas.mean.std.error,
std.error.abs.diff = abs(lmmix.std.error - SAS.std.error),
lmmix.df = lmmix.means$df,
SAS.df = sas.mean.df
) |>
show_table()| factor | level | lmmix estimate | SAS estimate | estimate abs diff | lmmix std error | SAS std error | std error abs diff | lmmix df | SAS df |
|---|---|---|---|---|---|---|---|---|---|
| Drug | 1 | 13.15682 | 13.1568 | 2.3e-05 | 1.529053 | 1.5290 | 5.3e-05 | 2.979813 | 2.98 |
| Drug | 2 | 18.14838 | 18.1484 | 1.6e-05 | 1.532408 | 1.5324 | 8.0e-06 | 3.006723 | 3.01 |
| Drug | 3 | 17.05158 | 17.0516 | 2.2e-05 | 1.532758 | 1.5327 | 5.8e-05 | 3.008745 | 3.01 |
| Time | 1 | 14.95434 | 14.9543 | 4.2e-05 | 1.396163 | 1.3961 | 6.3e-05 | 2.077523 | 2.08 |
| Time | 2 | 15.67001 | 15.6700 | 1.3e-05 | 1.398482 | 1.3985 | 1.8e-05 | 2.091459 | 2.09 |
| Time | 3 | 17.73243 | 17.7324 | 3.0e-05 | 1.403262 | 1.4032 | 6.2e-05 | 2.120423 | 2.12 |
Pairwise LS-mean differences
drug.pairs <- lsmeans(sas.fit, pairwise ~ Drug)$contrasts
time.pairs <- lsmeans(sas.fit, pairwise ~ Time)$contrasts
lmmix.pairs <- rbind(
tibble::add_column(drug.pairs, factor = "Drug", .before = 1),
tibble::add_column(time.pairs, factor = "Time", .before = 1)
)
sas.pair.estimate <- c(
-4.9916, -3.8948, 1.0968,
-0.7157, -2.7781, -2.0624
)
sas.pair.std.error <- c(
1.0990, 1.0987, 1.1043,
0.1845, 0.2714, 0.2056
)
sas.pair.df <- c(46.1, 46.1, 46.9, 68.1, 73.2, 68.6)
sas.pair.p.value <- c(
"0.00004", "0.0009", "0.3257",
"0.0002", "<0.0001", "<0.0001"
)
tibble::tibble(
factor = lmmix.pairs$factor,
contrast = lmmix.pairs$contrast,
lmmix.estimate = lmmix.pairs$estimate,
SAS.estimate = sas.pair.estimate,
estimate.abs.diff = abs(lmmix.estimate - SAS.estimate),
lmmix.std.error = lmmix.pairs$std.error,
SAS.std.error = sas.pair.std.error,
std.error.abs.diff = abs(lmmix.std.error - SAS.std.error),
lmmix.df = lmmix.pairs$df,
SAS.df = sas.pair.df,
lmmix.p.value = lmmix.pairs$p.value,
SAS.p.value = sas.pair.p.value
) |>
show_table()| factor | contrast | lmmix estimate | SAS estimate | estimate abs diff | lmmix std error | SAS std error | std error abs diff | lmmix df | SAS df | lmmix p value | SAS p value |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Drug | 1 - 2 | -4.991561 | -4.9916 | 3.9e-05 | 1.098965 | 1.0990 | 3.5e-05 | 46.10872 | 46.1 | 0.000040 | 0.00004 |
| Drug | 1 - 3 | -3.894756 | -3.8948 | 4.4e-05 | 1.098726 | 1.0987 | 2.6e-05 | 46.05487 | 46.1 | 0.000914 | 0.0009 |
| Drug | 2 - 3 | 1.096806 | 1.0968 | 6.0e-06 | 1.104277 | 1.1043 | 2.3e-05 | 46.92852 | 46.9 | 0.325690 | 0.3257 |
| Time | 1 - 2 | -0.715671 | -0.7157 | 2.9e-05 | 0.184507 | 0.1845 | 7.0e-06 | 68.11869 | 68.1 | 0.000239 | 0.0002 |
| Time | 1 - 3 | -2.778088 | -2.7781 | 1.2e-05 | 0.271413 | 0.2714 | 1.3e-05 | 73.23889 | 73.2 | 0.000000 | <0.0001 |
| Time | 2 - 3 | -2.062417 | -2.0624 | 1.7e-05 | 0.205595 | 0.2056 | 5.0e-06 | 68.60576 | 68.6 | 0.000000 | <0.0001 |
Estimates, covariance parameters, and standard errors are tested with
absolute tolerance 1e-3. Degrees of freedom are compared at
the precision stored in the targets. Type III F statistics are compared
after rounding to two decimal places. Inequalities such as
<0.0001 are checked as inequalities, not as invented
exact p-values.
Automated regression tests
The numerical claims in this vignette are enforced in the package test suite.
| test file | enforced comparison |
|---|---|
test-likelihood.R |
nlme::lme(), nlme::gls(), and
mmrm::mmrm() likelihood and covariance comparisons |
test-inference.R |
lmerTest::lmer() coefficient df and type
III Satterthwaite comparisons |
test-validation-sas-official.R |
official SAS examples 79.1, 79.2, 79.5, and 79.6, including covariance, likelihood, fixed effects, BLUPs, and supported type III statistics |
test-version-030.R |
fixed-band Toeplitz, covariance confidence intervals, and parametric-bootstrap likelihood-ratio tests |
test-validation-sas.R |
every stored PROC MIXED covariance, fixed-effect, LS-mean, and difference target |
test-covariance.R |
positive definiteness and structure-specific covariance checks |
test-methods.R |
S3 outputs, ggplot2 diagnostics, and
readable printed table headings |
Reproducibility information
The versions used to build this vignette are reported rather than assumed.
validation.packages <- c("lmmix", "nlme", "lmerTest", "mmrm")
tibble::tibble(
package = validation.packages,
version = vapply(
validation.packages,
function(package) {
if (requireNamespace(package, quietly = TRUE)) {
as.character(utils::packageVersion(package))
} else {
"not.installed"
}
},
character(1)
)
) |>
show_table()| package | version |
|---|---|
| lmmix | 0.4.0 |
| nlme | 3.1.169 |
| lmerTest | 3.2.1 |
| mmrm | 0.3.18 |
tibble::tibble(
R.version.string = R.version.string,
platform = R.version$platform
) |>
show_table()| R version string | platform |
|---|---|
| R version 4.6.1 (2026-06-24) | x86_64-pc-linux-gnu |
Boundaries of the evidence
The validation covers Kenward-Roger inference for overlapping random-effects and marginal models, crossed random intercepts, fixed-effect likelihood-ratio comparisons, and one official combined random-effect and banded-residual model. It does not validate generalized responses or large-scale sparse performance because these are outside the model scope. For the official SAS examples, values are transcribed from published output rather than generated by a SAS session in this repository. Combined-model Kenward-Roger inference still has structural tests only. An archived SAS log and output would be required to upgrade that evidence.