Neste documento, iremos aprender a realizar transformações e manipulações básicas de dados no R. Para isto, usaremos pacotes construídos exatamente para tal objetivo, como o reshape2, dplyr, tidyverse. Para rodar os comandos que exploraremos aqui, basta copiar os códigos presentes no documento, colar na aba “source” do RStudio, ou no “console” do R, e executar.
Este passo-a-passo tem dois objetivos principais:
Te ensinar a realizar, dentro do R, rotinas que você normalmente executaria no excel. Ao passar toda a sua rotina de análises para dentro do R, como código, você ganha em reprodutibilidade, automatização, integridade dos dados e fluidez de análise. Caso algum dado novo seja inserido na tabela ou alguma correção seja feita, basta executar novamente o código na tabela atualizada e, pronto. Você não perde mais tempo realizando todas as transformações novamente no excel ad infinitum a cada atualização da tabela. Além disto, e mais importante, toda manipulação dos dados passa a estar documentada. Ao manipular dados no excel, o histórico de modificações se perde. Ainda que você salve diferentes versões da tabela, guardando metadados do que foi modificado (o que raramente se faz), é muito mais fácil e confiável rastrear as manipulações em um Script do que em metadados de dezenas de tabelas.
Te estimular a “brincar” com as funções exploradas aqui. Você não aprendeu Português decorando frases e as repetindo. Foram necessários anos e anos de tentativa e erro, de alfabetização básica, antes que pudesse construir frases por si mesmo. R, assim como Português, é uma linguagem. O processo de aprendizagem é bem similar. Quando criança aprendíamos por repetição, usando Scripts copiados dos nossos pais. Mas, com o tempo, ficamos mais e mais independentes e hoje conseguimos reorganizar os elementos de uma frase e construir frases novas, com significados completamente novos. Isto só é possível porque aprendemos a sintaxe e semântica do Português. Para aprender R de verdade, o mesmo é necessário. Logo, embora este documento contenha blocos de código para realizar as manipulações de dados aqui exploradas, sugiro fortemente que você “brinque” com eles. Modifique os parâmetros, leia a documentação dos pacotes, tente combinar os códigos entre si, criar suas próprias “frases”. Alguns exercícios sugeridos aqui requerem isto explicitamente.
Obs: Uma vez que o objetivo aqui é ficar independente o máximo possível do excel, não o usaremos em nenhum momento. Toda tabela de dados que vamos utilizar será ou construida do zero, via código, ou carregada de algum banco de dados dentro R. Seu excel está aberto? Pode fechá-lo.
Estamos prontos para começar
Primeiro, vamos construir as tabelas com dados fictícios que iremos usar. Simularemos um banco de dados reportando a ocorrências de espécies hospedeiras em diferentes locais, e seus parasitos associados. Este tipo de dado, via de regra, é coletado em estudos que amostram algum grupo de hospedeiros (p.e: aves), e busca para parasitos associados a estes hospedeiros (p.e: ectoparasitos). A tabela costuma ter a seguinte estrutura de colunas, onde cada linha é um indivíduo de hospedeiro amostrado:
Coluna Local: onde o local onde aquele indivíduo foi amostrado é informado
Coluna Espécie do hospedeiro: onde a espécie à qual aquele indivíduo pertence é informada
Coluna Status de infecção: onde é informado se aquele indivíduo estava ou não infectado por parasitos. 1 para infectado, 0 para não-infectado.
Coluna Espécie do parasito: onde é informado, caso aquele indivíduo esteja infectado, qual a espécie de parasito.
Coluna Número de parasitos: onde é informado a quantidade de parasitos encontrados naquele indivíduo.
Perceba que no caso de um dado indivíduo não estar infectado (Status de infecção = 0) a coluna Espécie de parasito não será preenchida na respectiva linha. Estamos assumindo, para facilitar, que não é possível que o mesmo indivíduo esteja infectado por mais de uma espécie de parasito. Se pudesse, teríamos que repetir linhas para o mesmo indivíduo, uma linha para cada espécie de parasito reportada nele. Neste caso, precisaríamos de uma nova coluna no banco de dados? Se sim, qual informação esta coluna deveria conter?
Vamos construir e visualizar a tabela que ficará mais claro.
Começemos criando o dataframe onde vamos guardar os dados. Vamos assumir que nosso banco de dados tem 1000 (mil) linhas. Ou seja, coletamos 1000 indivíduos de hospedeiros. Criamos, então, uma tabela com as 5 colunas acima, e 1000 linhas, todas vazias.
Exercício: entender o que a função rep está fazendo.
df <- data.frame(Local = rep(NA,1000),
Host_sp = rep(NA,1000),
Status_inf = rep(NA,1000),
Para_sp = rep(NA,1000),
N_par = rep(NA,1000))
head(df)
Agora, vamos criar variáveis que nos informam quais locais foram amostrados, e quais espécies de hospedeiro e parasito podem, potencialmente, ocorrer nestes locais. Estas variáveis serão usadas abaixo para preencher o dataframe que criamos.
Para isto, usaremos uma função básica do R: paste. A função paste é uma função de concatenação. Ou seja, você informa quais caractéres ela deve concatenar. Abaixo vamos concatenar a palavra “Local” com as 10 primeiras letras do alfabeto, produzindo 10 locais: “Local_A”, “Local_B”, etc. Faremos o mesmo para gerar 20 espécies hospedeiras e 15 espécies de parasito.
Loc <- paste("Local",LETTERS[1:10], sep = "_") # Criando o conjunto de locais amostrados
print(Loc)
## [1] "Local_A" "Local_B" "Local_C" "Local_D" "Local_E" "Local_F" "Local_G"
## [8] "Local_H" "Local_I" "Local_J"
Sp_hos <- paste("Hos_sp",LETTERS[1:20], sep = "_") # Criando o conjunto de espécies de hospedeiros que ocorrem nos locais amostrados
print(Sp_hos)
## [1] "Hos_sp_A" "Hos_sp_B" "Hos_sp_C" "Hos_sp_D" "Hos_sp_E" "Hos_sp_F"
## [7] "Hos_sp_G" "Hos_sp_H" "Hos_sp_I" "Hos_sp_J" "Hos_sp_K" "Hos_sp_L"
## [13] "Hos_sp_M" "Hos_sp_N" "Hos_sp_O" "Hos_sp_P" "Hos_sp_Q" "Hos_sp_R"
## [19] "Hos_sp_S" "Hos_sp_T"
Sp_par <- paste("Par_sp",LETTERS[1:15], sep = "_") # Criando a conjunto de espécies de parasitos que ocorrem nos locais amostrados
print(Sp_par)
## [1] "Par_sp_A" "Par_sp_B" "Par_sp_C" "Par_sp_D" "Par_sp_E" "Par_sp_F"
## [7] "Par_sp_G" "Par_sp_H" "Par_sp_I" "Par_sp_J" "Par_sp_K" "Par_sp_L"
## [13] "Par_sp_M" "Par_sp_N" "Par_sp_O"
Com as variáveis criadas, vamos usá-las para sortear as espécies de hospedeiro nos locais, e as espécies de parasitos nos hospedeiros. Comecemos por sortear em qual local cada indivíduo foi coletado.
Para isto, vamos usar outra função básica: sample. A função sample é uma função de sorteio. Você informa um conjunto de elementos e pede que ela sortei N elementos deste conjunto, podendo ou não sortear o mesmo elemento mais de uma vez. No nosso caso, precisamos permitir que ela sorteie o mesmo elemento várias vezes (replace = T), uma vez que mais de um indivíduo pode ser encontrado no mesmo local, que diferentes indivíduos podem pertencer à mesma espécie e assim sucesisvamente.
N_amos <- 1000 # Criando uma variável para o número de amostras
df$Local <- sample(x = Loc, size = N_amos, replace = T) # Sorteando 1 elemento do conjunto de locais mil vezes.
head(df)
Pronto, preenchemos a coluna Local
Agora, sortearemos a qual espécie de hospedeiros cada indivíduo pertence:
df$Host_sp <- sample(x = Sp_hos, size = N_amos, replace = T) # Sorteando 1 elemento do conjunto de Especies de hospedeiros disponíveis mil vezes.
head(df)
Coluna Host_sp, que informa a espécie de hospedeiro, preenchida.
Uma vez que definimos onde cada indivíduo de hospedeiro foi encontrado, e qual sua espécie, precisamos definir se este indvíduo está infectado ou não. Para isto sortearemos 1s e 0s na coluna Status_inf. Existem infinitas formas de se fazer isto. Aqui, usaremos uma função que nos permitirá definir qual a prevalência esperada de infecção (N de individuos infectados / N total de indivíduos) queremos. Para isto, usaremos outra função básica do R: rbinom. Esta função faz parte de uma família de funções que permitem que se sortei números baseados em uma distribuição estatística subjacente de interesse. No caso da rbinom, sorteia-se 1 (infectado) ou 0 (não-infectado) com base em uma distribuição binomial cuja probabilidade de infecção é pré-definida. Essa não é uma aula de estatística. Logo, se não entendeu o conceito ainda, não tem problema. Volte nele mais tarde, leia a documentação da função, queime fosfato!
Prev <- 0.4 # Definindo a prevalência esperada
df$Status_inf <- rbinom(n = N_amos, size = 1, prob = Prev) # Sorteando o status de infecção de cada indivíduo, com base na prevalência escolhida, usando uma distribuição binomial.
head(df)
Pronto, definimos quais indivíduos estão infectados, agora precisamos definir por quais espécies de parasito, e quantos parasitos por hospedeiro. Faremos isto abaixo.
Faltam apenas duas colunas para preencher no dataframe: Para_sp e N_par. Porém, uma vez que não faz sentido definir uma espécie de parasito para um indivíduo de hospedeiro não infectado, usaremos a sintaxe básica do R para informar que o sorteio de espécies de parasitos deve ser feito apenas nas linhas com Status de infecção = 1.
N_infectado <- sum(df$Status_inf) # Descobrindo o número de hospedeiros infectados
df$Para_sp[df$Status_inf == 1] <- sample(x = Sp_par, size = N_infectado, replace = T) # Sorteando espécies de parasitos nos hospedeiros infectados.
head(df)
O que fizemos acima? Uma forma de tirar o máximo proveito do R é aprender sua sintáxe básica. O comando [ ], por exemplo, é muito útil para chamar subconjuntos do objeto na frente do qual ele é colocado. Quando colocado na frente de um vetor, como é o caso de qualquer coluna em um dataframe, os números dentro do [ ] informam quais elementos daquele vetor chamar. No caso, chamamos apenas as linhas da coluna Para_sp cuja linha na coluna Status_inf é igual 1, e sorteamos os nomes de parasitos apenas nestas linhas. Perceba que as linhas referentes a indivíduos de hospedeiros não infectados ficaram como NA.
O [ ] pode ser adaptado para chamar subconjuntos de matrizes ou dataframes inteiros, nos quais ele é preenchido com dois valores [x,y], sendo x as linhas a serem chamadas e y as colunas. Subconjuntos de listas, arrays, e outros objetos no R também podem ser chamadas com o [ ], mas com sintaxes ligeriamente diferentes. Fica de exercício aprender como usá-lo para listas e arrays.
Por fim, vamos definir um N de parasitos por hospedeiro. Porém, novamente, não faz sentido definir um N > 0 de parasitos em indivíduos de hospedeiros não infectados. Vamos, então, usando a mesma lógica da última seção, definir como 0 o N de parasitos em todos os indivíduos não infectados.
df$N_par[df$Status_inf == 0] <- 0 # Consegue entender o que fizemos aqui? Compare com o que fizemos na seção anterior.
head(df)
Por fim, vamos sortear o N de parasitos nos indivíduos infectados. Novamente, existem muitas formas de se fazer isto. A distribuição de abundância de espécies na natureza segue distribuições específicas, às vezes conhecidas para um dado grupo. Aqui, vamos simplificar e assumir uma distribuição de poisson, usando uma função prima da rbinom que usamos anteriormente, a rpois
df$N_par[df$Status_inf == 1] <- rpois(n = N_infectado, lambda = 5) # Consegue entender o que fizemos aqui? Compare com o que fizemos na seção anterior.
Pronto, nossa tabela de dados está completa.
Ainda na graduação, fazendo iniciação científica no laboratório de malária da UFMG, acompanhava o projeto de um pós-doutorando que pesquisava a diversidade de parasitos causadores de malária em aves. Recebíamos amostras de sangue coletadas em aves silvestres de diferentes espécies no campo, fazíamos testes moelculares para detectar quais amostras estavam infectadas por quais espécies de parasitos, e tabulávamos os dados. A tabela final era muito semelhante à que produzimos na seção anterior.
Um dia, para uma análise específica, precisávamos dos dados organizados em uma estrutura diferente da que havíamos tabulado os dados. Precisamos dos dados em formato de matriz de interação. Cada linha da tabela deveria ser uma espécie de hospedeiro, e cada coluna uma espécie de parasito, com as células da matriz mostrando o número de vezes em que aquela espécie de parasito foi encontrada naquela espécie de hospedeiro. Ficou sob minha responsabilidade converter a tabela em matriz. Havíamos coletado milhares de amostras, gerando uma tabela com milhares de linhas. Estava no começo da graduação, não entendia quase nada de excel, o que dirá de programação. Fui pra casa, peguei um bloco de notas, abri o excel e fui transformando a tabela na mão, avaliando linha por linha em qual hospedeiro cada parasito foi encontrado, e fazendo a soma. Demorei 2 dias (o final de semana inteiro).
Cheguei no laboratório na segunda-feira, contei minha saga para o pós-doutorando que me orientava, e ele: “Mas porque você não usou tabela dinâmica no excel?”. Não fazia a menor idéia do que ele estava falando. Ele sentou, deu dois cliques no excel e fez, em segundos, o trabalho que perdi o final de semana fazendo. Nesse dia me prometi nunca mais fazer uma tarefa sem pelo menos tentar automatizá-la antes.
Nesta seção vamos um passo além do excel, aprendendo a usar um pacote (reshape2) do R que realiza exatamente este tipo de conversão, nos dois sentidos: tabelas (chamadas de long format) para “matrizes” (wide format) e vice versa. Usaremos o dataframe que geramos na seção anterior.
O primeiro passo é instalar (caso não esteja instalado) e carregar o reshape2.
if(!require(reshape2)){install.packages("reshape2")}
## Carregando pacotes exigidos: reshape2
##
## Attaching package: 'reshape2'
## The following objects are masked from 'package:data.table':
##
## dcast, melt
library(reshape2)
Este pacote possui, basicamente, dois grupos de funções: dcast (que converte long para wide), e melt (que converte wide para long). Começemos pela dcast.
Na função dcast você informa a tabela que será convertida, qual coluna irá para linhas, qual irá para colunas, qual será usada para preencher as células da matriz, e qual função matemática/lógica será utilizada para agregar os dados. Exemplificando com a tabela que produzimos anteriormente. Se queremos construir uma matriz de interação, onde linhas são espécies de hospedeiros, colunas são espécies de parasitos, e as células representam o número total de indivíduos daquela espécie de parasito encontrada naquela espécie de hospedeiro, a função fica assim:
im <- dcast(data = na.omit(df), formula = Host_sp ~ Para_sp, value.var = "N_par", fun.aggregate = sum)
# Lembrando do uso do [ , ], consegue entender o que as duas linhas de código abaixo estão fazendo?
rownames(im) <- im[,1]
im <- im[,-1]
head(im)
O argumento data informa a tabela que será transformada. Perceba que usamos outra função básica do R extremamente útil, a na.omit, que exclui do dataframe todas as linhas com valores faltantes. Precisamos fazer isto para que hospedeiros não infectados (com NA na coluna Para_sp) não sejam considerados. O argumento formula usa a sintaxe básica do R para definir a estrutura da matriz (linhas antes do ~ / colunas depois do ~). O argumento value.var informa qual coluna da tabela contém os valores que serão usados para preencher as células da matriz, e o argumento fun.aggregate informa como estes valores serão agregados quando o mesmo par de linha/coluna (hospedeiro/parasito no caso) aparecer mais de uma vez.
E se, ao invés do número total de parasitos encontrados, quiséssemos a frequência de interação entre cada par parasito-hospedeiro. Ou seja, a quantidade de vezes que aquela espécie de parasito foi encontrada naquela espécie de hospedeiro. Bastaria, neste caso, trocar a função de agregação. Como vocês fariam neste caso?
Outra opção, ainda, seria montar uma matriz de presença/ausência, nas quais as células são iguais a 1 quando uma interação entre aquele par de espécies foi observada pelo menos uma vez, e 0 quando não foi observada nenhuma vez. De novo, existem várias formas de se fazer isto. Você poderia definir uma função de agregação que faça o trabalho (outro para casa), ou usar a matriz que construímos acima, convertendo valores > 0 para 1. O código abaixo faz isto.
im_pa <- as.data.frame(1*(im>0))
Consegue entender o código que usamos para converter a matriz ponderada em uma matriz de presença-ausência? De novo usamos a sintaxe básica do R. O que este código diz (de trás para frente) é:
(im>0): “quais valores de im são maiores que zero?”. Rode apenas esta parte do código e veja o que ele retorna. Inclusive, está é uma ótima dica: sempre que não conseguir entender o que um código está fazendo, divida-o em partes e vá rodando cada parte, de trás para frente, por vez. Olhe os outputs e tente entender os passos intermediários.
Se você fez corretamente, recebeu de resposta uma matriz com as mesmas dimensões da sua matriz de interação, mas na qual as células estão preenchidas com TRUE e FALSE; sendo TRUE para valores maiores 0 e FALSE para valores iguais a 0.
Dentro do R, quando usados em uma equação,
\(TRUE = 1\)
\(FALSE = 0\).
Logo, se multiplicarmos esta matriz lógica de TRUE e FALSE por 1, o resultado será
\(1 * TRUE = 1\)
\(1 * FALSE = 0\).
Ou seja, uma matriz de presença e ausência. Que é exatamente o que fizemos em:
1*(im>0).
Pronto. Temos nossa matriz de presença e ausência.
head(im_pa)
Assim como na tabela dinâmica, podemos agrupar os dados com base em mais de uma variável nas linhas. Para isto, basta modificar ligeiramente o argumento formula, adicionando todas as variáveis de interesse antes do ~.
im_l <- dcast(data = na.omit(df), formula = Host_sp + Local ~ Para_sp, value.var = "N_par", fun.aggregate = sum)
head(im_l)
Outra funcionalidade do reshape2 é a conversão oposta, de uma matrix (formato wide) para tabela (formato long). Para realizar tal transformação, basta usar a função melt. Ao aplicar esta função às matrizes de interação que produzimos acima, tais matrizes serão re-convertidas em tabelas com a estrutura que produzimos originalmente.
Se, na sua matriz de interação os nomes das espécies hospedeiras estão como nomes de linhas, como fizemos na matriz im, e não guardados em uma das colunas da matriz (como fizemos em im_l), basta aplicar o melt à matriz, sem se preocupar com os argumento da função.
df_melted <- melt(data = as.matrix(im))
colnames(df_melted) <- c("Host_sp","Para_sp","N_int")
head(df_melted)
Perceba, porém, dois pontos importantes.
Primeiro, na nossa tabela original (df), cada linha representava o número de parasitos reportados em um indivíduo de hospedeiro em um local. Quando convertemos este dataframe na matriz im, ignoramos o local, e agrupamos todas as interações reportadas em um par hospedeiro-parasito. Logo, ao reconvertermos a matriz im em uma tabela formato long, a informação dos locais foi perdida, e cada linha da tabela representa todas as interações entre um dado par hospedeiro-parasito, em todos os locais.
Segundo, note que a tabela que acabamos de produzir contempla todos os pares de interação hospedeiro-parasito possíveis, mesmo aqueles em que nenhuma interação foi reportada entre eles. Isto ocorre porque ela converte cada célula da matriz em uma linha, mesmo as células iguais a 0. Duas formas de corrigir isto: (i) filtrar a tabela, eliminando as linhas iguais a 0 na coluna N_int:
df_melted <- subset(df_melted, N_int != 0) # Filtrando apenas para linhas nas quais N_int é diferente (!=) de 0.
head(df_melted)
NA e pedir à função melt que ignore os NA:im[im == 0] <- NA # Convertendo 0 para NA
df_melted <- melt(data = as.matrix(im), na.rm = T) # Transformando a matriz, ignorando os NA.
head(df_melted)
Pode ser o caso, porém, como fizemos na matriz im_l que os nomes das espécies e locais sejam colunas da matriz. Neste caso, precisamos informar à função melt quais são estas colunas. Aproveite para definir os nomes das colunas na tabela usando a própria função. As colunas das variáveis identificadoras manterão seus nomes. Já as colunas onde os nomes das espécies de parasito e os números de interação serão salvas podem ser escolhidas dentro da função, usando, respectivamente, os argumentos variable.name e value.name.
im_l[im_l == 0] <- NA # Convertendo 0 para NA
df_l_melted <- melt(data = im_l, id.vars = c("Host_sp","Local"), variable.name = "Pars_sp",
value.name = "N_int", na.rm = T)
head(df_l_melted)
Porque a tabela (df_l_melted) que criamos a partir da matriz im_l tem menos linhas que a tabela original (df)? Dica: existem dois motivos, cada um responsável por um conjunto a menos de linhas. Existiam hospedeiros sem infecção por nenhum parasito na tabela original? Existiam casos de mais de um indivíduo da mesma espécie hospedeira infectado pela mesma espécie de parasito no mesmo local? Estas informações foram perdidas ou agregadas quando construímos a matriz de interação?
R é uma linguagem antiga. Sua sintáxe básica foi construída quando a ciência de dados começava a dar seus primeiros passos. De lá para cá muita coisa avançou na forma como manipulamos e visualizamos dados. Modificar as funções básicas da linguagem demandaria reconstruir milhões de linhas de código. Assim, as inovações e adaptações são, geralmente, feitas via novos pacotes. A família de pacotes Tidyverse incorpora várias destas evoluções em um conjunto de pacotes que buscam facilitar e otimizar a forma como manipulamos e visualizamos dados do R.
Dentre estes pacotes, estão:
ggplot2
dplyr
tidyr
tibble
dentre outros.
O domínio destes pacotes demanda horas e horas de leitura, trabalho e experimentação com suas funções. Nosso objetivo nesta seção é apresentar uma visão geral de um deles (dplyr), suas principais funções e funcionalidades, de forma a estimulá-lo a explorar por conta própria o universo tydiverse. Vamos começar instalando os pacotes que usaremos aqui.
if(!require(tidyverse)){install.packages("tidyverse")}
## Carregando pacotes exigidos: tidyverse
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.1 ──
## ✓ ggplot2 3.3.6 ✓ purrr 0.3.4
## ✓ tibble 3.1.8 ✓ stringr 1.4.0
## ✓ tidyr 1.2.0 ✓ forcats 0.5.1
## ✓ readr 1.4.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::between() masks data.table::between()
## x dplyr::filter() masks stats::filter()
## x dplyr::first() masks data.table::first()
## x dplyr::group_rows() masks kableExtra::group_rows()
## x dplyr::lag() masks stats::lag()
## x dplyr::last() masks data.table::last()
## x purrr::transpose() masks data.table::transpose()
library(tidyverse)
if(!require(dplyr)){install.packages("dplyr")}
library(dplyr)
Até o momento, tratamos dados no R como dataframes, ou matrizes. Um formato recente, otimizado para manipulação e visualização de dados, são as tibbles. Uma tibble é, essencialmente, um dataframe com funcionalidades que facilitam nossa vida quando manipulando os dados.
Veja por si mesmo. Vamos carregar duas tabelas de dados do pacote vegan. A tabela dune contem uma matriz de ocorrência de espécies (colunas) em diferentes locais (linhas). A tabela dune.taxon contem as informações taxonômicas de cada espécie (genêro, família, etc).
if(!require(vegan)){install.packages("vegan")}
## Carregando pacotes exigidos: vegan
## Carregando pacotes exigidos: permute
## Carregando pacotes exigidos: lattice
## This is vegan 2.5-7
library(vegan)
data("dune")
data("dune.taxon")
head(dune)
head(dune.taxon)
Assim como na seção anterior, vamos converter as matrizes de ocorrência e de variáveis ambientais em tabelas formato long usando o reshape2. Porém, desta vez, não vamos excluir da tabela em formato long as ocorrências não observadas de uma espécie em um local. Ou seja, não vamos excluir as linhas nas quais a coluna que informa quantos indivíduos daquela espécie foram reportadas naquele local for igual a 0.
dune_df <- melt(as.matrix(dune)) # Convertendo a matriz wide em tabela long
colnames(dune_df) <- c("Local","Especie","N") # renomeando as colunas
Vamos, agora, observar a diferença entre um dataframe e uma tibble. Transforme o objeto dune_df que acabamos de criar em uma tibble usando as função as_tibble.
dune_df <- as_tibble(dune_df)
print(dune_df)
## # A tibble: 600 × 3
## Local Especie N
## <int> <fct> <dbl>
## 1 1 Achimill 1
## 2 2 Achimill 3
## 3 3 Achimill 0
## 4 4 Achimill 0
## 5 5 Achimill 2
## 6 6 Achimill 2
## 7 7 Achimill 2
## 8 8 Achimill 0
## 9 9 Achimill 0
## 10 10 Achimill 4
## # … with 590 more rows
Perceba que ao printar o objeto você recebe automaticamente as dimensões (número de linhas e colunas) do mesmo, além de uma descrição de qual o tipo de variável em cada coluna (interger, fator, etc). Outro detalhe importante é que, para tibble, por padrão, apenas 10 linhas são mostradas. Por fim, se a sua tibble tivesse dezenas de colunas, o print automaticamente mostraria apenas as colunas que cabem na tela, com informação abaixo sobre as não mostradas (teste com uma tibble maior de para casa).
Mas, isto não é tudo. O mais importante: um objeto tipo tibble guarda informações que serão uteis nas manipulações de dados feitas pelo dplyr, como veremos abaixo.
No momento temos duas tabelas com informações diferentes sobre as espécies. A tabela dune_df, com as ocorrências das espécies nos locais em formato long, e a tabela dune.taxon com as informações taxonômicas de cada espécies. Como fazemos para juntas as duas tabelas em uma tabela única, adicionado as informações taxonômicas às informações de ocorrência? Basta usar uma família de funções extremamente úteis do dplyr, chamadas join. Aqui vamos focar nas left_join, que é a mais usada. Mas fica de sugestão ler a documentação das demais: right_join, full_join e inner_join, e entender o que elas estão fazendo.
A left_join une as colunas de duas tabelas (A e B) usando uma coluna existente em ambas como referência para produzir uma nova tabela C, com todas as informações presentes em A e B. Ela se chama left_join pois ela mantem na tabela C todos os elementos da coluna de referência existentes na tabela A, mas não necessariamente da tabela B.
Exemplificando para ficar mais claro. No nosso caso, a tabela A é a tabela com as ocorrências, a tabela B é a tabela com as informações taxonômicas, e a coluna com os nomes das espécies é a coluna de referência comum às duas tabelas. Neste caso, se existirem espécies na tabela A que não estão na tabela B (ou sej, se tivermos informação de ocorrência de uma dada espécie, mas não tivermos informação sobre sua taxônomia), as linhas referentes à taxonomia de tais espécies ficariam como NULL na tabela produzida (tabela C). Já, ao contrário, se tivermos espécies na tabela B não presentes na tabela A (ou seja, se tivermos informações taxônomicas de espécies que não estão presentes na tabela de ocorrências), tais espécies são ignoradas na construção da tabela final.
Vamos usar a função e ver o que acontece. Mas antes, perceba que o nome das espécies é o nome das linhas na coluna dune.taxon, e não uma coluna. Precisamos, antes, arrumar isto, e também converter a dune.taxon para tibble.
dune.taxon$Especie <- rownames(dune.taxon)
dune.taxon <- as_tibble(dune.taxon)
print(dune.taxon)
## # A tibble: 30 × 6
## Genus Family Order Superorder Subclass Especie
## <chr> <chr> <chr> <chr> <chr> <chr>
## 1 Achillea Asteraceae Asterales Asteranae Magnoliidae Achimill
## 2 Agrostis Poaceae Poales Lilianae Magnoliidae Agrostol
## 3 Aira Poaceae Poales Lilianae Magnoliidae Airaprae
## 4 Alopecurus Poaceae Poales Lilianae Magnoliidae Alopgeni
## 5 Anthoxanthum Poaceae Poales Lilianae Magnoliidae Anthodor
## 6 Bellis Asteraceae Asterales Asteranae Magnoliidae Bellpere
## 7 Bromus Poaceae Poales Lilianae Magnoliidae Bromhord
## 8 Chenopodium Amaranthaceae Caryophyllales Caryophyllanae Magnoliidae Chenalbu
## 9 Cirsium Asteraceae Asterales Asteranae Magnoliidae Cirsarve
## 10 Comarum Rosaceae Rosales Rosanae Magnoliidae Comapalu
## # … with 20 more rows
Pronto, agora podemos usar a left_join para unir as duas tabelas. No caso, como as duas tabelas têm uma coluna com o mesmo nome, a função entende implicitamente qual a coluna de referência. Se não fosse o caso, bastaria usar o argumento by, e informar quais colunas devem ser usadas no match, da seguinte forma: left_join(A, B, by = c(“NomedacolunaA” = “NomedacolunaB”).
dune.all <- left_join(dune_df, dune.taxon)
## Joining, by = "Especie"
print(dune.all)
## # A tibble: 600 × 8
## Local Especie N Genus Family Order Superorder Subclass
## <int> <chr> <dbl> <chr> <chr> <chr> <chr> <chr>
## 1 1 Achimill 1 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 2 2 Achimill 3 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 3 3 Achimill 0 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 4 4 Achimill 0 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 5 5 Achimill 2 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 6 6 Achimill 2 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 7 7 Achimill 2 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 8 8 Achimill 0 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 9 9 Achimill 0 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 10 10 Achimill 4 Achillea Asteraceae Asterales Asteranae Magnoliidae
## # … with 590 more rows
Outra função extremamente útil no dplyr é a group_by. Quando acoplada com outras funções, ela permite que manipulemos os dados de forma extremamente eficiente. Mas, antes de usá-la em conjunto com outras função, vamos usá-la separadamente. Quando usada separadamente, ela apenas adiciona um “meta_dado” à tibble, uma informação que será usada por outras funções do dplyr. Para ver como isto ocorre, vamos agrupar os dados por espécie na tabela dune.all e printar a tabela.
dune.all <- group_by(dune.all, Especie)
print(dune.all)
## # A tibble: 600 × 8
## # Groups: Especie [30]
## Local Especie N Genus Family Order Superorder Subclass
## <int> <chr> <dbl> <chr> <chr> <chr> <chr> <chr>
## 1 1 Achimill 1 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 2 2 Achimill 3 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 3 3 Achimill 0 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 4 4 Achimill 0 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 5 5 Achimill 2 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 6 6 Achimill 2 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 7 7 Achimill 2 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 8 8 Achimill 0 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 9 9 Achimill 0 Achillea Asteraceae Asterales Asteranae Magnoliidae
## 10 10 Achimill 4 Achillea Asteraceae Asterales Asteranae Magnoliidae
## # … with 590 more rows
Quando printamos a tibble dune.all agora, uma nova informação é adiciona: Groups: Especie (30). Esta informação está nos dizendo que a tibble agora entende que qualquer função de agregação que usarmos levará isto em conta. Existem diferentes funções de agregação no dplyr. Vamos trabalhar aqui com a summarise, que é uma função que sumariza valores de uma coluna de interesse, usando função estatísticas de sumário (média, desvio padrão, máximo e mínimo, etc), levando em conta o agrupamento definido para os dados.
Suponha, por exemplo, que queiramos saber o número médio de indivíduos reportados para cada espécie em todos os locais. Basta aplicar o código abaixo:
dune.N_medio_sp <- summarise(dune.all, N_medio = mean(N))
print(dune.N_medio_sp)
## # A tibble: 30 × 2
## Especie N_medio
## <chr> <dbl>
## 1 Achimill 0.8
## 2 Agrostol 2.4
## 3 Airaprae 0.25
## 4 Alopgeni 1.8
## 5 Anthodor 1.05
## 6 Bellpere 0.65
## 7 Bracruta 2.45
## 8 Bromhord 0.75
## 9 Callcusp 0.5
## 10 Chenalbu 0.05
## # … with 20 more rows
Note que não precisamos informar que o sumário deve ser feito por espécie, uma vez que a informação de agrupamento por espécie já está implicita na tibble dune.all após usarmos a função group_by.
Como você faria para saber o desvio padrão médio de indivíduos por espécie em todos os locais?
Um dos principais ganhos ao usar o universo tidyverse é otimizar nossos códigos. E uma das melhores formas de se fazer isto é usando um operador desenvolvido dentro do tidyverse, chamado pipe (%>%), o qual serve para encadear funções umas nas outras. Ou seja, pegar o output de uma função e usar como input na seguinte. Como a maior parte dos outputs in inputs no tidyverse são tabelas no formato tibble, o %>% permite que diminuamos consideravelmente o tamanho dos nossos códigos, e os tornem mais intuitvos. Vamos exemplificar para ficar mais claro.
Para realizar as duas tarefas que realizamos nas seções anteriores (agrupar os dados por espécie e sumarizar o N médio), usando o %>%, ficaria assim:
dune.N_medio_sp <- dune.all %>%
group_by(Especie) %>%
summarise(N_medio = mean(N))
print(dune.N_medio_sp)
## # A tibble: 30 × 2
## Especie N_medio
## <chr> <dbl>
## 1 Achimill 0.8
## 2 Agrostol 2.4
## 3 Airaprae 0.25
## 4 Alopgeni 1.8
## 5 Anthodor 1.05
## 6 Bellpere 0.65
## 7 Bracruta 2.45
## 8 Bromhord 0.75
## 9 Callcusp 0.5
## 10 Chenalbu 0.05
## # … with 20 more rows
O que o código acima está dizendo é:
Linha 1) dune.all %>%: Pegue a tabela dune.all e use-a abaixo (%>%)
Linha 2) group_by(Especie) %>%: Agrupe-a por espécie e use o resultado abaixo (%>%)
Linha 3) summarise(N_medio = mean(N)): sumarize a tabela agrupada por espécie pelo N médio em uma coluna chamada N_medio
O resultado é, então, salvo no objeto dune.N_medio_sp definido no começo do código.
E se além do N_médio, quiséssemos o desvio padrão médio do N de indivíduos por espécie, e o número de locais em que uma espécie ocorreu? Bastaria informar no summarise, definindo funções que calculem estas métricas:
dune.N_medio_sp <- dune.all %>%
group_by(Especie) %>%
summarise(N_medio = mean(N), N_sd = sd(N), N_locais = sum(N >= 1))
print(dune.N_medio_sp)
## # A tibble: 30 × 4
## Especie N_medio N_sd N_locais
## <chr> <dbl> <dbl> <int>
## 1 Achimill 0.8 1.24 7
## 2 Agrostol 2.4 2.68 10
## 3 Airaprae 0.25 0.786 2
## 4 Alopgeni 1.8 2.63 8
## 5 Anthodor 1.05 1.70 6
## 6 Bellpere 0.65 1.04 6
## 7 Bracruta 2.45 1.90 15
## 8 Bromhord 0.75 1.41 5
## 9 Callcusp 0.5 1.24 3
## 10 Chenalbu 0.05 0.224 1
## # … with 20 more rows
A função sd calcula o desvio padrão de uma variável, enquanto a função sum(N >= 1) está pedindo para que o summarise retorne, em uma nova coluna chamada N_locais, o número de locais em que o N de uma espécie é maior ou igual a 1 (ou seja, o número de locais em que aquela espécie definitivamente ocorre).
Imagine, agora, que gostaríamos de categorizar as espécies em “distribuição ampla vs. restritra” e “abundantes vs. raras”. Sendo amplamente distribuidas aquelas que ocorrem em mais locais do que a média de ocorrências geral das espécies, a abundantes aquelas com abundância média maior que a média das abundâncias médias geral das espécies. Ou seja, precisamos de mais duas colunas na tabela, que serão preenchidas a partir dos valores nas colunas N_locais e N_medio. Para isto, basta usarmos a função mutate junto ao operador pipe, como abaixo.
dune.N_medio_sp <- dune.all %>%
group_by(Especie) %>%
summarise(N_medio = mean(N), N_sd = sd(N), N_locais = sum(N >= 1)) %>%
mutate(Distribuicao = ifelse(N_locais - mean(N_locais) > 0, "Ampla","Restrita"),
Abundancia = ifelse(N_medio - mean(N_medio) > 0, "Abundante","Rara"))
print(dune.N_medio_sp)
## # A tibble: 30 × 6
## Especie N_medio N_sd N_locais Distribuicao Abundancia
## <chr> <dbl> <dbl> <int> <chr> <chr>
## 1 Achimill 0.8 1.24 7 Ampla Rara
## 2 Agrostol 2.4 2.68 10 Ampla Abundante
## 3 Airaprae 0.25 0.786 2 Restrita Rara
## 4 Alopgeni 1.8 2.63 8 Ampla Abundante
## 5 Anthodor 1.05 1.70 6 Restrita Rara
## 6 Bellpere 0.65 1.04 6 Restrita Rara
## 7 Bracruta 2.45 1.90 15 Ampla Abundante
## 8 Bromhord 0.75 1.41 5 Restrita Rara
## 9 Callcusp 0.5 1.24 3 Restrita Rara
## 10 Chenalbu 0.05 0.224 1 Restrita Rara
## # … with 20 more rows
Perceba que as possibilidades são infinitas, e dependem unicamente da sua critividade mais domínio da linguagem. À media que vamos avançando, o código vai ficando cada vez mais elaborado. Não se espera que você entenda tudo que está sendo feito, cada passo e cada função, hoje. Mas, dê um help na função ifelse, entenda o que ela está fazendo e como ela se acopla dentro da função mutate.
Que tal reordenarmos a tabela de forma a que espécies amplamente distribuidas e mais abundantes fiquem nas linhas superiores da tabela. Basta usar a função arrange, informando quais colunas devem ser usadas para ordenar os dados. Perceba que estamos re-escrevendo todo o código todas as vezes para deixar claro que o pipe permite que se realiza várias transformações em cadeia.
dune.N_medio_sp <- dune.all %>%
group_by(Especie) %>%
summarise(N_medio = mean(N), N_sd = sd(N), N_locais = sum(N >= 1)) %>%
mutate(Distribuicao = ifelse(N_locais - mean(N_locais) > 0, "Ampla","Restrita"),
Abundancia = ifelse(N_medio - mean(N_medio) > 0, "Abundante","Rara")) %>%
arrange(N_locais, N_medio)
print(dune.N_medio_sp)
## # A tibble: 30 × 6
## Especie N_medio N_sd N_locais Distribuicao Abundancia
## <chr> <dbl> <dbl> <int> <chr> <chr>
## 1 Chenalbu 0.05 0.224 1 Restrita Rara
## 2 Cirsarve 0.1 0.447 1 Restrita Rara
## 3 Empenigr 0.1 0.447 1 Restrita Rara
## 4 Comapalu 0.2 0.616 2 Restrita Rara
## 5 Airaprae 0.25 0.786 2 Restrita Rara
## 6 Vicilath 0.2 0.523 3 Restrita Rara
## 7 Hyporadi 0.45 1.23 3 Restrita Rara
## 8 Trifprat 0.45 1.23 3 Restrita Rara
## 9 Callcusp 0.5 1.24 3 Restrita Rara
## 10 Salirepe 0.55 1.39 3 Restrita Rara
## # … with 20 more rows
Alguma coisa deu errado? As espécies amplamente distribuidas e abundantes ficaram acima?
O problema é que, por padrão, a arrange ordena do menor para o maior. Para modificar isto, basta adicionar a função desc antes do nome da coluna, como abaixo:
dune.N_medio_sp <- dune.all %>%
group_by(Especie) %>%
summarise(N_medio = mean(N), N_sd = sd(N), N_locais = sum(N >= 1)) %>%
mutate(Distribuicao = ifelse(N_locais - mean(N_locais) > 0, "Ampla","Restrita"),
Abundancia = ifelse(N_medio - mean(N_medio) > 0, "Abundante","Rara")) %>%
arrange(desc(N_locais), desc(N_medio))
print(dune.N_medio_sp)
## # A tibble: 30 × 6
## Especie N_medio N_sd N_locais Distribuicao Abundancia
## <chr> <dbl> <dbl> <int> <chr> <chr>
## 1 Scorautu 2.7 1.56 18 Ampla Abundante
## 2 Trifrepe 2.35 1.90 16 Ampla Abundante
## 3 Bracruta 2.45 1.90 15 Ampla Abundante
## 4 Poaprat 2.4 1.85 14 Ampla Abundante
## 5 Poatriv 3.15 2.81 13 Ampla Abundante
## 6 Lolipere 2.9 2.83 12 Ampla Abundante
## 7 Agrostol 2.4 2.68 10 Ampla Abundante
## 8 Alopgeni 1.8 2.63 8 Ampla Abundante
## 9 Planlanc 1.3 1.95 7 Ampla Abundante
## 10 Sagiproc 1 1.56 7 Ampla Rara
## # … with 20 more rows
Uma pergunta comum em ecologia é sobre a relação entre abundância e distribuição. Será que as espécies mais abundantes são mais amplamente distribuidas? Vamos testar esta hipótese, visualmente, no nosso bancod de dados?
Para isto, usaremos outro pacote do multiverso tydiverse, o ggplot2.
if(!require(ggplot2)){install.packages("ggplot2")}
library(ggplot2)
ggplot(dune.N_medio_sp, aes(x = N_medio, y = N_locais)) +
geom_point(aes(colour = Distribuicao, shape = Abundancia), size = 5) +
geom_smooth(method = "lm", colour = "black") +
theme_bw() +
xlab(label = "N médio") +
ylab(label = "Número de locais") +
theme(axis.title = element_text(size = 15), axis.text = element_text(size = 12.5))
## `geom_smooth()` using formula 'y ~ x'
Mas, esta não é uma aula de visualização de dados. Logo, o código do ggplot2 fica para outra hora :)