Cientista de dados, aquele ser que pode ser imaginado como um super-geek por trás dos modelos solucionadores de mistérios. Bom isso acontece, mas o que pouca gente conta é que um bom pedaço do tempo desse profissional é investido em dataPrep, nada cool, costuma ser demorado e delicado, essa última parte quer dizer que “não, não da para automatizar 100% do dataPrep, o ideal é sempre ter um humano se certificando de que tudo está bem”. É inevitável analisar alguns histogramas, estudar dados faltantes, fazer filtros, agrupamentos e tudo o mais.
É aí que entra o data.table. Ah, data.table! Essa belezinha simplesmente é o jeito mais legal de virar sua base de dados ao avesso de forma “braçal”, é uma biblioteca que permite ao usuário adotar uma ‘linguagem’ de manipulção de base que é curta e, na grande maioria das vezes, bem mais rápida que a concorrência.
Ok, ok, isso é fala de fã, admito. O que vou fazer aqui então é dar uma pequena demonstração de como um comando pode ser passado do aclamado dplyr para datatable de forma simples, uma vez que se entende a estrutura, e como isso faz diferença no desempenho do script. Para isso vamos começar construindo uma base de 20 milhões de linhas e 10 colunas.
library(data.table)
library(dplyr)
library(purrr) # For map() function
library(microbenchmark) # For benchmarking
library(tidyverse)
dfs <- rep(2e7,10)
set.seed(123456)
myList <- map(dfs, #amostra de 20kk de valores no range de 1:1e5, 10 vezes
base::sample,
x = 1:1e5,
replace = TRUE) %>%
map(matrix, # em cada posicao da lista, quebra o vetor em 10 e joga cada parte como coluna de uma matriz
ncol = 10) %>%
map(data.frame) # transforma todas matrizes da lista em data.frames
df3 <- data.table::rbindlist(myList) # une as bases na lista
Antes de prosseguir, vamos dar uma olhada na base.
df3
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10
1: 87921 55793 81282 39316 43389 54449 60047 18947 21528 46857
2: 78535 91446 36566 17892 6147 33818 67607 22926 26595 30526
3: 6326 76229 60797 92675 1069 23344 51051 30793 47039 52544
4: 76518 66161 48164 35810 32729 87865 21972 73380 88603 11411
5: 57728 2418 17606 59877 72380 39148 85925 4679 824 28162
---
19999996: 61381 43589 70131 41942 32774 19965 4749 52238 50814 50884
19999997: 45927 4094 38977 76729 90735 21795 9222 63427 70221 26999
19999998: 99080 70908 9367 14237 39944 77173 36196 95234 31328 47128
19999999: 60724 37080 4661 49043 70521 87968 38904 10466 16827 46864
20000000: 4670 63739 27229 73348 72747 21393 29047 91010 44935 96431
Agora suponha que estamos na fase de dataPrep desta base, tentando descobrir coisas sobre ela como : “ Qual o valor minimo de X3 agrupado por X1?”. Vamos escrever isso em dplyr e datatable para entender como é simples traduzir de um para o outro.
# escrevendo em datatable
teste1 <- df3[,.(min_X3 = min(X3)),.(X1)][order(X1)]
# escrevendo em dplyr
teste2 <- df3 %>%
group_by(X1) %>%
summarize(min_X3 = min(X3))
# conferindo se as saídas são iguais
all.equal(teste1,as.data.table(teste2))
[1] TRUE
Detalhando o que acontece no código acima, partindo do entendimento de dplyr
output <- data %>% # primeiro passo: Fornecer a base a ser manipulada
group_by(var) %>% # segundo passo: informar variável de agrupamento
summarize( var_label = func(var2)) # terceiro passo: aplicar função na variável de interesse, dando resultado por grupo.
# A tradução para datatable pode ser confusa no princípio porque apesar de ter uma ordem cronológica, no datatable talvez essa ordem não seja tão intuitiva.
# estrutura básica para operar com datatable
data[filtro, .(summarize), .(group_by)]
# para entender ideia de ordem no datatable temos que:
data[ filtro passo 1, .(summarize passo 1), .(group_by passo 1)]
# para ir ao passo 2 teríamos
data2 <- data[ filtro passo 1, .(summarize passo 1), .(group_by passo 1)]
data2[ filtro passo 2, .(summarize passo 2), .(group_by passo 2)]
# mas a beleza do pacote é fazer com que você precise enderessar objetos o mínimo possível então da para escrever assim
output <- data[ filtro passo 1, .(summarize passo 1), .(group_by passo 1)][ filtro passo 2, .(summarize passo 2), .(group_by passo 2)] ... [ filtro passo n, .(summarize passo n), .(group_by passo n)]
#vale lembrar que é por conta da estrutura básica que quando declaramos teste1, precisamos começar com vírgula, para indicar que não queremos filtros no nosso cálculo.
#Fora isso basta copiar o que você colocaria no 'summarize' para o espaço após a primeira virgula e copiar o que você colocaria no 'group_by' após a segunda vírgula.
Agora que já sabemos como gerar a mesma operação por grupo nos dois pacotes, vamos comparar o desempenho com microbenchmark, esta função executa uma expressão um dado número de vezes e conta o tempo gasto em cada iteração. Para esse caso, foram feitas 100 execuções de cada expressão.
time_champs <- microbenchmark({
df3[,.(min_X3 = min(X3)),.(X1)][order(X1)]
}, times = 100L)
time_nonchamps <- microbenchmark({
df3 %>%
group_by(X1) %>%
summarize(min_X3 = min(X3))
}, times = 100L)
1 segundo com datatable e uma variação muito pequena desse tempo quando repetimos a ação várias vezes, já o dplyr leva de 2 à 4 segundos. A coisa fica pior para o dplyr se tivermos interesse num grupo maior e em mais resultados no summarize.
time_champs2 <- microbenchmark({
df3[,.(min_X3 = min(X3),
max_X3 = max(X3)),.(X1,X2)][order(X1,X2)]
}, times = 10L)
time_nonchamps2 <- microbenchmark({
df3 %>%
group_by(X1,X2) %>%
summarize(min_X3 = min(X3),
max_X3 = max(X3))
}, times = 10L)
Aqui o dplyr levou de 28 à 230 segundos, contra 5.5 à 5.7 segundos do datatable.
Supondo que isso não seja suficiente para te convencer a dar uma chance ao datatable, vamos à um caso de join que tem uma solução muito eficaz e interessante em datatable.
d1<-data.table(v1=c("a","b","c","d","d","b","a","c","a","d","b","a"),
v2=(seq(1:12)),V3=rep(1:4,times=3))
head(d1)
v1 v2 V3
1: a 1 1
2: b 2 2
3: c 3 3
4: d 4 4
5: d 5 1
6: b 6 2
d2<-data.table(v1=c("a","b","c","d"),v3=c(3,2,1,4),v4=c("y","x","t","e"))
head(d2)
v1 v3 v4
1: a 3 y
2: b 2 x
3: c 1 t
4: d 4 e
O objetivo é colocar a informação da coluna v4 na base d1 para os casos onde os valores de v1 e v3 são equivalentes nas duas bases. No dplyr para resolver esse problema seria necessário usar um join, que copiaria d1 e ameaça explodir sua memória se d1 for grande. Com datatable, duas linhas resolvem o problema.
setkey(d1, v1, V3)
d1[d2, v4 := v4][]
# checando o resultado
v1 v2 V3 v4
1: a 1 1 <NA>
2: a 9 1 <NA>
3: a 7 3 y
4: a 12 4 <NA>
5: b 2 2 x
6: b 6 2 x
7: b 11 3 <NA>
8: c 3 3 <NA>
9: c 8 4 <NA>
10: d 5 1 <NA>
11: d 10 2 <NA>
12: d 4 4 e
Mágico, hein? Mas o que aconteceu?
- Primeiro indicamos as variáveis de d1 que serão consideradas chave.
- Em seguida temos d1[d2] que escrito dessa forma equivale a um right_join entre as bases utilizando as chaves definidas, mas ainda não é esse o objetivo, porque se o match não existir o que queremos ver em v4 é
NA
. - := equivale ao mutate e vai adicionar a coluna de acordo com a referência, sendo assim as linhas sem match serão
NA
. - Por fim, o [] no final serve para printar o resultado.
Fato curioso sobre este exemplo, é que ele é um caso real, que gerou ‘pergunta no stackoverflow’ uns anos atrás. A pessoa tinha uma base grande em mãos e já havia tentado loop, que se mostrou impraticável, depois partiu para merge
e sqldf
que não deram conta do problema.
Vale ressaltar que o data.table é tudo isso sim, mas ele não é para big data, ele é um last resort, se ficar impraticável com data.table é porque tem que partir uma ferramenta específica para manipular big data.
Últimas considerações
-
A inspiração principal para esse post veio quando eu comecei a perceber a frequência com que problemas do meu dia a dia, envolvendo manipulações pesadas, podiam ser resolvidos com datatable. Como inspiração secundária os créditos vão para ‘este post’ onde eu descobri que o jeito mais eficaz de agregar bases numa lista é com
data.table::rbindlist
, além de pegar emprestado seu método de gerar aquela base enorme que uso no início, então créditos duplos a ele. -
Quer dar uma chance ao data.table, mas tá magoado com a sintaxe dele e/ou receoso de sair do dplyr que é tão organizado e intuitivo? ‘Aqui’ você encontra um de-para muito bem feito de ações importantes nas duas bibliotecas.
-
Para interessados, o datatable está sendo implementado para Python, isso deixa bem bacana a transição de uma linguagem para outra pois sair de
dplyr
parapandas
não é trivial, mas se você usa data.table, vai manipular bases nas duas linguagens sem estresse. Mais detalhes ‘aqui -
Por último, porém talvez o mais importante para quem foi seduzido pelo datatable, ‘neste post do stackoverflow’ um camarada muito solícito faz uma análise detalhada da luta
data.table vs dplyr
, levando em consideração os pilaresvelocidade
,uso de memória
,sintaxe
efeatures
.
E aí, partiu usar data.table?!