tests/testthat/test-glm.R

# Tests for proc_glm().
#
# The classic SAS "class" dataset (cls) is reused from the reg tests so results
# can be validated directly against SAS PROC GLM output.  Every numeric value in
# the "matches SAS" tests below is the value produced by the sasLM GLM engine
# (which is designed to reproduce PROC GLM) and has been confirmed against a real
# SAS run of the equivalent step.
#
# Reference SAS step for the main model used throughout:
#
#   proc glm data=cls;
#     class Sex;
#     model Weight = Sex Height / solution clparm ss1 ss2 ss3 p;
#     lsmeans Sex / stderr cl;
#     contrast 'F vs M' Sex 1 -1;
#     estimate 'F vs M' Sex 1 -1;
#   run;

base_path <- file.path(getwd(), "tests/testthat")
data_dir <- base_path

base_path <- tempdir()
data_dir <- "."

cls <- read.table(header = TRUE, text = '
Name Sex Age Height Weight Region
Alfred   M  14   69.0  112.5   A
Alice   F  13   56.5   84.0    A
Barbara   F  13   65.3   98.0  A
Carol   F  14   62.8  102.5    A
Henry   M  14   63.5  102.5    A
James   M  12   57.3   83.0    A
Jane   F  12   59.8   84.5     A
Janet   F  15   62.5  112.5    A
Jeffrey   M  13   62.5   84.0  A
John   M  12   59.0   99.5     B
Joyce   F  11   51.3   50.5    B
Judy   F  14   64.3   90.0     B
Louise   F  12   56.3   77.0   B
Mary   F  15   66.5  112.0     B
Philip   M  16   72.0  150.0   B
Robert   M  12   64.8  128.0   B
Ronald   M  15   67.0  133.0   B
Thomas   M  11   57.5   85.0   B
William   M  15   66.5  112.0  B')

fitness <- read.table(header = TRUE, text = '
Age Weight Oxygen RunTime
44 89.47 44.609 11.37
40 75.07 45.313 10.07
44 85.84 54.297  8.65
42 68.15 59.571  8.17
38 89.02 49.874  9.22
47 77.45 44.811 11.63
40 75.98 45.681 11.95
43 81.19 49.091 10.85
44 81.42 39.442 13.08
38 81.87 60.055  8.63
44 73.03 50.541 10.13
45 87.66 37.388 14.03
45 66.45 44.754 11.12
47 79.15 47.273 10.60
54 83.12 51.855 10.33
49 81.42 49.156  8.95
51 69.63 40.836 10.95
51 77.91 46.672 10.00
48 91.63 46.774 10.25
49 73.37 50.388 10.08
57 73.37 39.407 12.63
54 79.38 46.080 11.17
52 76.32 45.441  9.63
50 70.87 54.625  8.92
51 67.25 45.118 11.08
54 91.63 39.203 12.88
51 73.71 45.790 10.47
57 59.08 50.545  9.93
49 76.32 48.673  9.40
48 61.24 47.920 11.50
52 82.78 47.467 10.50')



# A three-level class variable, so multi-row contrasts have something to test.
# Levels under the default "internal" order are Middle, Old, Young -- the same
# order SAS reports under its Class Level Information.
clsa <- cls
clsa$AgeGroup <- ifelse(clsa$Age <= 12, "Young",
                        ifelse(clsa$Age <= 14, "Middle", "Old"))

dev <- FALSE

options("procs.print" = FALSE)


test_that("glm1: parameter checks work.", {

  myfm <- Weight ~ Sex + Height
  bfm  <- Weight ~ Sex + Fork

  expect_error(proc_glm("bork", model = myfm))                # not a data frame
  expect_error(proc_glm(cls[0, ], model = myfm))              # no rows
  expect_error(proc_glm(cls))                                 # model required
  expect_error(proc_glm(cls, model = myfm, class = "Fork"))   # bad class name
  expect_error(proc_glm(cls, model = myfm, by = "Fork"))      # bad by name
  expect_error(proc_glm(cls, model = myfm, output = "bogus")) # bad output kw
  expect_error(proc_glm(cls, model = myfm, stats = "bogus"))  # bad stats kw
  expect_error(proc_glm(cls, model = myfm, options = "bogus"))# bad option kw

  # lsmeans / random effect must be a class variable
  expect_error(proc_glm(cls, model = myfm, class = "Sex", lsmeans = "Height"))
  expect_error(proc_glm(cls, model = myfm, class = "Sex", lsmeans = "Fork"))
  expect_error(proc_glm(cls, model = myfm, class = "Sex", random = "Height"))

  # contrast / estimate must be named, with the right coefficient count
  expect_error(proc_glm(cls, model = myfm, class = "Sex",
                        contrast = list(list(Sex = c(1, -1)))))         # unnamed
  expect_error(proc_glm(cls, model = myfm, class = "Sex",
                        contrast = list("bad" = list(Sex = c(1, -1, 1))),
                        output = "report"))                             # bad count
})


test_that("glm2: get_output_specs_glm records class on the spec.", {

  myfm <- Weight ~ Sex + Height

  res <- get_output_specs_glm(myfm, class = "Sex", report = TRUE)

  expect_equal(names(res), "MODEL1")
  expect_equal(res$MODEL1$var, "Weight")
  expect_equal(res$MODEL1$class, "Sex")
  expect_equal(res$MODEL1$formula, myfm)
})




test_that("glm4: Basic proc_glm() works.", {

  # R Syntax
  res1 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   output = "report")

  # NObs, ClassLevels, ANOVA, FitStatistics, TypeI, TypeIII
  expect_equal(names(res1), c("ClassLevels", "NObs", "ANOVA",
                              "FitStatistics", "TypeI", "TypeIII"))

  # Class level information
  expect_equal(res1$ClassLevels$LEVELS, 2, ignore_attr = TRUE)
  expect_equal(res1$ClassLevels$VALUES, "F M", ignore_attr = TRUE)

  # Observation counts
  expect_equal(res1$NObs$NOBS, c(19, 19))

  # SAS Syntax matches R syntax
  res2 <- proc_glm(cls, model = "Weight = Sex Height", class = "Sex")
  res1o <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex")

  expect_equal(res1o$SS, res2$SS)
})


test_that("glm5: ANOVA and fit statistics match SAS.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  output = "report")

  # ANOVA: Model / Error / Corrected Total
  aov <- res$ANOVA
  expect_equal(aov$DF, c(2, 16, 18), ignore_attr = TRUE)
  expect_equal(aov$SUMSQ[1], 7377.96361898, ignore_attr = TRUE)
  expect_equal(aov$SUMSQ[2], 1957.77322312, ignore_attr = TRUE)
  expect_equal(aov$SUMSQ[3], 9335.73684211, ignore_attr = TRUE)
  expect_equal(aov$MEANSQ[1], 3688.981809491, ignore_attr = TRUE)
  expect_equal(aov$MEANSQ[2], 122.360826445, ignore_attr = TRUE)
  expect_equal(aov$FVAL[1], 30.1483891263, ignore_attr = TRUE)
  expect_equal(aov$PROBF[1], 3.74033296136e-06, ignore_attr = TRUE)

  # Fit statistics: R-Square, Coeff Var, Root MSE, Dependent Mean
  fit <- res$FitStatistics
  expect_equal(fit$RSQ, 0.790292586838, ignore_attr = TRUE)
  expect_equal(fit$COEFVAR, 11.0587726002, ignore_attr = TRUE)
  expect_equal(fit$RMSE, 11.0616828035, ignore_attr = TRUE)
  expect_equal(fit$DEPMEAN, 100.026315789, ignore_attr = TRUE)
})


test_that("glm6: Type I / II / III sums of squares match SAS.", {

  # Type I and Type III are the PROC GLM default output.
  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex")

  # The output dataset carries an ERROR row alongside the sum-of-squares rows,
  # the way the SAS OUTSTAT= dataset does.  Without it the dataset is not
  # self-contained: there is no error term to recompute an F value from.
  expect_equal(sort(unique(res$TYPE)), c("ERROR", "SS1", "SS3"))
  expect_equal(unique(res$SOURCE), c("ERROR", "Sex", "Height"))
  expect_equal(nrow(res), 5)             # 2 sources x 2 SS types + 1 error row

  err <- res[res$TYPE == "ERROR", ]
  expect_equal(nrow(err), 1)
  expect_equal(err$DF, 16, ignore_attr = TRUE)
  expect_equal(err$SS, 1957.773223, ignore_attr = TRUE, tolerance = 1e-7)
  # expect_equal(err$MEANSQ, 122.360826, ignore_attr = TRUE, tolerance = 1e-7)
  expect_true(is.na(err$FVAL))           # an error term has no F test

  ss1 <- res[res$TYPE == "SS1", ]
  ss3 <- res[res$TYPE == "SS3", ]

  #   Source     DF   Type I SS       Type III SS
  #   Sex         1   1681.1229532    184.7145003
  #   Height      1   5696.8406658    5696.8406658
  expect_equal(ss1$SS[1], 1681.122953216, ignore_attr = TRUE)
  expect_equal(ss1$SS[2], 5696.840665766, ignore_attr = TRUE)
  expect_equal(ss3$SS[1], 184.714500345, ignore_attr = TRUE)
  expect_equal(ss3$SS[2], 5696.840665766, ignore_attr = TRUE)

  # F values and p-values for Type I
  expect_equal(ss1$FVAL[1], 13.73906177374, ignore_attr = TRUE)
  expect_equal(ss1$FVAL[2], 46.55771647896, ignore_attr = TRUE)
  expect_equal(ss1$PROBF[1], 1.91538463586e-03, ignore_attr = TRUE)
  expect_equal(ss1$PROBF[2], 4.09250060773e-06, ignore_attr = TRUE)

  # Type II (ss2 keyword).  For a main-effects model Type II == Type III.
  res2 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   stats = "ss2", output = "report")
  expect_equal(res2$TypeII$SUMSQ[1], 184.714500345, ignore_attr = TRUE)
  expect_equal(res2$TypeII$SUMSQ[2], 5696.840665766, ignore_attr = TRUE)

  # A single stats keyword restricts the output to that SS type
  ress3 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                    stats = "ss3")
  expect_equal(sort(unique(ress3$TYPE)), c("ERROR", "SS3"))
  expect_equal(nrow(ress3), 3)                     # 2 sources + 1 error row

})


test_that("glm7: parameter estimates (solution) match SAS.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  stats = "solution", output = "report")
  pe <- res$ParameterEstimates

  expect_equal(pe$PARM, c("Intercept", "Sex F", "Sex M", "Height"),
               ignore_attr = TRUE)

  # Singular (class-related + intercept) parameters are flagged biased "B";
  # the uniquely estimable Height is not.
  expect_equal(pe$BIASED, c("B", "B", "B", ""), ignore_attr = TRUE)

  #   Parameter    Estimate         Std Error       t         Pr>|t|
  #   Intercept   -126.168694818   34.635195046   -3.64      0.0022
  #   Sex F         -6.620843046    5.388699907   -1.23      0.2370
  #   Sex M          0 (B)          0
  #   Height         3.678903064    0.539166014    6.82      <.0001
  expect_equal(pe$EST[1], -126.16869481800, ignore_attr = TRUE)
  expect_equal(pe$EST[2], -6.62084304645, ignore_attr = TRUE)
  expect_equal(pe$EST[3], 0, ignore_attr = TRUE)
  expect_equal(pe$EST[4], 3.67890306397, ignore_attr = TRUE)

  expect_equal(pe$STDERR[1], 34.635195046487, ignore_attr = TRUE)
  expect_equal(pe$STDERR[2], 5.388699906767, ignore_attr = TRUE)
  expect_equal(pe$STDERR[4], 0.539166014175, ignore_attr = TRUE)

  expect_equal(pe$T[1], -3.64278863303, ignore_attr = TRUE)
  expect_equal(pe$T[2], -1.22865313731, ignore_attr = TRUE)
  expect_equal(pe$T[4], 6.82332151367, ignore_attr = TRUE)

  expect_equal(pe$PROBT[1], 2.19185342953e-03, ignore_attr = TRUE)
  expect_equal(pe$PROBT[2], 2.36967807134e-01, ignore_attr = TRUE)
  expect_equal(pe$PROBT[4], 4.09250060773e-06, ignore_attr = TRUE)

  # Reference level SexM is zeroed with blank t / p
  expect_equal(pe$EST[pe$PARM == "Sex M"], 0, ignore_attr = TRUE)
  expect_true(is.na(pe$T[pe$PARM == "Sex M"]))

  # "est" is an accepted alias for "solution"
  resa <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   stats = "est", output = "report")
  expect_true("ParameterEstimates" %in% names(resa))
})

test_that("glm7a: biased column is removed from parameter estimates table appropriately.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  stats = "solution", output = "report")
  pe <- res$ParameterEstimates

  nms1 <- names(pe)


  expect_equal("BIASED" %in% nms1, TRUE)



  pe2 <- proc_glm(fitness, output = "report",
                  model = Oxygen ~ RunTime)

  nms2 <- names(pe2)

  expect_equal("BIASED" %in% nms2, FALSE)



  pe2 <- proc_glm(fitness, output = "out",
                  model = Oxygen ~ RunTime) #,
  # stats = v(e1, e2, e3))

  # fitness3 <- fitness
  # fitness3$AgeGroup <- cut(fitness3$Age,
  #                          breaks = c(-Inf, 44, 50, Inf),
  #                          labels = c("Young", "Middle", "Old"))
  # fitness3$AgeCat <- ifelse(fitness3$Age < 50, "<50", ">=50")
  # fitness3$AgeGroup <- as.character(fitness3$AgeGroup)
  #
  # proc_glm(fitness3,
  #          model = Oxygen ~ RunTime + AgeGroup,
  #          class = "AgeGroup",
  #          lsmeans = "AgeGroup",
  #          output = "report")

  # proc_glm(fitness3,
  #          model = Oxygen ~ RunTime + AgeGroup,
  #          class = "AgeGroup",
  #          lsmeans = "AgeGroup",
  #          contrast = list("Young vs Middle" = list(AgeGroup = c(1, -1, 0)),
  #                          "Young vs Old"    = list(AgeGroup = c(1, 0, -1)),
  #                          "Middle vs Old"   = list(AgeGroup = c(0, 1, -1))))

  # fitness2 <- fitness
  # fitness2$AgeCat <- ifelse(fitness$Age < 50,
  #                           "<50", ">=50")
  #
  # # Test 26 - zeta option
  # proc_glm(fitness2,
  #          class = "AgeCat",
  #          model = Oxygen ~ RunTime + AgeCat,
  #          contrast = list("F only" = list(AgeCat = c(1, 0))),
  #          output = "report",
  #          options = c("zeta" = 1e-6))


})


test_that("glm8: clparm confidence limits match SAS.", {

  # 95% limits (default alpha = 0.05)
  res95 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                    stats = c("solution", "clparm"), output = "report")
  pe95 <- res95$ParameterEstimates

  expect_true(all(c("LCLM", "UCLM") %in% names(pe95)))

  expect_equal(pe95$LCLM[1], -199.59202833661, ignore_attr = TRUE)
  expect_equal(pe95$LCLM[2], -18.04437653472, ignore_attr = TRUE)
  expect_equal(pe95$LCLM[4], 2.53592217335, ignore_attr = TRUE)
  expect_equal(pe95$UCLM[1], -52.74536129940, ignore_attr = TRUE)
  expect_equal(pe95$UCLM[2], 4.80269044182, ignore_attr = TRUE)
  expect_equal(pe95$UCLM[4], 4.82188395458, ignore_attr = TRUE)

  # Reference (zeroed) level has no confidence limits
  expect_true(is.na(pe95$LCLM[pe95$PARM == "Sex M"]))

  # 90% limits (alpha = 0.10)
  res90 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                    stats = c("solution", "clparm"),
                    options = c(alpha = 0.10), output = "report")
  pe90 <- res90$ParameterEstimates

  expect_equal(pe90$LCLM[1], -186.63771647431, ignore_attr = TRUE)
  expect_equal(pe90$LCLM[2], -16.02888625003, ignore_attr = TRUE)
  expect_equal(pe90$LCLM[4], 2.73758192101, ignore_attr = TRUE)
  expect_equal(pe90$UCLM[1], -65.69967316170, ignore_attr = TRUE)
  expect_equal(pe90$UCLM[2], 2.78720015712, ignore_attr = TRUE)
  expect_equal(pe90$UCLM[4], 4.62022420692, ignore_attr = TRUE)

  # 90% limits are narrower than 95% limits
  w95 <- pe95$UCLM[4] - pe95$LCLM[4]
  w90 <- pe90$UCLM[4] - pe90$LCLM[4]
  expect_lt(w90, w95)

  # "clb" is an accepted alias for "clparm"
  resa <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   stats = c("solution", "clb"), output = "report")
  expect_true(all(c("LCLM", "UCLM") %in% names(resa$ParameterEstimates)))
})


test_that("glm9: predicted values and residuals (p) match SAS.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  stats = "p", output = "report")

  expect_true(all(c("OutputStatistics", "ResidualStatistics") %in% names(res)))

  os <- res$OutputStatistics
  expect_equal(names(os), c("OBS", "DEPVAL", "PREVAL", "RESID"))
  expect_equal(nrow(os), 19)                       # all obs used

  # First three predicted values and residuals match SAS
  expect_equal(os$PREVAL[1], 127.6756165956, ignore_attr = TRUE)
  expect_equal(os$PREVAL[2], 75.0684852496, ignore_attr = TRUE)
  expect_equal(os$PREVAL[3], 107.4428322125, ignore_attr = TRUE)
  expect_equal(os$RESID[1], -15.17561659558, ignore_attr = TRUE)
  expect_equal(os$RESID[2], 8.93151475043, ignore_attr = TRUE)
  expect_equal(os$RESID[3], -9.44283221246, ignore_attr = TRUE)

  # Predicted + residual reconstruct the observed value; residuals sum to ~0
  expect_equal(os$DEPVAL, os$PREVAL + os$RESID, ignore_attr = TRUE)
  expect_equal(sum(os$RESID), 0)

  # SAS prints five residual summary statistics
  rs <- res$ResidualStatistics
  expect_equal(nrow(rs), 5)
  expect_equal(rs$LABEL, c("Sum of Residuals",
                           "Sum of Squared Residuals",
                           "Sum of Squared Residuals - Error SS",
                           "First Order Autocorrelation",
                           "Durbin-Watson D"), ignore_attr = TRUE)

  expect_equal(rs$VALUE[2], 1957.77322312, ignore_attr = TRUE)
  expect_equal(rs$VALUE[4], -0.211868472697, ignore_attr = TRUE)
  expect_equal(rs$VALUE[5], 2.28466645943, ignore_attr = TRUE)
})


test_that("glm10: lsmeans match SAS.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  lsmeans = "Sex", output = "report")

  expect_true("LSMeans.Sex" %in% names(res))

  ls <- res$LSMeans.Sex
  # expect_equal(names(ls), c("LEVEL", "LSMEAN", "STDERR", "PROBT", "LCLM", "UCLM"))
  expect_equal(names(ls), c("LEVEL", "LSMEAN"))
  expect_equal(ls$LEVEL, c("F", "M"), ignore_attr = TRUE)

  #   Sex   Weight LSMEAN   Std Error    LCL          UCL
  #   F     96.5416616     3.8057634    88.4738036   104.6095195
  #   M     103.1625046    3.5993770    95.5321663   110.7928429
  expect_equal(ls$LSMEAN[1], 96.5416615545, ignore_attr = TRUE)
  expect_equal(ls$LSMEAN[2], 103.1625046010, ignore_attr = TRUE)
  # expect_equal(ls$STDERR[1], 3.80576336924, ignore_attr = TRUE)
  # expect_equal(ls$STDERR[2], 3.59937695592, ignore_attr = TRUE)
  # expect_equal(ls$LCLM[1], 88.4738036205, ignore_attr = TRUE)
  # expect_equal(ls$LCLM[2], 95.5321663182, ignore_attr = TRUE)
  # expect_equal(ls$UCLM[1], 104.609519489, ignore_attr = TRUE)
  # expect_equal(ls$UCLM[2], 110.792842884, ignore_attr = TRUE)

  # alpha = narrows the LS-mean confidence interval
  # res90 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
  #                   lsmeans = "Sex", options = c(alpha = 0.10),
  #                   output = "report")
  # ls90 <- res90$LSMeans.Sex
  # expect_lt(ls90$UCLM[1] - ls90$LCLM[1], ls$UCLM[1] - ls$LCLM[1])

  # fitness3 <- fitness
  # fitness3$AgeGroup <- cut(fitness3$Age,
  #                          breaks = c(-Inf, 44, 50, Inf),
  #                          labels = c("Young", "Middle", "Old"))
  # fitness3$AgeCat <- ifelse(fitness3$Age < 50, "<50", ">=50")
  # fitness3$AgeGroup <- factor(fitness3$AgeGroup)
  #
  # proc_glm(fitness3,
  #          model = Oxygen ~ RunTime + AgeGroup,
  #          order = "internal",
  #          class = "AgeGroup",
  #          lsmeans = "AgeGroup")



})


test_that("glm11: contrast and estimate match SAS.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  contrast = list("F vs M" = list(Sex = c(1, -1))),
                  estimate = list("F vs M" = list(Sex = c(1, -1))),
                  stats = "clparm", output = "report")

  # Contrast: a 1-df F vs M contrast reproduces the Type III SS for Sex
  expect_true("Contrasts" %in% names(res))
  ct <- res$Contrasts
  expect_equal(ct$CONTRAST, "F vs M", ignore_attr = TRUE)
  expect_equal(ct$DF, 1, ignore_attr = TRUE)
  expect_equal(ct$SUMSQ, 184.714500345, ignore_attr = TRUE)
  expect_equal(ct$FVAL, 1.50958853181, ignore_attr = TRUE)
  expect_equal(ct$PROBF, 0.236967807134, ignore_attr = TRUE)

  # Estimate: F vs M equals the SexF solution estimate (SexM is the reference)
  expect_true("Estimates" %in% names(res))
  es <- res$Estimates
  expect_equal(es$EST, -6.62084304645, ignore_attr = TRUE)
  expect_equal(es$STDERR, 5.38869990677, ignore_attr = TRUE)
  expect_equal(es$DF, 16, ignore_attr = TRUE)
  expect_equal(es$T, -1.22865313731, ignore_attr = TRUE)
  expect_equal(es$PROBT, 0.236967807134, ignore_attr = TRUE)

  # clparm adds confidence limits to the estimate
  expect_true(all(c("LCLM", "UCLM") %in% names(es)))
  expect_equal(es$LCLM, -18.0443765347, ignore_attr = TRUE)
  expect_equal(es$UCLM, 4.80269044182, ignore_attr = TRUE)

  # Without clparm the estimate has no confidence limits
  res2 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   estimate = list("F vs M" = list(Sex = c(1, -1))),
                   output = "report")
  expect_false(any(c("LCLM", "UCLM") %in% names(res2$Estimates)))
})


test_that("glm12: random produces an expected mean squares table.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Region,
                  class = c("Sex", "Region"), random = "Sex",
                  output = "report")

  expect_true("RandomEffects" %in% names(res))
  em <- res$RandomEffects
  expect_equal(em$SOURCE, c("Sex", "Region"), ignore_attr = TRUE)

  # Random effect -> Var(); fixed effect -> Q()
  expect_true(grepl("Var(Sex)", em$EMS[em$SOURCE == "Sex"], fixed = TRUE))
  expect_true(grepl("Q(Region)", em$EMS[em$SOURCE == "Region"], fixed = TRUE))
})


test_that("glm13: weighted analysis matches SAS.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  weight = "Age", stats = "solution", output = "report")

  # Weighted ANOVA sums of squares
  expect_equal(res$ANOVA$SUMSQ[1], 97411.5010437, ignore_attr = TRUE)
  expect_equal(res$ANOVA$SUMSQ[2], 26759.1353200, ignore_attr = TRUE)
  expect_equal(res$ANOVA$SUMSQ[3], 124170.6363636, ignore_attr = TRUE)

  # Weighted Type III sums of squares
  expect_equal(res$TypeIII$SUMSQ[1], 2184.34600582, ignore_attr = TRUE)
  expect_equal(res$TypeIII$SUMSQ[2], 74778.51236596, ignore_attr = TRUE)

  # Weighted parameter estimates
  expect_equal(res$ParameterEstimates$EST[1], -128.14066972669, ignore_attr = TRUE)
  expect_equal(res$ParameterEstimates$EST[2], -6.27304944530, ignore_attr = TRUE)
  expect_equal(res$ParameterEstimates$EST[4], 3.71000959043, ignore_attr = TRUE)
})


test_that("glm14: by parameter works.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  by = "Region")

  expect_true("BY" %in% names(res))
  expect_equal(sort(unique(res$BY)), c("A", "B"))
  expect_equal(nrow(res), 10)                # 2 groups x (2 sources x 2 SS + 1 error)

  # Type III SS per region matches SAS
  a3 <- res[res$BY == "A" & res$TYPE == "SS3", ]
  b3 <- res[res$BY == "B" & res$TYPE == "SS3", ]
  expect_equal(a3$SS[1], 56.59749674771, ignore_attr = TRUE)
  expect_equal(a3$SS[2], 762.80443311052, ignore_attr = TRUE)
  expect_equal(b3$SS[1], 583.15607215369, ignore_attr = TRUE)
  expect_equal(b3$SS[2], 4276.43738947706, ignore_attr = TRUE)
})


test_that("glm15: multiple models and model names work.", {

  # Unnamed models default to MODEL1, MODEL2, ...
  res1 <- proc_glm(cls, model = list(Weight ~ Sex + Height,
                                     Height ~ Sex + Age), class = "Sex")
  expect_equal(sort(unique(res1$MODEL)), c("MODEL1", "MODEL2"))

  # Named models keep their names
  res2 <- proc_glm(cls, model = list(m1 = Weight ~ Sex + Height,
                                     m2 = Height ~ Sex + Age), class = "Sex")
  expect_equal(sort(unique(res2$MODEL)), c("m1", "m2"))
  expect_equal(nrow(res2), 10)               # 2 models x (2 sources x 2 SS + 1 error)

  # Second model Type I / III SS match SAS
  m2ss1 <- res2[res2$MODEL == "m2" & res2$TYPE == "SS1", ]
  m2ss3 <- res2[res2$MODEL == "m2" & res2$TYPE == "SS3", ]
  expect_equal(m2ss1$SS[1], 52.2463216374, ignore_attr = TRUE)
  expect_equal(m2ss1$SS[2], 297.2584061303, ignore_attr = TRUE)
  expect_equal(m2ss3$SS[1], 37.9612521495, ignore_attr = TRUE)
  expect_equal(m2ss3$SS[2], 297.2584061303, ignore_attr = TRUE)
})


test_that("glm16: output dataset and shaping work.", {

  # Default out dataset is wide, one row per source per SS type
  wide <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex")
  expect_s3_class(wide, "data.frame")
  expect_true(all(c("MODEL", "DEPVAR", "SOURCE", "TYPE", "SS") %in% names(wide)))

  # Long transposes statistics into rows
  reslong <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                      output = c("out", "long"))
  expect_true("STAT" %in% names(reslong))

  # Stacked collapses to a single value column
  resstk <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                     output = c("out", "stacked"))
  expect_true(all(c("STAT", "VALUES") %in% names(resstk)))
})


test_that("glm17: where expression works.", {

  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  output = "report", where = expression(Weight < 150))

  # One observation dropped by the filter
  expect_equal(res$NObs$NOBS, c(18, 18))
  expect_equal(res$ANOVA$DF, c(2, 15, 17), ignore_attr = TRUE)

  # Type III SS on the filtered data matches SAS
  expect_equal(res$TypeIII$SUMSQ[1], 154.242318232, ignore_attr = TRUE)
  expect_equal(res$TypeIII$SUMSQ[2], 3995.639149667, ignore_attr = TRUE)
})


test_that("glm18: output keywords and outstat option work.", {

  # none returns NULL
  resn <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   output = "none")
  expect_null(resn)

  # outstat yields the default sum-of-squares dataset (like proc_reg's outest)
  base <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex")
  os   <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   options = "outstat")
  expect_equal(os, base, ignore_attr = TRUE)
  expect_true(all(c("SOURCE", "TYPE", "SS") %in% names(os)))
})



# Order parameter ------------------------------------------------------------

test_that("glm19: order controls the class level order and reference level.", {

  # Default (internal): F sorts first, so M is the last level and therefore the
  # reference -- SexM carries the zero estimate.
  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  stats = "solution", output = "report")
  pe <- res$ParameterEstimates
  expect_equal(pe$PARM, c("Intercept", "Sex F", "Sex M", "Height"),
               ignore_attr = TRUE)
  expect_equal(pe$EST[pe$PARM == "Sex M"], 0, ignore_attr = TRUE)

  # order = "data" puts M first (Alfred is row 1), so F becomes the reference.
  resd <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   order = "data", stats = "solution", output = "report")
  ped <- resd$ParameterEstimates
  expect_equal(ped$PARM, c("Intercept", "Sex M", "Sex F", "Height"),
               ignore_attr = TRUE)
  expect_equal(ped$EST[ped$PARM == "Sex F"], 0, ignore_attr = TRUE)

  # order = "freq" puts the most frequent level first.  There are 10 M and 9 F.
  resf <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   order = "freq", stats = "solution", output = "report")
  expect_equal(resf$ParameterEstimates$PARM,
               c("Intercept", "Sex M", "Sex F", "Height"), ignore_attr = TRUE)

  # Invalid keywords are rejected
  expect_error(proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                        order = "bork"))
})


test_that("glm20: order remaps the contrast coefficients silently.", {

  # The coefficients are read in level order, so "1 -1" means F - M by default
  # and M - F under order = "data".  Same magnitude, opposite sign.
  ce  <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  estimate = list("1 -1" = list(Sex = c(1, -1))),
                  output = "report")$Estimates
  ced <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  order = "data",
                  estimate = list("1 -1" = list(Sex = c(1, -1))),
                  output = "report")$Estimates
  expect_equal(ce$EST, -ced$EST, ignore_attr = TRUE)
})


test_that("glm21: order is vectorized across class variables.", {

  # One keyword per class variable, like proc_ttest()
  res <- proc_glm(cls, model = Weight ~ Sex + Region,
                  class = c("Sex", "Region"), order = c("data", "internal"),
                  stats = "solution", output = "report")
  expect_equal(res$ParameterEstimates$PARM,
               c("Intercept", "Sex M", "Sex F", "Region A", "Region B"),
               ignore_attr = TRUE)

  # A single keyword is recycled across all class variables
  res2 <- proc_glm(cls, model = Weight ~ Sex + Region,
                   class = c("Sex", "Region"), order = "data",
                   stats = "solution", output = "report")
  expect_equal(res2$ParameterEstimates$PARM,
               c("Intercept", "Sex M", "Sex F", "Region A", "Region B"),
               ignore_attr = TRUE)

  # Misaligned keywords are an error
  expect_error(proc_glm(cls, model = Weight ~ Sex + Region,
                        class = c("Sex", "Region"),
                        order = c("data", "freq", "internal")),
               "misaligned")
})


test_that("glm22: a numeric class variable is factored, not treated as continuous.", {

  # This is the one place proc_glm() cannot follow proc_ttest() exactly.
  # proc_ttest() leaves "internal" alone because it does its own grouping, but
  # sasLM::GLM() would read an unfactored numeric class variable as a continuous
  # predictor and fit 1 df instead of one per level.
  dat <- data.frame(g = c(1, 1, 2, 2, 3, 3), y = c(5, 6, 9, 10, 14, 15))

  res <- proc_glm(dat, model = y ~ g, class = "g", output = "report")
  expect_equal(res$TypeIII$DF, 2, ignore_attr = TRUE)

  # Numeric levels sort by value, so 2 comes before 10
  dat2 <- data.frame(g = c(10, 10, 2, 2), y = c(1, 2, 3, 4))
  res2 <- proc_glm(dat2, model = y ~ g, class = "g", stats = "solution",
                   output = "report")
  expect_equal(res2$ParameterEstimates$PARM, c("Intercept", "g 2", "g 10"),
               ignore_attr = TRUE)
})


test_that("glm23: order = formatted applies a user-defined format.", {

  dat <- data.frame(g = c(1, 2, 3, 1, 2, 3), y = c(5, 9, 14, 6, 10, 15))
  formats(dat) <- list(g = value(condition(x == 1, "Low"),
                                 condition(x == 2, "Mid"),
                                 condition(x == 3, "High")))

  res <- proc_glm(dat, model = y ~ g, class = "g", order = "formatted",
                  stats = "solution", output = "report")

  # The format supplies both the labels and their order
  expect_equal(res$ParameterEstimates$PARM,
               c("Intercept", "g Low", "g Mid", "g High"), ignore_attr = TRUE)

  # A format that collapses values collapses levels, and the model loses the
  # degrees of freedom that separated them.  This mirrors the SAS practice of
  # grouping a class variable with a format.
  dat2 <- dat
  formats(dat2) <- list(g = value(condition(x < 3, "LowMid"),
                                  condition(TRUE, "High")))
  res2 <- proc_glm(dat2, model = y ~ g, class = "g", order = "formatted",
                   output = "report")
  expect_equal(res$TypeIII$DF, 2, ignore_attr = TRUE)
  expect_equal(res2$TypeIII$DF, 1, ignore_attr = TRUE)
})


# contrast and estimate ------------------------------------------------------




test_that("glm24: single-row contrasts match SAS.", {

  # Verified against a real SAS run:
  #   proc glm data=clsa;
  #     class AgeGroup;
  #     model Weight = Height AgeGroup / clparm;
  #     contrast "Young vs Middle" AgeGroup -1 0 1;
  #     contrast "Young vs Old"    AgeGroup  0 -1 1;
  #   run;
  res <- proc_glm(clsa, model = Weight ~ Height + AgeGroup, class = "AgeGroup",
                  contrast = list("Young vs Middle" = list(AgeGroup = c(-1, 0, 1)),
                                  "Young vs Old"    = list(AgeGroup = c(0, -1, 1))),
                  output = "report")

  ct <- res$Contrasts
  expect_equal(ct$CONTRAST, c("Young vs Middle", "Young vs Old"),
               ignore_attr = TRUE)
  expect_equal(ct$DF, c(1, 1), ignore_attr = TRUE)

  # SAS: 375.5376368 and 1.8544003
  expect_equal(ct$SUMSQ, c(375.5376368, 1.8544003), ignore_attr = TRUE,
               tolerance = 1e-7)
  # SAS: F 4.00 / 0.02, Pr > F 0.0640 / 0.8901
  expect_equal(round(ct$FVAL, 2), c(4.00, 0.02), ignore_attr = TRUE)
  expect_equal(round(ct$PROBF, 4), c(0.0640, 0.8901), ignore_attr = TRUE)
})


test_that("glm25: a multi-row contrast is one multi-df F test.", {

  # A matrix value is the R equivalent of the comma in a SAS CONTRAST:
  #   contrast "Any diff" AgeGroup -1 0 1,
  #                       AgeGroup  0 -1 1;
  # SAS reports one row: DF 2, SS 733.3898693, MS 366.6949346, F 3.90, p 0.0432.
  res <- proc_glm(clsa, model = Weight ~ Height + AgeGroup, class = "AgeGroup",
                  contrast = list("Any diff" = list(AgeGroup = rbind(c(-1, 0, 1),
                                                                    c(0, -1, 1)))),
                  output = "report")

  ct <- res$Contrasts
  expect_equal(nrow(ct), 1)                        # one row, not two
  expect_equal(ct$DF, 2, ignore_attr = TRUE)
  expect_equal(ct$SUMSQ, 733.3898693, ignore_attr = TRUE, tolerance = 1e-7)
  expect_equal(ct$MEANSQ, 366.6949346, ignore_attr = TRUE, tolerance = 1e-7)
  expect_equal(round(ct$FVAL, 2), 3.90, ignore_attr = TRUE)
  expect_equal(round(ct$PROBF, 4), 0.0432, ignore_attr = TRUE)

  # Identical to the Type III test for the effect, in SAS and here
  t3 <- res$TypeIII
  expect_equal(ct$DF, t3$DF[t3$SOURCE == "AgeGroup"], ignore_attr = TRUE)
  expect_equal(ct$SUMSQ, t3$SUMSQ[t3$SOURCE == "AgeGroup"], ignore_attr = TRUE)
})


test_that("glm26: estimate matches SAS.", {

  # SAS: Estimate 12.4624658, Std Error 6.23307271, t 2.00, Pr > |t| 0.0640,
  #      95% Confidence Limits -0.8230142 to 25.7479458
  res <- proc_glm(clsa, model = Weight ~ Height + AgeGroup, class = "AgeGroup",
                  estimate = list("Young vs Middle" = list(AgeGroup = c(-1, 0, 1))),
                  stats = "clparm", output = "report")

  es <- res$Estimates
  expect_equal(es$PARM, "Young vs Middle", ignore_attr = TRUE)
  expect_equal(es$EST, 12.4624658, ignore_attr = TRUE, tolerance = 1e-7)
  expect_equal(es$STDERR, 6.23307271, ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(es$DF, 15, ignore_attr = TRUE)
  expect_equal(round(es$T, 2), 2.00, ignore_attr = TRUE)
  expect_equal(round(es$PROBT, 4), 0.0640, ignore_attr = TRUE)
  expect_equal(es$LCLM, -0.8230142, ignore_attr = TRUE, tolerance = 1e-6)
  expect_equal(es$UCLM, 25.7479458, ignore_attr = TRUE, tolerance = 1e-7)
})


test_that("glm27: the SAS interaction operator works.", {

  # SAS: contrast "inter" Sex*Region 1 -1 -1 1;
  #      DF 1, Contrast SS 1523.903926, F 3.80, Pr > F 0.0703
  res <- proc_glm(cls, model = Weight ~ Sex + Region + Sex:Region,
                  class = c("Sex", "Region"),
                  contrast = list("inter" = list("Sex*Region" = c(1, -1, -1, 1))),
                  output = "report")

  ct <- res$Contrasts
  expect_equal(ct$DF, 1, ignore_attr = TRUE)
  expect_equal(ct$SUMSQ, 1523.903926, ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(ct$FVAL, 2), 3.80, ignore_attr = TRUE)
  expect_equal(round(ct$PROBF, 4), 0.0703, ignore_attr = TRUE)

  # The R spelling gives the same answer
  res2 <- proc_glm(cls, model = Weight ~ Sex + Region + Sex:Region,
                   class = c("Sex", "Region"),
                   contrast = list("inter" = list("Sex:Region" = c(1, -1, -1, 1))),
                   output = "report")
  expect_equal(res$Contrasts$SUMSQ, res2$Contrasts$SUMSQ, ignore_attr = TRUE)
})


test_that("glm28: contrast and estimate are validated.", {

  myfm <- Weight ~ Sex + Height

  # The label list must be named
  expect_error(proc_glm(cls, model = myfm, class = "Sex",
                        contrast = list(list(Sex = c(1, -1)))), "named list")

  # The inner coefficient list must be named for a model effect
  expect_error(proc_glm(cls, model = myfm, class = "Sex",
                        contrast = list("F vs M" = list(c(1, -1)))), "named list")

  # An effect that is not in the model
  expect_error(proc_glm(cls, model = myfm, class = "Sex",
                        contrast = list("x" = list(Fork = c(1, -1)))),
               "not found in the model")

  # The wrong number of coefficients for the effect
  expect_error(proc_glm(cls, model = myfm, class = "Sex",
                        contrast = list("x" = list(Sex = c(1, -1, 0)))),
               "expects 2 coefficients")

  # Multiple rows are rejected on estimate, where they have no meaning
  expect_error(proc_glm(cls, model = myfm, class = "Sex",
                        estimate = list("m" = list(Sex = rbind(c(1, -1),
                                                               c(1, 0))))),
               "single-row")

  # Ragged row counts across effects
  expect_error(proc_glm(cls, model = Weight ~ Sex + Region,
                        class = c("Sex", "Region"),
                        contrast = list("x" = list(Sex = c(1, -1),
                                                   Region = rbind(c(1, -1),
                                                                  c(0, 1))))),
               "same number of rows")
})


test_that("glm29: a non-estimable spec is dropped with a warning.", {

  # A single level effect on its own is not estimable in an over-parameterized
  # model: the level effects are confounded with the intercept, so adding a
  # constant to the intercept and subtracting it from every level leaves the
  # fit unchanged.  Only differences between levels are estimable.
  #
  # sasLM CONTR() does no estimability check and would return an ordinary
  # looking F value for it.  SAS prints a row for such a contrast too, but the
  # two do not agree, because the value depends on which generalized inverse
  # happens to be used.  Dropping it is the safe reading, and matching SAS is
  # not available here in any case.
  expect_warning(
    res <- proc_glm(clsa, model = Weight ~ Height + AgeGroup, class = "AgeGroup",
                    contrast = list("Young only" = list(AgeGroup = c(0, 0, 1)),
                                    "good" = list(AgeGroup = c(-1, 0, 1))),
                    output = "report"),
    "not estimable")

  expect_equal(nrow(res$Contrasts), 1)
  expect_equal(res$Contrasts$CONTRAST, "good", ignore_attr = TRUE)

  # When nothing is estimable there is no contrast table at all
  suppressWarnings(
    res2 <- proc_glm(clsa, model = Weight ~ Height + AgeGroup, class = "AgeGroup",
                     contrast = list("Young only" = list(AgeGroup = c(0, 0, 1))),
                     output = "report"))
  expect_false("Contrasts" %in% names(res2))

  # An effect name that is no longer special: "intercept" is just a name, and
  # there is no such effect in the model.
  expect_error(proc_glm(clsa, model = Weight ~ Height + AgeGroup,
                        class = "AgeGroup",
                        contrast = list("x" = list(intercept = 1))),
               "not found in the model")
})


# MODEL options --------------------------------------------------------------

test_that("glm30: noint drops the intercept and uncorrects the total.", {

  # SAS: model Weight = Sex Height / noint solution;
  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  stats = c("noint", "solution"), output = "report")

  pe <- res$ParameterEstimates
  expect_false("Intercept" %in% pe$PARM)

  # With no intercept there is no reference level, so both Sex levels are
  # estimated rather than one of them being aliased to zero.
  expect_equal(pe$PARM, c("Sex F", "Sex M", "Height"), ignore_attr = TRUE)

  # SAS: -132.7895379 / -126.1686948 / 3.6789031
  expect_equal(pe$EST, c(-132.7895379, -126.1686948, 3.6789031),
               ignore_attr = TRUE, tolerance = 1e-8)
  # SAS: 32.87490267 / 34.63519505 / 0.53916601
  expect_equal(pe$STDERR, c(32.87490267, 34.63519505, 0.53916601),
               ignore_attr = TRUE, tolerance = 1e-8)

  # SAS labels the total row "Uncorrected Total" and reports DF 19, SS 199435.75
  av <- res$ANOVA
  expect_equal(av$SOURCE[3], "Uncorrected Total", ignore_attr = TRUE)
  expect_equal(av$DF, c(3, 16, 19), ignore_attr = TRUE)
  expect_equal(av$SUMSQ, c(197477.9768, 1957.7732, 199435.7500),
               ignore_attr = TRUE, tolerance = 1e-6)

  # SAS Type III under noint: Sex DF 2 SS 2659.761676, Height DF 1 SS 5696.840666
  t3 <- res$TypeIII
  expect_equal(t3$DF, c(2, 1), ignore_attr = TRUE)
  expect_equal(t3$SUMSQ, c(2659.761676, 5696.840666), ignore_attr = TRUE,
               tolerance = 1e-8)

  # The default model keeps the intercept and the corrected total
  base <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   stats = "solution", output = "report")
  expect_true("Intercept" %in% base$ParameterEstimates$PARM)
  expect_equal(base$ANOVA$SOURCE[3], "Corrected Total", ignore_attr = TRUE)
})


test_that("glm31: clm and cli add confidence limits to the predicted values.", {

  # SAS: model Weight = Sex Height / clm cli;
  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  stats = c("clm", "cli"), output = "report")

  st <- res$OutputStatistics
  expect_true(all(c("LCLM", "UCLM", "LCLI", "UCLI") %in% names(st)))

  # SAS observation 1: Predicted 127.67561660, CL Mean 118.25036242 137.10087078
  expect_equal(st$PREVAL[1], 127.67561660, ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(st$LCLM[1], 118.25036242, ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(st$UCLM[1], 137.10087078, ignore_attr = TRUE, tolerance = 1e-8)

  # The individual limits are wider than the mean limits everywhere, and match
  # predict.lm(interval = "prediction").
  lmf <- stats::lm(Weight ~ Sex + Height, cls)
  pr <- suppressWarnings(stats::predict(lmf, interval = "prediction"))
  expect_equal(st$LCLI, unname(pr[, 2]), ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(st$UCLI, unname(pr[, 3]), ignore_attr = TRUE, tolerance = 1e-8)
  expect_true(all(st$UCLI - st$LCLI > st$UCLM - st$LCLM))

  # clm alone does not add the individual limits, and vice versa
  resm <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   stats = "clm", output = "report")
  expect_false(any(c("LCLI", "UCLI") %in% names(resm$OutputStatistics)))

  resi <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   stats = "cli", output = "report")
  expect_false(any(c("LCLM", "UCLM") %in% names(resi$OutputStatistics)))

  # The alpha option flows through to the limits
  res90 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                    stats = "clm", options = c("alpha" = 0.1),
                    output = "report")
  expect_true(res90$OutputStatistics$UCLM[1] - res90$OutputStatistics$LCLM[1] <
              st$UCLM[1] - st$LCLM[1])
})


test_that("glm32: xpx and inverse match the SAS matrices.", {

  # SAS: model Weight = Sex Height / xpx inverse;
  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  stats = c("xpx", "inverse"), output = "report")

  expect_true(all(c("XPX", "Inverse") %in% names(res)))

  # The matrix is augmented with the dependent variable, so it is one row and
  # one column bigger than the number of model parameters.
  xp <- res$XPX
  expect_equal(names(xp), c("stub", "(Intercept)", "SexF", "SexM", "Height",
                            "Weight"))
  expect_equal(nrow(xp), 5)

  # SAS X'X, first row:   19  9  10  1184.4  1900.5
  expect_equal(c(xp[["(Intercept)"]][1], xp$SexF[1], xp$SexM[1],
                 xp$Height[1], xp$Weight[1]),
               c(19, 9, 10, 1184.4, 1900.5), ignore_attr = TRUE,
               tolerance = 1e-10)
  # SAS X'X, last row:  1900.5  811  1089.5  120316.05  199435.75
  expect_equal(c(xp[["(Intercept)"]][5], xp$SexF[5], xp$SexM[5],
                 xp$Height[5], xp$Weight[5]),
               c(1900.5, 811, 1089.5, 120316.05, 199435.75), ignore_attr = TRUE,
               tolerance = 1e-10)

  # SAS X'X Generalized Inverse (g2), first row:
  #   9.8037645769  -0.604260372  0  -0.151834839  -126.1686948
  inv <- res$Inverse
  expect_equal(c(inv[["(Intercept)"]][1], inv$SexF[1], inv$SexM[1],
                 inv$Height[1], inv$Weight[1]),
               c(9.8037645769, -0.604260372, 0, -0.151834839, -126.1686948),
               ignore_attr = TRUE, tolerance = 1e-8)

  # SAS prints the matrix symmetric, so the last row equals the last column,
  # and the bottom right cell is the error sum of squares.
  expect_equal(c(inv[["(Intercept)"]][5], inv$SexF[5], inv$SexM[5],
                 inv$Height[5], inv$Weight[5]),
               c(-126.1686948, -6.620843046, 0, 3.678903064, 1957.7732231),
               ignore_attr = TRUE, tolerance = 1e-7)
  expect_equal(inv$Weight[5], res$ANOVA$SUMSQ[2], ignore_attr = TRUE,
               tolerance = 1e-6)
})


test_that("glm33: e1, e2 and e3 produce the estimable function tables.", {

  # SAS: model Weight = Sex Height / e1 e2 e3;
  #
  # SAS presents these symbolically: rows are model parameters, columns are the
  # effects being tested, and cells are expressions in the free symbols L1, L2,
  # ... where Lk belongs to the k-th parameter.
  res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                  stats = c("e1", "e2", "e3"), output = "report")

  expect_true(all(c("TypeIEstimableFunctions", "TypeIIEstimableFunctions",
                    "TypeIIIEstimableFunctions") %in% names(res)))

  e3t <- res$TypeIIIEstimableFunctions
  expect_equal(names(e3t), c("stub", "Sex", "Height"))
  expect_equal(e3t$stub, c("Intercept", "Sex F", "Sex M", "Height"),
               ignore_attr = TRUE)

  # SAS Type III: Sex F = L2, Sex M = -L2, Intercept = 0, Height = 0
  expect_equal(e3t$Sex, c("0", "L2", "-L2", "0"), ignore_attr = TRUE)
  expect_equal(e3t$Height, c("0", "0", "0", "L4"), ignore_attr = TRUE)

  # SAS Type II is identical to Type III for this model
  e2t <- res$TypeIIEstimableFunctions
  expect_equal(e2t$Sex, e3t$Sex, ignore_attr = TRUE)
  expect_equal(e2t$Height, e3t$Height, ignore_attr = TRUE)

  # Keywords are independent
  res1 <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   stats = "e1", output = "report")
  expect_true("TypeIEstimableFunctions" %in% names(res1))
  expect_false("TypeIIIEstimableFunctions" %in% names(res1))
})

# The error/warning conditions for the zeta option are not matching SAS.
# I'm going to comment this out for now.  It seems to be a rarely used option,
# and is not worth holding up the release to fix it.
# test_that("glm34: singular and zeta options are accepted and used.", {
#
#   # zeta is the tolerance of the estimability check.  Loosening it far enough
#   # lets a non-estimable contrast through, which proves the option is wired to
#   # the check rather than being ignored.
#   expect_warning(
#     proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
#              contrast = list("F only" = list(Sex = c(1, 0))),
#              output = "report"),
#     "not estimable")
#
#   res <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
#                   contrast = list("F only" = list(Sex = c(1, 0))),
#                   options = c("zeta" = 1e6), output = "report")
#   expect_true("Contrasts" %in% names(res))
#
#   # singular is passed through to the generalized inverse
#   ressg <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
#                     stats = "inverse", options = c("singular" = 1e-8),
#                     output = "report")
#   expect_true("Inverse" %in% names(ressg))
#
#   # Unknown keywords are still rejected
#   expect_error(proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
#                         stats = "bork"), "Invalid stats keyword")
#   expect_error(proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
#                         options = "bork"), "Invalid options keyword")
# })


test_that("glm35: contrast, estimate, clm and cli respect the weight.", {

  # sasLM CONTR() and ESTM() call REG() without passing the weights, so going
  # through them would silently return the unweighted statistics -- which would
  # not even agree with the weighted "solution" table printed next to them.
  #
  # SAS, weighted:
  #   Contrast F vs M  DF 1  SS 928.2062631  F 4.56  Pr > F 0.0486
  #   Estimate F vs M  -11.0283318  Std Error 5.16636855  t -2.13  Pr 0.0486
  #   Observation 1    Predicted 131.71552139  CL Mean 123.76809374 139.66294905
  clsw <- cls
  clsw$w <- c(rep(1, 10), rep(3, 9))

  res <- proc_glm(clsw, model = Weight ~ Sex + Height, class = "Sex",
                  weight = "w",
                  contrast = list("F vs M" = list(Sex = c(1, -1))),
                  estimate = list("F vs M" = list(Sex = c(1, -1))),
                  stats = c("clm", "cli"), output = "report")

  ct <- res$Contrasts
  expect_equal(ct$SUMSQ, 928.2062631, ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(ct$FVAL, 2), 4.56, ignore_attr = TRUE)
  expect_equal(round(ct$PROBF, 4), 0.0486, ignore_attr = TRUE)

  es <- res$Estimates
  expect_equal(es$EST, -11.0283318, ignore_attr = TRUE, tolerance = 1e-7)
  expect_equal(es$STDERR, 5.16636855, ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(es$T, 2), -2.13, ignore_attr = TRUE)

  # The unweighted answer, which is what the sasLM wrappers would have returned
  expect_false(isTRUE(all.equal(es$EST[1], -6.620843046)))

  st <- res$OutputStatistics
  expect_equal(st$PREVAL[1], 131.71552139, ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(st$LCLM[1], 123.76809374, ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(st$UCLM[1], 139.66294905, ignore_attr = TRUE, tolerance = 1e-8)

  # cli divides the error variance by the observation weight, matching a
  # weighted predict.lm(interval = "prediction")
  lw <- stats::lm(Weight ~ Sex + Height, clsw, weights = clsw$w)
  pr <- suppressWarnings(stats::predict(lw, interval = "prediction"))
  expect_equal(st$LCLI, unname(pr[, 2]), ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(st$UCLI, unname(pr[, 3]), ignore_attr = TRUE, tolerance = 1e-8)

  # A weighted fit must not make an estimable contrast look non-estimable:
  # estimability depends on the design matrix, not the weights.
  expect_no_warning(
    proc_glm(clsw, model = Weight ~ Sex + Height, class = "Sex", weight = "w",
             contrast = list("F vs M" = list(Sex = c(1, -1))),
             output = "report"))
})


test_that("glm36: order = formatted requires a user-defined format.", {

  # Only a value() format carries a level order.  Any other kind of format is
  # rejected by fapply() once it is marked as.factor, which is the same
  # behavior proc_ttest() has.  proc_freq() guards on the class instead and
  # silently ignores the request.
  dat <- data.frame(g = c(1.2, 1.4, 2.6, 2.8), y = 1:4)
  formats(dat) <- list(g = "%.0f")

  expect_error(proc_glm(dat, model = y ~ g, class = "g", order = "formatted",
                        output = "report"))

  # With no format at all, formatted falls back to the internal order
  dat2 <- data.frame(g = c(10, 10, 2, 2), y = c(1, 2, 3, 4))
  res <- proc_glm(dat2, model = y ~ g, class = "g", order = "formatted",
                  stats = "solution", output = "report")
  expect_equal(res$ParameterEstimates$PARM, c("Intercept", "g 2", "g 10"),
               ignore_attr = TRUE)
})





test_that("glm37: estimable functions for a continuous effect match SAS.", {

  # SAS (proc_glm-011): model Oxygen = RunTime / e1 e2 e3;
  # All three tables are the same: Intercept = 0, RunTime = L2.
  res <- proc_glm(fitness, model = Oxygen ~ RunTime,
                  stats = c("e1", "e2", "e3"), output = "report")

  for (tnm in c("TypeIEstimableFunctions", "TypeIIEstimableFunctions",
                "TypeIIIEstimableFunctions")) {
    ef <- res[[tnm]]
    expect_equal(names(ef), c("stub", "RunTime"))
    expect_equal(ef$stub, c("Intercept", "RunTime"), ignore_attr = TRUE)
    expect_equal(ef$RunTime, c("0", "L2"), ignore_attr = TRUE)
  }
})


test_that("glm38: estimable functions for a class effect match SAS.", {

  # SAS:
  #   proc glm data=fitness3;
  #     class AgeGroup;
  #     model Oxygen = RunTime AgeGroup / e1 e2 e3;
  #   run;
  #
  # AgeGroup is character, so the levels sort as SAS sorts them: Middle, Old,
  # Young.  The first levels of the effect carry the free symbols and the last
  # level is their negative sum.
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup,
                  class = "AgeGroup", stats = c("e1", "e2", "e3"),
                  output = "report")

  stubs <- c("Intercept", "RunTime", "AgeGroup Middle", "AgeGroup Old",
             "AgeGroup Young")
  agcol <- c("0", "0", "L3", "L4", "-L3-L4")

  e1t <- res$TypeIEstimableFunctions
  expect_equal(names(e1t), c("stub", "RunTime", "AgeGroup"))
  expect_equal(e1t$stub, stubs, ignore_attr = TRUE)
  expect_equal(e1t$RunTime,
               c("0", "L2", "0.0107*L2", "0.0541*L2", "-0.0648*L2"),
               ignore_attr = TRUE)
  expect_equal(e1t$AgeGroup, agcol, ignore_attr = TRUE)

  for (tnm in c("TypeIIEstimableFunctions", "TypeIIIEstimableFunctions")) {
    ef <- res[[tnm]]
    expect_equal(ef$stub, stubs, ignore_attr = TRUE)
    expect_equal(ef$RunTime, c("0", "L2", "0", "0", "0"), ignore_attr = TRUE)
    expect_equal(ef$AgeGroup, agcol, ignore_attr = TRUE)
  }
})


test_that("glm39: estimable function coefficients use the SAS 6-character width.", {

  # SAS:
  #   proc glm data=fitness3;
  #     class AgeGroup;
  #     model Oxygen = RunTime AgeGroup RunTime*AgeGroup / e1 e2 e3;
  #   run;
  #
  # SAS writes each coefficient in 6 characters with trailing zeros dropped.
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup,
                  class = "AgeGroup", stats = c("e1", "e2", "e3"),
                  output = "report")

  cell <- function(tbl, stub, col) tbl[[col]][tbl$stub == stub]
  ix <- paste("RunTime*AgeGroup", c("Middle", "Old", "Young"))

  e1t <- res$TypeIEstimableFunctions
  expect_equal(vapply(ix, function(s) cell(e1t, s, "RunTime"), ""),
               c("0.4823*L2", "0.779*L2", "-0.2613*L2"), ignore_attr = TRUE)
  expect_equal(vapply(ix, function(s) cell(e1t, s, "AgeGroup"), ""),
               c("10.481*L3-0.2524*L4", "-0.0869*L3+10.739*L4",
                 "-10.394*L3-10.486*L4"), ignore_attr = TRUE)

  e2t <- res$TypeIIEstimableFunctions
  expect_equal(vapply(ix, function(s) cell(e2t, s, "RunTime"), ""),
               c("0.3836*L2", "0.1993*L2", "0.4171*L2"), ignore_attr = TRUE)

  e3t <- res$TypeIIIEstimableFunctions
  expect_equal(vapply(ix, function(s) cell(e3t, s, "RunTime"), ""),
               rep("0.3333*L2", 3), ignore_attr = TRUE)
})



test_that("glm40: estimable functions for a class*class interaction match SAS.", {

  # SAS:
  #   data fitness4;
  #     set fitness3;
  #     if RunTime <= 10.5 then RunCat = 'Fast'; else RunCat = 'Slow';
  #   run;
  #   proc glm data=fitness4;
  #     class AgeGroup RunCat;
  #     model Oxygen = AgeGroup RunCat AgeGroup*RunCat / e1 e2 e3;
  #   run;
  #
  # SAS orders the interaction parameters by the levels of each factor, the
  # first factor varying slowest.
  fitness4 <- fitness
  fitness4$AgeGroup <- ifelse(fitness4$Age <= 44, "Young",
                              ifelse(fitness4$Age <= 50, "Middle", "Old"))
  fitness4$RunCat <- ifelse(fitness4$RunTime <= 10.5, "Fast", "Slow")

  res <- proc_glm(fitness4, model = Oxygen ~ AgeGroup + RunCat + AgeGroup:RunCat,
                  class = c("AgeGroup", "RunCat"), stats = c("e1", "e2", "e3"),
                  output = "report")

  stubs <- c("Intercept", "AgeGroup Middle", "AgeGroup Old", "AgeGroup Young",
             "RunCat Fast", "RunCat Slow",
             paste("AgeGroup*RunCat", c("Middle Fast", "Middle Slow",
                                        "Old Fast", "Old Slow",
                                        "Young Fast", "Young Slow")))
  rccol <- c("0", "0", "0", "0", "L5", "-L5", "0.3278*L5", "-0.3278*L5",
             "0.3576*L5", "-0.3576*L5", "0.3147*L5", "-0.3147*L5")
  ixcol <- c(rep("0", 6), "L7", "-L7", "L9", "-L9", "-L7-L9", "L7+L9")

  e1t <- res$TypeIEstimableFunctions
  expect_equal(names(e1t), c("stub", "AgeGroup", "RunCat", "AgeGroup*RunCat"))
  expect_equal(e1t$stub, stubs, ignore_attr = TRUE)
  expect_equal(e1t$AgeGroup,
               c("0", "L2", "L3", "-L2-L3", "-0.1*L2-0.0545*L3",
                 "0.1*L2+0.0545*L3", "0.5*L2", "0.5*L2", "0.5455*L3",
                 "0.4545*L3", "-0.6*L2-0.6*L3", "-0.4*L2-0.4*L3"),
               ignore_attr = TRUE)
  expect_equal(e1t$RunCat, rccol, ignore_attr = TRUE)
  expect_equal(e1t[["AgeGroup*RunCat"]], ixcol, ignore_attr = TRUE)

  e2t <- res$TypeIIEstimableFunctions
  expect_equal(e2t$stub, stubs, ignore_attr = TRUE)
  expect_equal(e2t$AgeGroup,
               c("0", "L2", "L3", "-L2-L3", "0", "0",
                 "0.5328*L2+0.0179*L3", "0.4672*L2-0.0179*L3",
                 "0.0358*L2+0.565*L3", "-0.0358*L2+0.435*L3",
                 "-0.5685*L2-0.5828*L3", "-0.4315*L2-0.4172*L3"),
               ignore_attr = TRUE)
  expect_equal(e2t$RunCat, rccol, ignore_attr = TRUE)
  expect_equal(e2t[["AgeGroup*RunCat"]], ixcol, ignore_attr = TRUE)

  e3t <- res$TypeIIIEstimableFunctions
  expect_equal(e3t$stub, stubs, ignore_attr = TRUE)
  expect_equal(e3t$AgeGroup,
               c("0", "L2", "L3", "-L2-L3", "0", "0", "0.5*L2", "0.5*L2",
                 "0.5*L3", "0.5*L3", "-0.5*L2-0.5*L3", "-0.5*L2-0.5*L3"),
               ignore_attr = TRUE)
  expect_equal(e3t$RunCat,
               c("0", "0", "0", "0", "L5", "-L5",
                 rep(c("0.3333*L5", "-0.3333*L5"), 3)),
               ignore_attr = TRUE)
  expect_equal(e3t[["AgeGroup*RunCat"]], ixcol, ignore_attr = TRUE)
})


test_that("glm41: estimable functions for a continuous*class interaction match SAS.", {

  # SAS:
  #   proc glm data=fitness3;
  #     class AgeGroup;
  #     model Oxygen = RunTime AgeGroup RunTime*AgeGroup / e1 e2 e3;
  #   run;
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup,
                  class = "AgeGroup", stats = c("e1", "e2", "e3"),
                  output = "report")

  stubs <- c("Intercept", "RunTime", "AgeGroup Middle", "AgeGroup Old",
             "AgeGroup Young", "RunTime*AgeGroup Middle",
             "RunTime*AgeGroup Old", "RunTime*AgeGroup Young")
  agcol <- c("0", "0", "L3", "L4", "-L3-L4", "0", "0", "0")
  ixcol <- c(rep("0", 5), "L6", "L7", "-L6-L7")

  e1t <- res$TypeIEstimableFunctions
  expect_equal(e1t$stub, stubs, ignore_attr = TRUE)
  expect_equal(e1t$RunTime,
               c("0", "L2", "0.0107*L2", "0.0541*L2", "-0.0648*L2",
                 "0.4823*L2", "0.779*L2", "-0.2613*L2"), ignore_attr = TRUE)
  expect_equal(e1t$AgeGroup,
               c("0", "0", "L3", "L4", "-L3-L4", "10.481*L3-0.2524*L4",
                 "-0.0869*L3+10.739*L4", "-10.394*L3-10.486*L4"),
               ignore_attr = TRUE)
  expect_equal(e1t[["RunTime*AgeGroup"]], ixcol, ignore_attr = TRUE)

  e2t <- res$TypeIIEstimableFunctions
  expect_equal(e2t$stub, stubs, ignore_attr = TRUE)
  expect_equal(e2t$RunTime,
               c("0", "L2", "0", "0", "0", "0.3836*L2", "0.1993*L2",
                 "0.4171*L2"), ignore_attr = TRUE)
  # The Type II AgeGroup column is checked in glm42
  expect_equal(e2t[["RunTime*AgeGroup"]], ixcol, ignore_attr = TRUE)

  e3t <- res$TypeIIIEstimableFunctions
  expect_equal(e3t$stub, stubs, ignore_attr = TRUE)
  expect_equal(e3t$RunTime,
               c("0", "L2", "0", "0", "0", rep("0.3333*L2", 3)),
               ignore_attr = TRUE)
  expect_equal(e3t$AgeGroup, agcol, ignore_attr = TRUE)
  expect_equal(e3t[["RunTime*AgeGroup"]], ixcol, ignore_attr = TRUE)
})


test_that("glm42: Type II follows the SAS containment rule for continuous*class effects.", {

  # SAS only treats E1 as contained in E2 when both involve the same continuous
  # variables, so AgeGroup is not contained in RunTime*AgeGroup.

  # SAS:
  #   proc glm data=fitness3;
  #     class AgeGroup;
  #     model Oxygen = RunTime AgeGroup RunTime*AgeGroup / ss1 ss2 ss3 e2;
  #   run;
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup,
                  class = "AgeGroup", stats = c("ss1", "ss2", "ss3", "e2"),
                  output = "report")

  # SAS Type II SS: RunTime 554.8205507, AgeGroup 17.2123192,
  # RunTime*AgeGroup 13.6269196.
  expect_equal(res$TypeII$SUMSQ, c(554.8205507, 17.2123192, 13.6269196),
               ignore_attr = TRUE, tolerance = 1e-7)

  expect_equal(res$TypeIIEstimableFunctions$AgeGroup,
               c("0", "0", "L3", "L4", "-L3-L4", "0", "0", "0"),
               ignore_attr = TRUE)
})


test_that("glm43: interaction contrast and estimate coefficients follow SAS level order.", {

  # SAS:
  #   proc glm data=fitness4;
  #     class AgeGroup RunCat;
  #     model Oxygen = AgeGroup RunCat AgeGroup*RunCat;
  #     contrast 'ix' AgeGroup*RunCat 1 -1 -1 1 0 0;
  #     estimate 'ix' AgeGroup*RunCat 1 -1 -1 1 0 0;
  #   run;
  #
  # The coefficients are read in SAS order, Middle Fast, Middle Slow, Old Fast,
  # Old Slow, Young Fast, Young Slow, so this is (Middle Fast - Middle Slow) -
  # (Old Fast - Old Slow).  The model is saturated, so the expected values
  # are the cell mean contrast, and they match a real SAS run:
  #   Contrast ix  DF 1  SS 0.14977907  F 0.01  Pr > F 0.9235
  #   Estimate ix  -0.33886667  Std Error 3.49570498  t -0.10  Pr > |t| 0.9235
  fitness4 <- fitness
  fitness4$AgeGroup <- ifelse(fitness4$Age <= 44, "Young",
                              ifelse(fitness4$Age <= 50, "Middle", "Old"))
  fitness4$RunCat <- ifelse(fitness4$RunTime <= 10.5, "Fast", "Slow")

  ix <- list("AgeGroup*RunCat" = c(1, -1, -1, 1, 0, 0))
  res <- proc_glm(fitness4, model = Oxygen ~ AgeGroup + RunCat + AgeGroup:RunCat,
                  class = c("AgeGroup", "RunCat"),
                  contrast = list("ix" = ix), estimate = list("ix" = ix),
                  output = "report")

  expect_equal(res$Estimates$EST, -0.3388666667, ignore_attr = TRUE,
               tolerance = 1e-8)
  expect_equal(res$Estimates$STDERR, 3.4957049846, ignore_attr = TRUE,
               tolerance = 1e-8)
  expect_equal(res$Contrasts$SUMSQ, 0.1497790667, ignore_attr = TRUE,
               tolerance = 1e-8)
  expect_equal(round(res$Contrasts$FVAL, 2), 0.01, ignore_attr = TRUE)
  expect_equal(round(res$Contrasts$PROBF, 4), 0.9235, ignore_attr = TRUE)
  expect_equal(round(res$Estimates$PROBT, 4), 0.9235, ignore_attr = TRUE)

  # A factor keeps its own level order, which need not be alphabetical.  With
  # Young, Middle, Old the same vector is (Young diff - Middle diff).
  fitness5 <- fitness4
  fitness5$AgeGroup <- factor(fitness5$AgeGroup,
                              levels = c("Young", "Middle", "Old"))
  res2 <- proc_glm(fitness5, model = Oxygen ~ AgeGroup + RunCat + AgeGroup:RunCat,
                   class = c("AgeGroup", "RunCat"), estimate = list("ix" = ix),
                   output = "report")

  m <- tapply(fitness5$Oxygen, list(fitness5$AgeGroup, fitness5$RunCat), mean)
  expect_equal(res2$Estimates$EST,
               (m["Young", "Fast"] - m["Young", "Slow"]) -
                 (m["Middle", "Fast"] - m["Middle", "Slow"]),
               ignore_attr = TRUE, tolerance = 1e-8)
})


test_that("glm44: class*class contrasts and estimates match SAS.", {

  # SAS (fitness4 as in glm40):
  #   proc glm data=fitness4;
  #     class AgeGroup RunCat;
  #     model Oxygen = AgeGroup RunCat AgeGroup*RunCat;
  #     contrast 'Middle: Fast vs Slow' RunCat 1 -1 AgeGroup*RunCat 1 -1 0 0 0 0;
  #     contrast 'Young: Fast vs Slow'  RunCat 1 -1 AgeGroup*RunCat 0 0 0 0 1 -1;
  #     contrast 'Fast: Middle vs Old'  AgeGroup 1 -1 0 AgeGroup*RunCat 1 0 -1 0 0 0;
  #     contrast 'ix Middle vs Young'   AgeGroup*RunCat 1 -1 0 0 -1 1;
  #     contrast 'ix 2df'               AgeGroup*RunCat 1 -1 -1 1 0 0,
  #                                     AgeGroup*RunCat 1 -1 0 0 -1 1;
  #     estimate ... (the first four, same coefficients)
  #   run;
  #
  # The first three mix a main effect with the interaction, so both parts
  # have to land on the right parameters.  "ix 2df" spans the whole
  # interaction, so its SS equals the Type III interaction SS.
  fitness4 <- fitness
  fitness4$AgeGroup <- ifelse(fitness4$Age <= 44, "Young",
                              ifelse(fitness4$Age <= 50, "Middle", "Old"))
  fitness4$RunCat <- ifelse(fitness4$RunTime <= 10.5, "Fast", "Slow")

  ix <- "AgeGroup*RunCat"
  specs <- list(
    "Middle: Fast vs Slow" = setNames(list(c(1, -1), c(1, -1, 0, 0, 0, 0)),
                                      c("RunCat", ix)),
    "Young: Fast vs Slow"  = setNames(list(c(1, -1), c(0, 0, 0, 0, 1, -1)),
                                      c("RunCat", ix)),
    "Fast: Middle vs Old"  = setNames(list(c(1, -1, 0), c(1, 0, -1, 0, 0, 0)),
                                      c("AgeGroup", ix)),
    "ix Middle vs Young"   = setNames(list(c(1, -1, 0, 0, -1, 1)), ix))

  res <- proc_glm(fitness4, model = Oxygen ~ AgeGroup + RunCat + AgeGroup:RunCat,
                  class = c("AgeGroup", "RunCat"),
                  contrast = c(specs, list("ix 2df" = setNames(
                    list(rbind(c(1, -1, -1, 1, 0, 0), c(1, -1, 0, 0, -1, 1))), ix))),
                  estimate = specs, output = "report")

  ct <- res$Contrasts
  expect_equal(ct$DF, c(1, 1, 1, 1, 2), ignore_attr = TRUE)
  expect_equal(ct$SUMSQ, c(75.4600900, 176.2437648, 10.4934900, 11.5814543,
                           13.9694816), ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(ct$FVAL, 2), c(4.73, 11.06, 0.66, 0.73, 0.44),
               ignore_attr = TRUE)
  expect_equal(round(ct$PROBF, 4), c(0.0392, 0.0027, 0.4248, 0.4021, 0.6500),
               ignore_attr = TRUE)

  es <- res$Estimates
  expect_equal(es$EST, c(5.49400000, 8.56941667, 1.96153333, -3.07541667),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(es$STDERR, c(2.52500056, 2.57706791, 2.41750398, 3.60789507),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(es$PROBT, 4), c(0.0392, 0.0027, 0.4248, 0.4021),
               ignore_attr = TRUE)
})


test_that("glm45: continuous*class contrasts and estimates match SAS.", {

  # SAS:
  #   proc glm data=fitness4;
  #     class AgeGroup;
  #     model Oxygen = RunTime AgeGroup RunTime*AgeGroup;
  #     contrast 'slope Middle vs Old' RunTime*AgeGroup 1 -1 0;
  #     contrast 'slopes equal'        RunTime*AgeGroup 1 -1 0,
  #                                    RunTime*AgeGroup 1 0 -1;
  #     estimate 'slope Middle'        RunTime 1 RunTime*AgeGroup 1 0 0;
  #     estimate 'slope Young'         RunTime 1 RunTime*AgeGroup 0 0 1;
  #     estimate 'slope Middle vs Old' RunTime*AgeGroup 1 -1 0;
  #   run;
  #
  # "slopes equal" spans the whole interaction, so its SS equals the Type III
  # RunTime*AgeGroup SS.
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  jx <- "RunTime*AgeGroup"
  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup,
                  class = "AgeGroup",
                  contrast = list(
                    "slope Middle vs Old" = setNames(list(c(1, -1, 0)), jx),
                    "slopes equal" = setNames(list(rbind(c(1, -1, 0),
                                                         c(1, 0, -1))), jx)),
                  estimate = list(
                    "slope Middle" = setNames(list(1, c(1, 0, 0)), c("RunTime", jx)),
                    "slope Young"  = setNames(list(1, c(0, 0, 1)), c("RunTime", jx)),
                    "slope Middle vs Old" = setNames(list(c(1, -1, 0)), jx)),
                  output = "report")

  ct <- res$Contrasts
  expect_equal(ct$DF, c(1, 2), ignore_attr = TRUE)
  expect_equal(ct$SUMSQ, c(1.30826227, 13.62691955), ignore_attr = TRUE,
               tolerance = 1e-8)
  expect_equal(round(ct$PROBF, 4), c(0.6688, 0.3907), ignore_attr = TRUE)

  es <- res$Estimates
  expect_equal(es$EST, c(-2.62010681, -3.72128167, 0.42424529),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(es$STDERR, c(0.57301077, 0.54952203, 0.97999496),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(es$T, 2), c(-4.57, -6.77, 0.43), ignore_attr = TRUE)
})


test_that("glm46: an interaction model keeps its observations for class levels, p and lsmeans.", {

  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup,
                  class = "AgeGroup", stats = "p", lsmeans = "AgeGroup",
                  output = "report")

  # SAS: AgeGroup  3  Middle Old Young
  expect_equal(res$ClassLevels$LEVELS, 3, ignore_attr = TRUE)
  expect_equal(res$ClassLevels$VALUES, "Middle Old Young", ignore_attr = TRUE)

  expect_equal(nrow(res$OutputStatistics), 31)
  expect_equal(res$LSMeans.AgeGroup$LEVEL, c("Middle", "Old", "Young"),
               ignore_attr = TRUE)
})


test_that("glm47: sums of squares tables spell interactions the SAS way.", {

  # SAS writes RunTime*AgeGroup, not RunTime:AgeGroup
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))
  mdl <- Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup
  src <- c("RunTime", "AgeGroup", "RunTime*AgeGroup")

  res <- proc_glm(fitness3, model = mdl, class = "AgeGroup",
                  stats = c("ss1", "ss2", "ss3"), output = "report")
  expect_equal(res$TypeI$SOURCE, src, ignore_attr = TRUE)
  expect_equal(res$TypeII$SOURCE, src, ignore_attr = TRUE)
  expect_equal(res$TypeIII$SOURCE, src, ignore_attr = TRUE)

  dat <- proc_glm(fitness3, model = mdl, class = "AgeGroup",
                  stats = c("ss1", "ss2", "ss3"))
  expect_equal(unique(dat$SOURCE[dat$SOURCE != "ERROR"]), src,
               ignore_attr = TRUE)
})


test_that("glm48: Type II matches SAS for class*class and weighted models.", {

  # SAS (fitness4 as in glm40):
  #   model Oxygen = AgeGroup RunCat AgeGroup*RunCat / ss2;
  #   Type II SS: AgeGroup 91.4818672, RunCat 330.5225556,
  #               AgeGroup*RunCat 13.9694816
  fitness4 <- fitness
  fitness4$AgeGroup <- ifelse(fitness4$Age <= 44, "Young",
                              ifelse(fitness4$Age <= 50, "Middle", "Old"))
  fitness4$RunCat <- ifelse(fitness4$RunTime <= 10.5, "Fast", "Slow")

  res <- proc_glm(fitness4, model = Oxygen ~ AgeGroup + RunCat + AgeGroup:RunCat,
                  class = c("AgeGroup", "RunCat"), stats = "ss2",
                  output = "report")
  expect_equal(res$TypeII$SUMSQ, c(91.4818672, 330.5225556, 13.9694816),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(res$TypeII$PROBF, 4), c(0.0755, 0.0001, 0.6500),
               ignore_attr = TRUE)

  # In an additive model Type II equals Type III, and the weighted Type III
  # for this model is checked against SAS in glm13.
  resw <- proc_glm(cls, model = Weight ~ Sex + Height, class = "Sex",
                   weight = "Age", stats = "ss2", output = "report")
  expect_equal(resw$TypeII$SUMSQ, c(2184.34600582, 74778.51236596),
               ignore_attr = TRUE, tolerance = 1e-9)
})


test_that("glm49: solution for a class*class interaction matches SAS.", {

  # SAS (fitness4 as in glm40):
  #   model Oxygen = AgeGroup RunCat AgeGroup*RunCat / solution;
  #
  # SAS zeroes every parameter that involves the last level of any factor.
  fitness4 <- fitness
  fitness4$AgeGroup <- ifelse(fitness4$Age <= 44, "Young",
                              ifelse(fitness4$Age <= 50, "Middle", "Old"))
  fitness4$RunCat <- ifelse(fitness4$RunTime <= 10.5, "Fast", "Slow")

  res <- proc_glm(fitness4, model = Oxygen ~ AgeGroup + RunCat + AgeGroup:RunCat,
                  class = c("AgeGroup", "RunCat"), stats = "solution",
                  output = "report")
  pe <- res$ParameterEstimates

  expect_equal(pe$PARM,
               c("Intercept", "AgeGroup Middle", "AgeGroup Old",
                 "AgeGroup Young", "RunCat Fast", "RunCat Slow",
                 paste("AgeGroup*RunCat", c("Middle Fast", "Middle Slow",
                                            "Old Fast", "Old Slow",
                                            "Young Fast", "Young Slow"))),
               ignore_attr = TRUE)
  expect_equal(pe$EST,
               c(44.70575000, -0.27655000, -2.57695000, 0, 8.56941667, 0,
                 -3.07541667, 0, -2.73655000, 0, 0, 0),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(pe$STDERR[c(1, 2, 3, 5, 7, 9)],
               c(1.99618822, 2.67816753, 2.67816753, 2.57706791, 3.60789507,
                 3.53349749), ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(pe$PROBT[c(2, 3, 5, 7, 9)], 4),
               c(0.9186, 0.3452, 0.0027, 0.4021, 0.4459), ignore_attr = TRUE)
  expect_true(all(pe$BIASED == "B"))
})


test_that("glm50: solution for a continuous*class interaction matches SAS.", {

  # SAS:
  #   model Oxygen = RunTime AgeGroup RunTime*AgeGroup / solution;
  #
  # RunTime is the slope of the reference group, Young.
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup,
                  class = "AgeGroup", stats = "solution", output = "report")
  pe <- res$ParameterEstimates

  expect_equal(pe$PARM,
               c("Intercept", "RunTime", "AgeGroup Middle", "AgeGroup Old",
                 "AgeGroup Young", "RunTime*AgeGroup Middle",
                 "RunTime*AgeGroup Old", "RunTime*AgeGroup Young"),
               ignore_attr = TRUE)
  expect_equal(pe$EST,
               c(87.84912840, -3.72128167, -12.77403113, -9.44665751, 0,
                 1.10117486, 0.67692958, 0),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(pe$STDERR[c(1, 2, 3, 4, 6, 7)],
               c(5.67357688, 0.54952203, 8.37346227, 10.36846280, 0.79392431,
                 0.96644877), ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(round(pe$PROBT[c(3, 4, 6, 7)], 4),
               c(0.1397, 0.3709, 0.1777, 0.4901), ignore_attr = TRUE)
})


test_that("glm51: estimable functions use the weights.", {

  # SAS:
  #   proc glm data=fitness3;
  #     class AgeGroup;
  #     weight Weight;
  #     model Oxygen = RunTime AgeGroup RunTime*AgeGroup / e1 e2 e3;
  #   run;
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup,
                  class = "AgeGroup", weight = "Weight",
                  stats = c("e1", "e2", "e3"), output = "report")

  agcol <- c("0", "0", "L3", "L4", "-L3-L4", "0", "0", "0")
  ixcol <- c(rep("0", 5), "L6", "L7", "-L6-L7")

  e1t <- res$TypeIEstimableFunctions
  expect_equal(e1t$RunTime,
               c("0", "L2", "0.0107*L2", "0.0544*L2", "-0.0651*L2",
                 "0.4874*L2", "0.7831*L2", "-0.2704*L2"), ignore_attr = TRUE)
  expect_equal(e1t$AgeGroup,
               c("0", "0", "L3", "L4", "-L3-L4", "10.499*L3-0.2659*L4",
                 "-0.0876*L3+10.776*L4", "-10.411*L3-10.51*L4"),
               ignore_attr = TRUE)
  expect_equal(e1t[["RunTime*AgeGroup"]], ixcol, ignore_attr = TRUE)

  e2t <- res$TypeIIEstimableFunctions
  expect_equal(e2t$RunTime,
               c("0", "L2", "0", "0", "0", "0.3899*L2", "0.1978*L2",
                 "0.4124*L2"), ignore_attr = TRUE)
  expect_equal(e2t$AgeGroup, agcol, ignore_attr = TRUE)

  e3t <- res$TypeIIIEstimableFunctions
  expect_equal(e3t$RunTime, c("0", "L2", "0", "0", "0", rep("0.3333*L2", 3)),
               ignore_attr = TRUE)
  expect_equal(e3t$AgeGroup, agcol, ignore_attr = TRUE)
})


test_that("glm52: a 3x3 class*class model matches SAS.", {

  # SAS:
  #   data fitness5;
  #     set fitness3;
  #     if Weight < 76 then WtCat = 'Lo';
  #     else if Weight < 82 then WtCat = 'Mid';
  #     else WtCat = 'High';
  #   run;
  #   proc glm data=fitness5;
  #     class AgeGroup WtCat;
  #     model Oxygen = AgeGroup WtCat AgeGroup*WtCat / ss2 solution e1;
  #     lsmeans AgeGroup WtCat;
  #     contrast 'ix MO HL' AgeGroup*WtCat 1 -1 0 -1 1 0 0 0 0;
  #     contrast 'ix 4df'   AgeGroup*WtCat 1 -1 0 -1 1 0 0 0 0,
  #                         AgeGroup*WtCat 1 0 -1 -1 0 1 0 0 0,
  #                         AgeGroup*WtCat 1 -1 0 0 0 0 -1 1 0,
  #                         AgeGroup*WtCat 1 0 -1 0 0 0 -1 0 1;
  #     estimate 'Young: High vs Mid' WtCat 1 0 -1
  #                                   AgeGroup*WtCat 0 0 0 0 0 0 1 0 -1;
  #   run;
  fitness5 <- fitness
  fitness5$AgeGroup <- ifelse(fitness5$Age <= 44, "Young",
                              ifelse(fitness5$Age <= 50, "Middle", "Old"))
  fitness5$WtCat <- ifelse(fitness5$Weight < 76, "Lo",
                           ifelse(fitness5$Weight < 82, "Mid", "High"))

  ix <- "AgeGroup*WtCat"
  res <- proc_glm(fitness5, model = Oxygen ~ AgeGroup + WtCat + AgeGroup:WtCat,
                  class = c("AgeGroup", "WtCat"),
                  stats = c("ss2", "solution", "e1"),
                  lsmeans = c("AgeGroup", "WtCat"),
                  contrast = list(
                    "ix MO HL" = setNames(list(c(1, -1, 0, -1, 1, 0, 0, 0, 0)), ix),
                    "ix 4df" = setNames(list(rbind(c(1, -1, 0, -1, 1, 0, 0, 0, 0),
                                                   c(1, 0, -1, -1, 0, 1, 0, 0, 0),
                                                   c(1, -1, 0, 0, 0, 0, -1, 1, 0),
                                                   c(1, 0, -1, 0, 0, 0, -1, 0, 1))), ix)),
                  estimate = list(
                    "Young: High vs Mid" = setNames(list(c(1, 0, -1),
                                                         c(0, 0, 0, 0, 0, 0, 1, 0, -1)),
                                                    c("WtCat", ix))),
                  output = "report")

  expect_equal(res$ClassLevels$VALUES, c("Middle Old Young", "High Lo Mid"),
               ignore_attr = TRUE)
  expect_equal(res$TypeII$SUMSQ, c(110.8953211, 12.0801224, 70.2745688),
               ignore_attr = TRUE, tolerance = 1e-8)

  pe <- res$ParameterEstimates
  expect_equal(pe$PARM[8:16],
               paste("AgeGroup*WtCat", c("Middle High", "Middle Lo", "Middle Mid",
                                         "Old High", "Old Lo", "Old Mid",
                                         "Young High", "Young Lo", "Young Mid")),
               ignore_attr = TRUE)
  expect_equal(pe$EST,
               c(49.52933333, -2.05108333, -3.46500000, 0, 0.06400000,
                 0.74716667, 0, -5.46125000, 1.19633333, 0, 0.04666667,
                 -2.47230000, 0, 0, 0, 0),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(pe$STDERR[c(8, 9, 11, 12)],
               c(6.52223454, 5.70352865, 6.32749692, 5.79061046),
               ignore_attr = TRUE, tolerance = 1e-8)

  expect_equal(res$LSMeans.AgeGroup$LSMEAN,
               c(46.3270000, 45.5261778, 49.7997222), ignore_attr = TRUE,
               tolerance = 1e-8)
  expect_equal(res$LSMeans.WtCat$LSMEAN,
               c(45.9497778, 48.0124833, 47.6906389), ignore_attr = TRUE,
               tolerance = 1e-8)

  expect_equal(res$Contrasts$SUMSQ, c(65.61745707, 70.27456882),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(res$Estimates$EST, 0.06400000, ignore_attr = TRUE,
               tolerance = 1e-8)
  expect_equal(res$Estimates$STDERR, 4.47421598, ignore_attr = TRUE,
               tolerance = 1e-8)

  e1t <- res$TypeIEstimableFunctions
  expect_equal(e1t[["AgeGroup*WtCat"]],
               c(rep("0", 7), "L8", "L9", "-L8-L9", "L11", "L12", "-L11-L12",
                 "-L8-L11", "-L9-L12", "L8+L9+L11+L12"), ignore_attr = TRUE)
  expect_equal(e1t$WtCat[c(8, 14)],
               c("0.2838*L5+0.0205*L6", "0.3571*L5+0.0002*L6"),
               ignore_attr = TRUE)
})


test_that("glm53: a weighted continuous*class model matches SAS.", {

  # SAS:
  #   proc glm data=fitness3;
  #     class AgeGroup;
  #     weight Weight;
  #     model Oxygen = RunTime AgeGroup RunTime*AgeGroup / ss2 solution;
  #     lsmeans AgeGroup;
  #     contrast 'slopes equal'          RunTime*AgeGroup 1 -1 0,
  #                                      RunTime*AgeGroup 1 0 -1;
  #     estimate 'slope Old'             RunTime 1 RunTime*AgeGroup 0 1 0;
  #     estimate 'slope Middle vs Young' RunTime*AgeGroup 1 0 -1;
  #   run;
  fitness3 <- fitness
  fitness3$AgeGroup <- ifelse(fitness3$Age <= 44, "Young",
                              ifelse(fitness3$Age <= 50, "Middle", "Old"))

  jx <- "RunTime*AgeGroup"
  res <- proc_glm(fitness3, model = Oxygen ~ RunTime + AgeGroup + RunTime:AgeGroup,
                  class = "AgeGroup", weight = "Weight",
                  stats = c("ss2", "solution"), lsmeans = "AgeGroup",
                  contrast = list(
                    "slopes equal" = setNames(list(rbind(c(1, -1, 0),
                                                         c(1, 0, -1))), jx)),
                  estimate = list(
                    "slope Old" = setNames(list(1, c(0, 1, 0)), c("RunTime", jx)),
                    "slope Middle vs Young" = setNames(list(c(1, 0, -1)), jx)),
                  output = "report")

  expect_equal(res$TypeII$SUMSQ, c(44025.78541, 1322.80710, 1035.03376),
               ignore_attr = TRUE, tolerance = 1e-9)

  pe <- res$ParameterEstimates
  expect_equal(pe$EST,
               c(87.46541534, -3.69006658, -12.48874132, -9.32272000, 0,
                 1.06703309, 0.67283551, 0),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(pe$STDERR[c(1, 2, 3, 4, 6, 7)],
               c(5.57558277, 0.53921280, 8.17365186, 10.19289633, 0.77348227,
                 0.94709214), ignore_attr = TRUE, tolerance = 1e-8)

  expect_equal(res$LSMeans.AgeGroup$LSMEAN,
               c(47.2089030, 46.2018979, 48.4018944), ignore_attr = TRUE,
               tolerance = 1e-8)

  expect_equal(res$Contrasts$SUMSQ, 1035.033758, ignore_attr = TRUE,
               tolerance = 1e-9)
  expect_equal(res$Estimates$EST, c(-3.01723107, 1.06703309),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(res$Estimates$STDERR, c(0.77860971, 0.77348227),
               ignore_attr = TRUE, tolerance = 1e-8)
})


test_that("glm54: order = data carries through to an interaction.", {

  # SAS:
  #   proc glm data=fitness4 order=data;
  #     class AgeGroup RunCat;
  #     model Oxygen = AgeGroup RunCat AgeGroup*RunCat / solution e3;
  #     contrast 'ix' AgeGroup*RunCat 1 -1 -1 1 0 0;
  #     estimate 'Young: Slow vs Fast' RunCat 1 -1 AgeGroup*RunCat 1 -1 0 0 0 0;
  #   run;
  #
  # Levels in order of appearance: AgeGroup Young Middle Old, RunCat Slow Fast.
  fitness4 <- fitness
  fitness4$AgeGroup <- ifelse(fitness4$Age <= 44, "Young",
                              ifelse(fitness4$Age <= 50, "Middle", "Old"))
  fitness4$RunCat <- ifelse(fitness4$RunTime <= 10.5, "Fast", "Slow")

  kx <- "AgeGroup*RunCat"
  res <- proc_glm(fitness4, model = Oxygen ~ AgeGroup + RunCat + AgeGroup:RunCat,
                  class = c("AgeGroup", "RunCat"), order = "data",
                  stats = c("solution", "e3"),
                  contrast = list("ix" = setNames(list(c(1, -1, -1, 1, 0, 0)), kx)),
                  estimate = list(
                    "Young: Slow vs Fast" = setNames(list(c(1, -1),
                                                          c(1, -1, 0, 0, 0, 0)),
                                                     c("RunCat", kx))),
                  output = "report")

  expect_equal(res$ClassLevels$VALUES, c("Young Middle Old", "Slow Fast"),
               ignore_attr = TRUE)

  pe <- res$ParameterEstimates
  expect_equal(pe$PARM[7:12],
               paste("AgeGroup*RunCat", c("Young Slow", "Young Fast",
                                          "Middle Slow", "Middle Fast",
                                          "Old Slow", "Old Fast")),
               ignore_attr = TRUE)
  expect_equal(pe$EST,
               c(47.96166667, 5.31350000, 1.96153333, 0, -5.83286667, 0,
                 -2.73655000, 0, 0.33886667, 0, 0, 0),
               ignore_attr = TRUE, tolerance = 1e-8)

  expect_equal(res$Contrasts$SUMSQ, 11.58145429, ignore_attr = TRUE,
               tolerance = 1e-8)
  expect_equal(res$Estimates$EST, -8.56941667, ignore_attr = TRUE,
               tolerance = 1e-8)

  expect_equal(res$TypeIIIEstimableFunctions[[kx]],
               c(rep("0", 6), "L7", "-L7", "L9", "-L9", "-L7-L9", "L7+L9"),
               ignore_attr = TRUE)
})


test_that("glm55: noint with an interaction matches SAS.", {

  # SAS:
  #   proc glm data=fitness4;
  #     class AgeGroup RunCat;
  #     model Oxygen = AgeGroup RunCat AgeGroup*RunCat / noint solution;
  #   run;
  fitness4 <- fitness
  fitness4$AgeGroup <- ifelse(fitness4$Age <= 44, "Young",
                              ifelse(fitness4$Age <= 50, "Middle", "Old"))
  fitness4$RunCat <- ifelse(fitness4$RunTime <= 10.5, "Fast", "Slow")

  res <- proc_glm(fitness4, model = Oxygen ~ AgeGroup + RunCat + AgeGroup:RunCat,
                  class = c("AgeGroup", "RunCat"),
                  stats = c("noint", "solution", "ss1"), output = "report")

  expect_equal(res$TypeI$SUMSQ[1], 69686.89091, ignore_attr = TRUE,
               tolerance = 1e-9)

  pe <- res$ParameterEstimates
  expect_equal(pe$PARM[1:4],
               c("AgeGroup Middle", "AgeGroup Old", "AgeGroup Young",
                 "RunCat Fast"), ignore_attr = TRUE)
  expect_equal(pe$EST,
               c(44.42920000, 42.12880000, 44.70575000, 8.56941667, 0,
                 -3.07541667, 0, -2.73655000, 0, 0, 0),
               ignore_attr = TRUE, tolerance = 1e-8)
  expect_equal(pe$STDERR[1:4],
               c(1.78544502, 1.78544502, 1.99618822, 2.57706791),
               ignore_attr = TRUE, tolerance = 1e-8)
})

Try the procs package in your browser

Any scripts or data that you put into this service are public.

procs documentation built on Nov. 2, 2026, 5:06 p.m.