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
Si je comprends bien, cette question comprend deux parties:
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()
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)
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
.
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()
...
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.
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.
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
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
...
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.