Answer to: Calculate multilple stats for two or more columns in R data.table
Score: 2
By total coincidence I was working today on a helper function calls_across that does exactly what you want. It pairs with the env programming mechanism in recent versions of {data.table}.
[Edit: as promised, now allowing aliasing the functions on the fly via a named character vector (which preserves GForce)]
library(data.table)
set.seed(1) # please use a seed for reproducibility!!!
dt <- data.table(class = rep(letters[1:3], 4), val1=sample(12), val2=sample(12)*10)
query <- calls_across(
c(maximo="max", minimo="min"),
c("val1", "val2"),
na.rm=TRUE,
order="col_fun"
)
dt[, j, by=class, env=list(j=query)]
#> class val1_maximo val1_minimo val2_maximo val2_minimo
#> <char> <int> <int> <num> <num>
#> 1: a 11 1 120 40
#> 2: b 12 2 110 10
#> 3: c 10 5 90 30
Output with verbose=TRUE:
dt[, j, by=class, env=list(j=query), verbose=TRUE]
#> Argument 'by' after substitute: class
#> Argument 'j' after substitute: list(val1_maximo = max(val1, na.rm = TRUE), val1_minimo = min(val1, na.rm = TRUE), val2_maximo = max(val2, na.rm = TRUE), val2_minimo = min(val2, na.rm = TRUE))
#> [...]
#> GForce optimized j to 'list(gmax(val1, na.rm = TRUE), gmin(val1, na.rm = TRUE), gmax(val2, na.rm = TRUE), gmin(val2, na.rm = TRUE))' (see ?GForce)
#> Making each group and running j (GForce TRUE) ...
#> [...]
Note: {data.table}'s "GForce" optimisations of common stats functions avoid (slow) split-apply-combine when there is a by, so it's important to take advantage of them.
I will change the code to allow such aliasing in the funs argument (using a named character vector). (Let me do that later and edit at some point.)[done]
Here is the source code, still a bit WIP [but now revised to allow optional aliases]:
calls_across <- function(
funs,
cols,
...,
order = c("fun_col","col_fun"),
named = TRUE,
naming = order,
sep = "_"
){
order <- match.arg(order)
stopifnot(naming %in% c("fun_col","col_fun"))
.make_call <- function(fun, cols, ...) {
dots <- as.list(substitute(list(...)))[-1]
cols <- if (is.list(cols)) {
lapply(cols, \(x) if (is.character(x)) as.name(x) else x)
} else {
if (is.character(cols)) lapply(cols, as.name) else cols
}
as.call(c(list(as.name(fun)), cols, dots))
}
if (isTRUE(named)) fun_nms <- if (!is.null(names(funs))) names(funs) else funs
if (match.arg(order)=="fun_col") {
ans <- lapply(funs, \(f) lapply(cols, \(c) .make_call(f,c,...)))
if (isTRUE(named)) {
.f <- rep(fun_nms, each=length(cols))
.c <- rep(cols,times=length(funs))
}
} else {
ans <- lapply(cols, \(c) lapply(funs, \(f) .make_call(f,c,...)))
if (isTRUE(named)) {
.f <- rep(fun_nms, times=length(cols))
.c <- rep(cols, each=length(funs))
}
}
ans <- do.call(c, ans)
if (isTRUE(named)) names(ans) <- if (naming=="fun_col") paste(.f,.c,sep=sep) else paste(.c,.f,sep=sep)
ans
}
It allows separate flexibility over i) the order and ii) the names of the output columns (fun-then-col or col-then-fun). Please notice from the examples that it also permits additional arguments, such as na.rm=TRUE.
View Question ↗
Question
Parent Entity
Score: 2 • Views: 62
Site: stackoverflow
SaaS Metrics