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:

a <- c(1, 2, "c", 4)
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:

data_frame <- head(iris)
elemento_unico_inteiro <- 1
um_na <- NA
vetor_string <- letters[1:5]
modelo_regressao <- lm(mpg ~ wt, data = mtcars)

minha_lista <- list(data_frame = data_frame, 
                    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.

minha_lista[[2]]
## [1] 1
minha_lista$vetor_string
## [1] "a" "b" "c" "d" "e"
# o comando abaixo retorna NULL pq "um_na" não é um nome de
# nenhum elemento da lista
minha_lista$um_na
## 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:

meu_vetor <- c(1, -3, 5, -10)
# 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:

minha_lista <- list(1, 3, 5, 10)
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 
VETOR_OU_LISTA %>% map(FUNCAO_PARA_APLICAR, ARGUMENTOS_OPCIONAIS)

Existem três maneiras de especificar a função para usar no map():

  • Uma função existente
# definir uma lista composta por vetores
v <- list(v1 = c(1, 3, 5), v2 = c(2, 4, 6), v3 = c(7, 8, 9))
# 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, em function(x) abaixo, x é como se fosse uma representação genérica de cada elemento da lista v. 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 de map(). 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
arquivos <- dir("dados/hourly-energy-consumption/",
                # 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:

df1 <-read_csv(arquivos[1])
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
nome_regiao <- str_split(basename(arquivos[1]), "_")[[1]][1]


df1_mes <- df1 %>% 
  # mudar nome das colunas
  purrr::set_names(c("horario", "consumo")) %>% 
  # 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:

agregar_dados <- function(arquivo_csv){
  
  # str_split() quebra um string em mais de um baseado no separador especificado
  nome_regiao <- str_split(basename(arquivo_csv), "_")[[1]][1]
  
  # ler arquivo para um dataframe
  dframe <- read_csv(arquivo_csv)
  
  # criar novo dataframe
  dframe_mes <- dframe %>% 
    # mudar nome das colunas
    purrr::set_names(c("horario", "consumo")) %>% 
    # 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ó:

df_mes_geral <- arquivos %>% 
  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.