Percursos
/
R
R
/
Programa
/
Dataframes
Da

Dataframes em R

1 exercício

Sobre Dataframes

Noutras 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 termos de tipo.
  • As listas são unidimensionais e os elementos podem ser de tipos heterogéneos.
  • As matrizes e arrays são multidimensionais e homogéneos.

Neste conceito, vamos ver formas de armazenar dados multidimensionais e heterogéneos. Na prática, a maior parte dos dados do mundo real é assim, por isso estamos agora a chegar ao cerne de como o R é (sobretudo) usado na prática.

Variantes de dataframe

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

Este programa vai focar-se sobretudo nos tibbles, mas é útil conhecer algumas alternativas.

O data.table

Os tibbles (descritos abaixo) e os data.tables são duas tentativas muito respeitadas de melhorar o tradicional data.frame do R base. Há inevitavelmente muita discussão sobre qual deles é "melhor". Talvez haja algum consenso em torno dos seguintes pontos (mesmo que sejam criticados por serem simplistas).

  • O tibble está otimizado sobretudo para a facilidade de utilização e para a integração com o ecossistema Tidyverse.
  • O data.table está otimizado sobretudo para a potência bruta e a escalabilidade, especialmente ao trabalhar com conjuntos de dados muito grandes.

De qualquer forma, o data.table não está disponível no Exercism, por isso é mencionado aqui apenas por uma questão de completude.

O data.frame

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

Um exemplo para ilustrar isto:

# 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 carateres, uma coluna de números e uma coluna de valores Boolean. Quando aumentarmos a escala, esta é uma forma intuitiva de representar muitos conjuntos de dados do mundo real.

O tibble

O modelo do data.frame é antigo.

Décadas de experiência, a par da mudança nos padrões de utilização do R, levaram a uma reformulação que criou uma alternativa modernizada no Tidyverse: os tibbles.

Comparados com o R base, os tibbles têm:

  • Predefinições diferentes, para reduzir problemas comuns.
  • Menor tendência para coagir tipos de dados durante a entrada.
  • Mensagens de erro mais numerosas e mais claras.
  • Formatos de apresentação diferentes e, normalmente, melhores.

Em suma, um tibble procura "fazer menos e queixar-se mais", também descrito como "preguiçoso e mal-humorado".

Os tipos são geralmente 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 trabalho novo, usar tibbles provavelmente ajuda-te a escrever código mais robusto. No entanto, há imenso código e dados legados no mundo do R, por isso é provável que o data.frame continue a ser comum durante 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

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

Trabalhar com tibbles

Os tibbles são uma parte central do Tidyverse, por isso carrega-os com library(tibble) ou library(tidyverse).

A documentação é bastante extensa, ao estilo do Tidyverse:

Criar um tibble

Da forma mais simples, podemos usar a função tibble() para juntar vetores coluna, tal como em data.frame(). Um exemplo disto foi mostrado numa secção anterior.

Se for mais conveniente introduzir os valores linha a 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, há dezenas de formas de criar tibbles, uma vez que são o formato de saída predefinido de uma grande variedade de funções do Tidyverse. Voltaremos a isto num conceito futuro.

Manipular um tibble

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

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

Já falámos do dplyr anteriormente, no conceito Switch. Nesse conceito usámos apenas algumas funções utilitárias, mas agora podemos começar a explorar o resto desta grande biblioteca.

Subconjuntos

Os dataframes, incluindo os tibbles, podem ser tratados como listas de vetores coluna, por isso a indexação de listas 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 com indexação ao estilo das matrizes.

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 são geralmente mais flexíveis e convenientes, e serão o foco do resto deste conceito.

Operações por coluna

Obtém uma única coluna com pull(), indicando o nome ou o número sequencial (números negativos contam da direita para a esquerda).

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

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

Para obter várias colunas, a função adequada é select(), que é muito versátil. Obtém (ou remove) colunas com base em propriedades do seu nome ou tipo.

# 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

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

Os nomes de coluna que são identificadores válidos em R não precisam de aspas dentro de um select(). Os nomes inválidos (por exemplo, os que incluem espaços) podem ser envolvidos em acentos graves, embora talvez seja melhor renomeá-los.

A função select() pode trabalhar com várias funções auxiliares para escolher nomes de colunas: starts_with, contains, num_range e várias outras. O matches permite correspondências completas com expressões regulares. Consulta a documentação para mais detalhes.

Tanto poder parece um exagero com o nosso dataframe de brinquedo sobre linguagens. Felizmente, o tibble starwars está 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

Claramente, o dplyr oferece formas poderosas de selecionar colunas pelo nome.

Será que podemos fazer algo semelhante com os nomes das linhas?

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

Se quiseres nomes, coloca-os numa coluna <chr> (normalmente a coluna 1), usada como qualquer outra coluna. Funções de importação como as_tibble() criam-na automaticamente ao importar dados com linhas nomeadas.

Se esta limitação dos nomes de linhas parecer estranhamente restritiva, lembra-te de que a maioria dos grandes sistemas de bases de dados trata as tabelas da mesma forma: Oracle, SQL Server, PostgreSQL, MySQL...

Obtém as linhas que correspondem a um critério com filter(), ou exclui-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 filtragem podem ser arbitrariamente complexos, mas baseiam-se sempre no conteúdo das linhas.

Se soubermos os números das linhas, podemos usar várias 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, usa distinct().

Modificar um tibble

Primeira ressalva: o comportamento predefinido de copy-on-modify faz com que o tibble original permaneça normalmente inalterado.

A maioria das modificações é aplicada coluna a 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 é limpar os nomes importados para os tornar mais fáceis de trabalhar em R, removendo espaços em branco e impondo um formato consistente para nomes relacionados.

Repara na sintaxe dentro de rename(). O conteúdo da coluna oldname fica ligado ao nome newname, daí a ordem.

A ordem das colunas pode ser alterada com relocate(). Por predefinição, a(s) coluna(s) indicada(s) são movidas para a(s) posição(ões) mais à esquerda, mas pode usar-se um argumento .before ou .after 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 alterações maiores, mutate() permite-te:

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

É claro que mutate() é poderoso e potencialmente confuso, e uma razão para estarmos muito gratos pelo copy-on-modify.

Não há razão óbvia para nos preocuparmos com o índice de massa corporal das personagens de Star Wars, mas, por precaução:

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 queres operar sobre um subconjunto de colunas com funções como mutate(), a sequência select() |> mutate() do exemplo acima é uma opção. Só as colunas selecionadas estarão no resultado.

Em alternativa, pode ser conveniente usar pick() dentro da chamada a 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>

Só as colunas passadas a pick são usadas na mutação, mas todas as colunas são devolvidas.

As operações por linha são menos comuns para modificar um único tibble (a junção de vários tibbles será abordada num 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

Os dataframes, tradicionais ou tibbles, são centrais na forma como o R moderno é normalmente usado.

A maioria das funções do Tidyverse (não só as do dplyr) recebe tibbles como valores de entrada e/ou cria tibbles como resultado.

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

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

Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Dataframes