Em outras partes do programa, já vimos vários tipos de dados com características diferentes.
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.
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.
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).
tibble é otimizado principalmente para facilidade de uso e integração com o ecossistema Tidyverse.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.
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.
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:
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.
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:
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.
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.
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.
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
É 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().
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:
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
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).