Meta-programación de Data.table
(1)
Creo que la meta-programación es el término correcto aquí.
Quiero ser capaz de usar data.table de manera similar a como usaría MySQL en decir una aplicación web. Es decir, los usuarios de la web usan algún front-end web (como el servidor Shiny, por ejemplo) para seleccionar una base de datos, seleccionar columnas para filtrar, seleccionar columnas para agrupar, seleccionar columnas para agregar y funciones de agregación. Quiero usar R y data.table como un back-end para consultas, agregación, etc. Supongamos que el front-end existe y R tiene estas variables como cadenas de caracteres y están validados, etc.
Escribí la siguiente función para compilar la expresión data.table y usar la funcionalidad de meta-programación parse / eval de R para ejecutarla. ¿Es esta una manera razonable de hacer esto?
I incluye todo el código relevante para probar esto. Fuente este código (después de leerlo por seguridad!) Y ejecutar test_agg_meta () para probarlo. Es solo un comienzo. Podría agregar más funcionalidad.
Pero mi pregunta principal es si estoy demasiado pensando en esto. ¿Existe una forma más directa de utilizar data.table cuando todas las entradas son indeterminadas de antemano sin recurrir a la metaprogramación parse / eval?
También conozco la declaración "con" y algunos de los otros métodos funcionales sin azúcar, pero no sé si pueden encargarse de todos los casos.
require(data.table)
fake_data<-function(num=12){
#make some fake data
x=1:num
lets=letters[1:num]
data=data.table(
u=rep(c("A","B","C"),floor(num/3)),
v=x %%2, w=lets, x=x, y=x^2, z=1-x)
return(data)
}
data_table_meta<-function(
#aggregate a data.table meta-programmatically
data_in=fake_data(),
filter_cols=NULL,
filter_min=NULL,
filter_max=NULL,
groupby_cols=NULL,
agg_cols=setdiff(names(data_in),groupby_cols),
agg_funcs=NULL,
verbose=F,
validate=T,
jsep="_"
){
all_cols=names(data_in)
if (validate) {
stopifnot(length(filter_cols) == length(filter_min))
stopifnot(length(filter_cols) == length(filter_max))
stopifnot(filter_cols %in% all_cols)
stopifnot(groupby_cols %in% all_cols)
stopifnot(length(intersect(agg_cols,groupby_cols)) == 0)
stopifnot((length(agg_cols) == length(agg_funcs)) | (length(agg_funcs)==1) | (length(agg_funcs)==0))
}
#build the command
#defaults
i_filter=""
j_select=""
n_agg_funcs=length(agg_funcs)
n_agg_cols=length(agg_cols)
n_groupby_cols=length(groupby_cols)
if (n_agg_funcs == 0) {
#NULL
print("NULL")
j_select=paste(agg_cols,collapse=",")
j_select=paste("list(",j_select,")")
} else {
agg_names=paste(agg_funcs,agg_cols,sep=jsep)
jsels=paste(agg_names,"=",agg_funcs,"(",agg_cols,")",sep="")
if (n_groupby_cols>0) jsels=c(jsels,"N_Rows_Aggregated=.N")
j_select=paste(jsels,collapse=",")
j_select=paste("list(",j_select,")")
}
groupby=""
if (n_groupby_cols>0) {
groupby=paste(groupby_cols,collapse=",")
groupby=paste("by=list(",groupby,")",sep="")
}
n_filter_cols=length(filter_cols)
if (n_filter_cols > 0) {
i_filters=rep("",n_filter_cols)
for (i in 1:n_filter_cols) {
i_filters[i]=paste(" (",filter_cols[i]," >= ",filter_min[i]," & ",filter_cols[i]," <= ",filter_max[i],") ",sep="")
}
i_filter=paste(i_filters,collapse="&")
}
command=paste("data_in[",i_filter,",",j_select,",",groupby,"]",sep="")
if (verbose == 2) {
print("all_cols:")
print(all_cols)
print("filter_cols:")
print(filter_cols)
print("agg_cols:")
print(agg_cols)
print("filter_min:")
print(filter_min)
print("filter_max:")
print(filter_max)
print("groupby_cols:")
print(groupby_cols)
print("agg_cols:")
print(agg_cols)
print("agg_funcs:")
print(agg_funcs)
print("i_filter")
print(i_filter)
print("j_select")
print(j_select)
print("groupby")
print(groupby)
print("command")
print(command)
}
print(paste("evaluating command:",command))
eval(parse(text=command))
}
my_agg<-function(data=fake_data()){
data_out=data[
i=x<=5,
j=list(
mean_x=mean(x),
mean_y=mean(y),
sum_z=sum(z),
N_Rows_Aggregated=.N
),
by=list(u,v)]
return(data_out)
}
my_agg_meta<-function(data=fake_data()){
#should give same results as my_agg
data_out=data_table_meta(data,
filter_cols=c("x"),
filter_min=c(-10000),
filter_max=c(5),
groupby_cols=c("u","v"),
agg_cols=c("x","y","z"),
agg_funcs=c("mean","mean","sum"),
verbose=T,
validate=T,
jsep="_")
return(data_out)
}
test_agg_meta<-function(){
stopifnot(all(my_agg()==my_agg_meta()))
print("Congrats, you passed the test")
}
Si bien sus funciones ciertamente parecen interesantes, creo que está preguntando si hay otras formas de hacerlo.
Personalmente, me gusta usar algo como esto:
## SAMPLE DATA
DT1 <- data.table(id=sample(LETTERS[1:4], 20, TRUE), Col1=1:20, Col2=rnorm(20))
DT2 <- data.table(id=sample(LETTERS[3:8], 20, TRUE), Col1=sample(100:500, 20), Col2=rnorm(20))
DT3 <- data.table(id=sample(LETTERS[19:20], 20, TRUE), Col1=sample(100:500, 20), Col2=rnorm(20))
ACCEDER A UNA TABLA POR REFERENCIA AL NOMBRE DE LA MESA:
Esto es sencillo, muy parecido a cualquier objeto en R
# use strings to select the table
tablesSelected <- "DT3"
# use get to access them
get(tablesSelected)
# and we can perform operations:
get(tablesSelected)[, list(C1mean=mean(Col1), C2mean=mean(Col2))]
SELECCIONANDO COLUMNAS POR REFERENCIA
Para seleccionar columnas por referencia a sus nombres, use el argumento .SDcols
. Dado un vector de nombres de columna:
columnsSelected <- c("Col1", "Col2")
Asigna ese vector al argumento .SDcols:
## Here we are simply accessing those columns
DT3[, .SD, .SDcols = columnsSelected]
También podemos aplicar una función a cada columna nombrada en el vector de cadena:
## apply a function to each column
DT3[, lapply(.SD, mean), .SDcols = columnsSelected]
Tenga en cuenta que si nuestro objetivo es simplemente dar salida a las columnas, podemos desactivarlo with
:
# This works for displaying
DT3[, columnsSelected, with=FALSE]
Sin embargo, si se usa with=FALSE
, no podemos operar directamente en las columnas de la manera habitual
## This does NOT work:
DT3[, someFunc(columnsSelected), with=FALSE]
## This DOES work:
DT3[, someFunc(.SD), .SDcols=columnsSelected]
## This also works, but is less ideal, ie assigning to new columns is more cumbersome
DT3[, columnsSelected, with=FALSE][, someFunc(.SD)]
También podemos usar get
, pero es un poco más complicado. Lo dejo aquí para referencia, pero .SDcols
es el camino a seguir
## we need to use `get`, but inside `j`
## AND IN A WRAPPER FUNCTION <~~~~~ THIS IS VITAL
DT3[, lapply(columnsSelected, function(.col) get(.col))]
## We can execute functions on the columns:
DT3[, lapply(columnsSelected, function(.col) mean( get(.col) ))]
## And of course, we can use more involved-functions, much like any *ply call:
# using .SDcols
DT3[, lapply(.SD, function(.col) c(mean(.col) + 2*sd(.col), mean(.col) - 2*sd(.col))), .SDcols = columnsSelected]
# using `get` and assigning the value to a var.
# Note that this method has memory drawbacks, so using .SDcols is preferred
DT3[, lapply(columnsSelected, function(.col) {TheCol <- get(.col); c(mean(TheCol) + 2*sd(TheCol), mean(TheCol) - 2*sd(TheCol))})]
Como referencia, si prueba lo siguiente, notará que no producen los resultados que buscamos.
## this DOES NOT work
DT3[, columnsSelected]
## netiher does this
DT3[, eval(columnsSelected)]
## still does not work:
DT3[, lapply(columnsSelected, get)]
Si desea cambiar el nombre de las columnas:
# Using the `.SDcols` method: change names using `setnames` (lowercase "n")
DT3[, setnames(.SD, c("new.Name1", "new.Name2")), .SDcols =columnsSelected]
# Using the `get` method:
## The names of the new columns will be the names of the `columnsSelected` vector
## Thus, if we want to preserve the names, use the following:
names(columnsSelected) <- columnsSelected
DT3[, lapply(columnsSelected, function(.col) get(.col))]
## we can also use this trick to give the columns new names
names(columnsSelected) <- c("new.Name1", "new.Name2")
DT3[, lapply(columnsSelected, function(.col) get(.col))]
Claramente, usar .SDcols es más fácil y más elegante.
¿Qué tal?
# `by` is straight forward, you can use a vector of strings in the `by` argument.
# lets add another column to show how to use two columns in `by`
DT3[, secondID := sample(letters[1:2], 20, TRUE)]
# here is our string vector:
byCols <- c("id", "secondID")
# and here is our call
DT3[, lapply(columnsSelected, function(.col) mean(get(.col))), by=byCols]
PONIENDOLO TODO JUNTO
Podemos acceder a data.table por referencia a su nombre y luego seleccionar sus columnas también por su nombre:
get(tablesSelected)[, .SD, .SDcols=columnsSelected]
## OR WITH MULTIPLE TABLES
tablesSelected <- c("DT1", "DT3")
lapply(tablesSelected, function(.T) get(.T)[, .SD, .SDcols=columnsSelected])
# we may want to name the vector for neatness, since
# the resulting list inherits the names.
names(tablesSelected) <- tablesSelected
ESTA ES LA MEJOR PARTE:
Dado que tanto en data.table
es pass-by-reference, es fácil tener una lista de tablas, una lista separada de columnas para agregar y otra lista de columnas para operar, y poner todas juntas para agregar realizar operaciones similares. - pero con diferentes entradas - en todas sus tablas. A diferencia de hacer algo similar con data.frame
, no hay necesidad de reasignar el resultado final.
newColumnsToAdd <- c("UpperBound", "LowerBound")
FunctionToExecute <- function(vec) c(mean(vec) - 2*sd(vec), mean(vec) + 2*sd(vec))
# note the list of column names per table!
columnsUsingPerTable <- list("DT1" = "Col1", DT2 = "Col2", DT3 = "Col1")
tablesSelected <- names(columnsUsingPerTable)
byCols <- c("id")
# TADA:
dummyVar <- # I use `dummyVar` because I do not want to display the output
lapply(tablesSelected, function(.T)
get(.T)[, c(newColumnsToAdd) := lapply(.SD, FunctionToExecute), .SDcols=columnsUsingPerTable[[.T]], by=byCols ] )
# Take a look at the tables now:
DT1
DT2
DT3