Explore antes de modelar

Tutorial prático de análise exploratória de dados em ovinocaprinocultura

Autor

Fernando Machado Haesbaert | Embrapa Caprinos e Ovinos

NotaComo usar este tutorial

Execute os blocos na ordem, copiando-os pelo botão de cópia disponível no HTML renderizado. Este material acompanha a aula Explorar os dados. Os dados são simulados para fins didáticos e não representam resultados reais da Embrapa.

1 Objetivos de aprendizagem

Ao final, você deverá conseguir reconhecer a estrutura dos dados, identificar ausências e valores discrepantes, resumir tendência central e dispersão, visualizar distribuições, comparar grupos, investigar associações e formular hipóteses para a modelagem.

O roteiro é: conferir → resumir → visualizar → comparar → relacionar → interpretar.

2 1. Preparar o ambiente

O tidyverse reúne ferramentas para organizar, resumir e visualizar dados. Instale os pacotes uma única vez, se necessário.

# Instale uma única vez, se necessário:
# install.packages("tidyverse")
# install.packages("visdat")
# install.packages("GGally")

library(tidyverse)

No RStudio, execute cada bloco com Run. Para gerar o tutorial, salve este arquivo como .qmd e clique em Render. O botão de cópia de código estará disponível na versão HTML.

3 2. Simular dados de um lote de animais

Cada linha representa um animal. Vamos criar dados de grupo genético, dieta, peso, ECC, FAMACHA, OPG e ganho médio diário (GMD).

set.seed(123) # torna a simulação reproduzível
n <- 80       # número de animais simulados

dados <- tibble(
  animal = sprintf("C%03d", 1:n),
  grupo_genetico = sample(c("Santa Inês", "Cruzado"), n, replace = TRUE),
  dieta = sample(c("Dieta A", "Dieta B"), n, replace = TRUE),
  peso_inicial = round(rnorm(n, mean = 24, sd = 3), 1),
  ecc_inicial = sample(seq(2, 4.5, by = 0.5), n, replace = TRUE),
  famacha = sample(1:5, n, replace = TRUE),
  opg = round(rlnorm(n, meanlog = log(500), sdlog = 0.8))
) |>
  mutate(
    # A diferença média entre dietas é programada na simulação.
    gmd_simulado = rnorm(
      n,
      mean = if_else(dieta == "Dieta B", 0.230, 0.200),
      sd = 0.045
    ),
    dias = 60,
    peso_final = round(peso_inicial + gmd_simulado * dias, 1),
    gmd = round((peso_final - peso_inicial) / dias, 3),
    ecc_final = pmin(
      5,
      pmax(1, ecc_inicial +
             sample(seq(-0.5, 1.5, by = 0.5), n, replace = TRUE))
    )
  ) |>
  select(-gmd_simulado)

head(dados)
# A tibble: 6 × 11
  animal grupo_genetico dieta   peso_inicial ecc_inicial famacha   opg  dias
  <chr>  <chr>          <chr>          <dbl>       <dbl>   <int> <dbl> <dbl>
1 C001   Santa Inês     Dieta B         24           2         1   983    60
2 C002   Santa Inês     Dieta B         25.2         4         3  1080    60
3 C003   Santa Inês     Dieta A         22.9         2         4   864    60
4 C004   Cruzado        Dieta B         25.9         3.5       2   164    60
5 C005   Santa Inês     Dieta B         23.3         3         5   987    60
6 C006   Cruzado        Dieta B         25           2         1   350    60
# ℹ 3 more variables: peso_final <dbl>, gmd <dbl>, ecc_final <dbl>

Funções usadas: tibble() cria uma tabela; sample() sorteia categorias; rnorm() e rlnorm() geram dados simulados; mutate() cria colunas; round() arredonda; pmin() e pmax() limitam os valores; select() mantém ou remove colunas.

Aviso

A base é artificial. A dieta B recebeu uma média de GMD um pouco maior por construção, portanto isso não é uma descoberta científica. Também não foi definida uma estrutura de baias ou unidades experimentais coletivas; não use a simulação para inferir efeitos reais.

3.1 ECC inicial e final

ggplot(dados, aes(x = ecc_inicial, y = ecc_final)) +
  geom_jitter(width = 0.10, height = 0.10, alpha = 0.55,
              color = "#28785B") +
  geom_smooth(method = "lm", se = FALSE, color = "#005A9C") +
  labs(
    title = "ECC inicial e final dos animais simulados",
    x = "ECC inicial", y = "ECC final"
  ) +
  theme_minimal(base_size = 12)

geom_jitter() desloca levemente pontos sobrepostos. Como o ECC é registrado em incrementos de 0,5 e tem limites, trate-o como um escore ordinal; a reta é uma síntese exploratória, não uma prova causal.

Reflita: há concentração nos limites da escala? Os pontos se distribuem de modo semelhante em todos os valores?

4 3. Conhecer a estrutura da base

glimpse(dados)  # nomes, tipos e exemplos
Rows: 80
Columns: 11
$ animal         <chr> "C001", "C002", "C003", "C004", "C005", "C006", "C007",…
$ grupo_genetico <chr> "Santa Inês", "Santa Inês", "Santa Inês", "Cruzado", "S…
$ dieta          <chr> "Dieta B", "Dieta B", "Dieta A", "Dieta B", "Dieta B", …
$ peso_inicial   <dbl> 24.0, 25.2, 22.9, 25.9, 23.3, 25.0, 27.3, 25.3, 23.0, 2…
$ ecc_inicial    <dbl> 2.0, 4.0, 2.0, 3.5, 3.0, 2.0, 4.5, 2.0, 2.5, 3.5, 2.5, …
$ famacha        <int> 1, 3, 4, 2, 5, 1, 4, 3, 1, 2, 3, 3, 1, 2, 1, 2, 2, 2, 5…
$ opg            <dbl> 983, 1080, 864, 164, 987, 350, 575, 531, 704, 510, 132,…
$ dias           <dbl> 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60, 60,…
$ peso_final     <dbl> 40.5, 33.6, 33.7, 40.0, 34.7, 39.7, 40.4, 37.2, 30.1, 4…
$ gmd            <dbl> 0.275, 0.140, 0.180, 0.235, 0.190, 0.245, 0.218, 0.198,…
$ ecc_final      <dbl> 1.5, 3.5, 1.5, 5.0, 4.5, 3.0, 4.5, 2.0, 3.5, 3.5, 2.0, …
dim(dados)      # número de linhas e colunas
[1] 80 11
summary(dados)  # resumo básico de cada variável
    animal          grupo_genetico        dieta            peso_inicial  
 Length:80          Length:80          Length:80          Min.   :17.80  
 Class :character   Class :character   Class :character   1st Qu.:22.07  
 Mode  :character   Mode  :character   Mode  :character   Median :23.70  
                                                          Mean   :23.85  
                                                          3rd Qu.:25.73  
                                                          Max.   :30.60  
  ecc_inicial      famacha          opg              dias      peso_final   
 Min.   :2.00   Min.   :1.00   Min.   :  97.0   Min.   :60   Min.   :29.10  
 1st Qu.:2.50   1st Qu.:2.00   1st Qu.: 325.8   1st Qu.:60   1st Qu.:33.55  
 Median :3.00   Median :3.00   Median : 553.5   Median :60   Median :36.35  
 Mean   :3.15   Mean   :2.85   Mean   : 736.1   Mean   :60   Mean   :36.73  
 3rd Qu.:4.00   3rd Qu.:4.00   3rd Qu.: 990.0   3rd Qu.:60   3rd Qu.:40.00  
 Max.   :4.50   Max.   :5.00   Max.   :2552.0   Max.   :60   Max.   :46.30  
      gmd           ecc_final    
 Min.   :0.1120   Min.   :1.500  
 1st Qu.:0.1800   1st Qu.:2.500  
 Median :0.2135   Median :3.500  
 Mean   :0.2147   Mean   :3.525  
 3rd Qu.:0.2450   3rd Qu.:4.500  
 Max.   :0.3150   Max.   :5.000  

Cada linha representa um animal neste exemplo; cada coluna representa uma variável. A unidade observacional pode ser diferente da unidade experimental. Se uma dieta for aplicada à baia inteira, por exemplo, a baia pode ser a unidade experimental e os animais, unidades observacionais.

5 4. Investigar dados ausentes

Vamos introduzir ausências artificiais para praticar sua identificação.

set.seed(321)
dados <- dados |>
  mutate(
    ecc_final = replace(ecc_final, sample(1:n, 5), NA_real_),
    opg = replace(opg, sample(1:n, 8), NA_real_)
  )

NA indica informação ausente. is.na() identifica ausências; sum() conta; across() aplica a mesma operação a várias colunas.

dados |>
  summarise(
    across(
      c(peso_inicial, peso_final, gmd, ecc_final, famacha, opg),
      ~ sum(is.na(.x))
    )
  )
# A tibble: 1 × 6
  peso_inicial peso_final   gmd ecc_final famacha   opg
         <int>      <int> <int>     <int>   <int> <int>
1            0          0     0         5       0     8

5.1 Visualizar as ausências

# Instale uma única vez, se necessário:
# install.packages("visdat")
library(visdat)

dados |>
  select(peso_inicial, peso_final, gmd, ecc_final, famacha, opg) |>
  visdat::vis_miss()

Pergunte: em quais variáveis faltam dados? Em dados longitudinais reais, as ausências se concentram em determinada avaliação, lote ou condição sanitária? O gráfico localiza ausências, mas não explica sozinho por que ocorreram.

Aviso

Não substitua automaticamente NA por zero. Zero pode ser uma medida válida e não equivale a “não medido”. A decisão depende da causa da ausência e da pergunta científica.

6 5. Resumir tendência central e dispersão

A média e a mediana descrevem o centro. O desvio-padrão (DP) e o intervalo interquartil (IIQ) ajudam a descrever a variabilidade.

dados |>
  summarise(
    n = sum(!is.na(gmd)),
    media = mean(gmd, na.rm = TRUE),
    mediana = median(gmd, na.rm = TRUE),
    desvio_padrao = sd(gmd, na.rm = TRUE),
    IIQ = IQR(gmd, na.rm = TRUE),
    minimo = min(gmd, na.rm = TRUE),
    maximo = max(gmd, na.rm = TRUE)
  )
# A tibble: 1 × 7
      n media mediana desvio_padrao   IIQ minimo maximo
  <int> <dbl>   <dbl>         <dbl> <dbl>  <dbl>  <dbl>
1    80 0.215   0.214        0.0467 0.065  0.112  0.315
  • Média: soma dividida pelo número de observações válidas.
  • Mediana: valor central dos dados ordenados.
  • DP: descreve o espalhamento dos valores em torno da média.
  • IIQ: diferença entre o terceiro e o primeiro quartis.
  • Mínimo e máximo: extremos observados.

na.rm = TRUE manda a função ignorar valores ausentes naquele cálculo.

6.1 Comparar o GMD entre dietas

dados |>
  group_by(dieta) |>
  summarise(
    n = sum(!is.na(gmd)),
    media_gmd = mean(gmd, na.rm = TRUE),
    dp_gmd = sd(gmd, na.rm = TRUE),
    mediana_gmd = median(gmd, na.rm = TRUE),
    IIQ_gmd = IQR(gmd, na.rm = TRUE),
    .groups = "drop"
  )
# A tibble: 2 × 6
  dieta       n media_gmd dp_gmd mediana_gmd IIQ_gmd
  <chr>   <int>     <dbl>  <dbl>       <dbl>   <dbl>
1 Dieta A    36     0.207 0.0462        0.2   0.047 
2 Dieta B    44     0.221 0.0466        0.23  0.0527

group_by() separa os dados por grupo; summarise() calcula estatísticas dentro de cada grupo.

Reflita: as médias são diferentes? As medianas contam história semelhante? A diferença entre médias é grande em relação à dispersão?

Nota

O DP descreve a variabilidade entre observações; não mede diretamente a precisão da média. A precisão também depende do número de unidades independentes e da estrutura amostral. O erro-padrão descreve a incerteza amostral de um estimador; o intervalo de confiança depende do estimador, do método e do desenho.

7 6. Visualizar a distribuição do GMD

Um resumo numérico não revela toda a estrutura dos dados. O histograma ajuda a identificar assimetria, concentrações e possíveis subgrupos.

ggplot(dados, aes(x = gmd)) +
  geom_histogram(bins = 15, fill = "#80B89A", color = "white") +
  labs(
    title = "Como se distribui o GMD?",
    x = "GMD (kg/animal/dia)", y = "Número de animais"
  ) +
  theme_classic(base_size = 12)

O argumento bins controla o número aproximado de intervalos. Observe: a distribuição parece simétrica? Há caudas longas, lacunas ou mais de um pico? Um padrão bimodal pode sugerir subgrupos, mas é preciso investigar outras variáveis antes de propor uma explicação biológica.

7.1 Boxplot e possíveis valores discrepantes

ggplot(dados, aes(y = gmd)) +
  geom_boxplot(fill = "#B9D8C5", color = "#16543F") +
  labs(
    title = "Há valores que merecem investigação?",
    x = NULL, y = "GMD (kg/animal/dia)"
  ) +
  theme_classic(base_size = 12)

O boxplot resume mediana, quartis e dispersão. Valores fora dos limites usuais são possíveis discrepantes, não necessariamente erros.

7.2 Selecionar registros para inspeção

O critério comum sinaliza valores abaixo de (Q_1 - 1{,}5 IIQ) ou acima de (Q_3 + 1{,}5 IIQ).

q1 <- quantile(dados$gmd, 0.25, na.rm = TRUE)
q3 <- quantile(dados$gmd, 0.75, na.rm = TRUE)
iiq <- IQR(dados$gmd, na.rm = TRUE)

limite_inferior <- q1 - 1.5 * iiq
limite_superior <- q3 + 1.5 * iiq

dados |>
  filter(gmd < limite_inferior | gmd > limite_superior) |>
  select(animal, dieta, grupo_genetico, peso_inicial, peso_final, gmd)
# A tibble: 0 × 6
# ℹ 6 variables: animal <chr>, dieta <chr>, grupo_genetico <chr>,
#   peso_inicial <dbl>, peso_final <dbl>, gmd <dbl>

filter() seleciona linhas que atendem a uma condição. O código apenas lista registros para inspeção; não os exclui. Confira unidade, transcrição, registro original e plausibilidade biológica antes de decidir o que fazer.

8 7. Comparar grupos sem esconder os animais

ggplot(dados, aes(x = dieta, y = gmd, color = dieta)) +
  geom_boxplot(outlier.shape = NA) +
  geom_jitter(width = 0.12, alpha = 0.7, size = 2) +
  labs(
    title = "Como varia o GMD entre dietas?",
    x = "Dieta", y = "GMD (kg/animal/dia)"
  ) +
  theme_classic(base_size = 12) +
  theme(legend.position = "none")

O boxplot resume a distribuição; geom_jitter() mostra as observações individuais. outlier.shape = NA evita desenhar novamente os valores extremos, já visíveis nos pontos.

A comparação gráfica é exploratória: não substitui inferência nem corrige falhas de delineamento. Se o tratamento foi aplicado às baias, os animais de uma mesma baia não devem ser tratados automaticamente como réplicas independentes.

9 8. Investigar relações entre variáveis

Cada ponto do gráfico abaixo representa um animal. A cor distingue grupos genéticos.

ggplot(
  dados,
  aes(x = peso_inicial, y = gmd, color = grupo_genetico)
) +
  geom_point(size = 2.5, alpha = 0.8) +
  geom_smooth(method = "lm", se = FALSE) +
  labs(
    title = "Peso inicial e GMD estão associados?",
    x = "Peso inicial (kg)",
    y = "GMD (kg/animal/dia)",
    color = "Grupo genético"
  ) +
  theme_classic(base_size = 12)

geom_smooth(method = "lm") adiciona uma reta linear exploratória por grupo. Ela resume uma tendência linear, mas não demonstra causalidade nem garante que a relação linear seja adequada.

9.1 Correlação geral

cor(
  dados$peso_inicial,
  dados$gmd,
  use = "complete.obs"
)
[1] 0.07312221

cor() calcula, por padrão, a correlação de Pearson. use = "complete.obs" considera apenas pares sem valores ausentes.

O coeficiente (r) varia de -1 a 1: valores próximos de 1 indicam associação linear positiva; próximos de -1, associação linear negativa; próximos de zero indicam pouca associação linear, mas ainda pode haver relação não linear.

9.2 Correlação e teste por grupo genético

resumo_cor <- dados |>
  group_by(grupo_genetico) |>
  group_modify(~ {
    df <- .x |>
      select(peso_inicial, gmd) |>
      drop_na()

    teste <- cor.test(
      df$peso_inicial,
      df$gmd,
      method = "pearson"
    )

    tibble(
      n = nrow(df),
      correlacao = unname(teste$estimate),
      p_valor = teste$p.value
    )
  }) |>
  ungroup()

resumo_cor
# A tibble: 2 × 4
  grupo_genetico     n correlacao p_valor
  <chr>          <int>      <dbl>   <dbl>
1 Cruzado           32   0.199      0.275
2 Santa Inês        48  -0.000347   0.998

cor.test() estima a correlação e testa a hipótese nula de correlação populacional igual a zero, sob as condições do método. O valor de p não mede a força da associação nem sua relevância biológica.

Aviso

Cuidado com este exemplo: o GMD foi calculado usando peso final e peso inicial. Logo, o peso inicial participa matematicamente da construção do GMD. A associação observada pode refletir essa dependência matemática e não deve ser interpretada automaticamente como efeito biológico independente.

Uma correlação significativa em um grupo e não significativa em outro não prova que as correlações sejam diferentes. Para comparar correlações, é necessário um teste apropriado para essa diferença, além de considerar os tamanhos amostrais e os padrões gráficos.

10 9. Mesma média, diferentes dispersões

Dois grupos podem ter a mesma média e heterogeneidade muito diferente. Esta segunda base é independente da simulação anterior.

set.seed(456)

exemplo <- tibble(
  grupo = rep(c("Menor dispersão", "Maior dispersão"), each = 30),
  gmd = c(
    rnorm(30, mean = 0.200, sd = 0.015),
    rnorm(30, mean = 0.200, sd = 0.050)
  )
) |>
  group_by(grupo) |>
  mutate(
    # Ajuste didático para que ambas as médias sejam exatamente 0,200.
    gmd = gmd - mean(gmd) + 0.200
  ) |>
  ungroup()
exemplo |>
  group_by(grupo) |>
  summarise(
    n = n(),
    media = mean(gmd),
    desvio_padrao = sd(gmd),
    IIQ = IQR(gmd),
    .groups = "drop"
  )
# A tibble: 2 × 5
  grupo               n media desvio_padrao    IIQ
  <chr>           <int> <dbl>         <dbl>  <dbl>
1 Maior dispersão    30   0.2        0.0421 0.0491
2 Menor dispersão    30   0.2        0.0173 0.0264
ggplot(exemplo, aes(x = grupo, y = gmd, color = grupo)) +
  geom_jitter(width = 0.10, size = 2, alpha = 0.7) +
  stat_summary(
    fun = mean, geom = "point", shape = 23,
    size = 4, fill = "white", color = "black"
  ) +
  labs(
    title = "Mesma média, diferentes distribuições",
    subtitle = "Dados simulados para fins didáticos",
    x = NULL, y = "GMD (kg/animal/dia)"
  ) +
  theme_classic(base_size = 12) +
  theme(legend.position = "none")

O losango branco representa a média; cada ponto representa uma observação. A média isolada não descreve a uniformidade dos animais.

Discussão: se dois grupos apresentam o mesmo GMD médio, mas um é mais heterogêneo, isso pode importar para o manejo e a padronização dos lotes. A relevância depende do objetivo produtivo e do contexto biológico.

11 10. Atividade complementar: matriz de gráficos

Esta atividade é opcional e um pouco mais avançada. Primeiro, use a matriz padrão de GGally::ggpairs().

# Instale uma única vez, se necessário:
# install.packages("GGally")
library(GGally)

dados_grafico <- dados |>
  select(grupo_genetico, peso_inicial, peso_final, gmd) |>
  drop_na()

GGally::ggpairs(
  dados_grafico,
  columns = 2:4,
  mapping = aes(color = grupo_genetico, alpha = 0.7),
  lower = list(continuous = wrap("points", size = 1.8)),
  diag = list(continuous = wrap("densityDiag", alpha = 0.4))
) +
  theme_classic(base_size = 11)

  • Diagonal: distribuições de cada variável.
  • Triângulo inferior: diagramas de dispersão entre pares.
  • Triângulo superior: resumos das relações, conforme a configuração padrão.

Desafio: substitua peso_final por ecc_inicial. Compare as distribuições e identifique um padrão que mereça investigação. Escreva uma frase sobre o que o gráfico mostra e outra sobre o que ainda precisaria ser testado.

Lembre-se: peso_final e gmd compartilham informação matemática, pois o GMD foi calculado usando os pesos inicial e final.

12 11. Da exploração à hipótese científica

Para interpretar os resultados, responda:

  1. Padrão observado: o que aparece no resumo ou gráfico?
  2. Qualidade dos dados: há ausências, inconsistências ou valores a conferir?
  3. Explicações plausíveis: que mecanismos biológicos ou de manejo poderiam explicar o padrão?
  4. Alternativas: lote, idade, sexo, grupo genético ou condição sanitária poderiam estar relacionados?
  5. Próximo passo: que análise é coerente com a pergunta, a variável resposta, o delineamento e a dependência entre observações?
NotaSíntese
  • Resumir não é compreender toda a estrutura dos dados.
  • Gráficos são instrumentos de diagnóstico e interpretação, não decoração.
  • Observação extrema não é sinônimo de erro.
  • Correlação não demonstra causalidade.
  • Significância estatística não é sinônimo de relevância biológica.
  • O modelo deve respeitar a pergunta científica, o delineamento e a estrutura dos dados.

13 12. Referências para aprofundamento

  • WICKHAM, Hadley; ÇETINKAYA-RUNDEL, Mine; GROLEMUND, Garrett. R para Ciência de Dados. 2. ed. 2024.
  • MONTGOMERY, Douglas C. Design and Analysis of Experiments. 8. ed. 2013.
  • ÇETINKAYA-RUNDEL, Mine; HARDIN, Johanna. Introduction to Modern Statistics. 2. ed. 2024.
  • KNAFLIC, Cole Nussbaumer. Storytelling with Data: A Data Visualization Guide for Business Professionals. 2015.

Curso de Atualização: Estatística Experimental e Ciência de Dados para Pesquisa em Ovinocaprinocultura — Embrapa Caprinos e Ovinos.

Dados simulados para fins didáticos; não representam resultados de rebanhos reais.