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:

## [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
##   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.

## [1] 1
## [1] "a" "b" "c" "d" "e"
## NULL

Vetores podem ser transformandos em listas usando a função de coerção as.list():

## [[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:

## [1] "data_frame"             "elemento_unico_inteiro"
## [3] "meu_na"                 "vetor_string"          
## [5] "modelo_regressao"

A função str() pode user usada para inspecionar a estrutura da 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:

## [1]  1  3  5 10

No caso de listas, não é bem assim que funciona:

## 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.

## [[1]]
## [1] 1
## 
## [[2]]
## [1] 3
## 
## [[3]]
## [1] 5
## 
## [[4]]
## [1] 10

Veja a diferença no output:

## [1] "list"
## [[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():

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

  • Uma função existente
## $v1
## [1] 1.000000 1.732051 2.236068
## 
## $v2
## [1] 1.414214 2.000000 2.449490
## 
## $v3
## [1] 2.645751 2.828427 3.000000
## $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.
## $v1
## [1]  1  9 25
## 
## $v2
## [1]  4 16 36
## 
## $v3
## [1] 49 64 81
## $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:
## $v1
## [1]  1  9 25
## 
## $v2
## [1]  4 16 36
## 
## $v3
## [1] 49 64 81
## $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):

##        v1        v2        v3 
## "numeric" "numeric" "numeric"
##  v1  v2  v3 
##  81 144 576

Dá até para garantir que o resultado de map() seja um dataframe com map_dfr() ou map_dfc():

## # A tibble: 3 x 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:

## 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.

Para fins de demonstração, o código abaixo mostra como seria executar o processo descrito acima para apenas um dos arquivos:

## # A tibble: 6 x 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.

## [1] "AEP_hourly.csv"
## `summarise()` regrouping output by 'regiao' (override with `.groups` argument)
## # A tibble: 12 x 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:

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ó:

## # A tibble: 6 x 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.