11 Estruturas complexas de dados
11.1 Introdução a listas
Nós já falamos sobre vetores, que são as principais estruturas unidimensionais de dados e que só aceitam elementos da mesma classe:
<- c(1, 2, "c", 4)
a class(a)
## [1] "character"
O R também possui uma estrutura de dados que pode armazenar, literalmente, qualquer tipo de objeto: as listas, criadas com a função list()
.
No exemplo abaixo uma série de objetos de classes diferentes são armazenadas:
<- head(iris)
data_frame <- 1
elemento_unico_inteiro <- NA
um_na <- letters[1:5]
vetor_string <- lm(mpg ~ wt, data = mtcars)
modelo_regressao
<- list(data_frame = data_frame,
minha_lista elemento_unico_inteiro = elemento_unico_inteiro,
# este elemento abaixo não vai possuir um nome
um_na, vetor_string = vetor_string,
modelo_regressao = modelo_regressao)
# Conferindo o output:
minha_lista
## $data_frame
## Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1 5.1 3.5 1.4 0.2 setosa
## 2 4.9 3.0 1.4 0.2 setosa
## 3 4.7 3.2 1.3 0.2 setosa
## 4 4.6 3.1 1.5 0.2 setosa
## 5 5.0 3.6 1.4 0.2 setosa
## 6 5.4 3.9 1.7 0.4 setosa
##
## $elemento_unico_inteiro
## [1] 1
##
## [[3]]
## [1] NA
##
## $vetor_string
## [1] "a" "b" "c" "d" "e"
##
## $modelo_regressao
##
## Call:
## lm(formula = mpg ~ wt, data = mtcars)
##
## Coefficients:
## (Intercept) wt
## 37.285 -5.344
Pelo output já percebemos que a maneira como extraímos um elemento de um vetor é diferente da de uma lista. No primeiro, usamos um par de colchetes ([]
), no segundo usamos dois pares ([[]]
) ou também cifrão ($
), que só funciona caso o elemento da lista possua um nome.
2]] minha_lista[[
## [1] 1
$vetor_string minha_lista
## [1] "a" "b" "c" "d" "e"
# o comando abaixo retorna NULL pq "um_na" não é um nome de
# nenhum elemento da lista
$um_na minha_lista
## NULL
Vetores podem ser transformandos em listas usando a função de coerção as.list()
:
as.list(vetor_string)
## [[1]]
## [1] "a"
##
## [[2]]
## [1] "b"
##
## [[3]]
## [1] "c"
##
## [[4]]
## [1] "d"
##
## [[5]]
## [1] "e"
Inserir um nome em uma lista é simples com o uso da função names()
, que pode alterar os nomes da lista inteira ou de apenas um elemento, como no exemplo abaixo:
names(minha_lista)[3] <- "meu_na"
names(minha_lista)
## [1] "data_frame" "elemento_unico_inteiro" "meu_na" "vetor_string" "modelo_regressao"
A função str()
pode user usada para inspecionar a estrutura da lista:
str(minha_lista)
## List of 5
## $ data_frame :'data.frame': 6 obs. of 5 variables:
## ..$ Sepal.Length: num [1:6] 5.1 4.9 4.7 4.6 5 5.4
## ..$ Sepal.Width : num [1:6] 3.5 3 3.2 3.1 3.6 3.9
## ..$ Petal.Length: num [1:6] 1.4 1.4 1.3 1.5 1.4 1.7
## ..$ Petal.Width : num [1:6] 0.2 0.2 0.2 0.2 0.2 0.4
## ..$ Species : Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1
## $ elemento_unico_inteiro: num 1
## $ meu_na : logi NA
## $ vetor_string : chr [1:5] "a" "b" "c" "d" ...
## $ modelo_regressao :List of 12
## ..$ coefficients : Named num [1:2] 37.29 -5.34
## .. ..- attr(*, "names")= chr [1:2] "(Intercept)" "wt"
## ..$ residuals : Named num [1:32] -2.28 -0.92 -2.09 1.3 -0.2 ...
## .. ..- attr(*, "names")= chr [1:32] "Mazda RX4" "Mazda RX4 Wag" "Datsun 710" "Hornet 4 Drive" ...
## ..$ effects : Named num [1:32] -113.65 -29.116 -1.661 1.631 0.111 ...
## .. ..- attr(*, "names")= chr [1:32] "(Intercept)" "wt" "" "" ...
## ..$ rank : int 2
## ..$ fitted.values: Named num [1:32] 23.3 21.9 24.9 20.1 18.9 ...
## .. ..- attr(*, "names")= chr [1:32] "Mazda RX4" "Mazda RX4 Wag" "Datsun 710" "Hornet 4 Drive" ...
## ..$ assign : int [1:2] 0 1
## ..$ qr :List of 5
## .. ..$ qr : num [1:32, 1:2] -5.657 0.177 0.177 0.177 0.177 ...
## .. .. ..- attr(*, "dimnames")=List of 2
## .. .. .. ..$ : chr [1:32] "Mazda RX4" "Mazda RX4 Wag" "Datsun 710" "Hornet 4 Drive" ...
## .. .. .. ..$ : chr [1:2] "(Intercept)" "wt"
## .. .. ..- attr(*, "assign")= int [1:2] 0 1
## .. ..$ qraux: num [1:2] 1.18 1.05
## .. ..$ pivot: int [1:2] 1 2
## .. ..$ tol : num 1e-07
## .. ..$ rank : int 2
## .. ..- attr(*, "class")= chr "qr"
## ..$ df.residual : int 30
## ..$ xlevels : Named list()
## ..$ call : language lm(formula = mpg ~ wt, data = mtcars)
## ..$ terms :Classes 'terms', 'formula' language mpg ~ wt
## .. .. ..- attr(*, "variables")= language list(mpg, wt)
## .. .. ..- attr(*, "factors")= int [1:2, 1] 0 1
## .. .. .. ..- attr(*, "dimnames")=List of 2
## .. .. .. .. ..$ : chr [1:2] "mpg" "wt"
## .. .. .. .. ..$ : chr "wt"
## .. .. ..- attr(*, "term.labels")= chr "wt"
## .. .. ..- attr(*, "order")= int 1
## .. .. ..- attr(*, "intercept")= int 1
## .. .. ..- attr(*, "response")= int 1
## .. .. ..- attr(*, ".Environment")=<environment: R_GlobalEnv>
## .. .. ..- attr(*, "predvars")= language list(mpg, wt)
## .. .. ..- attr(*, "dataClasses")= Named chr [1:2] "numeric" "numeric"
## .. .. .. ..- attr(*, "names")= chr [1:2] "mpg" "wt"
## ..$ model :'data.frame': 32 obs. of 2 variables:
## .. ..$ mpg: num [1:32] 21 21 22.8 21.4 18.7 18.1 14.3 24.4 22.8 19.2 ...
## .. ..$ wt : num [1:32] 2.62 2.88 2.32 3.21 3.44 ...
## .. ..- attr(*, "terms")=Classes 'terms', 'formula' language mpg ~ wt
## .. .. .. ..- attr(*, "variables")= language list(mpg, wt)
## .. .. .. ..- attr(*, "factors")= int [1:2, 1] 0 1
## .. .. .. .. ..- attr(*, "dimnames")=List of 2
## .. .. .. .. .. ..$ : chr [1:2] "mpg" "wt"
## .. .. .. .. .. ..$ : chr "wt"
## .. .. .. ..- attr(*, "term.labels")= chr "wt"
## .. .. .. ..- attr(*, "order")= int 1
## .. .. .. ..- attr(*, "intercept")= int 1
## .. .. .. ..- attr(*, "response")= int 1
## .. .. .. ..- attr(*, ".Environment")=<environment: R_GlobalEnv>
## .. .. .. ..- attr(*, "predvars")= language list(mpg, wt)
## .. .. .. ..- attr(*, "dataClasses")= Named chr [1:2] "numeric" "numeric"
## .. .. .. .. ..- attr(*, "names")= chr [1:2] "mpg" "wt"
## ..- attr(*, "class")= chr "lm"
A maneira mais produtiva de se usar listas em seus projetos é para automatizar a aplicação de uma determinada função (ou funções) para todos os elementos de uma lista. Suponha, por exemplo, que você precise importar dezenas de arquivos csv, fazer algumas limpezas e manipulações de dados, construir modelos de Machine Learning e depois salvar os resultados no computador. Seria muito tedioso fazer isso manualmente, mas é para esse tipo de operação que listas se tornam muito úteis.
O pacote purrr
possui uma série de comandos para aplicar funções a elementos de uma lista. O R base até possui as funções da família apply (apply()
, tapply()
, lapply()
, etc), mas estas estão entrando em desuso devido à adoção do purrr
.
11.2 Introdução ao pacote purrr
11.2.1 map()
Nós já vimos que o R aplica uma função a cada elemento de um vetor de uma forma muito simples:
<- c(1, -3, 5, -10)
meu_vetor # extrair o modulo de cada elemento do vetor acima
abs(meu_vetor)
## [1] 1 3 5 10
No caso de listas, não é bem assim que funciona:
<- list(1, 3, 5, 10)
minha_lista abs(minha_lista)
## Error in abs(minha_lista): non-numeric argument to mathematical function
É necessário usar uma outra função para aplicar uma função a cada elemento da lista. É aqui que introduzimos a função map()
, do pacote purrr
. O primeiro argumento é a estrutura de dados sobre a qual se deseja iterar e o segundo é a função que será aplicada a cada elemento.
O pacote purrr
faz parte do tidyverse
.
library(tidyverse)
library(purrr)
map(minha_lista, abs)
## [[1]]
## [1] 1
##
## [[2]]
## [1] 3
##
## [[3]]
## [1] 5
##
## [[4]]
## [1] 10
Veja a diferença no output:
class(minha_lista)
## [1] "list"
map(minha_lista, class)
## [[1]]
## [1] "numeric"
##
## [[2]]
## [1] "numeric"
##
## [[3]]
## [1] "numeric"
##
## [[4]]
## [1] "numeric"
De maneira genérica, é assim que são usados os parâmetros de map()
:
map(.x, .f, ...)
# ou
map(VETOR_OU_LISTA, FUNCAO_PARA_APLICAR, ARGUMENTOS_OPCIONAIS)
# que é equivalente a
%>% map(FUNCAO_PARA_APLICAR, ARGUMENTOS_OPCIONAIS) VETOR_OU_LISTA
Existem três maneiras de especificar a função para usar no map()
:
- Uma função existente
# definir uma lista composta por vetores
<- list(v1 = c(1, 3, 5), v2 = c(2, 4, 6), v3 = c(7, 8, 9))
v # aplicar a raiz quadrada a todos os vetores
map(v, sqrt)
## $v1
## [1] 1.000000 1.732051 2.236068
##
## $v2
## [1] 1.414214 2.000000 2.449490
##
## $v3
## [1] 2.645751 2.828427 3.000000
# calcular a soma dos elementos de cada vetor
map(v, sum)
## $v1
## [1] 9
##
## $v2
## [1] 12
##
## $v3
## [1] 24
- Uma função “anônima,” definida dentro da própria
map()
. Veja que, emfunction(x)
abaixo,x
é como se fosse uma representação genérica de cada elemento da listav
. Em inglês isso se chama placeholder.
# elevar cada elemento de cada vetor ao quadrado
map(v, function(x) x^2)
## $v1
## [1] 1 9 25
##
## $v2
## [1] 4 16 36
##
## $v3
## [1] 49 64 81
# elevar a soma dos elementos do vetor ao quadrado
map(v, function(x) sum(x)^2)
## $v1
## [1] 81
##
## $v2
## [1] 144
##
## $v3
## [1] 576
- Uma fórmula. Deve-se começar com o símbolo
~
para iniciar uma função e.x
para se referir ao seu input, que corresponde a cada elemento da lista especificada no primeiro argumento demap()
. Traduzindo os dois comandos anteriores para esta sintaxe, ficaria assim:
map(v, ~ .x^2)
## $v1
## [1] 1 9 25
##
## $v2
## [1] 4 16 36
##
## $v3
## [1] 49 64 81
map(v, ~ sum(.x)^2)
## $v1
## [1] 81
##
## $v2
## [1] 144
##
## $v3
## [1] 576
11.2.2 Funções derivadas de map()
A função map()
retorna uma lista. Contudo, se você sabe que sua função deve retornar um resultado em que todos os elementos pertencem a uma mesma classe, é possível usar as funções derivadas de map, como map_chr()
(character) e map_dbl()
(numérico):
map_chr(v, class)
## v1 v2 v3
## "numeric" "numeric" "numeric"
map_dbl(v, ~ sum(.x)^2)
## v1 v2 v3
## 81 144 576
Dá até para garantir que o resultado de map()
seja um dataframe com map_dfr()
ou map_dfc()
:
map_dfc(v, function(x) x * 2)
## # A tibble: 3 × 3
## v1 v2 v3
## <dbl> <dbl> <dbl>
## 1 2 4 14
## 2 6 8 16
## 3 10 12 18
É possível e simples encadear uma sequência de comandos map()
com o pipe:
%>%
v map(~ .x * 2) %>%
map_dbl(sum)
## v1 v2 v3
## 18 24 48
11.3 Ideia de projeto: Aplicando uma série de funções a uma lista de arquivos
Este dataset no Kaggle traz o consumo médio de energia elétrica por região nos Estados Unidos. A página disponibiliza 13 arquivos csv, um para cada região.
Suponha que, para cada região, desejamos ler o arquivo, padronizar os nomes das duas colunas, acrescentar uma coluna identificando a região do arquivo, calcular o consumo médio por mês do ano e juntar os dataframes. Seria tortuoso fazer isso para cada arquivo manualmente, por isso nos valemos do pacote purrr
para sistematizar esse processo.
Baixe o dataset e salve em uma pasta chamada “dados.” Descompacte o arquivo zip e uma nova pasta será criada.
# listar os arquivos contidos na pasta baixada
<- dir("dados/hourly-energy-consumption/",
arquivos # listar apenas arquivos que contem o padrao abaixo
pattern = "_hourly.csv",
full.names = TRUE)
Para fins de demonstração, o código abaixo mostra como seria executar o processo descrito acima para apenas um dos arquivos:
<-read_csv(arquivos[1])
df1 head(df1)
## # A tibble: 6 × 2
## Datetime AEP_MW
## <dttm> <dbl>
## 1 2004-12-31 01:00:00 13478
## 2 2004-12-31 02:00:00 12865
## 3 2004-12-31 03:00:00 12577
## 4 2004-12-31 04:00:00 12517
## 5 2004-12-31 05:00:00 12670
## 6 2004-12-31 06:00:00 13038
Para extrair o nome do arquivo, note que o padrão é NOMEREGIAO_hourly. Por isso, podemos usar str_split() para “quebrar” o string em dois e pegar apenas o primeiro elemento.
# basename() retorna apenas o nome do arquivo, sem o diretorio
basename(arquivos[1])
## [1] "AEP_hourly.csv"
# str_split() quebra um string em mais de um baseado no separador especificado
<- str_split(basename(arquivos[1]), "_")[[1]][1]
nome_regiao
<- df1 %>%
df1_mes # mudar nome das colunas
::set_names(c("horario", "consumo")) %>%
purrr# criar uma coluna contendo o mes da data
mutate(mes = lubridate::month(horario)) %>%
# criar uma coluna contendo o nome da regiao do arquivo
mutate(regiao = nome_regiao) %>%
# agrupar os dados e calcular a media
group_by(regiao, mes) %>%
summarise(consumo_medio = mean(consumo))
## `summarise()` has grouped output by 'regiao'. You can override using the `.groups` argument.
df1_mes
## # A tibble: 12 × 3
## # Groups: regiao [1]
## regiao mes consumo_medio
## <chr> <dbl> <dbl>
## 1 AEP 1 17431.
## 2 AEP 2 17023.
## 3 AEP 3 15377.
## 4 AEP 4 13824.
## 5 AEP 5 14006.
## 6 AEP 6 15630.
## 7 AEP 7 16350.
## 8 AEP 8 16425.
## 9 AEP 9 14657.
## 10 AEP 10 13939.
## 11 AEP 11 14930.
## 12 AEP 12 16446.
A solução para aplicar o código acima para todos os arquivos csv diferentes de maneira elegante no R é o sistematizar, transformando-o em uma função:
<- function(arquivo_csv){
agregar_dados
# str_split() quebra um string em mais de um baseado no separador especificado
<- str_split(basename(arquivo_csv), "_")[[1]][1]
nome_regiao
# ler arquivo para um dataframe
<- read_csv(arquivo_csv)
dframe
# criar novo dataframe
<- dframe %>%
dframe_mes # mudar nome das colunas
::set_names(c("horario", "consumo")) %>%
purrr# criar uma coluna contendo o mes da data
mutate(mes = lubridate::month(horario)) %>%
# criar uma coluna contendo o nome da regiao do arquivo
mutate(regiao = nome_regiao) %>%
# agrupar os dados e calcular a media
group_by(regiao, mes) %>%
summarise(consumo_medio = mean(consumo))
# retornar novo dataframe
dframe_mes
}
Como sabemos que a função agregar_dados()
deve retornar um dataframe, usamos map_dfr()
para, além de gerar um dataframe por arquivo, juntá-los em um dataframe só:
<- arquivos %>%
df_mes_geral map_dfr(agregar_dados)
head(df_mes_geral)
## # A tibble: 6 × 3
## # Groups: regiao [1]
## regiao mes consumo_medio
## <chr> <dbl> <dbl>
## 1 AEP 1 17431.
## 2 AEP 2 17023.
## 3 AEP 3 15377.
## 4 AEP 4 13824.
## 5 AEP 5 14006.
## 6 AEP 6 15630.