From 497ce80733257fa764741ee8659d43f158beb8ca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 17:49:06 +0000 Subject: [PATCH] Match test columns positionally when names cannot align them NNS.stack(cbind(x_1, x_2), y, IVs.test = cbind(test.x_1, test.x_2)) errored with "[IVs.test] columns must exactly match [IVs.train]". cbind() names its columns after the supplied expressions, so the test set carried names describing the test variables rather than the training predictors, and the schema guard rejected an alignment the user never asked for. Column names only carry alignment information when both sides refer to the same predictors, so reconcile them in three cases instead of one: * same set of names -> reorder into the training order, which still catches columns handed over in a different order than training; * no names in common -> the names cannot express an alignment at all, so keep the supplied order and match positionally, per the documented "compatible dimensions" contract; * partial overlap -> ambiguous, and in practice a wrong or misspelled column, so keep the error. The rule lives in one internal helper shared by NNS.stack, NNS.boost, and NNS.reg, replacing six separate name checks that had drifted apart: the vector paths additionally rejected duplicate names outright, and NNS.reg rejected any single prediction column whose name differed from the lone training predictor, which no alignment could ever depend on. Also document the matching rule on IVs.test and point.est. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VFXdYLDGRfLQqSTAMu3LBT --- R/Boost.R | 56 ++----- R/Regression.R | 61 ++++--- R/Stack.R | 38 ++--- man/NNS.boost.Rd | 2 +- man/NNS.reg.Rd | 2 +- man/NNS.stack.Rd | 2 +- tests/testthat/test-predictor-name-matching.R | 154 ++++++++++++++++++ 7 files changed, 225 insertions(+), 90 deletions(-) create mode 100644 tests/testthat/test-predictor-name-matching.R diff --git a/R/Boost.R b/R/Boost.R index 61db34613..4b51e4643 100644 --- a/R/Boost.R +++ b/R/Boost.R @@ -4,7 +4,7 @@ #' #' @param IVs.train a matrix or data frame of variables of numeric or factor data types. #' @param DV.train a numeric or factor vector with compatible dimensions to \code{(IVs.train)}. -#' @param IVs.test a matrix or data frame of variables of numeric or factor data types with compatible dimensions to \code{(IVs.train)}. If NULL, will use \code{(IVs.train)} as default. +#' @param IVs.test a matrix or data frame of variables of numeric or factor data types with compatible dimensions to \code{(IVs.train)}. If NULL, will use \code{(IVs.train)} as default. Columns are matched to \code{(IVs.train)} by name when the two share the same predictor names, and positionally when they share no names at all (as in \code{cbind(test.x_1, test.x_2)} against \code{cbind(x_1, x_2)}). Names that only partly overlap the training predictors are ambiguous and return an error. #' @param type \code{NULL} (default). To perform a classification of discrete integer classes from factor target variable \code{(DV.train)} with a base category of 1, set to \code{(type = "CLASS")}, else for continuous \code{(DV.train)} set to \code{(type = NULL)}. #' @param depth options: (integer, NULL, "max"); \code{(depth = NULL)}(default) Specifies the \code{order} parameter in the \link{NNS.reg} routine, assigning a number of splits in the regressors, analogous to tree depth. #' @param learner.trials integer; 100 (default) Sets the number of trials to obtain an accuracy \code{threshold} level. If the number of all possible feature combinations is less than selected value, the minimum of the two values will be used. @@ -141,20 +141,13 @@ NNS.boost <- function(IVs.train, names(x) <- train_names } else if (length(x) == p) { supplied <- names(x) - if (!is.null(supplied) && all(nzchar(supplied)) && - !identical(make.unique(supplied, sep = "."), train_names)) { - # Align a named test row by the training predictor names rather - - # than silently renaming positionally supplied values. - - if (anyDuplicated(supplied) || - !setequal(supplied, train_names)) { - stop( - "Named [IVs.test] values must exactly match the training predictors.", - call. = FALSE - ) - } - x <- x[train_names] + if (!is.null(supplied) && all(nzchar(supplied))) { + # Normalize duplicate names exactly as the training frame does, then + # align a named test row by the training predictor names only when + # the two name sets describe the same predictors. + supplied <- make.unique(supplied, sep = ".") + ordering <- .nns_match_predictor_names(supplied, train_names, "IVs.test") + if (!is.null(ordering)) x <- x[ordering] } x <- as.data.frame(as.list(x), check.names = FALSE, @@ -175,37 +168,16 @@ NNS.boost <- function(IVs.train, call. = FALSE) } - if (!had_column_names) { - names(x) <- train_names - } else { + if (had_column_names) { # Normalize duplicate names with make.unique() identically to the - # training frame (and to NNS.reg), so cbind(x, x) test input aligns - # with the c("x", "x.1") training columns rather than erroring. - - names(x) <- make.unique(names(x), sep = ".") - missing_names <- setdiff(train_names, names(x)) - extra_names <- setdiff(names(x), train_names) - if (length(missing_names) || length(extra_names)) { - stop( - sprintf( - "[IVs.test] columns must exactly match [IVs.train]. Missing: %s; extra: %s.", - if (length(missing_names)) - paste(missing_names, collapse = ", ") - else - "none", - if (length(extra_names)) - paste(extra_names, collapse = ", ") - else - "none" - ), - call. = FALSE - ) - } - x <- x[, train_names, drop = FALSE] + supplied <- make.unique(names(x), sep = ".") + ordering <- .nns_match_predictor_names(supplied, train_names, "IVs.test") + if (!is.null(ordering)) x <- x[, ordering, drop = FALSE] } - + names(x) <- train_names + x } diff --git a/R/Regression.R b/R/Regression.R index e4f59a17d..f980fa461 100644 --- a/R/Regression.R +++ b/R/Regression.R @@ -141,6 +141,37 @@ out } +# Reconcile supplied prediction / test column names with the training predictor +# names. Names only carry alignment information when both sides refer to the +# same predictors, so three cases are distinguished: +# +# * same set of names -> reorder the supplied columns into the training +# order, which catches columns handed over in a different order; +# * no names in common -> the names cannot express an alignment at all, as in +# cbind(test.x_1, test.x_2) against cbind(x_1, x_2), so keep the supplied +# order and match positionally per the documented "compatible dimensions"; +# * partial overlap -> ambiguous, and in practice a wrong or misspelled +# column, so error. +# +# [supplied] and [train.names] must already be make.unique() normalized and of +# equal length. Returns the permutation to apply to the supplied columns, or +# NULL when they are to be taken in the order supplied. +.nns_match_predictor_names <- function(supplied, train.names, label) { + if (setequal(supplied, train.names)) return(match(train.names, supplied)) + if (!any(supplied %in% train.names)) return(NULL) + + missing.names <- setdiff(train.names, supplied) + extra.names <- setdiff(supplied, train.names) + stop(sprintf( + paste0("[%s] columns must exactly match the training predictors, or ", + "share no names with them to be matched positionally. ", + "Missing: %s; extra: %s."), + label, + if (length(missing.names)) paste(missing.names, collapse = ", ") else "none", + if (length(extra.names)) paste(extra.names, collapse = ", ") else "none" + ), call. = FALSE) +} + .nns_reg_prepare_points <- function(point.est, train.names) { if (is.null(point.est)) return(NULL) p <- length(train.names) @@ -151,16 +182,13 @@ names(out) <- train.names return(out) } - supplied.name <- colnames(point.est) out <- as.data.frame(point.est, check.names = FALSE, stringsAsFactors = FALSE) if (ncol(out) != 1L) { stop("[point.est] must contain exactly one predictor column.", call. = FALSE) } - if (!is.null(supplied.name) && nzchar(supplied.name[1L]) && - !identical(supplied.name[1L], train.names[1L])) { - stop("Named [point.est] columns must exactly match the training predictors.", - call. = FALSE) - } + # A lone column can only align one way, so its name never selects between + # predictors: cbind(test.x) against a single training predictor is matched + # positionally. names(out) <- train.names return(out) } @@ -174,12 +202,8 @@ # Apply the same duplicate-name normalization used for the training frame. # Example: c(x = ..., x = ...) becomes c("x", "x.1") on both sides. supplied <- make.unique(supplied, sep = ".") - if (!setequal(supplied, train.names)) { - stop("Named [point.est] values must exactly match the training predictors.", - call. = FALSE) - } - names(point.est) <- supplied - point.est <- point.est[train.names] + ordering <- .nns_match_predictor_names(supplied, train.names, "point.est") + if (!is.null(ordering)) point.est <- point.est[ordering] } out <- as.data.frame(as.list(point.est), check.names = FALSE, stringsAsFactors = FALSE) @@ -199,15 +223,10 @@ # Normalize prediction names identically before validating/reordering so # cbind(x, x) matches training columns c("x", "x.1") by position. supplied.names <- make.unique(supplied.names, sep = ".") - if (!setequal(supplied.names, train.names)) { - stop("Named [point.est] columns must exactly match the training predictors.", - call. = FALSE) - } - names(out) <- supplied.names - out <- out[, train.names, drop = FALSE] - } else { - names(out) <- train.names + ordering <- .nns_match_predictor_names(supplied.names, train.names, "point.est") + if (!is.null(ordering)) out <- out[, ordering, drop = FALSE] } + names(out) <- train.names out } @@ -647,7 +666,7 @@ #' @param dim.red.method options: ("cor", "NNS.dep", "NNS.caus", "all", "equal", \code{numeric vector}, NULL) method for determining synthetic X* coefficients (per Dana and Dawes (2004)). Selection of a method automatically engages the dimension reduction regression. The default is \code{NULL} for full multivariate regression. \code{(dim.red.method = "NNS.dep")} uses \link{NNS.dep} for nonlinear dependence weights, while \code{(dim.red.method = "NNS.caus")} uses \link{NNS.caus} for causal weights. \code{(dim.red.method = "cor")} uses standard linear correlation for weights. \code{(dim.red.method = "all")} averages all methods for further feature engineering. \code{(dim.red.method = "equal")} uses unit weights. Alternatively, user can specify a numeric vector of coefficients. #' @param tau options("ts", NULL); \code{NULL}(default) To be used in conjunction with \code{(dim.red.method = "NNS.caus")} or \code{(dim.red.method = "all")}. If the regression is using time-series data, set \code{(tau = "ts")} for more accurate causal analysis. #' @param type \code{NULL} (default). To perform a classification, set to \code{(type = "CLASS")}. Like a logistic regression, it is not necessary for target variable of two classes e.g. [0, 1]. -#' @param point.est a numeric or factor vector with compatible dimensions to \code{x}. Returns the fitted value \code{y.hat} for any value of \code{x}. +#' @param point.est a numeric or factor vector with compatible dimensions to \code{x}. Returns the fitted value \code{y.hat} for any value of \code{x}. Columns are matched to \code{x} by name when the two share the same predictor names, and positionally when they share no names at all (as in \code{cbind(test.x_1, test.x_2)} against \code{cbind(x_1, x_2)}). Names that only partly overlap the training predictors are ambiguous and return an error. #' @param location Sets the legend location within the plot, per the \code{x} and \code{y} co-ordinates used in base graphics \link{legend}. #' @param return.values logical; \code{TRUE} (default), set to \code{FALSE} in order to only display a regression plot and call values as needed. #' @param plot logical; \code{TRUE} (default) To plot regression. diff --git a/R/Stack.R b/R/Stack.R index c81bccae8..b772f19d2 100644 --- a/R/Stack.R +++ b/R/Stack.R @@ -4,7 +4,7 @@ #' #' @param IVs.train a vector, matrix or data frame of variables of numeric or factor data types. #' @param DV.train a numeric or factor vector with compatible dimensions to \code{(IVs.train)}. -#' @param IVs.test a vector, matrix or data frame of variables of numeric or factor data types with compatible dimensions to \code{(IVs.train)}. If NULL, will use \code{(IVs.train)} as default. +#' @param IVs.test a vector, matrix or data frame of variables of numeric or factor data types with compatible dimensions to \code{(IVs.train)}. If NULL, will use \code{(IVs.train)} as default. Columns are matched to \code{(IVs.train)} by name when the two share the same predictor names, and positionally when they share no names at all (as in \code{cbind(test.x_1, test.x_2)} against \code{cbind(x_1, x_2)}). Names that only partly overlap the training predictors are ambiguous and return an error. #' @param type \code{NULL} (default). To perform a classification of discrete integer classes from factor target variable \code{(DV.train)} with a base category of 1, set to \code{(type = "CLASS")}, else for continuous \code{(DV.train)} set to \code{(type = NULL)}. Like a logistic regression, this setting is not necessary for target variable of two classes e.g. [0, 1]. #' @param obj.fn expression; \code{expression(sum((predicted - actual)^2))} (default) Sum of squared errors is the default objective function. Any \code{expression()} using the specific terms \code{predicted} and \code{actual} can be used. #' @param objective options: ("min", "max") \code{"min"} (default) Select whether to minimize or maximize the objective function \code{obj.fn}. @@ -145,13 +145,13 @@ NNS.stack <- function(IVs.train, names(x) <- train_names } else if (length(x) == p) { supplied <- names(x) - if (!is.null(supplied) && all(nzchar(supplied)) && - !identical(make.unique(supplied, sep = "."), train_names)) { - if (anyDuplicated(supplied) || !setequal(supplied, train_names)) { - stop("Named [IVs.test] values must exactly match the training predictors.", - call. = FALSE) - } - x <- x[train_names] + if (!is.null(supplied) && all(nzchar(supplied))) { + # Normalize duplicate names exactly as the training frame does, then + # align by name only when the two name sets describe the same + # predictors. + supplied <- make.unique(supplied, sep = ".") + ordering <- .nns_match_predictor_names(supplied, train_names, "IVs.test") + if (!is.null(ordering)) x <- x[ordering] } x <- as.data.frame(as.list(x), check.names = FALSE, stringsAsFactors = FALSE) @@ -171,26 +171,16 @@ NNS.stack <- function(IVs.train, call. = FALSE) } - if (!had_names) { - names(x) <- train_names - } else { + if (had_names) { # Normalize duplicate names with make.unique() identically to the # training frame (and to NNS.reg), so cbind(x, x) test input aligns # with the c("x", "x.1") training columns rather than erroring. - names(x) <- make.unique(names(x), sep = ".") - missing_names <- setdiff(train_names, names(x)) - extra_names <- setdiff(names(x), train_names) - if (length(missing_names) || length(extra_names)) { - stop(sprintf( - paste0("[IVs.test] columns must exactly match [IVs.train]. ", - "Missing: %s; extra: %s."), - if (length(missing_names)) paste(missing_names, collapse = ", ") else "none", - if (length(extra_names)) paste(extra_names, collapse = ", ") else "none" - ), call. = FALSE) - } - x <- x[, train_names, drop = FALSE] + supplied <- make.unique(names(x), sep = ".") + ordering <- .nns_match_predictor_names(supplied, train_names, "IVs.test") + if (!is.null(ordering)) x <- x[, ordering, drop = FALSE] } - + names(x) <- train_names + x } diff --git a/man/NNS.boost.Rd b/man/NNS.boost.Rd index 92904e75f..7439b698f 100644 --- a/man/NNS.boost.Rd +++ b/man/NNS.boost.Rd @@ -33,7 +33,7 @@ NNS.boost( \item{DV.train}{a numeric or factor vector with compatible dimensions to \code{(IVs.train)}.} -\item{IVs.test}{a matrix or data frame of variables of numeric or factor data types with compatible dimensions to \code{(IVs.train)}. If NULL, will use \code{(IVs.train)} as default.} +\item{IVs.test}{a matrix or data frame of variables of numeric or factor data types with compatible dimensions to \code{(IVs.train)}. If NULL, will use \code{(IVs.train)} as default. Columns are matched to \code{(IVs.train)} by name when the two share the same predictor names, and positionally when they share no names at all (as in \code{cbind(test.x_1, test.x_2)} against \code{cbind(x_1, x_2)}). Names that only partly overlap the training predictors are ambiguous and return an error.} \item{type}{\code{NULL} (default). To perform a classification of discrete integer classes from factor target variable \code{(DV.train)} with a base category of 1, set to \code{(type = "CLASS")}, else for continuous \code{(DV.train)} set to \code{(type = NULL)}.} diff --git a/man/NNS.reg.Rd b/man/NNS.reg.Rd index d8f9f0428..ff849d32c 100644 --- a/man/NNS.reg.Rd +++ b/man/NNS.reg.Rd @@ -44,7 +44,7 @@ NNS.reg( \item{type}{\code{NULL} (default). To perform a classification, set to \code{(type = "CLASS")}. Like a logistic regression, it is not necessary for target variable of two classes e.g. [0, 1].} -\item{point.est}{a numeric or factor vector with compatible dimensions to \code{x}. Returns the fitted value \code{y.hat} for any value of \code{x}.} +\item{point.est}{a numeric or factor vector with compatible dimensions to \code{x}. Returns the fitted value \code{y.hat} for any value of \code{x}. Columns are matched to \code{x} by name when the two share the same predictor names, and positionally when they share no names at all (as in \code{cbind(test.x_1, test.x_2)} against \code{cbind(x_1, x_2)}). Names that only partly overlap the training predictors are ambiguous and return an error.} \item{location}{Sets the legend location within the plot, per the \code{x} and \code{y} co-ordinates used in base graphics \link{legend}.} diff --git a/man/NNS.stack.Rd b/man/NNS.stack.Rd index 5d985a656..ff309c83d 100644 --- a/man/NNS.stack.Rd +++ b/man/NNS.stack.Rd @@ -32,7 +32,7 @@ NNS.stack( \item{DV.train}{a numeric or factor vector with compatible dimensions to \code{(IVs.train)}.} -\item{IVs.test}{a vector, matrix or data frame of variables of numeric or factor data types with compatible dimensions to \code{(IVs.train)}. If NULL, will use \code{(IVs.train)} as default.} +\item{IVs.test}{a vector, matrix or data frame of variables of numeric or factor data types with compatible dimensions to \code{(IVs.train)}. If NULL, will use \code{(IVs.train)} as default. Columns are matched to \code{(IVs.train)} by name when the two share the same predictor names, and positionally when they share no names at all (as in \code{cbind(test.x_1, test.x_2)} against \code{cbind(x_1, x_2)}). Names that only partly overlap the training predictors are ambiguous and return an error.} \item{type}{\code{NULL} (default). To perform a classification of discrete integer classes from factor target variable \code{(DV.train)} with a base category of 1, set to \code{(type = "CLASS")}, else for continuous \code{(DV.train)} set to \code{(type = NULL)}. Like a logistic regression, this setting is not necessary for target variable of two classes e.g. [0, 1].} diff --git a/tests/testthat/test-predictor-name-matching.R b/tests/testthat/test-predictor-name-matching.R new file mode 100644 index 000000000..450d0f761 --- /dev/null +++ b/tests/testthat/test-predictor-name-matching.R @@ -0,0 +1,154 @@ +test_that("test columns sharing no names with training are matched positionally", { + # cbind() names its columns after the supplied expressions, so a test set + # built as cbind(test.x_1, test.x_2) carries names that describe nothing + # about the training predictors x_1 / x_2. Those names cannot express an + # alignment, so the columns are taken in the order supplied. + set.seed(123) + x_1 <- rnorm(120); x_2 <- rnorm(120) + y <- 10 * x_1 + 10 * x_2 + rnorm(120) + + set.seed(321) + test.x_1 <- rnorm(20); test.x_2 <- rnorm(20) + + named <- NNS.stack(cbind(x_1, x_2), y, IVs.test = cbind(test.x_1, test.x_2), + method = 1, folds = 2, ncores = 1, status = FALSE)$stack + bare <- NNS.stack(cbind(x_1, x_2), y, + IVs.test = unname(cbind(test.x_1, test.x_2)), + method = 1, folds = 2, ncores = 1, status = FALSE)$stack + + expect_length(named, 20L) + expect_equal(named, bare, tolerance = 1e-12) +}) + +test_that("foreign test names do not silently reorder columns", { + set.seed(4) + x <- data.frame(a = rnorm(80), b = rnorm(80)) + y <- x$a - 2 * x$b + + ordered <- x[1:5, c("a", "b")] + reversed <- x[1:5, c("b", "a")] + + foreign <- setNames(ordered, c("zz", "yy")) + foreign.reversed <- setNames(reversed, c("zz", "yy")) + + base <- NNS.reg(x, y, point.est = ordered, plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est + + # Positional, so the same values in the same order agree ... + expect_equal( + NNS.reg(x, y, point.est = foreign, plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est, + base, tolerance = 1e-12 + ) + # ... and swapping the columns changes the answer, since the names are not + # consulted at all. + expect_false(isTRUE(all.equal( + NNS.reg(x, y, point.est = foreign.reversed, plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est, + base + ))) +}) + +test_that("matching names are still aligned by name, not by position", { + set.seed(5) + x <- data.frame(a = rnorm(80), b = rnorm(80)) + y <- x$a - 2 * x$b + + expect_equal( + NNS.reg(x, y, point.est = x[1:5, c("a", "b")], plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est, + NNS.reg(x, y, point.est = x[1:5, c("b", "a")], plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est, + tolerance = 1e-12 + ) +}) + +test_that("partially overlapping names remain an error", { + # A test set that names some training predictors and not others is + # ambiguous, and in practice a wrong or misspelled column. + set.seed(6) + x <- data.frame(a = rnorm(60), b = rnorm(60)) + y <- x$a + x$b + + bad <- setNames(x[1:5, ], c("a", "c")) + + expect_error( + NNS.reg(x, y, point.est = bad, plot = FALSE, residual.plot = FALSE), + "exactly match" + ) + expect_error( + NNS.stack(x, y, IVs.test = bad, method = 1, folds = 2, ncores = 1, + status = FALSE), + "exactly match" + ) +}) + +test_that("named test vectors follow the same rule", { + set.seed(7) + x <- data.frame(a = rnorm(60), b = rnorm(60)) + y <- x$a - 2 * x$b + + base <- NNS.reg(x, y, point.est = c(a = 0.5, b = -0.5), plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est + + # Permutation of the training names is aligned by name. + expect_equal( + NNS.reg(x, y, point.est = c(b = -0.5, a = 0.5), plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est, + base, tolerance = 1e-12 + ) + # Names shared with nothing in training fall back to position. + expect_equal( + NNS.reg(x, y, point.est = c(q = 0.5, z = -0.5), plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est, + base, tolerance = 1e-12 + ) + expect_error( + NNS.reg(x, y, point.est = c(a = 0.5, z = -0.5), plot = FALSE, + residual.plot = FALSE), + "exactly match" + ) +}) + +test_that("a single predictor accepts any test column name", { + set.seed(8) + x <- rnorm(60); y <- x^2 + rnorm(60) + + expect_equal( + NNS.reg(x, y, point.est = cbind(test.x = x[1:4]), plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est, + NNS.reg(x, y, point.est = x[1:4], plot = FALSE, + residual.plot = FALSE, ncores = 1)$Point.est, + tolerance = 1e-12 + ) +}) + +test_that("NNS.boost applies the same test-column rule", { + set.seed(9) + x <- data.frame(a = rnorm(80), b = rnorm(80), c = rnorm(80)) + y <- as.numeric(x$a + x$b > 0) + + foreign <- setNames(x[1:6, ], c("t.a", "t.b", "t.c")) + expect_error( + NNS.boost(IVs.train = x, DV.train = y, IVs.test = foreign, + epochs = 8, learner.trials = 5, folds = 1, status = FALSE), + NA + ) + + partial <- setNames(x[1:6, ], c("a", "t.b", "t.c")) + expect_error( + NNS.boost(IVs.train = x, DV.train = y, IVs.test = partial, + epochs = 8, learner.trials = 5, folds = 1, status = FALSE), + "exactly match" + ) + + by.name <- NNS.boost(IVs.train = x, DV.train = y, + IVs.test = x[1:6, c("c", "a", "b")], + epochs = 8, learner.trials = 5, folds = 1, + status = FALSE, seed = 7)$results + in.order <- NNS.boost(IVs.train = x, DV.train = y, + IVs.test = x[1:6, c("a", "b", "c")], + epochs = 8, learner.trials = 5, folds = 1, + status = FALSE, seed = 7)$results + expect_equal(by.name, in.order, tolerance = 1e-12) +})