Noutras partes do currículo, já vimos vários tipos de dados com características diferentes.
Este Conceito vai abordar formas de guardar dados multidimensionais e heterogéneos. Na prática, a maioria dos dados do mundo real é assim, por isso estamos agora a chegar ao cerne da forma como o R é (sobretudo) usado na prática.
Ao longo das décadas, o R foi adicionando vários tipos de dados para lidar com dados tabulares.
Este currículo vai centrar-se sobretudo em tibbles, mas é útil conhecer as alternativas.
data.frame
No R Base, um data.frame é uma list de vectors de igual comprimento.
Podemos pensar nele como uma tabela retangular de dados, em que cada coluna é homogénea, mas cada linha pode (e costuma) conter 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
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.
tibble
O design do data.frame é antigo.
Décadas de experiência, a par da mudança dos padrões de utilização do R, levaram a uma reformulação, criando uma alternativa modernizada no Tidyverse: os tibbles.
Em comparação com o R Base, os tibbles têm:
Em suma, um tibble procura "fazer menos e queixar-se mais", também descrito como "preguiçoso e rabugento".
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 vai ajudar-te a criar código mais robusto.
No entanto, o código e os dados legados são muito abundantes 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
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.
Os tibbles são uma parte central do Tidyverse, por isso adiciona-os com library(tibble) ou library(tidyverse).
Da forma mais simples, podemos usar a função tibble() para juntar vetores coluna, tal como em data.frame().
Vimos um exemplo disto numa secção anterior.
Se for mais cómodo introduzir os valores linha a linha, a função correspondente é tribble().
Na prática, há dezenas de formas de criar tibbles, pois são o formato de saída predefinido de uma grande variedade de funções do Tidyverse.
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 correspondente a usar é o dplyr.
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
Também se pode indexar um dataframe 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 cómodas e serão o foco do resto deste Conceito.
Obtém uma única coluna com pull(), indicando o nome ou o número sequencial (números negativos para contar 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 é extremamente versátil.
Obtém (ou descarta) colunas com base em propriedades do seu nome ou do seu 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
# Use type of column
tbl |> select(where(is.numeric))
# A tibble: 4 × 1
#> created
#> <dbl>
#> 1 1957
#> 2 1993
#> 3 1991
#> 4 2012
É possível usar vários critérios, com os operadores Boolean &, | e ! (and, or not).
Tanto poder parece um exagero com o nosso dataframe de brinquedo de linguagens.
Felizmente, o tibble starwars está incluído no dplyr, o que nos dá algo maior para praticar.
# 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
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
Primeiro aviso: o comportamento predefinido copy-on-modify faz com que o tibble original normalmente fique 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.
Repara na sintaxe dentro de rename().
O conteúdo da coluna oldname é associado ao nome newname, daí a ordem.
A ordem das colunas pode ser alterada com relocate().
As colunas especificadas são movidas para a posição mais à esquerda por predefinição, 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, o mutate() permite-te:
NULL.Claramente, o mutate() é poderoso, potencialmente confuso e um motivo para estarmos muito gratos pelo copy-on-modify.
Não há nenhuma razão óbvia para nos preocuparmos com o índice de massa corporal das personagens de Star Wars, mas, só 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
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: 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
Um grande pomar de cerejeiras tem geralmente um pequeno inventário de árvores excedentes que gostaria de oferecer como madeira. Isto ajuda a manter o pomar saudável e, ao mesmo tempo, proporciona receita extra. Pediram-te que ajudasses a programar algumas funcionalidades para ajudar no processo de seleção de árvores quando um potencial cliente telefona.
A biblioteca incorporada datasets tem muitos conjuntos de dados de exemplo, e este exercício usa o conjunto de dados trees.
Para chamar o conjunto de dados, basta usar o nome trees.
library(datasets) # loading is optional
trees |> head(3)
#> A data.frame: 3 × 3
#> Girth Height Volume
#> <dbl> <dbl> <dbl>
#> 1 8.3 70 10.3
#> 2 8.6 65 10.3
#> 3 8.8 63 10.2
Repara nas três colunas: Girth, Height e Volume.
O conjunto de dados está inicialmente na forma de um data.frame, mas podes querer trabalhar com ele como um tibble.
Infelizmente, houve um erro administrativo que nunca foi corrigido e uma coluna do conjunto de dados chama-se Girth, mas na verdade é o Diameter.
Define um data.frame/tibble chamado tree_data que corrige o nome da coluna Girth em trees para Diameter.
head(tree_data, 3)
#> A tibble: 3 × 3
#> Diameter Height Volume
#> <dbl> <dbl> <dbl>
#> 8.3 70 10.3
#> 8.6 65 10.3
#> 8.8 63 10.2
O perímetro (ou seja, a circunferência) continua a ser uma medição útil, e o mesmo se passa com o peso da madeira.
O perímetro calcula-se facilmente com πD, em que D é o diâmetro.
O peso de toda a madeira pode ser calculado por ρV, em que ρ = 35 é a densidade da madeira de cerejeira e V é o volume da madeira.
Define a função girth_n_weight(data, rnd_digits), que recebe um dataframe e argumentos numéricos.
A função deve devolver um novo dataframe com as duas colunas adicionais: Girth e Weight, arredondadas ao número de algarismos apropriado indicado por rnd_digits.
girth_n_weight(tree_data, 1) |> head(3)
#> A tibble: 3 × 5
#> Diameter Height Volume Girth Weight
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 8.3 70 10.3 26.1 360.5
#> 8.6 65 10.3 27.0 360.5
#> 8.8 63 10.2 27.6 357.0
Para cada potencial cliente, o pomar guarda uma cópia especial do conjunto de dados para ajudar na venda.
Este conjunto de dados tem as colunas mais relevantes, Weight e Height, movidas para a frente e está ordenado por Weight.
Define a função orchard_copy(data, important_cols), que recebe um dataframe como valor de entrada.
Deve devolver um novo dataframe com Weight e Height movidas para a frente e as linhas ordenadas por Weight.
tree_data |> girth_n_weight() |> orchard_copy() |> head(3)
#> A tibble: 3 × 5
#> Weight Height Diameter Volume Girth
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 357 63 9 10 28
#> 360 70 8 10 26
#> 360 65 9 10 27
Cada potencial cliente recebe uma versão pessoal do conjunto de dados, com base nas suas preferências. Para isso, o cliente pode especificar: a altura mínima, a altura máxima e o peso máximo de qualquer árvore individual.
Define a função customer_copy(data, min_height, max_height, max_weight).
Devolve uma cópia do cliente do dataframe com as colunas Height, Weight, Diameter e Girth e com as linhas que estão dentro dos limites.
tree_data |>
girth_n_weight(1) |>
orchard_copy() |>
customer_copy(65, 75, 1500) |>
head(3)
#> A tibble: 3 × 4
#> Height Weight Diameter Girth
#> <dbl> <dbl> <dbl> <dbl>
#> 70 360.5 8.3 26.1
#> 65 360.5 8.6 27.0
#> 66 546.0 11.0 34.6
Inscreve-te no Exercism para aprenderes e dominares R com 21 conceitos111 exercícios, e mentoria humana real, tudo grátis.