Introdução à Análise de Dados no R

Desenho Experimental e Análise Avançada de Dados Ecológicos

João O. Santos

ISPA

2025-10-07

O R é Fácil!

Passos Básicos

  1. Carregar os pacotes úteis
  2. Redefinir opções, se for necessário
  3. Importar os dados
  4. Resumir e visualizar os dados
  5. Ajustar o modelo ou os modelos
  6. Comparar ou testar os modelos

Passos Básicos

  • Carregar os pacotes úteis
    • importar apenas os que usamos no script
  • Redefinir opções
    • escolher valores adequados à nossa análise
  • Importar os dados
    • ler os dados de um ficheiro para um objecto
  • Resumir e visualizar os dados
    • calcular estatísticas descritivas

Passos Básicos

  • Ajustar o modelo ou os modelos

  • Comparar ou testar os modelos

  • Interpretar os resultados

  • Repetir até a análise responder à pergunta—ou até aparecer outra pergunta

Quando Já Estivermos à Vontade

  • Podemos usar RMarkdown ou Quarto para gerar e actualizar relatórios automaticamente

  • Os resultados mudam; o relatório acompanha-os sem uma tarde de copiar-colar, esse conhecido método estatístico

Um Script de Produção Mostra os Passos

# Carregar pacotes
# Definir opções
# Importar dados
# Preparar dados
# Resumir e visualizar
# Ajustar modelos
# Inspeccionar e guardar resultados
  • As secções tornam a análise mais fácil de percorrer e rever

  • Um projecto pequeno pode reuni-las num único script

  • Num projecto maior, cada fase pode ter o seu próprio script

Carregar Pacotes

Comecemos por um exemplo simples.

# Agrupar e resumir dados.
library(dplyr)
# Construir gráficos.
library(ggplot2)
# Usar os dados dos pinguins.
library(palmerpenguins)

Redefinir Opções

# Definir os contrastes para variáveis categoriais.
options(contrasts = c("contr.sum", "contr.poly"))

Importar Dados

# Habitualmente, leríamos os dados com uma função read.alguma_coisa().
# ds <- read.csv(file.choose())
# Por agora, trabalhamos com os dados que o pacote já fornece.
ds <- penguins
  • Assim, podemos concentrar-nos no R antes de discutirmos formatos de ficheiros e caminhos que misteriosamente funcionam só no computador do autor

O R Transforma Valores

mean(c(2, 4, 6, 8))
[1] 5
  • c() devolve um vector

  • Esse vector torna-se um argumento de mean()

  • mean() devolve outro valor

  • Uma análise combina muitas transformações deste género

O Mesmo Cálculo: Funções «Encastradas»

summarise(group_by(ds, species), N = n(), M = mean(body_mass_g, na.rm = TRUE), SD = sd(body_mass_g, na.rm = TRUE))
species N M SD
Adelie 152 3700.662 458.5661
Chinstrap 68 3733.088 384.3351
Gentoo 124 5076.016 504.1162
  • Lemos da chamada mais interior para a mais exterior

  • Código compacto nem sempre é código fácil de ler

O Mesmo Cálculo: Parêntesis Alinhados

summarise(
    group_by(ds, species),
    N = n(),
    M = mean(body_mass_g, na.rm = TRUE),
    SD = sd(body_mass_g, na.rm = TRUE)
)
species N M SD
Adelie 152 3700.662 458.5661
Chinstrap 68 3733.088 384.3351
Gentoo 124 5076.016 504.1162
  • A indentação mostra quais são os argumentos de cada função

  • Continuamos a executar group_by() antes de summarise()

O Mesmo Cálculo: Resultados Intermédios

grouped_ds <- group_by(ds, species)

species_descs <- summarise(
    grouped_ds,
    N = n(),
    M = mean(body_mass_g, na.rm = TRUE),
    SD = sd(body_mass_g, na.rm = TRUE)
)

species_descs
species N M SD
Adelie 152 3700.662 458.5661
Chinstrap 68 3733.088 384.3351
Gentoo 124 5076.016 504.1162
  • Podemos inspeccionar ou reutilizar grouped_ds

  • É uma forma útil de encontrar erros numa análise mais longa

O Mesmo Cálculo: Pipe Nativo

species_descs <- ds |>
    group_by(species) |>
    summarise(
        N = n(),
        M = mean(body_mass_g, na.rm = TRUE),
        SD = sd(body_mass_g, na.rm = TRUE)
    )

species_descs
species N M SD
Adelie 152 3700.662 458.5661
Chinstrap 68 3733.088 384.3351
Gentoo 124 5076.016 504.1162
  • Pipe significa «tubo»: transporta um valor de um passo para o seguinte

  • Podemos ler |> como «e depois»

  • O valor à esquerda torna-se o primeiro argumento da função à direita

  • Este é o pipe nativo do R, não o operador %>% do tidyverse

Do «Tubo» aos Parêntesis

ds |>
    group_by(species) |>
    summarise(...)

corresponde, nesta cadeia simples, a:

summarise(
    group_by(ds, species),
    ...
)
  • O R transforma a primeira expressão na segunda quando analisa o código

  • Este tipo de sintaxe alternativa chama-se syntactic sugar

  • Escolhemos a forma que torna a transformação mais fácil de seguir

Resumir os Dados

summarise(group_by(ds, sex),
          N = n(),
          M = mean(body_mass_g, na.rm = TRUE),
          SD = sd(body_mass_g, na.rm = TRUE))
sex N M SD
female 165 3862.273 666.1720
male 168 4545.685 787.6289
NA 11 4005.556 679.3584

Resumir os Dados

summarise(group_by(ds, species),
          N = n(),
          M = mean(body_mass_g, na.rm = TRUE),
          SD = sd(body_mass_g, na.rm = TRUE))
species N M SD
Adelie 152 3700.662 458.5661
Chinstrap 68 3733.088 384.3351
Gentoo 124 5076.016 504.1162
  • Conseguem encontrar a diferença?

Resumir os Dados

  • Conseguem encontrar a diferença?
summarise(group_by(ds, species, sex),
          N = n(),
          M = mean(body_mass_g, na.rm = TRUE),
          SD = sd(body_mass_g, na.rm = TRUE))
species sex N M SD
Adelie female 73 3368.836 269.3801
Adelie male 73 4043.493 346.8116
Adelie NA 6 3540.000 477.1661
Chinstrap female 34 3527.206 285.3339
Chinstrap male 34 3938.971 362.1376
Gentoo female 58 4679.741 281.5783
Gentoo male 61 5484.836 313.1586
Gentoo NA 5 4587.500 338.1937

Resumir os Dados

  • Conseguem encontrar a diferença?
summarise(group_by(ds, species, sex),
          N = n(),
          Min = min(body_mass_g, na.rm = TRUE),
          Max = max(body_mass_g, na.rm = TRUE),
          M = mean(body_mass_g, na.rm = TRUE),
          SD = sd(body_mass_g, na.rm = TRUE))
species sex N Min Max M SD
Adelie female 73 2850 3900 3368.836 269.3801
Adelie male 73 3325 4775 4043.493 346.8116
Adelie NA 6 2975 4250 3540.000 477.1661
Chinstrap female 34 2700 4150 3527.206 285.3339
Chinstrap male 34 3250 4800 3938.971 362.1376
Gentoo female 58 3950 5200 4679.741 281.5783
Gentoo male 61 4750 6300 5484.836 313.1586
Gentoo NA 5 4100 4875 4587.500 338.1937

Resumir os Dados

summarise(group_by(ds, species, sex),
          N = n(),
          Min = mni(body_mass_g, na.rm = TRUE),
          Max = max(body_mass_g, na.rm = TRUE),
          M = mean(body_mass_g, na.rm = TRUE),
          SD = sd(body_mass_g, na.rm = TRUE))
  • Há um erro de nome/avaliação. Conseguem encontrá-lo e corrigi-lo?

Resumir os Dados

summarise(group_by(ds, species, sex),
          N = n(),
          Min = max(body_mass_g, na.rm = TRUE),
          Max = max(body_mass_g, na.rm = TRUE),
          M = mean(body_mass_g, na.rm = TRUE),
          SD = sd(body_mass_g, na.rm = TRUE))
species sex N Min Max M SD
Adelie female 73 3900 3900 3368.836 269.3801
Adelie male 73 4775 4775 4043.493 346.8116
Adelie NA 6 4250 4250 3540.000 477.1661
Chinstrap female 34 4150 4150 3527.206 285.3339
Chinstrap male 34 4800 4800 3938.971 362.1376
Gentoo female 58 5200 5200 4679.741 281.5783
Gentoo male 61 6300 6300 5484.836 313.1586
Gentoo NA 5 4875 4875 4587.500 338.1937
  • Há um erro semântico. Conseguem encontrá-lo e corrigi-lo?

Reutilizar uma Função Pequena

mean_no_na <- function(x) {
    mean(x, na.rm = TRUE)
}

mean_no_na(ds$body_mass_g)
[1] 4201.754
  • O nome descreve a decisão tomada sobre os valores omissos

  • Definimos essa decisão uma vez e podemos testá-la separadamente

  • Uma função recebe um valor e devolve outro

Usar uma Função Dentro de um Pipe

ds |>
    group_by(species, sex) |>
    summarise(
        N = sum(!is.na(body_mass_g)),
        M = mean_no_na(body_mass_g),
        SD = sd(body_mass_g, na.rm = TRUE)
    )
species sex N M SD
Adelie female 73 3368.836 269.3801
Adelie male 73 4043.493 346.8116
Adelie NA 5 3540.000 477.1661
Chinstrap female 34 3527.206 285.3339
Chinstrap male 34 3938.971 362.1376
Gentoo female 58 4679.741 281.5783
Gentoo male 61 5484.836 313.1586
Gentoo NA 4 4587.500 338.1937
  • dplyr::summarise() continua a ser a ferramenta que resume os dados

  • O pipe nativo apenas organiza a passagem do resultado entre os passos

As Funções Também Podem Ser Argumentos

ds$body_mass_g |>
    split(ds$species) |>
    lapply(mean_no_na)
$Adelie
[1] 3700.662

$Chinstrap
[1] 3733.088

$Gentoo
[1] 5076.016
  • split() devolve um vector por espécie

  • lapply() recebe mean_no_na como argumento

  • Separamos os dados, a operação e a repetição

Um Estilo Funcional

Privilegiamos transformações pequenas que:

  • recebem os valores necessários através de argumentos;

  • devolvem um resultado;

  • podem ser testadas isoladamente;

  • podem ser combinadas com outras transformações;

  • evitam alterações escondidas em objectos alheios.

  • O objectivo é obter clareza e reutilização, não enfiar um pipe em todas as linhas

Visualizar os Dados

# O ggplot2 usa uma gramática de gráficos para construir figuras.
library(ggplot2)

Visualizar os Dados

# Desenhar um diagrama de extremos e quartis da massa por espécie.
ggplot(ds, aes(x = species, y = body_mass_g)) +
    geom_boxplot()

Visualizar os Dados

ggplot(ds, aes(x = species, y = body_mass_g, fill = species)) +
    geom_boxplot() +
    theme_classic()
  • Conseguem encontrar as diferenças?

Visualizar os Dados

ggplot(ds, aes(x = sex, y = body_mass_g, fill = sex)) +
    geom_boxplot() +
    theme_classic()
  • Conseguem encontrar as diferenças?

Visualizar os Dados

ggplot(ds, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
    geom_point() +
    theme_classic()
  • Conseguem encontrar as diferenças?

Ajustar o Modelo

# Fórmula geral: lm(VD ~ VI, dados)
# Exemplo: massa em função do comprimento da barbatana.
model <- lm(body_mass_g ~ flipper_length_mm, ds)
  • Ao longo do semestre, vamos aprender o que este modelo faz e quando faz sentido

Testar Hipóteses

  • Podemos usar funções diferentes, consoante o modelo:
    • summary(model)
    • afex::nice(model)
    • car::Anova(model, type = 3)

Testar Hipóteses

summary(model)

Call:
lm(formula = body_mass_g ~ flipper_length_mm, data = ds)

Residuals:
     Min       1Q   Median       3Q      Max 
-1058.80  -259.27   -26.88   247.33  1288.69 

Coefficients:
                   Estimate Std. Error t value Pr(>|t|)    
(Intercept)       -5780.831    305.815  -18.90   <2e-16 ***
flipper_length_mm    49.686      1.518   32.72   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 394.3 on 340 degrees of freedom
  (2 observations deleted due to missingness)
Multiple R-squared:  0.759, Adjusted R-squared:  0.7583 
F-statistic:  1071 on 1 and 340 DF,  p-value: < 2.2e-16

Isso é Imenso!!!

  • Mentiste!!!! O R é complicado!
    • Calma…não é assim tanto. Por enquanto, é sobretudo pouco familiar
  • Vamos contar as linhas que realmente fazem alguma coisa

Um Script Típico

# Carregar o pacote.
library(palmerpenguins)

# Importar os dados.
ds <- penguins
# Ajustar o modelo.
model <- lm(body_mass_g ~ flipper_length_mm, ds)

# Inspeccionar os resultados.
summary(model)

Call:
lm(formula = body_mass_g ~ flipper_length_mm, data = ds)

Residuals:
     Min       1Q   Median       3Q      Max 
-1058.80  -259.27   -26.88   247.33  1288.69 

Coefficients:
                   Estimate Std. Error t value Pr(>|t|)    
(Intercept)       -5780.831    305.815  -18.90   <2e-16 ***
flipper_length_mm    49.686      1.518   32.72   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 394.3 on 340 degrees of freedom
  (2 observations deleted due to missingness)
Multiple R-squared:  0.759, Adjusted R-squared:  0.7583 
F-statistic:  1071 on 1 and 340 DF,  p-value: < 2.2e-16
  • São quatro linhas de código, quatro comentários e duas linhas em branco

Um Script de Análise Mais Completo: Preparar

# Carregar pacotes.
library(dplyr)
library(ggplot2)
library(palmerpenguins)

# Definir opções.
options(contrasts = c("contr.sum", "contr.poly"))

# Importar dados.
ds <- penguins

Um Script de Análise Mais Completo: Resumir

# Calcular estatísticas descritivas.
descs <- ds |>
    group_by(species) |>
    summarise(
        N = sum(!is.na(body_mass_g)),
        M = mean(body_mass_g, na.rm = TRUE),
        SD = sd(body_mass_g, na.rm = TRUE)
    )

Um Script de Análise Mais Completo: Visualizar

# Visualizar os dados.
graph <- ggplot(ds, aes(flipper_length_mm, body_mass_g,
                        color = species)) +
    geom_point() +
    theme_classic()

Um Script de Análise Mais Completo: Modelar e Inspeccionar

# Ajustar o modelo.
model <- lm(body_mass_g ~ flipper_length_mm, data = ds)

# Inspeccionar os resultados.
descs
species N M SD
Adelie 151 3700.662 458.5661
Chinstrap 68 3733.088 384.3351
Gentoo 123 5076.016 504.1162
graph
model |> summary()

Call:
lm(formula = body_mass_g ~ flipper_length_mm, data = ds)

Residuals:
     Min       1Q   Median       3Q      Max 
-1058.80  -259.27   -26.88   247.33  1288.69 

Coefficients:
                   Estimate Std. Error t value Pr(>|t|)    
(Intercept)       -5780.831    305.815  -18.90   <2e-16 ***
flipper_length_mm    49.686      1.518   32.72   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 394.3 on 340 degrees of freedom
  (2 observations deleted due to missingness)
Multiple R-squared:  0.759, Adjusted R-squared:  0.7583 
F-statistic:  1071 on 1 and 340 DF,  p-value: < 2.2e-16
  • Pacotes → opções → dados → descritivas → gráfico → modelo → resultados

  • Esta organização já se aproxima de uma análise de produção

Mais Tarde: Preparar uma Análise

library(afex)
library(emmeans)
library(ggplot2)

options(contrasts = c("contr.sum", "contr.poly"))
afex_options(es_aov = "pes", method_mixed = "KR")

data("obk.long", package = "afex")
  • Cada função representa uma operação que aprenderemos ao longo do curso

  • Não precisam de dominar isto hoje. É o destino, não o ponto de partida

Mais Tarde: Ajustar um Modelo com afex

m_value <- mixed(
    value ~ treatment * phase * hour + (phase | id),
    data = obk.long
)
  • afex::mixed() encapsula um ajuste que seria trabalhoso montar à mão

Mais Tarde: Obter Médias Marginais

emm_value <- emmeans(m_value, pairwise ~ treatment | phase * hour)
emm_ds <- as.data.frame(emm_value$emmeans)
  • emmeans() obtém médias marginais estimadas e comparações aos pares

Mais Tarde: Construir o Gráfico

graph <- ggplot(
    emm_ds,
    aes(phase, emmean, color = treatment, group = treatment)
) +
    geom_errorbar(aes(ymin = lower.CL, ymax = upper.CL), width = .1) +
    geom_line() +
    geom_point() +
    facet_wrap(~ hour) +
    theme_classic()

Mais Tarde: Inspeccionar o Gráfico

graph
  • O resultado do modelo alimenta a preparação do gráfico

  • A cadeia continua a ser: dados → função → resultado → função seguinte

Quando o Projecto Cresce: Estrutura

analysis/
├── 0-anonymize.R
├── 1-wrangle.R
├── 2-analyze.R
├── report.qmd
└── run_all.R
  • Separamos as fases sem esconder a sua ordem

Quando o Projecto Cresce: Orquestração

# run_all.R
source("0-anonymize.R")
source("1-wrangle.R")
source("2-analyze.R")
  • Cada script tem uma responsabilidade e declara as suas dependências

  • run_all.R rege a orquestra; não toca os instrumentos

Porque Usamos Esta Organização?

  • Os passos principais da análise continuam visíveis

  • As funções dão nomes a operações repetidas ou conceptualmente importantes

  • Os objectos intermédios expõem resultados que vale a pena verificar ou reutilizar

  • Os pipes nativos tornam claras as cadeias lineares de transformação

  • As funções encastradas continuam a ser úteis em expressões curtas

  • Não existe uma única sintaxe adequada a todos os trabalhos