Introdução

O objetivo deste rápido tutorial é introduzir algumas funções dos pacotes dplyr e tidyr úteis para manipulação de conjuntos de dados em R. Esta é uma tarefa essencial, uma vez que os dados nem sempre chegam ao pesquisador num formato que permita a aplicação direta dos recursos do computacionais. Portanto, colocar os dados em formato adequado muitas vezes pode ser tão importante quanto (e até mais trabalhoso) do que a análise em si.

O pacotes dplyr e tidyr possuem uma gama de funções que permitem remoldar os dados com apenas alguns comandos. Utilizaremos o conjuntos de dados flags (ver documentação) para fornecer uma abordagem prática para os principais comandos de dplyr e tidyr.

Dados

Primeiramente vamos baixar os dados diretamente da web. O banco de dados flags constitui-se numa lista de 194 países com 30 atributos sobre sua geografia (área, população) e suas bandeiras (presença de cores, faixas, estrelas, etc.)

OBS: como nos dados originais não há um cabeçalho com os nomes das variáveis, foi necessário adicionar manualmente.

# data wrangling tutorial:
library(plyr)
suppressPackageStartupMessages(library(dplyr))
library(tidyr)
library(microbenchmark)

# packages plyr, dplyr, tidyr

flags <- read.table("https://archive.ics.uci.edu/ml/machine-learning-databases/flags/flag.data", sep=",")

atributos <- c("name", "landmass", "zone", "area", "population", "language", "religion", "bars", "stripes", "colours",
               "red", "green", "blue", "gold", "white", "black", "orange", "mainhue", "circles", "crosses", "saltires",
               "quarters", "sunstars", "crescent", "triangle", "icon", "animate", "text", "topleft", "botright")

colnames(flags) <- atributos
fullData = flags
# temos o data frame, agora vamos seguir os passos do cheatsheet

Por enquanto, a variável flags é um dataframe. Para o uso do pacote dplyr é conveniente converter os dados para outra estrutura chamada tbl. Em seguida, a função distinct() permite remover linhas repetidas.

# transformar os dados para a classe tbl:
flags <- tbl_df(flags)
distinct(flags)
## # A tibble: 194 × 30
##               name landmass  zone  area population language religion  bars
##             <fctr>    <int> <int> <int>      <int>    <int>    <int> <int>
## 1      Afghanistan        5     1   648         16       10        2     0
## 2          Albania        3     1    29          3        6        6     0
## 3          Algeria        4     1  2388         20        8        2     2
## 4   American-Samoa        6     3     0          0        1        1     0
## 5          Andorra        3     1     0          0        6        0     3
## 6           Angola        4     2  1247          7       10        5     0
## 7         Anguilla        1     4     0          0        1        1     0
## 8  Antigua-Barbuda        1     4     0          0        1        1     0
## 9        Argentina        2     3  2777         28        2        0     0
## 10       Argentine        2     3  2777         28        2        0     0
## # ... with 184 more rows, and 22 more variables: stripes <int>,
## #   colours <int>, red <int>, green <int>, blue <int>, gold <int>,
## #   white <int>, black <int>, orange <int>, mainhue <fctr>, circles <int>,
## #   crosses <int>, saltires <int>, quarters <int>, sunstars <int>,
## #   crescent <int>, triangle <int>, icon <int>, animate <int>, text <int>,
## #   topleft <fctr>, botright <fctr>

Note que a observação relativa à Argentina é repetida, mas a função distinct() não removeu a linha, já que na linha repetida consta “Argentine”, portanto, as duas linhas não seriam iguais. O comando abaixo resolve este problema:

# removendo a linha repetida "Argentine"
for(i in 1:nrow(flags)){
  if(flags[i,1] == "Argentine")
    flags <- flags[-i, ]
}

Como o objetivo aqui é mostrar como trabalhar com o pacote, selecionaremos uma parte dos dados selecionar aleatoriamente uma parte dos dados. Isto pode ser feito de duas maneiras, a saber, selecionando uma porcentagem dos dados, utilizando a função sample_frac() ou um número determinado, utilizando a função sample_n().

Por exemplo, para selecionar aleatoriamente 80% dos objetos com sample_frac(), temos:

sample_frac(flags, 0.8, replace=FALSE)   
## # A tibble: 155 × 30
##                  name landmass  zone  area population language religion
##                <fctr>    <int> <int> <int>      <int>    <int>    <int>
## 1                Peru        2     3  1285         14        2        0
## 2            Zimbabwe        4     2   391          8       10        5
## 3               Haiti        1     4    28          6        3        0
## 4            Tanzania        4     2   945         18       10        5
## 5  Cape-Verde-Islands        4     4     4          0        6        0
## 6     Soloman-Islands        6     2    30          0        1        1
## 7            Botswana        4     2   600          1       10        5
## 8             Faeroes        3     4     1          0        6        1
## 9         North-Yemen        5     1   195          9        8        2
## 10              Japan        5     1   372        118        9        7
## # ... with 145 more rows, and 23 more variables: bars <int>,
## #   stripes <int>, colours <int>, red <int>, green <int>, blue <int>,
## #   gold <int>, white <int>, black <int>, orange <int>, mainhue <fctr>,
## #   circles <int>, crosses <int>, saltires <int>, quarters <int>,
## #   sunstars <int>, crescent <int>, triangle <int>, icon <int>,
## #   animate <int>, text <int>, topleft <fctr>, botright <fctr>

Vamos escolher aleatoriamente 5 países para que seja possível visualizar as saídas das funções. Em seguida, usamos o comando glimpse(), que permite visualizar os dados com os atributos como linhas e os objetos como colunas. A tabela apresenta 3 variáveis para facilitar a visualização:

# selecionar aleatoriamente um número de linhas dos dados
flags = sample_n(flags, 5, replace=FALSE)
glimpse(flags)
## Observations: 5
## Variables: 30
## $ name       <fctr> Indonesia, South-Korea, Luxembourg, Peru, Kenya
## $ landmass   <int> 6, 5, 3, 2, 4
## $ zone       <int> 2, 1, 1, 3, 1
## $ area       <int> 1904, 99, 3, 1285, 583
## $ population <int> 157, 39, 0, 14, 17
## $ language   <int> 10, 10, 4, 2, 10
## $ religion   <int> 2, 7, 0, 0, 5
## $ bars       <int> 0, 0, 0, 3, 0
## $ stripes    <int> 2, 0, 3, 0, 5
## $ colours    <int> 2, 4, 3, 2, 4
## $ red        <int> 1, 1, 1, 1, 1
## $ green      <int> 0, 0, 0, 0, 1
## $ blue       <int> 0, 1, 1, 0, 0
## $ gold       <int> 0, 0, 0, 0, 0
## $ white      <int> 1, 1, 1, 1, 1
## $ black      <int> 0, 1, 0, 0, 1
## $ orange     <int> 0, 0, 0, 0, 0
## $ mainhue    <fctr> red, white, red, red, red
## $ circles    <int> 0, 1, 0, 0, 1
## $ crosses    <int> 0, 0, 0, 0, 0
## $ saltires   <int> 0, 0, 0, 0, 0
## $ quarters   <int> 0, 0, 0, 0, 0
## $ sunstars   <int> 0, 0, 0, 0, 0
## $ crescent   <int> 0, 0, 0, 0, 0
## $ triangle   <int> 0, 0, 0, 0, 0
## $ icon       <int> 0, 1, 0, 0, 1
## $ animate    <int> 0, 0, 0, 0, 0
## $ text       <int> 0, 0, 0, 0, 0
## $ topleft    <fctr> red, white, red, red, black
## $ botright   <fctr> white, white, blue, red, green
library(knitr)
flags %>% select(.,name,area,population) %>% kable(.,caption="Seleção aleatória de 5 observações")
Seleção aleatória de 5 observações
name area population
Indonesia 1904 157
South-Korea 99 39
Luxembourg 3 0
Peru 1285 14
Kenya 583 17

Formatando os dados

A função gather() pertence ao pacote tidyr converte as colunas de dados para o formato chave/valor. Os atributos desejados são colocados na coluna “key” e seus respectivos valores na coluna “value”. Note que fazemos uso, pela primeira vez, do operador %>%, que torna o código em R mais legível. Por exemplo, a notação usual equivalente ao primeiro comando abaixo seria

gather(flags,key="key", value = "value", -name)

Por meio deste comando, convertemos os dados para a notação chave/valor, onde as chaves são todas as variáveis, exceto o nome dos países.

gathered_data = flags %>% gather(., key="key", value = "value", -name)
## Warning: attributes are not identical across measure variables; they will
## be dropped
gathered_data
## # A tibble: 145 × 3
##           name      key value
##         <fctr>    <chr> <chr>
## 1    Indonesia landmass     6
## 2  South-Korea landmass     5
## 3   Luxembourg landmass     3
## 4         Peru landmass     2
## 5        Kenya landmass     4
## 6    Indonesia     zone     2
## 7  South-Korea     zone     1
## 8   Luxembourg     zone     1
## 9         Peru     zone     3
## 10       Kenya     zone     1
## # ... with 135 more rows

A função spread, do pacote tidyr, pega as chaves e as transforma em atributos - como tínhamos anteriormente. Então, as funções spread e gather são complementares. Uma desfaz o que a outra faz.

gathered_data %>% spread(., key, value)
## # A tibble: 5 × 30
##          name animate  area  bars black  blue botright circles colours
## *      <fctr>   <chr> <chr> <chr> <chr> <chr>    <chr>   <chr>   <chr>
## 1   Indonesia       0  1904     0     0     0    white       0       2
## 2       Kenya       0   583     0     1     0    green       1       4
## 3  Luxembourg       0     3     0     0     1     blue       0       3
## 4        Peru       0  1285     3     0     0      red       0       2
## 5 South-Korea       0    99     0     1     1    white       1       4
## # ... with 21 more variables: crescent <chr>, crosses <chr>, gold <chr>,
## #   green <chr>, icon <chr>, landmass <chr>, language <chr>,
## #   mainhue <chr>, orange <chr>, population <chr>, quarters <chr>,
## #   red <chr>, religion <chr>, saltires <chr>, stripes <chr>,
## #   sunstars <chr>, text <chr>, topleft <chr>, triangle <chr>,
## #   white <chr>, zone <chr>

Para ordenar as observações de acordo com os valores de um atributo, use a função arrange(). Por exemplo, vamos ordenar os países de acordo com a área, do maior para o menor…

flags %>% arrange(., desc(area))
## # A tibble: 5 × 30
##          name landmass  zone  area population language religion  bars
##        <fctr>    <int> <int> <int>      <int>    <int>    <int> <int>
## 1   Indonesia        6     2  1904        157       10        2     0
## 2        Peru        2     3  1285         14        2        0     3
## 3       Kenya        4     1   583         17       10        5     0
## 4 South-Korea        5     1    99         39       10        7     0
## 5  Luxembourg        3     1     3          0        4        0     0
## # ... with 22 more variables: stripes <int>, colours <int>, red <int>,
## #   green <int>, blue <int>, gold <int>, white <int>, black <int>,
## #   orange <int>, mainhue <fctr>, circles <int>, crosses <int>,
## #   saltires <int>, quarters <int>, sunstars <int>, crescent <int>,
## #   triangle <int>, icon <int>, animate <int>, text <int>, topleft <fctr>,
## #   botright <fctr>

…e do menor para o maior:

#(do menor para o maior)
flags %>% arrange(., area)
## # A tibble: 5 × 30
##          name landmass  zone  area population language religion  bars
##        <fctr>    <int> <int> <int>      <int>    <int>    <int> <int>
## 1  Luxembourg        3     1     3          0        4        0     0
## 2 South-Korea        5     1    99         39       10        7     0
## 3       Kenya        4     1   583         17       10        5     0
## 4        Peru        2     3  1285         14        2        0     3
## 5   Indonesia        6     2  1904        157       10        2     0
## # ... with 22 more variables: stripes <int>, colours <int>, red <int>,
## #   green <int>, blue <int>, gold <int>, white <int>, black <int>,
## #   orange <int>, mainhue <fctr>, circles <int>, crosses <int>,
## #   saltires <int>, quarters <int>, sunstars <int>, crescent <int>,
## #   triangle <int>, icon <int>, animate <int>, text <int>, topleft <fctr>,
## #   botright <fctr>

Podemos selecionar observações de acordo com um critério lógico usando a função filter(). No exemplo, selecionamos apenas os países com população maior que 40 milhões e/ou área maior que 200.

# função filter
flags %>% filter(., population > 20, area>50)
## # A tibble: 2 × 30
##          name landmass  zone  area population language religion  bars
##        <fctr>    <int> <int> <int>      <int>    <int>    <int> <int>
## 1   Indonesia        6     2  1904        157       10        2     0
## 2 South-Korea        5     1    99         39       10        7     0
## # ... with 22 more variables: stripes <int>, colours <int>, red <int>,
## #   green <int>, blue <int>, gold <int>, white <int>, black <int>,
## #   orange <int>, mainhue <fctr>, circles <int>, crosses <int>,
## #   saltires <int>, quarters <int>, sunstars <int>, crescent <int>,
## #   triangle <int>, icon <int>, animate <int>, text <int>, topleft <fctr>,
## #   botright <fctr>
flags %>% filter(., population > 20 | area>50)
## # A tibble: 4 × 30
##          name landmass  zone  area population language religion  bars
##        <fctr>    <int> <int> <int>      <int>    <int>    <int> <int>
## 1   Indonesia        6     2  1904        157       10        2     0
## 2 South-Korea        5     1    99         39       10        7     0
## 3        Peru        2     3  1285         14        2        0     3
## 4       Kenya        4     1   583         17       10        5     0
## # ... with 22 more variables: stripes <int>, colours <int>, red <int>,
## #   green <int>, blue <int>, gold <int>, white <int>, black <int>,
## #   orange <int>, mainhue <fctr>, circles <int>, crosses <int>,
## #   saltires <int>, quarters <int>, sunstars <int>, crescent <int>,
## #   triangle <int>, icon <int>, animate <int>, text <int>, topleft <fctr>,
## #   botright <fctr>

No entanto, há uma contraindicação ao uso da função filter(): a função subset() cumpre o mesmo papel de modo muito mais eficiente, como pode ser visto nos testes de desempenho abaixo:

# note que a função subset é bem mais rápida:

microbenchmark(filter(flags, population > 20 | area>50), 
               subset(flags, population>20 | area>50))
## Unit: microseconds
##                                        expr     min      lq     mean
##  filter(flags, population > 20 | area > 50) 692.540 730.326 855.3812
##  subset(flags, population > 20 | area > 50) 134.434 150.644 186.4928
##    median       uq      max neval cld
##  786.6245 887.0295 2366.651   100   b
##  172.3845 198.4125  466.470   100  a
microbenchmark(filter(flags, population >20,area>50), 
               subset(flags, population>20,area>50)) 
## Unit: microseconds
##                                       expr     min      lq      mean
##  filter(flags, population > 20, area > 50) 748.192 803.660 941.43112
##  subset(flags, population > 20, area > 50)  63.289  75.728  98.91214
##    median       uq      max neval cld
##  867.1455 979.4240 3093.432   100   b
##   95.0750 111.0395  183.388   100  a

Para obter medidas-resumo das variáveis (mínimo, máximo, média, mediana, variância e desvio padrão), utilizam-se as funções summarise_each() (para todas as variáveis) e summarise() (para variáveis selecionadas).

Note que, quando aplicada para todas as colunas, o comando summarise_each(…, funs(mean)) retorna algumas advertências: elas referem-se ao fato de que não podemos calcular a média para fatores. No caso das variáveis booleanas, a média expressará a proporção de casos positivos, por exemplo, a proporção de países que têm azul em sua bandeira.

## Summarise data
flags %>% summarise_each(., funs(mean))
## Warning in mean.default(structure(c(83L, 155L, 103L, 137L, 94L), .Label =
## c("Afghanistan", : argument is not numeric or logical: returning NA
## Warning in mean.default(structure(c(7L, 8L, 7L, 7L, 7L), .Label =
## c("black", : argument is not numeric or logical: returning NA
## Warning in mean.default(structure(c(6L, 7L, 6L, 6L, 1L), .Label =
## c("black", : argument is not numeric or logical: returning NA
## Warning in mean.default(structure(c(8L, 8L, 2L, 7L, 5L), .Label =
## c("black", : argument is not numeric or logical: returning NA
## # A tibble: 1 × 30
##    name landmass  zone  area population language religion  bars stripes
##   <dbl>    <dbl> <dbl> <dbl>      <dbl>    <dbl>    <dbl> <dbl>   <dbl>
## 1    NA        4   1.6 774.8       45.4      7.2      2.8   0.6       2
## # ... with 21 more variables: colours <dbl>, red <dbl>, green <dbl>,
## #   blue <dbl>, gold <dbl>, white <dbl>, black <dbl>, orange <dbl>,
## #   mainhue <dbl>, circles <dbl>, crosses <dbl>, saltires <dbl>,
## #   quarters <dbl>, sunstars <dbl>, crescent <dbl>, triangle <dbl>,
## #   icon <dbl>, animate <dbl>, text <dbl>, topleft <dbl>, botright <dbl>
flags %>% summarise(., meanArea = mean(area), meanPop = max(population),meanBlack = mean(black), 
                    meanBlue = mean(blue), meanGold = mean(gold))  %>% kable(.,align='l')
meanArea meanPop meanBlack meanBlue meanGold
774.8 157 0.4 0.4 0

Ás vezes pode ser interessante agrupar observações por fatores de uma variável. Por exemplo, podemos agrupar os países de acordo com a divisão geográfica a qual pertencem. A função ddply() cumpre esta função. No exemplo abaixo, agrupamos os países de acordo com o continente e com a religião dominante.

OBS: nos dados utilizados aqui, América Central e do Norte foram agrupadas em um só continente.

# ddply function (agupar segundo alguma variável)

flags %>% ddply(., .(landmass)) 
##          name landmass zone area population language religion bars stripes
## 1        Peru        2    3 1285         14        2        0    3       0
## 2  Luxembourg        3    1    3          0        4        0    0       3
## 3       Kenya        4    1  583         17       10        5    0       5
## 4 South-Korea        5    1   99         39       10        7    0       0
## 5   Indonesia        6    2 1904        157       10        2    0       2
##   colours red green blue gold white black orange mainhue circles crosses
## 1       2   1     0    0    0     1     0      0     red       0       0
## 2       3   1     0    1    0     1     0      0     red       0       0
## 3       4   1     1    0    0     1     1      0     red       1       0
## 4       4   1     0    1    0     1     1      0   white       1       0
## 5       2   1     0    0    0     1     0      0     red       0       0
##   saltires quarters sunstars crescent triangle icon animate text topleft
## 1        0        0        0        0        0    0       0    0     red
## 2        0        0        0        0        0    0       0    0     red
## 3        0        0        0        0        0    1       0    0   black
## 4        0        0        0        0        0    1       0    0   white
## 5        0        0        0        0        0    0       0    0     red
##   botright
## 1      red
## 2     blue
## 3    green
## 4    white
## 5    white
flags %>% ddply(., .(religion))
##          name landmass zone area population language religion bars stripes
## 1  Luxembourg        3    1    3          0        4        0    0       3
## 2        Peru        2    3 1285         14        2        0    3       0
## 3   Indonesia        6    2 1904        157       10        2    0       2
## 4       Kenya        4    1  583         17       10        5    0       5
## 5 South-Korea        5    1   99         39       10        7    0       0
##   colours red green blue gold white black orange mainhue circles crosses
## 1       3   1     0    1    0     1     0      0     red       0       0
## 2       2   1     0    0    0     1     0      0     red       0       0
## 3       2   1     0    0    0     1     0      0     red       0       0
## 4       4   1     1    0    0     1     1      0     red       1       0
## 5       4   1     0    1    0     1     1      0   white       1       0
##   saltires quarters sunstars crescent triangle icon animate text topleft
## 1        0        0        0        0        0    0       0    0     red
## 2        0        0        0        0        0    0       0    0     red
## 3        0        0        0        0        0    0       0    0     red
## 4        0        0        0        0        0    1       0    0   black
## 5        0        0        0        0        0    1       0    0   white
##   botright
## 1     blue
## 2      red
## 3    white
## 4    green
## 5    white

Criando novas variáveis

Vamos supor que tenhamos interesse em saber quais os países que possuem verde+amarelo ou vermelho+branco sua bandeira. Podemos utilizar a função mutate(), que cria uma nova variável de acordo com operações sobre as variáveis existentes.

Criamos a função pairsVec() indicadora da presença de duas cores: a função recebe dois vetores booleanos (1 - TRUE e 0 - FALSE) e retorna outro vetor booleano em que 1 indica (TRUE, TRUE) e 0 indica os outros casos. Ou seja, quando temos as duas cores presentes na bandeira do i-ésimo país, o vetor resultante terá 1 na posição i.

Utilizamos pairsVec() como um dos argumentos da função mutate() para gerar duas novas variáveis: goldGrenn (indica se há amarelo e verde na bandeira) e redWhite (se há vermelho e branco na bandeira). Em seguida, utilizamos a função select() para imprimir apenas os nomes dos países e as variáveis recém criadas.

pairsVec = function(x,y){
  res = NULL;
  for(i in 1:length(x)){
    if(x[i] == 1 && y[i] == 1)
      res[i] <- 1
    else
      res[i] <- 0
  }
  res
}

mutate(flags, goldGrenn = pairsVec(gold, green), redWhite = pairsVec(red, white)) %>% select(.,name,goldGrenn:redWhite) %>% kable(.,align='l')
name goldGrenn redWhite
Indonesia 0 1
South-Korea 0 1
Luxembourg 0 1
Peru 0 1
Kenya 0 1

Caso haja interesse em aplicar uma função a todas as colunas, utiliza-se a função mutate_each(). No exemplo, aplicamos a média acumulada a cada uma das variáveis.

# aplicar uma função "janela" para cada uma das variáveis (atributos)
mutate_each(flags, funs(cummean))
## # A tibble: 5 × 30
##       name landmass     zone      area population language religion  bars
##      <dbl>    <dbl>    <dbl>     <dbl>      <dbl>    <dbl>    <dbl> <dbl>
## 1  83.0000 6.000000 2.000000 1904.0000  157.00000     10.0     2.00  0.00
## 2 119.0000 5.500000 1.500000 1001.5000   98.00000     10.0     4.50  0.00
## 3 113.6667 4.666667 1.333333  668.6667   65.33333      8.0     3.00  0.00
## 4 119.5000 4.000000 1.750000  822.7500   52.50000      6.5     2.25  0.75
## 5 114.4000 4.000000 1.600000  774.8000   45.40000      7.2     2.80  0.60
## # ... with 22 more variables: stripes <dbl>, colours <dbl>, red <dbl>,
## #   green <dbl>, blue <dbl>, gold <dbl>, white <dbl>, black <dbl>,
## #   orange <dbl>, mainhue <dbl>, circles <dbl>, crosses <dbl>,
## #   saltires <dbl>, quarters <dbl>, sunstars <dbl>, crescent <dbl>,
## #   triangle <dbl>, icon <dbl>, animate <dbl>, text <dbl>, topleft <dbl>,
## #   botright <dbl>

Combinando as funções ddply() e mutate(), podemos criar variáveis por grupos definidos anteriormente. Podemos, por exemplo, saber a porcentagem de países que possuem verde e amarelo na bandeira fazendo a média por grupos de modo bastante sintético. O próximo comando foi feito para todo com conjunto de dados (variável fullData()):

fullData %>% mutate(., goldGrenn = pairsVec(gold, green)) %>% ddply(., ~landmass, summarise, rate = mean(goldGrenn)) %>% kable(.,align='l')
landmass rate
1 0.3870968
2 0.4117647
3 0.1142857
4 0.4230769
5 0.0769231
6 0.2000000

O pacote dplyr também oferece recursos para particionar/agregar dados. Isto é feito por meio das funções select() e slice(). A primeira seleciona variáveis, enquanto a segunda seleciona observações.

Exemplo: selecionar as variáveis entre “name” e “language” (inclusive) e as 3 primeiras observações. Note que a função bind_rows() une as linhas de f1 e f2, formando o mesmo conjunto de dados original, como pode ser confirmado pela função setequal()

# selecionar as variáveis entre "name" e "language" (inclusive) e as 10 primeiras observações
f1 = flags %>% slice(., 1:3)
f2 = flags  %>% slice(., 4:5)

Note que a função bind_rows() une as linhas de f1 e f2, formando o mesmo conjunto de dados original, como pode ser confirmado pela função setequal()

# agora vamos juntar os dois:
setequal(bind_rows(f1,f2), flags)
## TRUE

Conclusão

Estes são alguns dos comandos do pacote dplyr. Este tutorial não é exaustivo, apenas pretende motivar o uso do pacote como uma boa solução para manipulação de conjuntos de dados. Há muita documentação na internet sobre o pacote, incluindo resumos rápidos.