O objetivo deste post é apresentar como utilizar o pacote data.table por meio de uma “tradução” das operações realizadas com o dplyr. Para um entendimento completo deste post, é importante que o leitor tenha uma boa noção de como o dplyr funciona.
Muitas vezes, ao trabalhar com dados muito pesados usando dplyr
, tive problemas relacionados à memória do computador (o uso da memória era tão grande que o computador travava) e à velocidade de execução das operações. A alternativa que encontrei foi aprender o pacote data.table
.
Você deve estar se perguntado: quais as vantagens e desvantagens desse pacote? Em geral, ele é mais eficiente em uso de memória e mais rápido do que o dplyr
. Por outro lado, a sintaxe do dplyr
é mais amigável e ele possui funções pra realização de operações mais complexas, como o pivot_longer()
e o pivot_wider()
por exemplo.
Vale comentar que existe um pacote recentemente lançado no CRAN chamado dtplyr
. A ideia desse pacote é usar a sintaxe do dplyr
para realizar operações com o data.table
. No entanto, eu prefiro usar diretamente o data.table
por já estar acostumado com a sintaxe e por não precisar de um intermediário, o que pode diminuir a eficiência do pacote.
Vamos então ao que interessa…
Vamos usar os dados de exportações brasileiras no ano de 2019. Para baixar, basta acessar este link e baixar os dados de exportações de 2019. Uma alternativa é fazer isso via R da seguinte forma:
download.file(
url = "http://www.mdic.gov.br/balanca/bd/comexstat-bd/ncm/EXP_2019.csv",
destfile = "../../dados/EXP_2019.csv"
)
Após o download dos dados, vamos carregá-los. Para isso, iremos usar a nossa primeira função do pacote data.table
chamada fread()
. Essa função é bastante eficiente na leitura dos dados e os converte para duas classes: data.table e data.frame (que são as classes que precisamos para usar o data.table
e dplyr
respectivamente).
# Carregar pacotes
library(dplyr)
library(data.table)
# Carregar dados
dados <- fread(
file = "../../dados/EXP_2019.csv",
colClasses = c(rep("character", 8), rep("integer", 3))
)
# Verificar a classe do objeto dados
class(dados)
[1] "data.table" "data.frame"
Com os dados carregados, vamos entender um pouco de sua estrutura. Existem 11 variáveis, sendo 8 delas códigos ou siglas que representam, por exemplo, o produto exportado, Unidade da Federação (UF) exportadora, país de destino, e as outras 3 são a quantidade exportada, peso exportado e valor de exportação. Além disso, os dados possuem mais de 1 milhão e 400 mil linhas. Para observamos sua estrutura, basta usar o código abaixo.
glimpse(dados)
Observations: 1,423,026
Variables: 11
$ CO_ANO <chr> "2019", "2019", "2019", "2019", "2019", "2019"...
$ CO_MES <chr> "12", "08", "07", "07", "02", "11", "09", "09"...
$ CO_NCM <chr> "19021900", "84149020", "08061000", "90328983"...
$ CO_UNID <chr> "10", "10", "10", "11", "11", "10", "10", "10"...
$ CO_PAIS <chr> "580", "756", "607", "586", "589", "158", "063...
$ SG_UF_NCM <chr> "ES", "SP", "ES", "SP", "SP", "SP", "SP", "PR"...
$ CO_VIA <chr> "00", "01", "00", "07", "04", "01", "07", "07"...
$ CO_URF <chr> "0727600", "0817800", "0727600", "0917500", "0...
$ QT_ESTAT <int64> 43, 4, 18, 88, 27, 54700, 158059, 2551, 433,...
$ KG_LIQUIDO <int64> 43, 4, 18, 299, 142, 54649, 158059, 2551, 89...
$ VL_FOB <int> 180, 154, 68, 30805, 5695, 109073, 321274, 139...
\[data\_table[i, j, \text{by}]\]
Como apresentado acima, para usarmos o data.table
precisamos primeiro identificar os dados em formato “data.table” que vamos manipular e em seguida abrir colchetes. Dentro dos colchetes, existem 3 partes. A primeira, antes da primeira vírgula (na estrutura acima representada pela letra i), apresenta operações realizadas nas linhas. A segunda parte, representada pela letra j, são operações nas colunas. Por fim, a última parte identifica eventuais agrupamentos nos dados para realizar operações em i ou j. Como vamos ver mais a frente, nem sempre todas as partes precisam estar explícitas em nossos códigos. Se vamos fazer apenas operações nas linhas, por exemplo, as partes j e by não precisar ser incluídas.
No data.table
, muitas vezes a mesma operação poder ser feita usando notações diferentes. Como será mostrado a diante, as seleção de colunas, por exemplo, pode ser feita usando o símbolo .() ou diretamente com um vetor de nomes de colunas. Do mesmo modo, a criação de nova coluna pode ser realizada assim ‘:=’ (col = valores) ou diretamente assim col := valores. Neste post, irei apresentar apenas uma das formas para não tornar a leitura muito longa. De todo mundo, inserindo os termos “data.table” e “cheat sheet” no google você encontrará diversos resumos de como utilizar esse pacote.
Vamos à tradução do dplyr
para o data.table
.
Vamos usar nossos dados para aprender filtrar informações usando o data.table
. Imagine, então, que queiramos observar apenas as operações realizadas em agosto de 2019. No dplyr
, fazemos o seguinte:
dados_dplyr <- dados %>%
filter(CO_MES == "08")
head(dados_dplyr)
CO_ANO CO_MES CO_NCM CO_UNID CO_PAIS SG_UF_NCM CO_VIA CO_URF
1 2019 08 84149020 10 756 SP 01 0817800
2 2019 08 44092900 16 607 PA 01 0217800
3 2019 08 84831019 11 097 SP 04 0817600
4 2019 08 71031000 10 386 RS 04 0817700
5 2019 08 76069200 10 232 SP 00 0817800
6 2019 08 72104910 10 741 SP 00 0817800
QT_ESTAT KG_LIQUIDO VL_FOB
1 4 4 154
2 151 178854 182820
3 1 8 39
4 0 0 119
5 1 1 84
6 40 40 420
Em data.table
, temos que realizar operações na parte i da estrutura apresentada acima. Basta fazermos o seguinte:
dados_dt <- dados[CO_MES == "08"]
head(dados_dt)
CO_ANO CO_MES CO_NCM CO_UNID CO_PAIS SG_UF_NCM CO_VIA CO_URF
1: 2019 08 84149020 10 756 SP 01 0817800
2: 2019 08 44092900 16 607 PA 01 0217800
3: 2019 08 84831019 11 097 SP 04 0817600
4: 2019 08 71031000 10 386 RS 04 0817700
5: 2019 08 76069200 10 232 SP 00 0817800
6: 2019 08 72104910 10 741 SP 00 0817800
QT_ESTAT KG_LIQUIDO VL_FOB
1: 4 4 154
2: 151 178854 182820
3: 1 8 39
4: 0 0 119
5: 1 1 84
6: 40 40 420
Repare que neste caso não há nenhuma vírgula dentro dos colchetes, ou seja, não incluímos operações nas colunas e não agrupamos os dados.
Para selecionar colunas no dplyr
, basta usar a função select()
. Vamos selecionar, então, a coluna de produtos, país e UF, além das colunas de valores.
dados_dplyr <- dados_dplyr %>%
select(CO_NCM, CO_PAIS, SG_UF_NCM, QT_ESTAT, KG_LIQUIDO, VL_FOB)
head(dados_dplyr)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB
1 84149020 756 SP 4 4 154
2 44092900 607 PA 151 178854 182820
3 84831019 097 SP 1 8 39
4 71031000 386 RS 0 0 119
5 76069200 232 SP 1 1 84
6 72104910 741 SP 40 40 420
A mesma operação no data.table
é feita usando a parte j da estrutura apresentada. Repare, que neste caso, precisamos incluir uma vírgula antes da seleção de colunas para sinalizar que estamos realizando operações na parte j, mesmo deixando a parte i em branco. A parte do agrupamento (by) não é necessária neste caso. No código abaixo, usamos a notação do data.table
.()
para não precisarmos escrever os nomes das colunas em um vetor, o que faria com que tivéssemos que incluir aspas em todos os nomes. Se preferir, também é possível usar um vetor para selecionar as colunas.
dados_dt <- dados_dt[, .(CO_NCM, CO_PAIS, SG_UF_NCM, QT_ESTAT, KG_LIQUIDO, VL_FOB)]
head(dados_dt)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB
1: 84149020 756 SP 4 4 154
2: 44092900 607 PA 151 178854 182820
3: 84831019 097 SP 1 8 39
4: 71031000 386 RS 0 0 119
5: 76069200 232 SP 1 1 84
6: 72104910 741 SP 40 40 420
A função mutate()
do dplyr
é usada para alterar colunas ou criar novas colunas. Vamos, em nosso exemplo, criar duas colunas, uma que transforme o valor exportado para milhões e uma coluna que receba um dos valores do vetor c("0", "1", NA)
(essa coluna é criada apenas para fins didáticos, sem significado específico). No dplyr
, isso é feito da seguinte forma:
set.seed(123)
dados_dplyr <- dados_dplyr %>%
mutate(
VL_FOB_MILHAO = VL_FOB/1e6,
LABEL = sample(c("0", "1", NA), nrow(dados_dplyr), replace = TRUE)
)
head(dados_dplyr)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB VL_FOB_MILHAO
1 84149020 756 SP 4 4 154 0.000154
2 44092900 607 PA 151 178854 182820 0.182820
3 84831019 097 SP 1 8 39 0.000039
4 71031000 386 RS 0 0 119 0.000119
5 76069200 232 SP 1 1 84 0.000084
6 72104910 741 SP 40 40 420 0.000420
LABEL
1 <NA>
2 <NA>
3 <NA>
4 1
5 <NA>
6 1
No data.table
faremos uso de uma outra notação especial: ':='
. Esse símbolo é usado justamente para fazer operações de criação e alterações de colunas. Como no caso anterior, a parte i é deixada em branco e a última parte não é incluída. Estamos, portanto, utilizando a parte j para a operação.
set.seed(123)
dados_dt <- dados_dt[
,
':=' (VL_FOB_MILHAO = VL_FOB/1e6,
LABEL = sample(c("0", "1", NA), nrow(dados_dplyr), replace = TRUE))
]
head(dados_dt)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB
1: 84149020 756 SP 4 4 154
2: 44092900 607 PA 151 178854 182820
3: 84831019 097 SP 1 8 39
4: 71031000 386 RS 0 0 119
5: 76069200 232 SP 1 1 84
6: 72104910 741 SP 40 40 420
VL_FOB_MILHAO LABEL
1: 0.000154 <NA>
2: 0.182820 <NA>
3: 0.000039 <NA>
4: 0.000119 1
5: 0.000084 <NA>
6: 0.000420 1
Note que novamente não utilizamos a última parte da estrutura por não estarmos fazendo nenhum tipo de agrupamento nos dados.
Existem algumas maneiras de substituir NAs de colunas específicas usando o dplyr
. Uma delas é usar a função mutate()
em conjunto com a função ifelse()
. Neste caso, alteraremos uma coluna a partir de uma condição em que, caso o elemento seja NA, nós o substituiremos por “2”.
dados_dplyr <- dados_dplyr %>%
mutate(LABEL = ifelse(is.na(LABEL), "2", LABEL))
head(dados_dplyr)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB VL_FOB_MILHAO
1 84149020 756 SP 4 4 154 0.000154
2 44092900 607 PA 151 178854 182820 0.182820
3 84831019 097 SP 1 8 39 0.000039
4 71031000 386 RS 0 0 119 0.000119
5 76069200 232 SP 1 1 84 0.000084
6 72104910 741 SP 40 40 420 0.000420
LABEL
1 2
2 2
3 2
4 1
5 2
6 1
No data.table
, usando o que aprendemos anteriormente, poderíamos fazer a substituição dos NAs usando a mesma condição acima na operação análoga ao mutate()
. No entanto, gostaria de mostrar uma outra forma de fazer tal operação que utiliza operações em linhas e em colunas ao mesmo tempo. Veja abaixo.
dados_dt <- dados_dt[is.na(LABEL), LABEL := "2"]
head(dados_dt)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB
1: 84149020 756 SP 4 4 154
2: 44092900 607 PA 151 178854 182820
3: 84831019 097 SP 1 8 39
4: 71031000 386 RS 0 0 119
5: 76069200 232 SP 1 1 84
6: 72104910 741 SP 40 40 420
VL_FOB_MILHAO LABEL
1: 0.000154 2
2: 0.182820 2
3: 0.000039 2
4: 0.000119 1
5: 0.000084 2
6: 0.000420 1
Neste caso, estamos dizendo ao data.table
para “filtrar” as linhas nas quais a coluna LABEL é NA e inserir o caractere “2” em seu lugar. Escrevi “filtrar” entre aspas porque aqui há apenas um filtro temporário. Ao final da operação, nossos dados continuam com o mesmo número de linhas, diferentemente do filtro que apresentamos anteriormente.
No dplyr
, para “dropar” colunas, basta usar a função select()
e incluir o sinal negativo antes do nome das colunas, como abaixo.
dados_dplyr <- dados_dplyr %>%
select(-VL_FOB_MILHAO, -LABEL)
head(dados_dplyr)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB
1 84149020 756 SP 4 4 154
2 44092900 607 PA 151 178854 182820
3 84831019 097 SP 1 8 39
4 71031000 386 RS 0 0 119
5 76069200 232 SP 1 1 84
6 72104910 741 SP 40 40 420
No data.table
temos que utilizar a notação análoga ao mutate()
e atribuir NULL aos elementos das colunas que queremos excluir.
dados_dt <- dados_dt[, ':=' (VL_FOB_MILHAO = NULL, LABEL = NULL)]
head(dados_dt)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB
1: 84149020 756 SP 4 4 154
2: 44092900 607 PA 151 178854 182820
3: 84831019 097 SP 1 8 39
4: 71031000 386 RS 0 0 119
5: 76069200 232 SP 1 1 84
6: 72104910 741 SP 40 40 420
Imagine que queiramos criar uma coluna que conte quantas vezes cada país aparece nos nossos dados, mas sem perder nenhuma informação ao final da operação, ou seja, mantendo o número de linhas de antes da contagem. Neste caso, podemos usar no dplyr
um group_by() em conjunto com o mutate(), como a seguir.
dados_dplyr <- dados_dplyr %>%
group_by(CO_PAIS) %>%
mutate(N_pais = n()) %>%
ungroup()
head(dados_dplyr)
# A tibble: 6 x 7
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB N_pais
<chr> <chr> <chr> <int64> <int64> <int> <int>
1 84149020 756 SP 4 4 154 1568
2 44092900 607 PA 151 178854 182820 1728
3 84831019 097 SP 1 8 39 3885
4 71031000 386 RS 0 0 119 1741
5 76069200 232 SP 1 1 84 789
6 72104910 741 SP 40 40 420 2823
Finalmente iremos utilizar a terceira parte da nossa estrutura do data.table
. Com esse pacote, vamos criar uma coluna como aprendemos anteriormente usando a notação especial .N para a contagem de linhas em conjunto com a parte by da estrutura do data.table
.
dados_dt <- dados_dt[, ':=' (N_pais = .N), by = .(CO_PAIS)]
head(dados_dt)
CO_NCM CO_PAIS SG_UF_NCM QT_ESTAT KG_LIQUIDO VL_FOB N_pais
1: 84149020 756 SP 4 4 154 1568
2: 44092900 607 PA 151 178854 182820 1728
3: 84831019 097 SP 1 8 39 3885
4: 71031000 386 RS 0 0 119 1741
5: 76069200 232 SP 1 1 84 789
6: 72104910 741 SP 40 40 420 2823
Se não quiséssemos definir o nome da nova coluna, bastaria escrever .N na parte j da estrutura e uma coluna chamada N seria criada com a contagem de linhas de acordo com o agrupamento definido.
Vamos agora descobrir qual valor o Brasil exportou para cada país por UF. Para isso, precisamos agrupar os dados por país e UF e agregar o valor exportado somando todas as linhas que apresentam o mesmo CO_PAIS e SG_UF_NCM. Fazemos isso no dplyr
da seguinte maneira.
dados_dplyr <- dados_dplyr %>%
group_by(CO_PAIS, SG_UF_NCM) %>%
summarise(VL_FOB = sum(VL_FOB)) %>%
ungroup()
head(dados_dplyr)
# A tibble: 6 x 3
CO_PAIS SG_UF_NCM VL_FOB
<chr> <chr> <int>
1 013 DF 70211
2 013 ES 60894
3 013 GO 35372
4 013 MG 21172
5 013 MS 101644
6 013 MT 5861
No data.table
novamente vamos utilizar a última parte da sintaxe, mas desta vez em conjunto com o símbolo .(). Segue o código.
dados_dt <- dados_dt[, .(VL_FOB = sum(VL_FOB)), by = .(CO_PAIS, SG_UF_NCM)]
head(dados_dt)
CO_PAIS SG_UF_NCM VL_FOB
1: 756 SP 48940232
2: 607 PA 1229280
3: 097 SP 52563137
4: 386 RS 26841890
5: 232 SP 2929105
6: 741 SP 38035742
Repare que a ordem dos nossos dados não é a mesma da apresentada pelo dplyr
. Vamos reordená-los para verificar que nossas tabelas continuam iguais.
dados_dt <- dados_dt[order(CO_PAIS, SG_UF_NCM)]
head(dados_dt)
CO_PAIS SG_UF_NCM VL_FOB
1: 013 DF 70211
2: 013 ES 60894
3: 013 GO 35372
4: 013 MG 21172
5: 013 MS 101644
6: 013 MT 5861
Tudo igual, não é mesmo?
Para finalizar, vamos realizar uma operação de join. Como sabemos, existem alguns tipos diferentes de joins. Vamos aqui como exemplo realizar um left join, ou seja, juntaremos dois conjuntos de dados por meio de chaves mantendo todas as linhas dos dados da esquerda. Primeiramente vamos criar um data frame com as siglas de alguns estados e seus respectivos nomes.
estados <- data.frame(
SG_UF_NCM = c("DF", "ES", "GO", "MG"),
NO_UF_NCM = c("Distrito Federal", "Espirito Santo", "Goias", "Minas Gerais")
)
No dplyr
realizamos tal operação usando a função left_join()
. Observe que no data frame estados criado acima existem apenas 4 estados. Portanto, ao realizarmos o left join, as linhas de todos os estados que não estão no data frame criado receberão NA na coluna NO_UF_NCM.
dados_dplyr <- dados_dplyr %>%
left_join(
x = .,
y = estados,
by = "SG_UF_NCM"
)
head(dados_dplyr)
# A tibble: 6 x 4
CO_PAIS SG_UF_NCM VL_FOB NO_UF_NCM
<chr> <chr> <int> <fct>
1 013 DF 70211 Distrito Federal
2 013 ES 60894 Espirito Santo
3 013 GO 35372 Goias
4 013 MG 21172 Minas Gerais
5 013 MS 101644 <NA>
6 013 MT 5861 <NA>
Como dito anteriormente, no data.table
a mesma operações podem ser realizadas de diferentes formas. Aqui, o left join será feito usando a função merge.data.table()
. Antes de fazer isso, precisamos lembrar de transformar o data frame como os nomes dos estados na classe data table. Para fazer isso, basta:
estados <- as.data.table(estados)
Agora podemos realizar o left join.
dados_dt <- merge.data.table(
x = dados_dt,
y = estados,
by = "SG_UF_NCM",
all.x = TRUE
)
dados_dt <- dados_dt[order(CO_PAIS, SG_UF_NCM)]
head(dados_dt)
SG_UF_NCM CO_PAIS VL_FOB NO_UF_NCM
1: DF 013 70211 Distrito Federal
2: ES 013 60894 Espirito Santo
3: GO 013 35372 Goias
4: MG 013 21172 Minas Gerais
5: MS 013 101644 <NA>
6: MT 013 5861 <NA>
Um detalhe importante da função merge.data.table()
é saber com usar os argumentos all, all.x e all.y. Definindo all = TRUE estamos realizando um full join, ou seja, todas as linhas de ambos os dados serão trazidas para o dado final. Se all.x = TRUE, realizamos o left joint. Analogamente, all.y = TRUE refere-se ao right join. Finalmente, se definirmos all = FALSE estamos realizando um inner join, ou seja, apenas as linhas em que houve match nas chaves definidas permanecerão nos dados finais.
Vimos que as operações básicas do dplyr
podem ser realizadas de maneira fácil com o data.table
e na maioria das vezes utilizando menos linhas de código. Além disso, o data.table
possui a vantagem de ser mais rápido e eficiente em relação ao uso de memória (não foi o objetivo deste post mostrar isso, mas você poderá encontrar facilmente comparações na internet). No entanto, pessoalmente eu prefiro na maior parte das vezes usar o dplyr
por ser bastante prático e intuitivo. Costumo usar o data.table
quando tenho que fazer operações pesadas como joins usando dados com milhões de linhas. De qualquer modo, vale a pena conhecer e aprender o data.table
.
For attribution, please cite this work as
Castro (2020, Jan. 29). Fulljoin: Problemas com memória ou tempo de execução no dplyr? Use o data.table. Retrieved from https://www.fulljoin.com.br/posts/2020-01-25-dplyr-para-datatable/
BibTeX citation
@misc{castro2020problemas, author = {Castro, Diego}, title = {Fulljoin: Problemas com memória ou tempo de execução no dplyr? Use o data.table}, url = {https://www.fulljoin.com.br/posts/2020-01-25-dplyr-para-datatable/}, year = {2020} }