7  As Classes de Armazenamento

Na prática com o R, logo verás que as classes básicas são armazenadas em estruturas de dados mais complexas, tal como tabelas (dataframes) e listas. Isso organiza e facilita o trabalho ao agregar todos os dados em um objeto apenas. Por exemplo, imagine realizar um estudo sobre as 63 ações que compõem o índice Ibovespa, onde a base de dados é composta por preços e volumes negociados ao longo de um ano. Caso fôssemos criar um vetor numérico de preços e de volumes para cada ação, teríamos uma quantidade de 126 objetos para lidar no nosso ambiente do R. Apesar de ser possível trabalhar dessa forma, o código resultante seria desorganizado, difícil de entender e passível de uma série de erros.

Uma maneira mais simples de organizar os nossos dados é criar um objeto com o nome my_data e alocar todos os preços e volumes ali. Todas as informações necessárias para executar a pesquisa estariam nesse objeto, facilitando a importação e exportação dos dados. Esses objetos que armazenam outros objetos de classe básica constituem a classe de estrutura de dados. Nessa classificação, estão incluídas tabelas (dataframes) e listas (list).

7.1 Dataframes

Traduzindo para o português, dataframe significa “estrutura ou organização de dados”. Grosso modo, um objeto da classe dataframe nada mais é do que uma tabela com linhas e colunas. Sem dúvida, o dataframe é o principal objeto utilizado no trabalho com o R e o mais importante de se estudar. Dados externos são geralmente importados no formato de tabelas. É na manipulação de tabelas que gastará a maior parte do tempo realizando a sua análise. Internamente, um dataframe é um tipo especial de lista, onde cada coluna é um vetor atômico com o mesmo número de elementos. Podemos organizar em um dataframe uma coluna com dados de texto, e outra com números, por exemplo.

Note que o formato tabular força a sincronização dos dados no sentido de linhas, isto é, cada caso de cada variável deve ser pareado com casos de outras variáveis. Apesar de simples, esse tipo de estruturação de dados é intuitiva e pode acomodar uma variedade de informações. Cada acréscimo de um pedaço novo de dados incrementa as linhas e cada novo tipo de informação, ou variável, incrementa as colunas da tabela.

Um dos pontos positivos na utilização do dataframe para a acomodação de dados é que funções de diferentes pacotes irão funcionar a partir dessa classe de objetos. Por exemplo, o pacote de manipulação de dados {dplyr} (Wickham et al. 2023), assim como o pacote de criação de figuras ggplot2, funcionam a partir de um dataframe. Esse objeto, portanto, está no centro de uma série de funcionalidades do R e, sem dúvida, é uma classe de objeto extremamente importante para aprender a utilizar corretamente.

O objeto dataframe é uma das classes nativas do R e vem implementado no pacote {base} (R Core Team 2023). Entretanto, o universe {tidyverse} (Wickham 2023) oferece sua própria versão de um dataframe, chamada tibble, a qual é utilizada sistematicamente em todos pacotes do {tidyverse} (Wickham 2023). A conversão de um dataframe para tibble é interna e automática. O tibble possui propriedades mais flexíveis que dataframes nativos, facilitando de forma significativa o seu uso. Seguindo a nossa preferência para o {tidyverse} (Wickham 2023), a partir de agora iremos utilizar tibbles como representantes de dataframes.

7.1.1 Criando dataframes

A criação de um dataframe do tipo tibble ocorre a partir da função dplyr::tibble() . Note que a criação de um dataframe nativo ocorre com a função data.frame() , enquanto a criação do tibble parte da função tibble::tibble() ou dplyr::tibble() . Para manter o código mais limpo, iremos dar preferência a dplyr::tibble() e utilizar o nome dataframe para se referir a um tibble. Veja o exemplo a seguir, onde criamos uma tabela correspondente a dados financeiros de diferentes ações.

# set tickers
ticker <- c(rep('ABEV3',4),
            rep('BBAS3', 4),
            rep('BBDC3', 4))

# set dates
ref_date <- as.Date(rep(c('2010-01-01', '2010-01-04',
                          '2010-01-05', '2010-01-06'),
                        3) )

# set prices
price <- c(736.67, 764.14, 768.63, 776.47,
           59.4  , 59.8  , 59.2  , 59.28,
           29.81 , 30.82 , 30.38 , 30.20)

# create tibble/dataframe
my_df <- tibble::tibble(ticker, ref_date , price)

# print it
print(my_df)
R> # A tibble: 12 × 3
R>    ticker ref_date   price
R>    <chr>  <date>     <dbl>
R>  1 ABEV3  2010-01-01 737. 
R>  2 ABEV3  2010-01-04 764. 
R>  3 ABEV3  2010-01-05 769. 
R>  4 ABEV3  2010-01-06 776. 
R>  5 BBAS3  2010-01-01  59.4
R>  6 BBAS3  2010-01-04  59.8
R>  7 BBAS3  2010-01-05  59.2
R>  8 BBAS3  2010-01-06  59.3
R>  9 BBDC3  2010-01-01  29.8
R> 10 BBDC3  2010-01-04  30.8
R> 11 BBDC3  2010-01-05  30.4
R> 12 BBDC3  2010-01-06  30.2

Observe que utilizamos a função rep() para replicar e facilitar a criação dos dados do dataframe anterior. Assim, não é necessário repetir os valores múltiplas vezes. Destaca-se que, no uso dos dataframes, podemos salvar todos os nossos dados em um único objeto, facilitando o acesso e a organização do código resultante.

Dica

O conteúdo de dataframes também pode ser visualizado no próprio RStudio. Para isso, basta clicar no nome do objeto na aba environment, canto superior direito da tela. Após isso, um visualizador aparecerá na tela principal do programa. Essa operação é nada mais que uma chamada a função View() . Portanto, poderíamos visualizar o dataframe anterior executando o comando View(my_df).

7.1.2 Inspecionando um dataframe

Após a criação do dataframe, o segundo passo é conhecer o seu conteúdo. Particularmente, é importante tomar conhecimento dos seguintes itens, em ordem de importância:

Número de linhas e colunas
O número de linhas e colunas da tabela resultante indicam se a operação de importação foi executada corretamente. Caso os valores forem diferentes do esperado, deve-se checar o arquivo de importação dos dados e se as opções de importação foram corretamente especificadas.
Nomes das colunas
É importante que a tabela importada tenha nomes que façam sentido e que sejam fáceis de acessar. Portanto, o segundo passo na inspeção de um dataframe é analisar os nomes das colunas e seus respectivos conteúdos. Confirme que cada coluna realmente apresenta um nome intuitivo e relacionado ao problema.
Classes das colunas
Cada coluna de um dataframe tem sua própria classe. É de suma importância que as classes dos dados estejam corretamente especificadas. Caso contrário, operações futuras podem resultar em um erro. Por exemplo, caso um vetor de valores numéricos seja importado com a classe de texto (character), qualquer operação matemática nesse vetor irá resultar em um erro no R.
Existência de dados omissos (NA)
Devemos também verificar o número de valores NA (not available) nas diferentes colunas. Sempre que você encontrar uma grande proporção de valores NA na tabela importada, você deve descobrir o que está acontecendo e se a informação está sendo importada corretamente. Conforme mencionado na Seção 6.6.2, os valores NA são contagiosos e transformarão qualquer objeto que interagir com um NA, também se tornará um NA.

Uma das funções mais recomendadas para se familiarizar com um dataframe é dplyr::glimpse() . Essa mostra na tela o nome e a classe das colunas, além do número de linhas/colunas. Abusamos dessa função nos capítulos anteriores. Veja um exemplo simples a seguir:

# check content of my_df
dplyr::glimpse(my_df)
R> Rows: 12
R> Columns: 3
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS…
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59…

Em muitas situações, o uso de dplyr::glimpse() é suficiente para entender se o processo de importação de dados ocorreu de forma satisfatória. Porém, uma análise mais profunda é entender qual a variação de cada coluna nos dados importados. Aqui entra o papel da função summary() :

# check variation my_df
summary(my_df)
R>     ticker             ref_date              price       
R>  Length:12          Min.   :2010-01-01   Min.   : 29.81  
R>  Class :character   1st Qu.:2010-01-03   1st Qu.: 30.71  
R>  Mode  :character   Median :2010-01-04   Median : 59.34  
R>                     Mean   :2010-01-04   Mean   :283.73  
R>                     3rd Qu.:2010-01-05   3rd Qu.:743.54  
R>                     Max.   :2010-01-06   Max.   :776.47

Note que summary() interpreta cada coluna de forma diferente. Para o primeiro caso, coluna ticker, mostra apenas o tamanho do vetor. No caso de datas e valores numéricos, essa apresenta o máximo, mínimo, mediana e quartis. Por exemplo, uma observação extrema (outlier) poderia ser facilmente identificada na análise da saída textual de summary() .

Uma alternativa moderna para summary() é skimr::skim() , que fornece mais detalhes sobre os dados:

# Check content of my_df
skimr::skim(my_df)

Você não só obtém as classes das colunas, mas também mais informações sobre as diferentes classes de dados:

  • Para a coluna tickers, você obtém o número de casos ausentes, valores únicos e mais;
  • Para a coluna dates, você obtém o mínimo e o máximo de dados, bem como o número de datas disponíveis;
  • Para colunas de valores numéricos, você obtém média, desvio padrão e quantis e mais;

Embora não seja suficiente, uma simples chamada a skimr::skim() pode fornecer uma grande quantidade de informações sobre os dados sendo importados.

O hábito de inspeção

Toda vez que se deparar com um novo dataframe no R, pegue o hábito de verificar o seu conteúdo com funções dplyr::glimpse() e skimr::skim() . Assim, poderá perceber problemas de importação e/ou conteúdo dos arquivos lidos. Com experiência irás perceber que muitos erros futuros em código podem ser sanados por uma simples inspeção das tabelas importadas.

7.1.3 Operador de pipeline

O operador de pipeline, ou sequenciamento em tradução livre, é uma ferramenta fundamental na análise de dados com R. Resumidamente, ele permite que operações de dados sejam realizadas sequencialmente e de forma modular, aumentando a legibilidade e a facilidade de manutenção do código resultante. O operador é amplamente utilizado no pacote {tidyverse} (Wickham 2023) e foi proposto pela primeira vez no pacote r cite_pkg(“magrittr”) com o símbolo %>%. Recentemente, na versão 4.1 do R, lançada em 18 de maio de 2021, um novo operador pipe (nativo) foi introduzido (|>), com grande aprovação por parte da comunidade.

Para explicar melhor o seu uso e benefício, imagine uma situação onde temos três funções para aplicar nos dados salvos em um dataframe. Cada função depende da saída de outra função, isto é, estamos manipulando os dados por etapas. Usando o operador de pipeline, podemos escrever o procedimento de manipulação dataframe com o seguinte código:

my_tab <- my_df |>
  fct1(arg1) |>
  fct2(arg2) |>
  fct3(arg3)

Usamos símbolo |> no final de cada linha para vincular as operações. As funções fct* são operações realizadas em cada etapa. O resultado de cada linha é passado para a próxima função de forma sequencial. Assim, não há necessidade de criar objetos intermediários. Veja a seguir duas formas alternativas de realizar a mesma operação sem o operador de pipeline:

# version 1
my_tab <- fct3(fct2(fct1(my_df,
                         arg1),
                    arg2),
               arg1)

# version 2
temp1 <- fct1(my_df, arg1)
temp2 <- fct2(temp1, arg2)
my_tab <- fct3(temp1, arg3)

Observe como as alternativas formam um código com estrutura estranha e passível a erros. Provavelmente não deves ter notado, mas ambos os códigos possuem erros de digitação. Para o primeiro, o último arg1 deveria ser arg3 e, no segundo, a função fct3 está usando o dataframe temp1 e não temp2. Este exemplo deixa claro como o uso de pipelines torna o código mais elegante e legível. A partir de agora iremos utilizar o operador |> de forma extensiva.

7.1.4 Acessando Colunas

Um objeto do tipo dataframe utiliza-se de diversos comandos e símbolos que também são usados em matrizes e listas. Para descobrir os nomes das colunas de um dataframe, temos duas funções: names() ou colnames() :

# check names of df
names(my_df)
R> [1] "ticker"   "ref_date" "price"
colnames(my_df)
R> [1] "ticker"   "ref_date" "price"

Ambas também podem ser usadas para modificar os nomes das colunas:

# set temp df
temp_df <- my_df

# check names
names(temp_df)
R> [1] "ticker"   "ref_date" "price"
# change names
names(temp_df) <- paste0('Col', 1:ncol(temp_df))

# check names
names(temp_df)
R> [1] "Col1" "Col2" "Col3"

Destaca-se que a forma de usar names() é bastante distinta das demais funções do R. Nesse caso, utilizamos a função ao lado esquerdo do símbolo de assign (<-). Internamente, o que estamos fazendo é definindo um atributo do objeto temp_df, o nome de suas colunas.

Para acessar uma determinada coluna, podemos utilizar o nome da mesma de diversas formas:

# isolate columns of df
my_ticker <- my_df$ticker
my_prices <- my_df[['price']]

# print contents
print(my_ticker)
R>  [1] "ABEV3" "ABEV3" "ABEV3" "ABEV3" "BBAS3" "BBAS3" "BBAS3"
R>  [8] "BBAS3" "BBDC3" "BBDC3" "BBDC3" "BBDC3"
print(my_prices)
R>  [1] 736.67 764.14 768.63 776.47  59.40  59.80  59.20  59.28
R>  [9]  29.81  30.82  30.38  30.20
Cuidado!

Toda vez que estiver acessando colunas de um dataframe, o resultado é um vetor atômico com os dados da coluna. Isso é importante saber pois as propriedades do objeto se modificam.

Note o uso do duplo colchetes ([[]]) para selecionar colunas. Vale apontar que, no R, um objeto da classe dataframe é representado internamente como uma lista, onde cada elemento é uma coluna. Isso é importante saber, pois alguns comandos de listas também funcionam para dataframes. Um exemplo é o uso de duplo colchetes ([[]]) para selecionar colunas por posição:

print(my_df[[2]])
R>  [1] "2010-01-01" "2010-01-04" "2010-01-05" "2010-01-06"
R>  [5] "2010-01-01" "2010-01-04" "2010-01-05" "2010-01-06"
R>  [9] "2010-01-01" "2010-01-04" "2010-01-05" "2010-01-06"

Para acessar linhas e colunas específicas de um dataframe, basta utilizar colchetes simples:

print(my_df[1:5,2])
R> # A tibble: 5 × 1
R>   ref_date  
R>   <date>    
R> 1 2010-01-01
R> 2 2010-01-04
R> 3 2010-01-05
R> 4 2010-01-06
R> 5 2010-01-01
print(my_df[1:5,c(1,2)])
R> # A tibble: 5 × 2
R>   ticker ref_date  
R>   <chr>  <date>    
R> 1 ABEV3  2010-01-01
R> 2 ABEV3  2010-01-04
R> 3 ABEV3  2010-01-05
R> 4 ABEV3  2010-01-06
R> 5 BBAS3  2010-01-01
print(my_df[1:5, ])
R> # A tibble: 5 × 3
R>   ticker ref_date   price
R>   <chr>  <date>     <dbl>
R> 1 ABEV3  2010-01-01 737. 
R> 2 ABEV3  2010-01-04 764. 
R> 3 ABEV3  2010-01-05 769. 
R> 4 ABEV3  2010-01-06 776. 
R> 5 BBAS3  2010-01-01  59.4

Essa seleção de colunas também pode ser realizada utilizando o nome das mesmas da seguinte forma:

print(my_df[1:3, c('ticker','price')])
R> # A tibble: 3 × 2
R>   ticker price
R>   <chr>  <dbl>
R> 1 ABEV3   737.
R> 2 ABEV3   764.
R> 3 ABEV3   769.

ou, pelo operador de pipeline e a função dplyr::select() :

library(dplyr)

my.temp <- my_df |>
  select(ticker, price) |>
  glimpse()
R> Rows: 12
R> Columns: 2
R> $ ticker <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS3"…
R> $ price  <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59.8…

7.1.5 Modificando um dataframe

Para criar novas colunas em um dataframe, basta utilizar a função dplyr::mutate() . Aqui iremos abusar do operador de pipeline (|>) para sequenciar as operações:

library(dplyr)

# add columns with mutate
my_df <- my_df |>
  mutate(ret = price/lag(price) -1,
         my_seq1 = 1:nrow(my_df),
         my_seq2 =  my_seq1 +9) |>
  glimpse()
R> Rows: 12
R> Columns: 6
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS…
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59…
R> $ ret      <dbl> NA, 0.037289424, 0.005875887, 0.010199966…
R> $ my_seq1  <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
R> $ my_seq2  <dbl> 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2…

Note que precisamos indicar o dataframe de origem dos dados, nesse caso o objeto my_df, e as colunas são definidas como argumentos em dplyr::mutate() . Observe também que usamos a coluna price na construção de ret, o retorno aritmético dos preços. Um caso especial é a construção de my_seq2 com base em my_seq1, isto é, antes mesmo dela ser explicitamente calculada já é possível utilizar a nova coluna para criar outra. Vale salientar que a nova coluna deve ter exatamente o mesmo número de elementos que as demais. Caso contrário, o R retorna uma mensagem de erro.

A maneira mais tradicional, e comumente encontrada em código, para criar novas colunas é utilizar o símbolo $:

# add new column with base R
my_df$my_seq3 <- 1:nrow(my_df)

# check it
glimpse(my_df)
R> Rows: 12
R> Columns: 7
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS…
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59…
R> $ ret      <dbl> NA, 0.037289424, 0.005875887, 0.010199966…
R> $ my_seq1  <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
R> $ my_seq2  <dbl> 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2…
R> $ my_seq3  <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

Portanto, o operador $ vale tanto para acessar quanto para criar novas colunas.

Para remover colunas de um dataframe, basta usar dplyr::select() com operador negativo para o nome das colunas indesejadas:

# removing columns
my_df_temp <- my_df |>
  select(-my_seq1, -my_seq2, -my_seq3) |>
  glimpse()
R> Rows: 12
R> Columns: 4
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS…
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59…
R> $ ret      <dbl> NA, 0.037289424, 0.005875887, 0.010199966…

No uso de funções nativas do R, a maneira tradicional de remover colunas é alocar o valor nulo (NULL):

# set temp df
temp_df <- my_df

# remove cols
temp_df$price <- NULL
temp_df$ref_date  <- NULL

# check it
glimpse(temp_df)
R> Rows: 12
R> Columns: 5
R> $ ticker  <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS3…
R> $ ret     <dbl> NA, 0.037289424, 0.005875887, 0.010199966,…
R> $ my_seq1 <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
R> $ my_seq2 <dbl> 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20…
R> $ my_seq3 <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

7.1.6 Filtrando um dataframe

Uma operação bastante comum no R é filtrar linhas de uma tabela de acordo com uma ou mais condições. Por exemplo, caso quiséssemos apenas os dados da ação ABEV3, poderíamos utilizar a função dplyr::filter() para filtrar a tabela:

library(dplyr)

# filter df for single stock
my_df_temp <- my_df |>
  filter(ticker == 'ABEV3') |>
  glimpse()
R> Rows: 4
R> Columns: 7
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3"
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010-…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47
R> $ ret      <dbl> NA, 0.037289424, 0.005875887, 0.010199966
R> $ my_seq1  <int> 1, 2, 3, 4
R> $ my_seq2  <dbl> 10, 11, 12, 13
R> $ my_seq3  <int> 1, 2, 3, 4

A função também aceita mais de uma condição. Veja a seguir onde filtramos os dados para 'ABEV3' em datas após ou iguais a '2010-01-05':

library(dplyr)
# filter df for single stock and date
my_df_temp <- my_df |>
  filter(ticker == 'ABEV3',
         ref_date >= as.Date('2010-01-05')) |>
  glimpse()
R> Rows: 2
R> Columns: 7
R> $ ticker   <chr> "ABEV3", "ABEV3"
R> $ ref_date <date> 2010-01-05, 2010-01-06
R> $ price    <dbl> 768.63, 776.47
R> $ ret      <dbl> 0.005875887, 0.010199966
R> $ my_seq1  <int> 3, 4
R> $ my_seq2  <dbl> 12, 13
R> $ my_seq3  <int> 3, 4

Aqui utilizamos o símbolo == para testar uma igualdade. Iremos estudar mais profundamente a classe de testes lógicos no Capítulo 6.

7.1.7 Ordenando um dataframe

Após a criação ou importação de um dataframe, pode-se ordenar seus componentes de acordo com os valores de alguma coluna. Um caso bastante comum em que é necessário realizar uma ordenação explícita é quando importamos dados financeiros em que as datas não estão em ordem crescente. Na grande maioria das situações, dados temporais devem estar ordenados de acordo com a antiguidade, isto é, dados mais recentes são alocados na última linha da tabela. Essa operação é realizada através do uso da função order() ou dplyr::arrange() .

Como exemplo, considere a criação de um dataframe com os valores a seguir:

# set df
my_df <- tibble::tibble(col1 = c(4,1,2),
                        col2 = c(1,1,3),
                        col3 = c('a','b','c'))

# print it
print(my_df)
R> # A tibble: 3 × 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     4     1 a    
R> 2     1     1 b    
R> 3     2     3 c

A função order() retorna os índices relativos à ordenação dos valores dados como entrada. Para o caso da primeira coluna de my_df, os índices dos elementos formadores do novo vetor, com seus valores ordenados em forma crescente, são:

idx <- order(my_df$col1)
print(idx)
R> [1] 2 3 1

Portanto, ao utilizar a saída da função order() como indexador do dataframe, acaba-se ordenando o mesmo de acordo com os valores da coluna col1. Veja a seguir:

my_df_2 <- my_df[order(my_df$col1), ]
print(my_df_2)
R> # A tibble: 3 × 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     1     1 b    
R> 2     2     3 c    
R> 3     4     1 a

Essa operação de ordenamento também pode ser realizada levando em conta mais de uma coluna. Veja o exemplo a seguir, onde se ordena o dataframe pelas colunas col2 e col1.

idx <- order(my_df$col2, my_df$col1)
my_df_3 <- my_df[idx, ]

print(my_df_3)
R> # A tibble: 3 × 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     1     1 b    
R> 2     4     1 a    
R> 3     2     3 c

No {tidyverse} (Wickham 2023), a forma de ordenar dataframes é pelo uso da função dplyr::arrange() . No caso de ordenamento decrescente, encapsulamos o nome das colunas com desc:

# sort ascending, by col1 and col2
my_df <- my_df |>
  dplyr::arrange(col1, col2) |>
  print()
R> # A tibble: 3 × 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     1     1 b    
R> 2     2     3 c    
R> 3     4     1 a
# sort descending, col1 and col2
my_df <- my_df |>
  dplyr::arrange(desc(col1), desc(col2)) |>
  print()
R> # A tibble: 3 × 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     4     1 a    
R> 2     2     3 c    
R> 3     1     1 b

O resultado prático no uso de dplyr::arrange() é o mesmo de order() . Um dos seus benefícios é a possibilidade de encadeamento de operações através do uso do pipeline.

7.1.8 Combinando e Agregando dataframes

Em muitas situações de análise de dados, tabelas de diferentes arquivos são importadas no R e, antes de analisar os dados, precisamos combinar as informações em um único objeto. Nos casos mais simples, onde as tabelas a serem agregadas possuem o mesmo formato, nós as juntamos de acordo com as linhas, verticalmente, ou colunas, horizontalmente. Para esse fim, temos as funções dplyr::bind_rows() e dplyr::bind_cols() no tidyverse e rbind() e cbind() nas funções nativas do R.

library(dplyr)

# set dfs
my_df_1 <- tibble(col1 = 1:5,
                  col2 = rep('a', 5))

my_df_2 <- tibble(col1 = 6:10,
                  col2 = rep('b', 5),
                  col3 = rep('c', 5))

# bind by row
my_df <- bind_rows(my_df_1, my_df_2) |>
  glimpse()
R> Rows: 10
R> Columns: 3
R> $ col1 <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
R> $ col2 <chr> "a", "a", "a", "a", "a", "b", "b", "b", "b", …
R> $ col3 <chr> NA, NA, NA, NA, NA, "c", "c", "c", "c", "c"

Note que, no exemplo anterior, os nomes das colunas são os mesmos. De fato, a função dplyr::bind_rows() procura os nomes iguais em ambos os objetos para fazer a junção dos dataframes corretamente. As colunas que não ocorrem em ambos objetos, tal como col3 no exemplo, saem como NA no objeto final. Já para o caso de dplyr::bind_cols() , os nomes das colunas devem ser diferentes, porém o número de linhas deve ser o mesmo.

# set dfs
my_df_1 <- tibble(col1 = 1:5, col2 = rep('a', 5))
my_df_2 <- tibble(col3 = 6:10, col4 = rep('b', 5))

# bind by column
my_df <- bind_cols(my_df_1, my_df_2) |>
  glimpse()
R> Rows: 5
R> Columns: 4
R> $ col1 <int> 1, 2, 3, 4, 5
R> $ col2 <chr> "a", "a", "a", "a", "a"
R> $ col3 <int> 6, 7, 8, 9, 10
R> $ col4 <chr> "b", "b", "b", "b", "b"

Para casos mais complexos, onde a junção deve ser realizada de acordo com algum índice tal como uma data, é possível juntar dataframes diferentes com o uso das funções da família dplyr::join* tal como dplyr::inner_join() , dplyr::left_join() , dplyr::right_join() , entre outras. A descrição de todas elas não cabe aqui. Iremos descrever apenas o caso mais provável, dplyr::inner_join() . Essa combina os dados, mantendo apenas os casos onde existe o índice em ambos.

# set df
my_df_1 <- dplyr::tibble(date = as.Date('2016-01-01')+0:10,
                  x = 1:11)

my_df_2 <- dplyr::tibble(date = as.Date('2016-01-05')+0:10,
                  y = seq(20,30, length.out = 11))

Note que os dataframes criados possuem uma coluna em comum, date. A partir desta coluna que agregamos as tabelas com dplyr::inner_join() :

# aggregate tables
my_df <- dplyr::inner_join(my_df_1, my_df_2)
R> Joining with `by = join_by(date)`
glimpse(my_df)
R> Rows: 7
R> Columns: 3
R> $ date <date> 2016-01-05, 2016-01-06, 2016-01-07, 2016-01-…
R> $ x    <int> 5, 6, 7, 8, 9, 10, 11
R> $ y    <dbl> 20, 21, 22, 23, 24, 25, 26

O R automaticamente verifica a existência de colunas com mesmo nome nos dataframes e realiza a junção por essas. Caso quiséssemos juntar dataframes onde os nomes das colunas para utilizar o índice não são iguais, temos duas soluções: modificar os nomes das colunas ou então utilizar argumento by em dplyr::inner_join() . Veja a seguir:

# set df
my_df_3 <- dplyr::tibble(ref_date = as.Date('2016-01-01')+0:10,
                  x = 1:11)

my_df_4 <- dplyr::tibble(my_date = as.Date('2016-01-05')+0:10,
                  y = seq(20,30, length.out = 11))

# join by my_df_3$ref_date and my_df_4$my_date
my_df <- dplyr::inner_join(my_df_3, my_df_4,
                    by = c('ref_date' = 'my_date'))

glimpse(my_df)
R> Rows: 7
R> Columns: 3
R> $ ref_date <date> 2016-01-05, 2016-01-06, 2016-01-07, 2016…
R> $ x        <int> 5, 6, 7, 8, 9, 10, 11
R> $ y        <dbl> 20, 21, 22, 23, 24, 25, 26

Para o caso de uso da função nativa de agregação de dataframes, merge() , temos que indicar explicitamente o nome da coluna com argumento by:

# aggregation with base R
my_df <- merge(my_df_1, my_df_2, by = 'date')

glimpse(my_df)
R> Rows: 7
R> Columns: 3
R> $ date <date> 2016-01-05, 2016-01-06, 2016-01-07, 2016-01-…
R> $ x    <int> 5, 6, 7, 8, 9, 10, 11
R> $ y    <dbl> 20, 21, 22, 23, 24, 25, 26

Note que, nesse caso, o dataframe resultante manteve apenas as informações compartilhadas entre ambos os objetos, isto é, aquelas linhas onde as datas em date eram iguais. Esse é o mesmo resultado quando no uso do dplyr::inner_join() .

As demais funções de agregação de tabelas – dplyr::left_join() , dplyr::right_join() e dplyr::full_join() – funcionam de forma muito semelhante a dplyr::inner_join() , exceto na escolha da saída. Por exemplo, dplyr::full_join() retorna todos os casos/linhas entre tabela 1 e 2, incluindo aqueles onde não tem o índice compartilhado. Para estes casos, a coluna do índice sairá como NA. Veja o exemplo a seguir:

# set df
my_df_5 <- dplyr::tibble(ref_date = as.Date('2016-01-01')+0:10,
                  x = 1:11)

my_df_6 <- dplyr::tibble(ref_date = as.Date('2016-01-05')+0:10,
                  y = seq(20,30, length.out = 11))

# combine with full_join
my_df <- dplyr::full_join(my_df_5, my_df_6)
R> Joining with `by = join_by(ref_date)`
# print it
print(my_df)
R> # A tibble: 15 × 3
R>    ref_date       x     y
R>    <date>     <int> <dbl>
R>  1 2016-01-01     1    NA
R>  2 2016-01-02     2    NA
R>  3 2016-01-03     3    NA
R>  4 2016-01-04     4    NA
R>  5 2016-01-05     5    20
R>  6 2016-01-06     6    21
R>  7 2016-01-07     7    22
R>  8 2016-01-08     8    23
R>  9 2016-01-09     9    24
R> 10 2016-01-10    10    25
R> 11 2016-01-11    11    26
R> 12 2016-01-12    NA    27
R> 13 2016-01-13    NA    28
R> 14 2016-01-14    NA    29
R> 15 2016-01-15    NA    30

7.1.9 Extensões ao dataframe

Um dos grandes benefícios no uso do R é a existência de pacotes para lidar com os problemas específicos dos usuários. Enquanto um objeto tabular do tipo tibble é suficiente para a maioria dos casos, existem benefícios no uso de uma classe alternativa. Ao longo do tempo, diversas soluções foram disponibilizadas por desenvolvedores.

Por exemplo, é muito comum trabalharmos com dados exclusivamente numéricos que são indexados ao tempo. Isto é, situações onde cada informação pertence a um índice temporal - um objeto da classe data/tempo. As linhas dessa tabela representam um ponto no tempo, enquanto as colunas indicam variáveis numéricas de interesse. Nesse caso, faria sentido representarmos os nossos dados como objetos do tipo {xts} (Ryan e Ulrich 2024). O grande benefício dessa opção é que a agregação e a manipulação de variáveis em função do tempo é muito fácil. Por exemplo, podemos transformar dados de frequência diária para a frequência semanal com apenas uma linha de comando. Além disso, diversas outras funções reconhecem automaticamente que os dados são indexados ao tempo. Um exemplo é a criação de uma figura com esses dados. Neste caso, o eixo horizontal da figura é automaticamente organizado com as datas.

Veja um caso a seguir, onde carregamos os dados anteriores como um objeto {xts} (Ryan e Ulrich 2024):

library(xts)

# set data
ticker <- c('ABEV3', 'BBAS3','BBDC3')

date <- as.Date(c('2010-01-01', '2010-01-04',
                  '2010-01-05', '2010-01-06'))

price_ABEV3 <- c(736.67, 764.14, 768.63, 776.47)
price_BBAS3 <- c(59.4, 59.8, 59.2, 59.28)
price_BBDC3 <- c(29.81, 30.82, 30.38, 30.20)

# build matrix
my_mat <- matrix(c(price_BBDC3, price_BBAS3, price_ABEV3),
                 nrow = length(date) )

# set xts object
my_xts <- xts(my_mat,
              order.by = date)

# set correct colnames
colnames(my_xts) <- ticker

# check it!
print(my_xts)
R>            ABEV3 BBAS3  BBDC3
R> 2010-01-01 29.81 59.40 736.67
R> 2010-01-04 30.82 59.80 764.14
R> 2010-01-05 30.38 59.20 768.63
R> 2010-01-06 30.20 59.28 776.47

O código anterior pode dar a impressão de que o objeto my_xts é semelhante a um dataframe, porém, não se engane. Por estar indexado a um vetor de tempo, objeto my_xts pode ser utilizado para uma série de procedimentos temporais, tal como uma agregação por período temporal. Veja o exemplo a seguir, onde agregamos duas variáveis de tempo através do cálculo de uma média a cada semana.

N <- 500

my_mat <- matrix(c(seq(1, N), seq(N, 1)), nrow=N)

my_xts <- xts(my_mat, order.by = as.Date('2016-01-01')+1:N)

my_xts.weekly.mean <- apply.weekly(my_xts, mean)

print(head(my_xts.weekly.mean))
R>             X.1   X.2
R> 2016-01-03  1.5 499.5
R> 2016-01-10  6.0 495.0
R> 2016-01-17 13.0 488.0
R> 2016-01-24 20.0 481.0
R> 2016-01-31 27.0 474.0
R> 2016-02-07 34.0 467.0

Em Finanças e Economia, as agregações com objetos {xts} (Ryan e Ulrich 2024) são extremamente úteis quando se trabalha com dados em frequências de tempo diferentes. Por exemplo, é muito comum que se agregue dados de transação no mercado financeiro em alta frequência para intervalos maiores. Assim, dados que ocorrem a cada segundo são agregados para serem representados de 15 em 15 minutos. Esse tipo de procedimento é facilmente realizado no R através da correta representação dos dados como objetos {xts} (Ryan e Ulrich 2024). Existem diversas outras funcionalidades desse pacote. Encorajo os usuários a ler o manual e aprender o que pode ser feito.

Indo além, existem diversos outros tipos de dataframes customizados. Por exemplo, o dataframe proposto pelo pacote {data.table} (Barrett et al. 2024) prioriza o tempo de operação nos dados e o uso de uma notação compacta para acesso e processamento. O {tibbletime} (Vaughan e Dancho 2023) é uma versão orientada pelo tempo para tibbles. Caso o usuário esteja necessitando realizar operações de agregação de tempo, o uso deste pacote é fortemente recomendado.

7.1.10 Outras Funções Úteis

head() - Retorna os primeiros n elementos de um dataframe.

my_df <- tibble(col1 = 1:5000, col2 = rep('a', 5000))
head(my_df, 5)
R> # A tibble: 5 × 2
R>    col1 col2 
R>   <int> <chr>
R> 1     1 a    
R> 2     2 a    
R> 3     3 a    
R> 4     4 a    
R> 5     5 a

tail() - Retorna os últimos n elementos de um dataframe.

tail(my_df, 5)
R> # A tibble: 5 × 2
R>    col1 col2 
R>   <int> <chr>
R> 1  4996 a    
R> 2  4997 a    
R> 3  4998 a    
R> 4  4999 a    
R> 5  5000 a

complete.cases() - Retorna um vetor lógico que testa se as linhas contêm apenas valores existentes e nenhum NA.

my_x <- c(1:5, NA, 10)
my_y <- c(5:10, NA)
my_df <- tibble(my_x, my_y)

print(my_df)
R> # A tibble: 7 × 2
R>    my_x  my_y
R>   <dbl> <int>
R> 1     1     5
R> 2     2     6
R> 3     3     7
R> 4     4     8
R> 5     5     9
R> 6    NA    10
R> 7    10    NA
print(complete.cases(my_df))
R> [1]  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE
print(which(!complete.cases(my_df)))
R> [1] 6 7

na.omit() - Retorna um dataframe sem as linhas onde valores NA são encontrados.

print(na.omit(my_df))
R> # A tibble: 5 × 2
R>    my_x  my_y
R>   <dbl> <int>
R> 1     1     5
R> 2     2     6
R> 3     3     7
R> 4     4     8
R> 5     5     9

unique() - Retorna um dataframe onde todas as linhas duplicadas são eliminadas e somente os casos únicos são mantidos.

my_df <- tibble(col1 = c(1,1,2,3,3,4,5),
                col2 = c('A','A','A','C','C','B','D'))

print(my_df)
R> # A tibble: 7 × 2
R>    col1 col2 
R>   <dbl> <chr>
R> 1     1 A    
R> 2     1 A    
R> 3     2 A    
R> 4     3 C    
R> 5     3 C    
R> 6     4 B    
R> 7     5 D
print(unique(my_df))
R> # A tibble: 5 × 2
R>    col1 col2 
R>   <dbl> <chr>
R> 1     1 A    
R> 2     2 A    
R> 3     3 C    
R> 4     4 B    
R> 5     5 D

7.2 Listas

Uma lista (list) é uma classe de objeto extremamente flexível e já tivemos contato com ela nos capítulos anteriores. Ao contrário de vetores atômicos, a lista não apresenta restrição alguma em relação aos tipos de elementos nela contidos. Podemos agrupar valores numéricos com caracteres, fatores com datas e até mesmo listas dentro de listas. Quando agrupamos vetores, também não é necessário que os mesmos tenham um número igual de elementos. Além disso, podemos dar um nome a cada elemento. Essas propriedades fazem da lista o objeto mais flexível para o armazenamento e estruturação de dados no R. Não é acidental o fato de que listas são muito utilizadas como retorno de funções.

7.2.1 Criando Listas

Uma lista pode ser criada através do comando list() , seguido por seus elementos separados por vírgula:

library(dplyr)

# create list
my_l <- list(c(1, 2, 3),
             c('a', 'b'),
             factor('A', 'B', 'C'),
             tibble(col1 = 1:5))

# use base::print
print(my_l)
R> [[1]]
R> [1] 1 2 3
R> 
R> [[2]]
R> [1] "a" "b"
R> 
R> [[3]]
R> [1] <NA>
R> Levels: C
R> 
R> [[4]]
R> # A tibble: 5 × 1
R>    col1
R>   <int>
R> 1     1
R> 2     2
R> 3     3
R> 4     4
R> 5     5
# use dplyr::glimpse
glimpse(my_l)
R> List of 4
R>  $ : num [1:3] 1 2 3
R>  $ : chr [1:2] "a" "b"
R>  $ : Factor w/ 1 level "C": NA
R>  $ : tibble [5 × 1] (S3: tbl_df/tbl/data.frame)
R>   ..$ col1: int [1:5] 1 2 3 4 5

Note que juntamos no mesmo objeto um vetor atômico numérico, outro de texto, um fator e um tibble. A apresentação de listas com o comando print() é diferente dos casos anteriores. Os elementos são separados verticalmente e os seus índices aparecem com duplo colchete ([[ ]]). Conforme será explicado logo a seguir, é dessa forma que os elementos de uma lista são armazenados e acessados.

Assim como para os demais tipos de objeto, os elementos de uma lista também podem ter nomes, o que facilita o entendimento e a interpretação das informações do problema em análise. Por exemplo, considere o caso de uma base de dados com informações sobre determinada ação negociada na bolsa. Nesse caso, podemos definir uma lista como:

# set named list
my_named_l <- list(ticker = 'TICK4',
                   market = 'Bovespa',
                   df_prices = tibble(P = c(1,1.5,2,2.3),
                                      ref_date = Sys.Date()+0:3))

# check content
glimpse(my_named_l)
R> List of 3
R>  $ ticker   : chr "TICK4"
R>  $ market   : chr "Bovespa"
R>  $ df_prices: tibble [4 × 2] (S3: tbl_df/tbl/data.frame)
R>   ..$ P       : num [1:4] 1 1.5 2 2.3
R>   ..$ ref_date: Date[1:4], format: "2024-04-29" ...
Use nomes em listas e dataframes!

Como regra geral no uso do R, sempre dê preferência ao acesso de elementos através de seus nomes, seja em listas, vetores ou dataframes. Isso evita erros, pois, ao modificar os dados e adicionar algum outro objeto na lista, é possível que o ordenamento interno mude e, portanto, a posição de determinado objeto pode acabar sendo modificada.

7.2.2 Acessando os Elementos de uma Lista

Os elementos de uma lista podem ser acessados através do uso de duplo colchete ([[ ]]), tal como em:

# accessing elements from list
print(my_named_l[[2]])
R> [1] "Bovespa"
print(my_named_l[[3]])
R> # A tibble: 4 × 2
R>       P ref_date  
R>   <dbl> <date>    
R> 1   1   2024-04-29
R> 2   1.5 2024-04-30
R> 3   2   2024-05-01
R> 4   2.3 2024-05-02

Também é possível acessar os elementos com um colchete simples ([ ]), porém, tome cuidado com essa operação, pois o resultado não vai ser o objeto em si, mas uma outra lista. Esse é um equívoco muito fácil de passar despercebido, resultando em erros no código. Veja a seguir:

# set list
my_l <- list('a',
             c(1,2,3),
             factor('a','b'))

# check classes
class(my_l[[2]])
R> [1] "numeric"
class(my_l[2])
R> [1] "list"

Caso tentarmos somar um elemento a my_l[2], teremos uma mensagem de erro:

my_l[2] + 1
R> Error in my_l[2] + 1: non-numeric argument to binary operator

Esse erro ocorre devido ao fato de que uma lista não tem operador de soma. Para corrigir, basta utilizar o duplo colchete, tal como em my_l[[2]]+1. O acesso a elementos de uma lista com colchete simples somente é útil quando estamos procurando uma sublista dentro de uma lista maior. No exemplo anterior, caso quiséssemos obter o primeiro e o segundo elemento da lista my_l, usaríamos:

# set new list
my_new_l <- my_l[c(1,2)]

# check contents
print(my_new_l)
R> [[1]]
R> [1] "a"
R> 
R> [[2]]
R> [1] 1 2 3
class(my_new_l)
R> [1] "list"

No caso de listas com elementos nomeados, os mesmos podem ser acessados por seu nome através do uso do símbolo $ tal como em my_named_l$df_prices ou [['nome']], tal como em my_named_l[['df_prices']]. Em geral, essa é uma forma mais eficiente e recomendada de interagir com os elementos de uma lista.

Dica

Saiba que a ferramenta de autocomplete do RStudio também funciona para listas. Para usar, digite o nome da lista seguido de $ e aperte tab. Uma caixa de diálogo com todos os elementos disponíveis na lista irá aparecer. A partir disso, basta selecionar apertando enter.

Veja os exemplos a seguir, onde são apresentadas as diferentes formas de se acessar uma lista.

# different ways to access a list
my_named_l$ticker
my_named_l$price
my_named_l[['ticker']]
my_named_l[['price']]

Vale salientar que também é possível acessar diretamente os elementos de um vetor que esteja dentro de uma lista através de colchetes encadeados. Veja a seguir:

# accessing elements of a vector in a list
my_l <- list(c(1,2,3),
             c('a', 'b'))

print(my_l[[1]][2])
R> [1] 2
print(my_l[[2]][1])
R> [1] "a"

Tal operação é bastante útil quando interessa apenas um elemento dentro de um objeto maior criado por alguma função.

7.2.3 Adicionando e Removendo Elementos de uma Lista

A remoção, adição e substituição de elementos de uma lista também são procedimentos fáceis. Para adicionar ou substituir, basta definir um novo objeto na posição desejada da lista:

# set list
my_l <- list('a', 1, 3)
dplyr::glimpse(my_l)
R> List of 3
R>  $ : chr "a"
R>  $ : num 1
R>  $ : num 3
# add new elements to list
my_l[[4]] <- c(1:5)
my_l[[2]] <- c('b')

# print result
dplyr::glimpse(my_l)
R> List of 4
R>  $ : chr "a"
R>  $ : chr "b"
R>  $ : num 3
R>  $ : int [1:5] 1 2 3 4 5

A operação também é possível com o uso de nomes e operador $:

# set list
my_l <- list(elem1 = 'a', name1=5)

# set new element
my_l$name2 <- 10
dplyr::glimpse(my_l)
R> List of 3
R>  $ elem1: chr "a"
R>  $ name1: num 5
R>  $ name2: num 10

Para remover elementos de uma lista, basta definir o elemento para o símbolo reservado NULL (nulo):

# set list
my_l <- list(text = 'b', num1 = 2, num2 = 4)
dplyr::glimpse(my_l)
R> List of 3
R>  $ text: chr "b"
R>  $ num1: num 2
R>  $ num2: num 4
# remove elements
my_l[[3]] <- NULL
dplyr::glimpse(my_l)
R> List of 2
R>  $ text: chr "b"
R>  $ num1: num 2
my_l$num1 <- NULL
dplyr::glimpse(my_l)
R> List of 1
R>  $ text: chr "b"

Outra maneira de retirar elementos de uma lista é utilizando um índice negativo para os elementos indesejados. Observe a seguir, onde eliminamos o segundo elemento de uma lista:

# set list
my_l <- list(a = 1, b = 'texto')

# remove second element
dplyr::glimpse(my_l[[-2]])
R>  num 1

Assim como no caso de vetores atômicos, essa remoção também pode ser realizada por condições lógicas. Veja a seguir:

# set list
my_l <- list(1, 2, 3, 4)

# remove elements by condition
my_l[my_l > 2] <- NULL
dplyr::glimpse(my_l)
R> List of 2
R>  $ : num 1
R>  $ : num 2

Porém, note que esse atalho só funciona porque todos os elementos de my_l são numéricos.

7.2.4 Processando os Elementos de uma Lista

Um ponto importante a ser destacado a respeito de listas é que os seus elementos podem ser processados e manipulados individualmente através de funções específicas. Este é um tópico particular de programação com o R, mas que vale a apresentação aqui.

Por exemplo, imagine uma lista com vetores numéricos de diferentes tamanhos, tal como a seguir:

# set list
my_l_num <- list(c(1, 2, 3),
                 seq(1:50),
                 seq(-5, 5, by = 0.5))

Caso quiséssemos calcular a média de cada elemento de my_l_num e apresentar o resultado na tela como um vetor, poderíamos fazer isso através de um procedimento simples, processando cada elemento individualmente:

# calculate mean of vectors
mean_1 <- mean(my_l_num[[1]])
mean_2 <- mean(my_l_num[[2]])
mean_3 <- mean(my_l_num[[3]])

# print it
print(c(mean_1, mean_2, mean_3))
R> [1]  2.0 25.5  0.0

O código anterior funciona, porém não é recomendado devido sua falta de escalabilidade. Isto é, caso aumentássemos o volume de dados ou objetos, o código não funcionaria corretamente. Se, por exemplo, tivéssemos um quarto elemento em my_l_num e quiséssemos manter essa estrutura do código, teríamos que adicionar uma nova linha mean_4 <- mean(my_l_num[[4]]) e modificar o comando de saída na tela para print(c(mean_1, mean_2, mean_3, mean_4)).

Uma maneira mais fácil, elegante e inteligente seria utilizar a função sapply() . Nela, basta indicar o nome do objeto de tipo lista e a função que queremos utilizar para processar cada elemento. Internamente, os cálculos são realizados automaticamente. Veja a seguir:

# using sapply
my_mean <- sapply(my_l_num, mean)

# print result
print(my_mean)
R> [1]  2.0 25.5  0.0

O uso da função sapply() é preferível por ser mais compacto e eficiente do que a alternativa – a criação de mean_1, mean_2 e mean_3. Note que o primeiro código, com médias individuais, só funciona para uma lista com três elementos. A função sapply() , ao contrário, funcionaria da mesma forma em listas de qualquer tamanho. Caso tivéssemos mais elementos, nenhuma modificação seria necessária no código anterior, o que o torna extensível a chegada de novos dados.

Essa visão e implementação de código voltado a procedimentos genéricos é um dos lemas para tornar o uso do R mais eficiente. A regra é simples: sempre escreva códigos que sejam adaptáveis a chegada de novos dados. Em inglês, isso é chamado de regra DRY (don’t repeat yourself). Caso você esteja repetindo códigos e abusando do control + c/control + v, como no exemplo anterior, certamente existe uma solução mais elegante e flexível que poderia ser utilizada. No R, existem diversas outras funções da família apply para esse objetivo.

7.2.5 Outras Funções Úteis

unlist() - Retorna os elementos de uma lista em um único vetor atômico.

my_named_l <- list(ticker = 'XXXX4',
                   price = c(1,1.5,2,3),
                   market = 'Bovespa')
my_unlisted <- unlist(my_named_l)
print(my_unlisted)
R>    ticker    price1    price2    price3    price4    market 
R>   "XXXX4"       "1"     "1.5"       "2"       "3" "Bovespa"
class(my_unlisted)
R> [1] "character"

as.list() - Converte um objeto para uma lista, tornando cada elemento um elemento da lista.

my_x <- 10:13
my_x_as_list <- as.list(my_x)
print(my_x_as_list)
R> [[1]]
R> [1] 10
R> 
R> [[2]]
R> [1] 11
R> 
R> [[3]]
R> [1] 12
R> 
R> [[4]]
R> [1] 13

names() - Retorna ou define os nomes dos elementos de uma lista. Assim como para o caso de nomear elementos de um vetor atômico, usa-se a função names() alocada ao lado esquerdo do símbolo <-.

my_l <- list(value1 = 1, value2 = 2, value3 = 3)
print(names(my_l))
R> [1] "value1" "value2" "value3"
my_l <- list(1,2,3)
names(my_l) <- c('num1', 'num2', 'num3')
print(my_l)
R> $num1
R> [1] 1
R> 
R> $num2
R> [1] 2
R> 
R> $num3
R> [1] 3

7.3 Exercícios


Q.1 - Usando a função dplyr::tibble() , crie um dataframe chamado my_df, o qual possui uma coluna chamada x contendo uma sequência de -100 a 100, e outra coluna chamada y com o valor da coluna x somado por 5. Quantos valores na coluna x são maiores que 10 e menores que 25?

  1. 2
  2. 7
  3. 10
  4. 4
  5. 14

Q.2 - Crie uma nova coluna no objeto my_df chamada cumsum_x, contendo a soma cumulativa de x (função cumsum() ). Nesta nova coluna, quantos valores são maiores que -3500?

  1. 89
  2. 29
  3. 48
  4. 10
  5. 67

Q.3 - Use a função dplyr::filter() e o operador de pipeline para filtrar my_df, mantendo apenas as linhas onde o valor da coluna y é maior que 0. Qual é o número de linhas na tabela resultante?

  1. 73
  2. 105
  3. 45
  4. 135
  5. 240

Q.4 - Caso não o tenha feito, repita os exercícios 1, 2 e 3 utilizando as funções do {tidyverse} (Wickham 2023) e o operador de pipeline.


Q.5 - Use o pacote {yfR} (Perlin 2023) para baixar dados de ações do Google (GOOG), de 2015-01-01 a 2017-10-28. Se o investidor tivesse comprado 1500 USD em ações do Google no primeiro dia dos dados e mantivesse o investimento até hoje, qual seria o valor da sua carteira?

  1. R$ 2.320,84
  2. R$ 2.921,25
  3. R$ 554,57
  4. R$ 1.143,32
  5. R$ 1.732,08