From f77d14caa36c12e2637817b946a746c52341cb42 Mon Sep 17 00:00:00 2001 From: "Logan C. Brooks" Date: Wed, 9 Oct 2024 17:26:41 -0700 Subject: [PATCH 1/3] Make `key_colnames` map to unique key + more demanding * Make `key_colnames.epi_archive` output epikey-time-version rather than just epikey-time. * Make `key_colnames.data.frame` require `other_keys` be provided. * Remove `key_colnames.default`. * Make `key_colnames` forbid passing `exclude` positionally. * Update downstream `revision_summary`. --- NAMESPACE | 2 +- R/key_colnames.R | 38 ++++++++++++++++++++------------------ R/revision_analysis.R | 4 ++-- man/key_colnames.Rd | 27 ++++++++++++++------------- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index e8b77b6da..26e7db77f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -39,7 +39,6 @@ S3method(guess_period,Date) S3method(guess_period,POSIXt) S3method(guess_period,default) S3method(key_colnames,data.frame) -S3method(key_colnames,default) S3method(key_colnames,epi_archive) S3method(key_colnames,epi_df) S3method(mean,epi_df) @@ -194,6 +193,7 @@ importFrom(rlang,arg_match) importFrom(rlang,caller_arg) importFrom(rlang,caller_env) importFrom(rlang,check_dots_empty) +importFrom(rlang,check_dots_empty0) importFrom(rlang,enquo) importFrom(rlang,enquos) importFrom(rlang,env) diff --git a/R/key_colnames.R b/R/key_colnames.R index 49c326748..ce2825aae 100644 --- a/R/key_colnames.R +++ b/R/key_colnames.R @@ -1,28 +1,29 @@ -#' Grab any keys associated to an epi_df +#' Get names of columns that form a (unique) key associated with an object #' -#' @param x a data.frame, tibble, or epi_df +#' This is entirely based on metadata and arguments passed; there are no +#' explicit checks that the key actually is unique in any associated data +#' structures. +#' +#' @param x an object, such as an [`epi_df`] #' @param ... additional arguments passed on to methods -#' @param other_keys an optional character vector of other keys to include -#' @param exclude an optional character vector of keys to exclude -#' @return If an `epi_df`, this returns all "keys". Otherwise `NULL`. +#' @param other_keys character vector; what besides `geo_value` and `time_value` +#' (if present) should we consider to be key columns? Used, e.g., if we +#' @param exclude an optional character vector of key column names to exclude +#' from the result +#' @return character vector #' @keywords internal #' @export -key_colnames <- function(x, ...) { +key_colnames <- function(x, ..., exclude = character(0L)) { UseMethod("key_colnames") } #' @rdname key_colnames -#' @method key_colnames default -#' @export -key_colnames.default <- function(x, ...) { - character(0L) -} - -#' @rdname key_colnames +#' @importFrom rlang check_dots_empty0 #' @method key_colnames data.frame #' @export -key_colnames.data.frame <- function(x, other_keys = character(0L), exclude = character(0L), ...) { +key_colnames.data.frame <- function(x, other_keys, ..., exclude = character(0L)) { assert_character(other_keys) + check_dots_empty0(...) assert_character(exclude) nm <- setdiff(c("geo_value", other_keys, "time_value"), exclude) intersect(nm, colnames(x)) @@ -31,7 +32,8 @@ key_colnames.data.frame <- function(x, other_keys = character(0L), exclude = cha #' @rdname key_colnames #' @method key_colnames epi_df #' @export -key_colnames.epi_df <- function(x, exclude = character(0L), ...) { +key_colnames.epi_df <- function(x, ..., exclude = character(0L)) { + check_dots_empty0(...) assert_character(exclude) other_keys <- attr(x, "metadata")$other_keys setdiff(c("geo_value", other_keys, "time_value"), exclude) @@ -40,8 +42,8 @@ key_colnames.epi_df <- function(x, exclude = character(0L), ...) { #' @rdname key_colnames #' @method key_colnames epi_archive #' @export -key_colnames.epi_archive <- function(x, exclude = character(0L), ...) { +key_colnames.epi_archive <- function(x, ..., exclude = character(0L)) { + check_dots_empty0(...) assert_character(exclude) - other_keys <- attr(x, "metadata")$other_keys - setdiff(c("geo_value", other_keys, "time_value"), exclude) + setdiff(c("geo_value", x$other_keys, "time_value", "version"), exclude) } diff --git a/R/revision_analysis.R b/R/revision_analysis.R index 279444896..8efc4fce2 100644 --- a/R/revision_analysis.R +++ b/R/revision_analysis.R @@ -85,7 +85,7 @@ revision_summary <- function(epi_arch, arg <- names(eval_select(rlang::expr(c(...)), allow_rename = FALSE, data = epi_arch$DT)) if (length(arg) == 0) { # Choose the first column that's not a key or version - arg <- setdiff(names(epi_arch$DT), c(key_colnames(epi_arch), "version"))[[1]] + arg <- setdiff(names(epi_arch$DT), key_colnames(epi_arch))[[1]] } else if (length(arg) > 1) { cli_abort("Not currently implementing more than one column at a time. Run each separately") } @@ -101,7 +101,7 @@ revision_summary <- function(epi_arch, # the max lag # # revision_tibble - keys <- key_colnames(epi_arch) + keys <- key_colnames(epi_arch, exclude = "version") revision_behavior <- epi_arch$DT %>% select(all_of(unique(c("geo_value", "time_value", keys, "version", arg)))) diff --git a/man/key_colnames.Rd b/man/key_colnames.Rd index f5e13837c..535a56045 100644 --- a/man/key_colnames.Rd +++ b/man/key_colnames.Rd @@ -2,35 +2,36 @@ % Please edit documentation in R/key_colnames.R \name{key_colnames} \alias{key_colnames} -\alias{key_colnames.default} \alias{key_colnames.data.frame} \alias{key_colnames.epi_df} \alias{key_colnames.epi_archive} -\title{Grab any keys associated to an epi_df} +\title{Get names of columns that form a (unique) key associated with an object} \usage{ -key_colnames(x, ...) +key_colnames(x, ..., exclude = character(0L)) -\method{key_colnames}{default}(x, ...) +\method{key_colnames}{data.frame}(x, other_keys, ..., exclude = character(0L)) -\method{key_colnames}{data.frame}(x, other_keys = character(0L), exclude = character(0L), ...) +\method{key_colnames}{epi_df}(x, ..., exclude = character(0L)) -\method{key_colnames}{epi_df}(x, exclude = character(0L), ...) - -\method{key_colnames}{epi_archive}(x, exclude = character(0L), ...) +\method{key_colnames}{epi_archive}(x, ..., exclude = character(0L)) } \arguments{ -\item{x}{a data.frame, tibble, or epi_df} +\item{x}{an object, such as an \code{\link{epi_df}}} \item{...}{additional arguments passed on to methods} -\item{other_keys}{an optional character vector of other keys to include} +\item{exclude}{an optional character vector of key column names to exclude +from the result} -\item{exclude}{an optional character vector of keys to exclude} +\item{other_keys}{character vector; what besides \code{geo_value} and \code{time_value} +(if present) should we consider to be key columns? Used, e.g., if we} } \value{ -If an \code{epi_df}, this returns all "keys". Otherwise \code{NULL}. +character vector } \description{ -Grab any keys associated to an epi_df +This is entirely based on metadata and arguments passed; there are no +explicit checks that the key actually is unique in any associated data +structures. } \keyword{internal} From 416a7e827bde2d42ed9850fead884f15e269245b Mon Sep 17 00:00:00 2001 From: "Logan C. Brooks" Date: Fri, 18 Oct 2024 12:51:48 -0700 Subject: [PATCH 2/3] `key_colnames`: +flexible on dfs, +rigid on edfs, +tsibble, +archive --- NAMESPACE | 1 + R/key_colnames.R | 93 ++++++++++++++-- man/epiprocess-package.Rd | 2 +- man/key_colnames.Rd | 44 +++++++- tests/testthat/test-key_colnames.R | 172 +++++++++++++++++++++++++++++ 5 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 tests/testthat/test-key_colnames.R diff --git a/NAMESPACE b/NAMESPACE index 26e7db77f..1269c5ef7 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -41,6 +41,7 @@ S3method(guess_period,default) S3method(key_colnames,data.frame) S3method(key_colnames,epi_archive) S3method(key_colnames,epi_df) +S3method(key_colnames,tbl_ts) S3method(mean,epi_df) S3method(print,epi_archive) S3method(print,epi_df) diff --git a/R/key_colnames.R b/R/key_colnames.R index ce2825aae..cc1f67c3b 100644 --- a/R/key_colnames.R +++ b/R/key_colnames.R @@ -6,14 +6,27 @@ #' #' @param x an object, such as an [`epi_df`] #' @param ... additional arguments passed on to methods -#' @param other_keys character vector; what besides `geo_value` and `time_value` -#' (if present) should we consider to be key columns? Used, e.g., if we +#' @param geo_keys optional character vector; which columns (if any) to consider +#' keys specifying the geographical region? Defaults to `"geo_value"` if +#' present; must be `"geo_value"` if `x` is an `epi_df`. +#' @param other_keys character vector; which columns (if any) to consider keys +#' specifying demographical or identifying/grouping information besides the +#' geographical region and time interval? Mandatory if `x` is a vanilla +#' `data.frame` or `tibble`. Optional if `x` is an `epi_df`; default is the +#' `epi_df`'s `other_keys`; if you provide `other_keys`, they must match the +#' default. (This behavior is to enable consistent and sane results when you +#' can't guarantee whether `x` is an `epi_df` or just a +#' `tibble`/`data.frame`.) +#' @param time_keys optional character vector; which columns (if any) to +#' consider keys specifying the time interval during which associated events +#' occurred? Defaults to `"time_value"` if present; must be `"time_value"` if +#' `x` is an `epi_df`. #' @param exclude an optional character vector of key column names to exclude #' from the result #' @return character vector #' @keywords internal #' @export -key_colnames <- function(x, ..., exclude = character(0L)) { +key_colnames <- function(x, ..., exclude = character()) { UseMethod("key_colnames") } @@ -21,28 +34,88 @@ key_colnames <- function(x, ..., exclude = character(0L)) { #' @importFrom rlang check_dots_empty0 #' @method key_colnames data.frame #' @export -key_colnames.data.frame <- function(x, other_keys, ..., exclude = character(0L)) { - assert_character(other_keys) +key_colnames.data.frame <- function(x, ..., + geo_keys = intersect("geo_value", names(x)), + other_keys, + time_keys = intersect("time_value", names(x)), + exclude = character()) { check_dots_empty0(...) + assert_character(geo_keys) + assert_character(time_keys) + assert_character(other_keys) assert_character(exclude) - nm <- setdiff(c("geo_value", other_keys, "time_value"), exclude) - intersect(nm, colnames(x)) + keys = c(geo_keys, other_keys, time_keys) + if (!all(keys %in% names(x))) { + cli_abort(c( + "Some of the specified key columns aren't present in `x`", + "i" = "Specified keys: {format_varnames(keys)}", + "i" = "Columns of x: {format_varnames(names(x))}", + "x" = "Missing keys: {format_varnames(setdiff(keys, names(x)))}" + ), class = "epiprocess__key_colnames__keys_not_in_colnames") + } + setdiff(keys, exclude) } #' @rdname key_colnames #' @method key_colnames epi_df #' @export -key_colnames.epi_df <- function(x, ..., exclude = character(0L)) { +key_colnames.epi_df <- function(x, ..., + geo_keys = "geo_value", + other_keys = NULL, + time_keys = "time_value", + exclude = character()) { check_dots_empty0(...) + if (!identical(geo_keys, "geo_value")) { + cli_abort('If `x` is an `epi_df`, then `geo_keys` must be `"geo_value"`', + class = "epiprocess__key_colnames__mismatched_geo_keys") + } + if (!identical(time_keys, "time_value")) { + cli_abort('If `x` is an `epi_df`, then `time_keys` must be `"time_value"`', + class = "epiprocess__key_colnames__mismatched_time_keys") + } + expected_other_keys <- attr(x, "metadata")$other_keys + if (is.null(other_keys)) { + other_keys <- expected_other_keys + } else { + if (!identical(other_keys, expected_other_keys)) { + cli_abort(c( + "The provided `other_keys` argument didn't match the `other_keys` of `x`", + "*" = "`other_keys` was {format_chr_with_quotes(other_keys)}", + "*" = "`expected_other_keys` was {format_chr_with_quotes(expected_other_keys)}", + "i" = "If you resolve this discrepancy by adjusting the metadata of `x`, you + shouldn't have to pass `other_keys =` here anymore unless you want to + continue to perform this check." + ), class = "epiprocess__key_colnames__mismatched_other_keys") + } + } assert_character(exclude) - other_keys <- attr(x, "metadata")$other_keys setdiff(c("geo_value", other_keys, "time_value"), exclude) } +#' @rdname key_colnames +#' @method key_colnames tbl_ts +#' @export +key_colnames.tbl_ts <- function(x, ..., exclude = character()) { + check_dots_empty0(...) + assert_character(exclude) + idx <- tsibble::index_var(x) + idx2 <- tsibble::index2_var(x) + if (!identical(idx, idx2)) { + cli_abort(c( + "`x` is in the middle of a re-indexing operation with `index_by()`; it's unclear + whether we should output the old unique key or the new unique key-to-be", + "i" = "Old index: {format_varname(idx)}", + "i" = "Pending new index: {format_varname(idx2)}", + "Please complete (e.g., with `summarise()`) or remove the re-indexing operation." + ), class = "epiprocess__key_colnames__incomplete_reindexing_operation") + } + setdiff(c(tsibble::key_vars(x), idx), exclude) +} + #' @rdname key_colnames #' @method key_colnames epi_archive #' @export -key_colnames.epi_archive <- function(x, ..., exclude = character(0L)) { +key_colnames.epi_archive <- function(x, ..., exclude = character()) { check_dots_empty0(...) assert_character(exclude) setdiff(c("geo_value", x$other_keys, "time_value", "version"), exclude) diff --git a/man/epiprocess-package.Rd b/man/epiprocess-package.Rd index 774d5f8ac..fe79c01e8 100644 --- a/man/epiprocess-package.Rd +++ b/man/epiprocess-package.Rd @@ -20,6 +20,7 @@ Useful links: Authors: \itemize{ + \item Nat DeFries \item Daniel McDonald \item Evan Ray \item Dmitry Shemetov @@ -30,7 +31,6 @@ Other contributors: \itemize{ \item Jacob Bien [contributor] \item Rafael Catoia [contributor] - \item Nat DeFries [contributor] \item Rachel Lobay [contributor] \item Ken Mawer [contributor] \item Chloe You [contributor] diff --git a/man/key_colnames.Rd b/man/key_colnames.Rd index 535a56045..925601a6d 100644 --- a/man/key_colnames.Rd +++ b/man/key_colnames.Rd @@ -4,16 +4,33 @@ \alias{key_colnames} \alias{key_colnames.data.frame} \alias{key_colnames.epi_df} +\alias{key_colnames.tbl_ts} \alias{key_colnames.epi_archive} \title{Get names of columns that form a (unique) key associated with an object} \usage{ -key_colnames(x, ..., exclude = character(0L)) +key_colnames(x, ..., exclude = character()) -\method{key_colnames}{data.frame}(x, other_keys, ..., exclude = character(0L)) +\method{key_colnames}{data.frame}( + x, + ..., + geo_keys = intersect("geo_value", names(x)), + other_keys, + time_keys = intersect("time_value", names(x)), + exclude = character() +) -\method{key_colnames}{epi_df}(x, ..., exclude = character(0L)) +\method{key_colnames}{epi_df}( + x, + ..., + geo_keys = "geo_value", + other_keys = NULL, + time_keys = "time_value", + exclude = character() +) -\method{key_colnames}{epi_archive}(x, ..., exclude = character(0L)) +\method{key_colnames}{tbl_ts}(x, ..., exclude = character()) + +\method{key_colnames}{epi_archive}(x, ..., exclude = character()) } \arguments{ \item{x}{an object, such as an \code{\link{epi_df}}} @@ -23,8 +40,23 @@ key_colnames(x, ..., exclude = character(0L)) \item{exclude}{an optional character vector of key column names to exclude from the result} -\item{other_keys}{character vector; what besides \code{geo_value} and \code{time_value} -(if present) should we consider to be key columns? Used, e.g., if we} +\item{geo_keys}{optional character vector; which columns (if any) to consider +keys specifying the geographical region? Defaults to \code{"geo_value"} if +present; must be \code{"geo_value"} if \code{x} is an \code{epi_df}.} + +\item{other_keys}{character vector; which columns (if any) to consider keys +specifying demographical or identifying/grouping information besides the +geographical region and time interval? Mandatory if \code{x} is a vanilla +\code{data.frame} or \code{tibble}. Optional if \code{x} is an \code{epi_df}; default is the +\code{epi_df}'s \code{other_keys}; if you provide \code{other_keys}, they must match the +default. (This behavior is to enable consistent and sane results when you +can't guarantee whether \code{x} is an \code{epi_df} or just a +\code{tibble}/\code{data.frame}.)} + +\item{time_keys}{optional character vector; which columns (if any) to +consider keys specifying the time interval during which associated events +occurred? Defaults to \code{"time_value"} if present; must be \code{"time_value"} if +\code{x} is an \code{epi_df}.} } \value{ character vector diff --git a/tests/testthat/test-key_colnames.R b/tests/testthat/test-key_colnames.R new file mode 100644 index 000000000..250c6049f --- /dev/null +++ b/tests/testthat/test-key_colnames.R @@ -0,0 +1,172 @@ +test_that("`key_colnames` on non-`epi_df`-like tibbles works as expected", { + + k1k2_tbl <- tibble::tibble(k1 = 1, k2 = 1) + + expect_equal( + key_colnames(k1k2_tbl, geo_keys = character(0L), time_keys = character(0L), other_keys = c("k1", "k2")), + c("k1", "k2") + ) + # `geo_keys` and `time_keys` are optional, and, in this case, inferred to be absent: + expect_equal( + key_colnames(k1k2_tbl, other_keys = c("k1", "k2")), + c("k1", "k2") + ) + # but `other_keys` is mandatory: + expect_error( + key_colnames(k1k2_tbl) + ) + + # Manually specifying keys that aren't there is an error: + expect_error( + key_colnames(k1k2_tbl, geo_keys = "bogus", other_keys = c("k1", "k2")), + class = "epiprocess__key_colnames__keys_not_in_colnames" + ) + expect_error( + key_colnames(k1k2_tbl, time_keys = "bogus", other_keys = c("k1", "k2")), + class = "epiprocess__key_colnames__keys_not_in_colnames" + ) + expect_error( + key_colnames(k1k2_tbl, other_keys = "bogus"), + class = "epiprocess__key_colnames__keys_not_in_colnames" + ) + + # We can specify non-`epi_df`-like geo keys: + expect_equal( + key_colnames(k1k2_tbl, geo_keys = c("k1", "k2"), other_keys = character(0L)), + c("k1", "k2") + ) + +}) + +test_that("`key_colnames` on `epi_df`s and similar tibbles works as expected", { + + gat_tbl <- tibble::tibble(geo_value = 1, age_group = 1, time_value = 1) + gat_edf <- as_epi_df(gat_tbl, other_keys = "age_group", as_of = 2) + + # For tbl: `geo_keys` and `time_keys` are optional, and, in this case, + # inferred to be (just) `geo_value` and (just) `time_value`: + expect_equal( + key_colnames(gat_tbl, other_keys = "age_group"), + c("geo_value", "age_group", "time_value") + ) + # and edfs give something compatible: + expect_equal( + key_colnames(gat_edf, other_keys = "age_group"), + c("geo_value", "age_group", "time_value") + ) + # though edfs don't have to specify the `other_keys`: + expect_equal( + key_colnames(gat_edf), + c("geo_value", "age_group", "time_value") + ) + # and they will balk if we write something intended to work for both tbls and + # edfs but mis-specify the `other_keys`: + expect_error( + key_colnames(gat_edf, other_keys = character(0L)), + class = "epiprocess__key_colnames__mismatched_other_keys" + ) + + # edfs also won't let us specify nonstandard geotime keys: + expect_error( + key_colnames(gat_edf, geo_keys = "time_value"), + class = "epiprocess__key_colnames__mismatched_geo_keys" + ) + expect_error( + key_colnames(gat_edf, time_keys = "geo_value"), + class = "epiprocess__key_colnames__mismatched_time_keys" + ) + + # For either class, `extra_keys` is not accepted: + expect_error( + key_colnames(gat_tbl, extra_keys = "age_group"), + class = "rlib_error_dots_nonempty" + ) + expect_error( + key_colnames(gat_edf, extra_keys = "age_group"), + class = "rlib_error_dots_nonempty" + ) + + # We can exclude keys: + expect_equal( + key_colnames(gat_tbl, other_keys = "age_group", exclude = c("time_value")), + c("geo_value", "age_group") + ) + expect_equal( + key_colnames(gat_tbl, other_keys = "age_group", exclude = c("geo_value", "time_value")), + c("age_group") + ) + expect_equal( + key_colnames(gat_edf, exclude = c("time_value")), + c("geo_value", "age_group") + ) + expect_equal( + key_colnames(gat_edf, exclude = c("geo_value", "time_value")), + c("age_group") + ) + +}) + +test_that("`key_colnames` on tsibbles works as expected", { + + k1k2i_tsbl <- tsibble::tsibble(k1 = 1, k2 = 1, i = 1, key = c(k1, k2), index = i) + + # Normal operation: + expect_equal(key_colnames(k1k2i_tsbl), c("k1", "k2", "i")) + + # Currently there is just bare-bones support for tsibbles to not output + # incompatible results based on `data.frame` inheritance: + expect_error( + key_colnames(k1k2i_tsbl, geo_keys = "k1"), + class = "rlib_error_dots_nonempty" + ) + expect_error( + key_colnames(k1k2i_tsbl, time_keys = "k1"), + class = "rlib_error_dots_nonempty" + ) + expect_error( + key_colnames(k1k2i_tsbl, other_keys = "k1"), + class = "rlib_error_dots_nonempty" + ) + + # We guard against confusing cases: + expect_error( + key_colnames(k1k2i_tsbl %>% tsibble::index_by(fake_coarser_i = i)), + class = "epiprocess__key_colnames__incomplete_reindexing_operation" + ) + +}) + +test_that("`key_colnames` on `epi_archive`s works as expected", { + + gatv_ea <- tibble(geo_value = 1, age_group = 1, time_value = 1, version = 2) %>% + as_epi_archive(other_keys = "age_group") + + # Basic operation: + expect_equal( + key_colnames(gatv_ea), + c("geo_value", "age_group", "time_value", "version") + ) + + # Since we shouldn't have uncertainty about whether we might have an archive + # or not, there's no need to provide compatibility with the key specification + # args: + expect_error( + key_colnames(gatv_ea, geo_keys = "k1"), + class = "rlib_error_dots_nonempty" + ) + expect_error( + key_colnames(gatv_ea, time_keys = "k1"), + class = "rlib_error_dots_nonempty" + ) + expect_error( + key_colnames(gatv_ea, other_keys = "k1"), + class = "rlib_error_dots_nonempty" + ) + + # Key exclusion works: + expect_equal( + key_colnames(gatv_ea, exclude = c("version", "time_value")), + c("geo_value", "age_group") + ) + +}) From d5fdc6c62ca6537ea1d7d44e6b281855f031510e Mon Sep 17 00:00:00 2001 From: "Logan C. Brooks" Date: Tue, 22 Oct 2024 14:19:33 -0700 Subject: [PATCH 3/3] Tweak key_colnames.epi_df(other_keys =) mismatch error text --- R/key_colnames.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/R/key_colnames.R b/R/key_colnames.R index cc1f67c3b..0d8e777f9 100644 --- a/R/key_colnames.R +++ b/R/key_colnames.R @@ -82,9 +82,10 @@ key_colnames.epi_df <- function(x, ..., "The provided `other_keys` argument didn't match the `other_keys` of `x`", "*" = "`other_keys` was {format_chr_with_quotes(other_keys)}", "*" = "`expected_other_keys` was {format_chr_with_quotes(expected_other_keys)}", - "i" = "If you resolve this discrepancy by adjusting the metadata of `x`, you - shouldn't have to pass `other_keys =` here anymore unless you want to - continue to perform this check." + "i" = "If you know that `x` will always be an `epi_df` and + resolve this discrepancy by adjusting the metadata of `x`, you + shouldn't have to pass `other_keys =` here anymore, + unless you want to continue to perform this check." ), class = "epiprocess__key_colnames__mismatched_other_keys") } }