Meta-programação Data.table

Eu acho que a meta-programação é o termo certo aqui.

Eu quero ser capaz de usar data.table muito como se usaria o MySQL em dizer um webapp. Ou seja, os usuários da Web usam algum front-end da Web (como o servidor Shiny, por exemplo) para selecionar uma base de dados, selecionar colunas para filtrar, selecionar colunas para agrupar, selecionar colunas para agregar e funções de agregação. Eu quero usar R e data.table como um backend para consulta, agregação, etc. Assuma que front-end existe e R tem essas variables ​​como cadeias de caracteres e elas são validadas etc.

Escrevi a seguinte function para construir a expressão data.table e usar a funcionalidade de metaprogramação parse / eval de R para executá-la. Esta é uma maneira razoável de fazer isso?

Eu inclui todo o código relevante para testar isso. Fonte este código (depois de lê-lo para segurança!) E execute test_agg_meta () para testá-lo. É só um começo. Eu poderia adicionar mais funcionalidade.

Mas minha principal questão é se estou excessivamente pensando nisso. Existe uma maneira mais direta de usar dados? Quando todos os insumos são indeterminados antes da mão sem recorrer à meta-programação parse / eval?

Eu também estou ciente da declaração “com” e de alguns outros methods sem açúcar, mas não sei se eles podem cuidar de todos os 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_meta0) 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") } 

Embora suas funções certamente pareçam interessantes, acredito que você esteja perguntando se há outras maneiras de fazer isso.
Pessoalmente, gosto de usar algo assim:

 ## 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)) 

ACESSANDO UMA TABELA POR REFERÊNCIA AO NOME DA TABELA:

Isso é simples, muito parecido com qualquer object em 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))] 

SELECIONANDO COLUNAS PELA REFERÊNCIA

Para selecionar colunas por referência a seus nomes, use o argumento .SDcols . Dado um vetor de nomes de colunas:

 columnsSelected < - c("Col1", "Col2") 

Atribua esse vetor ao argumento .SDcols:

 ## Here we are simply accessing those columns DT3[, .SD, .SDcols = columnsSelected] 

Podemos também aplicar uma function a cada coluna nomeada no vetor de string:

 ## apply a function to each column DT3[, lapply(.SD, mean), .SDcols = columnsSelected] 

Observe que, se nosso objective for simplesmente gerar as colunas, podemos desativar:

 # This works for displaying DT3[, columnsSelected, with=FALSE] 

No entanto, se usarmos with=FALSE , não poderemos operar diretamente nas colunas da maneira usual

 ## 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)] 

Nós também podemos usar get , mas é um pouco mais complicado. Estou deixando aqui para referência, mas .SDcols é o caminho a percorrer

 ## 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))})] 

Para referência, se você tentar o seguinte, perceberá que eles não produzem os resultados que estamos buscando.

  ## this DOES NOT work DT3[, columnsSelected] ## netiher does this DT3[, eval(columnsSelected)] ## still does not work: DT3[, lapply(columnsSelected, get)] 

Se você quiser alterar o nome das colunas:

 # 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, o uso de .SDcols é mais fácil e elegante.

Que tal by ?

 # `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] 

COLOCANDO TODOS JUNTOS

Podemos acessar o data.table por referência ao seu nome e, em seguida, selecionar suas colunas também por nome:

 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 É A MELHOR PARTE:

Como tanto em data.table é passado por referência, é fácil ter uma lista de tabelas, uma lista separada de colunas para adicionar e ainda outra lista de colunas para operar, e colocar todas juntas para adicionar operações similares - mas com inputs diferentes - em todas as suas mesas. Ao contrário de fazer algo semelhante com data.frame , não há necessidade de reatribuir o 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