web-dev-qa-db-fra.com

Plusieurs fonctions sur une liste de colonnes et générer automatiquement de nouveaux noms de colonne avec data.table

Comment ajuster une manipulation de table de données de sorte que, en plus de sum par catégorie de plusieurs colonnes, il calculerait également d'autres fonctions en même temps, telles que mean et compte (.N), et créerait automatiquement des noms de colonne: "somme c1", "somme c2", "somme c4", "moyenne c1", "moyenne c2", " signifie c4 "et de préférence aussi 1 colonne" compte "?

Mon ancienne solution était d'écrire 

mean col1 = ....
mean col2 = ....

Etc, dans la commande data.table 

Ce qui a fonctionné, mais horriblement inefficace je pense, et cela ne fonctionnera plus de le pré-coder si, dans la nouvelle version de l'application, les calculs dépendent des choix de l'utilisateur dans une application R Shiny, ainsi que du calcul des colonnes. 

J'ai parcouru pas mal de posts et d'articles de blog mais je n'ai pas encore trouvé le meilleur moyen de le faire. J'ai lu que dans certains cas, la manipulation peut devenir assez lente sur des tables de données volumineuses en fonction de l'approche utilisée (.sdcols, get, lapply et ou par =). J'ai donc ajouté un jeu de données factice

Mes données réelles sont autour de 100 000 lignes par 100 colonnes et environ 1 100 groupes.

library(data.table)
n = 100000
dt  = data.table(index=1:100000,
                 category = sample(letters[1:25], n, replace = T),
                 c1=rnorm(n,10000),
                 c2=rnorm(n,1000),
                 c3=rnorm(n,100),
                 c4 = rnorm(n,10)
)

# add more columns to test for big data tables 
lapply(c(paste('c', 5:100, sep ='')),
       function(addcol) dt[[addcol]] <<- rnorm(n,1000) )

# Simulate columns selected by shiny app user 

Colchoice <- c("c1", "c4")
FunChoice <- c(".N", "mean", "sum")

# attempt which now does just one function and doesn't add names
dt[, lapply(.SD, sum, na.rm=TRUE), by=category, .SDcols=Colchoice ]

Le résultat attendu est une ligne par groupe et une colonne pour chaque fonction pour chaque colonne sélectionnée. 

Category  Mean c1 Sum c1 Mean c4 ...
A
B
C
D
E
......

Peut-être une copie mais je n'ai pas trouvé la réponse exacte dont j'ai besoin 

8
Mark

Si je comprends bien, cette question comprend deux parties:

  1. Comment regrouper et agréger plusieurs fonctions sur une liste de colonnes et générer automatiquement de nouveaux noms de colonne.
  2. Comment passer les noms des fonctions en tant que vecteur de caractères.

Pour la partie 1, il s'agit de presque un doublon de Appliquer plusieurs fonctions à plusieurs colonnes dans data.table , mais avec l'exigence supplémentaire selon laquelle les résultats doivent être regroupés à l'aide de by =.

Par conséquent, la réponse de eddi doit être modifiée en ajoutant le paramètre recursive = FALSE dans l'appel à unlist():

my.summary = function(x) list(N = length(x), mean = mean(x), median = median(x))
dt[, unlist(lapply(.SD, my.summary), recursive = FALSE), 
   .SDcols = ColChoice, by = category]
    category c1.N   c1.mean c1.median c4.N   c4.mean c4.median
 1:        f 3974  9999.987  9999.989 3974  9.994220  9.974125
 2:        w 4033 10000.008  9999.991 4033 10.004261  9.986771
 3:        n 4025  9999.981 10000.000 4025 10.003686  9.998259
 4:        x 3975 10000.035 10000.019 3975 10.010448  9.995268
 5:        k 3957 10000.019 10000.017 3957  9.991886 10.007873
 6:        j 4027 10000.026 10000.023 4027 10.015663  9.998103
...

Pour la partie 2, nous devons créer my.summary() à partir d'un vecteur de caractères composé de noms de fonctions. Ceci peut être réalisé en "programmation sur le langage", c'est-à-dire en assemblant une expression sous forme de chaîne de caractères et en fin de compte de l'analyser et de l'évaluer:

my.summary <- 
  sapply(FunChoice, function(f) paste0(f, "(x)")) %>% 
  paste(collapse = ", ") %>% 
  sprintf("function(x) setNames(list(%s), FunChoice)", .) %>% 
  parse(text = .) %>% 
  eval()

my.summary
function(x) setNames(list(length(x), mean(x), sum(x)), FunChoice)
<environment: 0xe376640>

Alternativement, nous pouvons parcourir les catégories et rbind() les résultats par la suite:

library(magrittr)   # used only to improve readability
lapply(dt[, unique(category)],
       function(x) dt[category == x, 
                      c(.(category = x), unlist(lapply(.SD, my.summary))), 
                      .SDcols = ColChoice]) %>% 
  rbindlist()

Référence

Jusqu'à présent, 4 solutions data.table et une dplyr ont été publiées. Au moins une des réponses prétend être "super rapide". Donc, je voulais vérifier par un repère avec un nombre variable de lignes:

library(data.table)
library(magrittr)
bm <- bench::press(
  n = 10L^(2:6),
  {
    set.seed(12212018)
    dt <- data.table(
      index = 1:n,
      category = sample(letters[1:25], n, replace = T),
      c1 = rnorm(n, 10000),
      c2 = rnorm(n, 1000),
      c3 = rnorm(n, 100),
      c4 = rnorm(n, 10)
    )
    # use set() instead of <<- for appending additional columns
    for (i in 5:100) set(dt, , paste0("c", i), rnorm(n, 1000))
    tables()

    ColChoice <- c("c1", "c4")
    FunChoice <- c("length", "mean", "sum")
    my.summary <- function(x) list(length = length(x), mean = mean(x), sum = sum(x))

    bench::mark(
      unlist = {
        dt[, unlist(lapply(.SD, my.summary), recursive = FALSE),
           .SDcols = ColChoice, by = category]
      },
      loop_category = {
        lapply(dt[, unique(category)],
               function(x) dt[category == x, 
                              c(.(category = x), unlist(lapply(.SD, my.summary))), 
                              .SDcols = ColChoice]) %>% 
          rbindlist()
        },
      dcast = {
        dcast(dt, category ~ 1, fun = list(length, mean, sum), value.var = ColChoice)
        },
      loop_col = {
        lapply(ColChoice, function(col)
          dt[, setNames(lapply(FunChoice, function(f) get(f)(get(col))), 
                        paste0(col, "_", FunChoice)), 
             by=category]
        ) %>% 
          Reduce(function(x, y) merge(x, y, by="category"), .)
      },
      dplyr = {
        dt %>% 
          dplyr::group_by(category) %>% 
          dplyr::summarise_at(dplyr::vars(ColChoice), .funs = setNames(FunChoice, FunChoice))
      },
      check = function(x, y) 
        all.equal(setDT(x)[order(category)], 
                  setDT(y)[order(category)] %>%  
                    setnames(stringr::str_replace(names(.), "_", ".")),
                  ignore.col.order = TRUE,
                  check.attributes = FALSE
                  )
    )  
  }
)

Les résultats sont plus faciles à comparer lorsqu'ils sont tracés:

library(ggplot2)
autoplot(bm)

enter image description here

Veuillez noter l'échelle de temps logarithmique.

Pour ce test, l'approche unlist est toujours la méthode la plus rapide, suivie de dcast. dplyr rattrape son retard pour des problèmes de plus grande taille n. Les deux approches lapply/loop sont moins performantes. En particulier, l'approche de Parfait consistant à parcourir les colonnes et à fusionner les sous-résultats par la suite semble être assez sensible à la taille du problème n.

Edit: 2nd benchmark

Comme suggéré par jangorecki , j'ai répété le repère avec beaucoup plus de lignes et également avec un nombre variable de groupes. En raison de limitations de la mémoire, le problème le plus important est 10 M lignes par 102 colonnes, soit 7,7 Go de mémoire.

Ainsi, la première partie du code de référence est modifiée pour

bm <- bench::press(
  n_grp = 10^(1:3),
  n_row = 10L^seq(3, 7, by = 2),
  {
    set.seed(12212018)
    dt <- data.table(
      index = 1:n_row,
      category = sample(n_grp, n_row, replace = TRUE),
      c1 = rnorm(n_row),
      c2 = rnorm(n_row),
      c3 = rnorm(n_row),
      c4 = rnorm(n_row, 10)
    )
    for (i in 5:100) set(dt, , paste0("c", i), rnorm(n_row, 1000))
    tables()
    ...

enter image description here

Comme prévu par jangorecki , certaines solutions sont plus sensibles au nombre de groupes que d’autres. En particulier, les performances de loop_category se dégradent beaucoup plus fortement avec le nombre de groupes, tandis que dcast semble moins affecté. Pour moins de groupes, l'approche unlist est toujours plus rapide que dcast alors que pour de nombreux groupes, dcast est plus rapide. Cependant, pour les problèmes de taille plus importante, désélectionner semble être en avance sur dcast.

3
Uwe

Voici une réponse data.table:

funs_list <- lapply(FunChoice, as.symbol)
dcast(dt, category~1, fun=eval(funs_list), value.var = Colchoice)

C'est super rapide et fait ce que vous voulez.

6
Bram Van Rensbergen

Envisagez de créer une liste de tables de données dans lesquelles vous parcourez chaque ColChoice et appliquez chaque fonction de FuncChoice (définissez les noms en conséquence). Ensuite, pour fusionner toutes les tables de données, exécutez merge dans un appel Reduce. Utilisez également get pour récupérer des objets d’environnement (fonctions/colonnes).

Remarque : ColChoice a été renommé pour la casse du chameau et la fonction length remplace .N pour la forme fonctionnelle du compte:

set.seed(12212018)  # RUN BEFORE data.table() BUILD TO REPRODUCE OUTPUT
...

ColChoice <- c("c1", "c4")
FunChoice <- c("length", "mean", "sum")

output <- lapply(ColChoice, function(col)
                   dt[, setNames(lapply(FunChoice, function(f) get(f)(get(col))), 
                                 paste0(col, "_", FunChoice)), 
                      by=category]
          )

final_dt <- Reduce(function(x, y) merge(x, y, by="category"), output)

head(final_dt)

#    category c1_length   c1_mean   c1_sum c4_length   c4_mean   c4_sum
# 1:        a      3893 10000.001 38930003      3893  9.990517 38893.08
# 2:        b      4021 10000.028 40210113      4021  9.977178 40118.23
# 3:        c      3931 10000.008 39310030      3931  9.996538 39296.39
# 4:        d      3954 10000.010 39540038      3954 10.004578 39558.10
# 5:        e      4016  9999.998 40159992      4016 10.002131 40168.56
# 6:        f      3974  9999.987 39739947      3974  9.994220 39717.03
1
Parfait

Si les statistiques récapitulatives que vous devez calculer sont des éléments tels que mean, .N et (peut-être) median, que data.table optimise dans le code c au travers de, vous pouvez obtenir des performances plus rapides si vous convertissez la table en format long afin de pouvoir effectuer les opérations suivantes: calculs de manière à ce que les données puissent les optimiser:

> library(data.table)
> n = 100000
> dt  = data.table(index=1:100000,
                   category = sample(letters[1:25], n, replace = T),
                   c1=rnorm(n,10000),
                   c2=rnorm(n,1000),
                   c3=rnorm(n,100),
                   c4 = rnorm(n,10)
  )
> {lapply(c(paste('c', 5:100, sep ='')), function(addcol) dt[[addcol]] <<- rnorm(n,1000) ); dt}

> Colchoice <- c("c1", "c4")

> dt[, .SD
     ][, c('index', 'category', Colchoice), with=F
     ][, melt(.SD, id.vars=c('index', 'category'))
     ][, mean := mean(value), .(category, variable)
     ][, median := median(value), .(category, variable)
     ][, N := .N, .(category, variable)
     ][, value := NULL
     ][, index := NULL
     ][, unique(.SD)
     ][, dcast(.SD, category ~ variable, value.var=c('mean', 'median', 'N') 
     ]

    category mean_c1 mean_c4 median_c1 median_c4 N_c1 N_c4
 1:        a   10000  10.021     10000    10.041 4128 4128
 2:        b   10000  10.012     10000    10.003 3942 3942
 3:        c   10000  10.005     10000     9.999 3926 3926
 4:        d   10000  10.002     10000    10.007 4046 4046
 5:        e   10000   9.974     10000     9.993 4037 4037
 6:        f   10000  10.025     10000    10.015 4009 4009
 7:        g   10000   9.994     10000     9.998 4012 4012
 8:        h   10000  10.007     10000     9.986 3950 3950
...
0
Clayton Stanley

Il semble qu’il n’y ait pas de réponse directe utilisant data.table puisque personne n’a encore répondu à cette question. Je vais donc proposer une réponse basée sur dplyr qui devrait faire ce que vous voulez. J'utilise le jeu de données iris intégré pour l'exemple:

library(dplyr)
iris %>% 
   group_by(Species) %>% 
  summarise_at(vars(Sepal.Length, Sepal.Width), .funs = c(sum=sum,mean= mean), na.rm=TRUE)

## A tibble: 3 x 5
#  Species    Sepal.Length_sum Sepal.Width_sum Sepal.Length_mean Sepal.Width_mean
#  <fct>                 <dbl>           <dbl>             <dbl>            <dbl>
#1 setosa                 245.            171.              5.00             3.43
#2 versicolor             297.            138.              5.94             2.77
#3 virginica              323.            149.              6.60             2.97

ou en utilisant une entrée de vecteur de caractères pour les colonnes et les fonctions:

Colchoice <- c("Sepal.Length", "Sepal.Width")
FunChoice <- c("mean", "sum")
iris %>% 
  group_by(Species) %>% 
  summarise_at(vars(Colchoice), .funs = setNames(FunChoice, FunChoice), na.rm=TRUE)
## A tibble: 3 x 5
#  Species    Sepal.Length_mean Sepal.Width_mean Sepal.Length_sum Sepal.Width_sum
#  <fct>                  <dbl>            <dbl>            <dbl>           <dbl>
#1 setosa                  5.00             3.43             245.            171.
#2 versicolor              5.94             2.77             297.            138.
#3 virginica               6.60             2.97             323.            149.
0
docendo discimus