Trilhas
/
R
R
/
Programa
/
Dataframes
Da

Dataframes em R

1 exercício

Sobre Dataframes

Em outras partes do programa, já vimos vários tipos de dados com características diferentes.

  • Os vetores atômicos são unidimensionais e homogêneos em tipo.
  • As listas são unidimensionais e seus elementos podem ser de tipos heterogêneos.
  • As matrizes e arrays são multidimensionais e homogêneas.

Este conceito vai apresentar formas de armazenar dados multidimensionais e heterogêneos. Na prática, a maior parte dos dados do mundo real é assim, então agora estamos chegando ao cerne de como R é usado (na maioria das vezes) na prática.

Variantes de dataframes

Ao longo das décadas, R adicionou vários tipos de dados para lidar com dados tabulares.

Este programa vai focar principalmente em tibbles, mas é útil conhecer algumas alternativas.

O data.table

Tibbles (descritos abaixo) e data.tables são duas tentativas respeitadas de melhorar o tradicional data.frame do R base. Inevitavelmente há muita discussão sobre qual dos dois é "melhor". Talvez haja certo consenso em torno dos seguintes pontos (mesmo que sejam criticados por simplismo).

  • O tibble é otimizado principalmente para facilidade de uso e integração com o ecossistema Tidyverse.
  • O data.table é otimizado principalmente para poder bruto e escalabilidade, especialmente ao trabalhar com conjuntos de dados muito grandes.

De qualquer forma, o data.table não está disponível no Exercism, então ele é mencionado aqui apenas para fins de completude.

O data.frame

No R base, um data.frame é uma list de vectors de mesmo comprimento. Podemos pensar nele como uma tabela retangular de dados, em que cada coluna é homogênea, mas cada linha pode conter (e normalmente contém) tipos diferentes de dados.

Um exemplo para ilustrar isso:

# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)

# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#>   languages created has.syllabus
#> 1   Fortran    1957        FALSE
#> 2         R    1993         TRUE
#> 3    Python    1991         TRUE
#> 4     Julia    2012         TRUE

# look at the structure
str(df)
#> 'data.frame':	4 obs. of  3 variables:
 #> $ languages   : chr  "Fortran" "R" "Python" "Julia"
 #> $ created     : num  1957 1993 1991 2012
 #> $ has.syllabus: logi  FALSE TRUE TRUE TRUE

Temos uma coluna de strings de caracteres, uma coluna de números e uma coluna de valores Boolean. Quando ampliado, esse é um jeito intuitivo de representar muitas coleções de dados do mundo real.

O tibble

O design do data.frame é antigo.

Décadas de experiência, somadas a mudanças na forma como R é usado, levaram a um redesenho, criando uma alternativa modernizada no Tidyverse: os tibbles.

Comparados ao R base, os tibbles têm:

  • Padrões diferentes, para reduzir problemas comuns.
  • Menos disposição para forçar a conversão de tipos de dados na entrada.
  • Mensagens de erro mais numerosas e mais claras.
  • Formatos de exibição diferentes e, geralmente, melhores.

Em resumo, um tibble se propõe a "fazer menos e reclamar mais", o que também é descrito como "preguiçoso e mal-humorado".

Os tipos geralmente são intercambiáveis: qualquer função que aceite um data.frame também aceita um tibble, e vice-versa.

As conversões entre os tipos são fáceis, com as_tibble(df) e as.data.frame(tbl).

Para código novo, usar tibbles provavelmente vai ajudar você a escrever um código mais robusto. No entanto, código e dados legados são muito abundantes no mundo do R, então o data.frame provavelmente vai continuar comum por muito tempo.

# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE      
  
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#>  $ languages   : chr [1:4] "Fortran" "R" "Python" "Julia"
#>  $ created     : num [1:4] 1957 1993 1991 2012
#>  $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE

Repare no formato de impressão padrão: a linha de comentário com as dimensões é impressa automaticamente, e os tipos das colunas também são exibidos.

Trabalhando com tibbles

Os tibbles são parte central do Tidyverse, então adicione-os com library(tibble) ou library(tidyverse).

A documentação é bem extensa, no estilo do Tidyverse:

Criando um tibble

Da forma mais simples, podemos usar a função tibble() para juntar vetores de colunas, como fazemos com data.frame(). Um exemplo disso foi mostrado em uma seção anterior.

Se for mais conveniente inserir os valores linha por linha, a função correspondente é tribble().

tbl_r <- tribble(
  # column names marked with tilde prefix
  ~languages, ~created, ~has.syllabus,
  "Fortran", 1957, FALSE,
  "R", 1993, TRUE,
  "Python", 1991, TRUE,
  "Julia", 2012, TRUE
)

tbl_r
# A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE    

Na prática, existem dezenas de formas de criar tibbles, já que eles são o formato de saída padrão de uma grande variedade de funções do Tidyverse. Vamos voltar a isso em um conceito futuro.

Manipulando um tibble

O conceito de Programação Funcional comentou sobre a biblioteca purrr para manipular vetores e listas (estruturas de dados unidimensionais).

Para dataframes (tradicionais ou tibbles), a biblioteca correspondente é dplyr.

Já apresentamos o dplyr antes, no conceito de Switch. Lá usamos apenas algumas funções utilitárias, mas agora podemos começar a explorar o resto dessa grande biblioteca.

Selecionando subconjuntos

Dataframes, incluindo tibbles, podem ser tratados como listas de vetores de colunas, então a indexação de lista recupera uma coluna específica.

tbl
# A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE    

tbl$created
#> [1] 1957 1993 1991 2012

Um dataframe também pode ser indexado no estilo de matriz.

tbl[c(2, 4), 1:2]
  # A tibble: 2 × 2
#>   languages created
#>   <chr>       <dbl>
#> 1 R            1993
#> 2 Julia        2012

No R moderno com o ecossistema Tidyverse, as funções do dplyr costumam ser mais flexíveis e convenientes, e serão o foco do resto deste conceito.

Operações por coluna

Obtenha uma única coluna com pull() usando o nome ou o número sequencial (números negativos contam da direita para a esquerda).

tbl |> pull(created)
#> [1] 1957 1993 1991 2012

Esse é o mesmo resultado de tbl$created, mas usando uma função que se encaixa bem em pipelines.

Para obter várias colunas, a função apropriada é select(), que é bastante versátil. Obtenha (ou descarte) colunas com base em propriedades do nome ou do tipo delas.

# Range with position and/or name
tbl |> select(1:created)
  # A tibble: 4 × 2
#>   languages created
#>   <chr>       <dbl>
#> 1 Fortran      1957
#> 2 R            1993
#> 3 Python       1991
#> 4 Julia        2012

# Exclude a column
tbl |> select(!created)
  # A tibble: 4 × 2
#>   languages has.syllabus
#>   <chr>     <lgl>       
#> 1 Fortran   FALSE       
#> 2 R         TRUE        
#> 3 Python    TRUE        
#> 4 Julia     TRUE        

# Use type of column
tbl |> select(where(is.numeric))
  # A tibble: 4 × 1
#>   created
#>     <dbl>
#> 1    1957
#> 2    1993
#> 3    1991
#> 4    2012

Vários critérios são permitidos, usando os operadores Boolean &, | e ! (e, ou, não).

Nomes de coluna que são identificadores válidos em R não precisam de aspas dentro de um select(). Nomes inválidos (por exemplo, os que contêm espaços) podem ser colocados entre crases, embora renomeá-los talvez seja melhor.

A função select() pode trabalhar com uma série de funções auxiliares para escolher nomes de coluna: starts_with, contains, num_range e várias outras. O matches permite correspondência completa com expressões regulares. Consulte a documentação para mais detalhes.

Esse poder todo parece um exagero com nosso dataframe de brinquedo de linguagens. Felizmente, o tibble starwars vem incluído no dplyr, o que nos dá algo maior para praticar.

# limit display to top 3 rows of non-list columns
starwars |> 
  select(!where(is.list)) |> 
  head(3)
  # A tibble: 3 × 11
#>   name           height  mass hair_color skin_color  eye_color birth_year sex   gender    homeworld species
#>   <chr>           <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>     <chr>     <chr>  
#> 1 Luke Skywalker    172    77 blond      fair        blue              19 male  masculine Tatooine  Human  
#> 2 C-3PO             167    75 NA         gold        yellow           112 none  masculine Tatooine  Droid  
#> 3 R2-D2              96    32 NA         white, blue red               33 none  masculine Naboo     Droid  

# pick a subset of columns
starwars |> 
  select(name | ends_with("color")) |> 
  head(5)
  # A tibble: 5 × 4
#>   name           hair_color skin_color  eye_color
#>   <chr>          <chr>      <chr>       <chr>    
#> 1 Luke Skywalker blond      fair        blue     
#> 2 C-3PO          NA         gold        yellow   
#> 3 R2-D2          NA         white, blue red      
#> 4 Darth Vader    none       white       yellow   
#> 5 Leia Organa    brown      light       brown    

Operações por linha

Note

É claro que o dplyr oferece formas poderosas de selecionar colunas pelo nome.

Será que dá para fazer algo parecido com nomes de linha?

Não! Os dataframes tradicionais do R podem ter nomes de linha, mas (depois de um histórico de bugs e problemas de desempenho) nomes de linha não são permitidos em tibbles.

Se você quiser nomes, coloque-os em uma coluna <chr> (normalmente a coluna 1), usada como qualquer outra coluna. Funções de importação como as_tibble() criam isso automaticamente ao importar dados com linhas nomeadas.

Se essa limitação de nomes de linha parece restritiva demais, lembre-se de que a maioria dos grandes sistemas de banco de dados trata tabelas do mesmo jeito: Oracle, SQL Server, PostgreSQL, MySQL...

Obtenha as linhas que atendem a algum critério com filter(), ou exclua-as com filter_out().

starwars |> 
  select(name:mass) |> 
  filter(between(height, 150, 165) & !is.na(mass))
  # A tibble: 4 × 3
#>   name               height  mass
#>   <chr>               <int> <dbl>
#> 1 Leia Organa           150    49
#> 2 Beru Whitesun Lars    165    75
#> 3 Nien Nunb             160    68
#> 4 Ben Quadinaros        163    65

Os critérios de filtro podem ser arbitrariamente complexos, mas sempre se baseiam no conteúdo das linhas.

Se os números das linhas forem conhecidos, podemos usar uma variedade de funções slice().

starwars |> 
  select(name | homeworld) |> 
  slice(20:25)
  # A tibble: 6 × 2
#>   name             homeworld
#>   <chr>            <chr>    
#> 1 Palpatine        Naboo    
#> 2 Boba Fett        Kamino   
#> 3 IG-88            NA       
#> 4 Bossk            Trandosha
#> 5 Lando Calrissian Socorro  
#> 6 Lobot            Bespin   

# random sample of rows
starwars |> 
  select(name | homeworld) |> 
  slice_sample(n = 4)
  # A tibble: 4 × 2
#>   name            homeworld
#>   <chr>           <chr>    
#> 1 Shaak Ti        Shili    
#> 2 Luminara Unduli Mirial   
#> 3 Grievous        Kalee    
#> 4 Palpatine       Naboo    

Para remover linhas duplicadas, use distinct().

Modificando um tibble

Primeira ressalva: o padrão copy-on-modify significa que o tibble original normalmente permanece inalterado.

A maioria das modificações é aplicada por coluna.

Os nomes das colunas podem ser alterados com rename(newname = oldname), ou com rename_with() para aplicar uma função. Um uso típico seria limpar nomes importados para facilitar o trabalho com eles no R, removendo espaços em branco e forçando um formato consistente para nomes relacionados.

Repare na sintaxe dentro de rename(). O conteúdo da coluna oldname é vinculado ao nome newname, daí a ordem.

A ordem das colunas pode ser alterada com relocate(). As colunas especificadas são movidas para as posições mais à esquerda por padrão, mas um argumento .before ou .after pode ser usado para um posicionamento mais preciso.

sw <- starwars |> select(name:species) |> slice(1:4)
sw
  # A tibble: 4 × 11
#>   name           height  mass hair_color skin_color  eye_color birth_year sex   gender    homeworld species
#>   <chr>           <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>     <chr>     <chr>  
#> 1 Luke Skywalker    172    77 blond      fair        blue            19   male  masculine Tatooine  Human  
#> 2 C-3PO             167    75 NA         gold        yellow         112   none  masculine Tatooine  Droid  
#> 3 R2-D2              96    32 NA         white, blue red             33   none  masculine Naboo     Droid  
#> 4 Darth Vader       202   136 none       white       yellow          41.9 male  masculine Tatooine  Human  

sw |> relocate(c(species, homeworld), .after = name)
  # A tibble: 4 × 11
#>   name           species homeworld height  mass hair_color skin_color  eye_color birth_year sex   gender   
#>   <chr>          <chr>   <chr>      <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>    
#> 1 Luke Skywalker Human   Tatooine     172    77 blond      fair        blue            19   male  masculine
#> 2 C-3PO          Droid   Tatooine     167    75 NA         gold        yellow         112   none  masculine
#> 3 R2-D2          Droid   Naboo         96    32 NA         white, blue red             33   none  masculine
#> 4 Darth Vader    Human   Tatooine     202   136 none       white       yellow          41.9 male  masculine

Para mudanças maiores, o mutate() permite:

  • Criar novas colunas que são funções de colunas existentes.
  • Substituir uma coluna existente, criando uma nova coluna com o mesmo nome.
  • Excluir uma coluna, definindo seu valor como NULL

É claro que o mutate() é poderoso, potencialmente confuso, e um motivo para ficarmos muito gratos pelo copy-on-modify.

Não há motivo óbvio para se importar com o índice de massa corporal dos personagens de Star Wars, mas só por via das dúvidas:

starwars |> 
  select(c(name, species, height, mass)) |> 
  mutate(BMI = mass / (height / 100)^2) |> 
  head(4)
  # A tibble: 4 × 5
#>   name           species height  mass   BMI
#>   <chr>          <chr>    <int> <dbl> <dbl>
#> 1 Luke Skywalker Human      172    77  26.0
#> 2 C-3PO          Droid      167    75  26.9
#> 3 R2-D2          Droid       96    32  34.7
#> 4 Darth Vader    Human      202   136  33.3

Quando você quer operar sobre um subconjunto das colunas com funções como mutate(), a sequência select() |> mutate() do exemplo acima é uma opção. Apenas as colunas selecionadas estarão no resultado.

Como alternativa, pode ser conveniente usar pick() dentro da chamada de mutate():

starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.

Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:

```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
    # A tibble: 4 × 15
#>   name         height  mass hair_color skin_color eye_color birth_year sex   gender homeworld species films vehicles
#>   <chr>         <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr>  <chr>     <chr>   <lis> <list>  
#> 1 Luke Skywal…    172    77 blond      fair       blue            19   male  mascu… Tatooine  Human   <chr> <chr>   
#> 2 C-3PO           167    75 NA         gold       yellow         112   none  mascu… Tatooine  Droid   <chr> <chr>   
#> 3 R2-D2            96    32 NA         white, bl… red             33   none  mascu… Naboo     Droid   <chr> <chr>   
#> 4 Darth Vader     202   136 none       white      yellow          41.9 male  mascu… Tatooine  Human   <chr> <chr>   
   # ℹ 2 more variables: starships <list>, BMI <dbl>

Apenas as colunas indicadas no pick são usadas na mutação, mas todas as colunas são retornadas.

Operações por linha são menos comuns ao modificar um único tibble (a junção de vários tibbles será discutida em um conceito posterior).

Uma exceção: arrange() ordena as linhas pelos valores de uma ou mais colunas.

tbl
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE  

tbl |> arrange(languages)
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 Julia        2012 TRUE        
#> 3 Python       1991 TRUE        
#> 4 R            1993 TRUE     

Resumo

Dataframes, tradicionais ou tibbles, são centrais para a forma como o R moderno costuma ser usado.

A maioria das funções do Tidyverse (não só as do dplyr) recebe tibbles como entrada e/ou os produz como saída.

Este conceito apresentou apenas uma breve introdução, arranhando mal a superfície do que é possível.

Conceitos posteriores vão discutir vários outros aspectos dos dataframes (dentro das limitações técnicas do Exercism).

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Dataframes