7  Análise exploratória de dados

Antes de qualquer modelo, olhe os dados. A exploração tem quatro tarefas (Zuur et al. 2010): encontrar erros e outliers, entender a estrutura dos dados — o que conta como réplica —, ver as distribuições que os modelos terão de descrever e detectar colinearidade entre variáveis que um modelo terá dificuldade em separar. Pular essa etapa não economiza tempo: os problemas continuam lá, e aparecem depois como resultados estranhos de modelo.

As lagoas simuladas dos capítulos anteriores são limpas demais para isso. Este capítulo usa dados reais, com todos os problemas que dados reais têm.

NotaDe onde vem este capítulo

Como o Capítulo 10 e o Capítulo 11, este capítulo é inspirado no ecoR (Oliveira 2026), o wiki do curso de R do Instituto de Biociências da Universidade de São Paulo, produzido e mantido por Alexandre Adalardo de Oliveira. A sequência — conferir o data frame, estatísticas descritivas, gráficos univariados, relações entre variáveis e o quarteto de Anscombe — segue o módulo de análise exploratória do ecoR. O texto, os dados, os exemplos e os exercícios daqui foram escritos do zero para Julia; os autores do ecoR não revisaram este livro.

Se quiser ir além, o material original está disponível gratuitamente (CC BY-SA 4.0):

NotaOs dados: ATLANTIC BIRD TRAITS

O ATLANTIC BIRD TRAITS (Rodrigues et al. 2019) reúne medidas morfológicas de aves das florestas atlânticas da América do Sul: 72 483 registros, compilados de coleções de museu, projetos de anilhamento e da literatura entre 1820 e 2018, por 179 autores. O artigo de dados afirma que não há restrições de direitos autorais nem de propriedade sobre o seu uso, e pede que seja citado quando usado em publicações ou no ensino — como aqui.

O data/atlantic_bird_traits.csv mantém todos os registros e 21 das 83 colunas originais, com os nomes e códigos originais. Nada foi limpo: encontrar os problemas é o objetivo. O scripts/prepare_birds.jl documenta como o subconjunto foi feito a partir do arquivo original, que está no material suplementar do artigo.

using TidierData
using DataFrames, CSV
using Statistics, StatsBase
using GLM, RDatasets, Distributions
using CairoMakie

CairoMakie.activate!(type = "png")
arquivo_aves = joinpath("..", "data", "atlantic_bird_traits.csv")
"../data/atlantic_bird_traits.csv"
NotaExercícios deste capítulo

As respostas são números calculados a partir dos dados das aves. Comece cada sessão com:

include("exercises/checar.jl")
using TidierData, DataFrames, CSV, Statistics, StatsBase, GLM, Distributions
arquivo_aves = joinpath("data", "atlantic_bird_traits.csv")

A primeira seção mostra como ler o arquivo; os exercícios seguintes supõem que você o leu desse jeito, em birds, com as colunas renomeadas.

7.1 Primeiro contato: lendo o arquivo

Leia o arquivo como ele vem e olhe o tipo de cada coluna:

bruto = CSV.read(arquivo_aves, DataFrame)
size(bruto)
(72483, 21)
first(describe(bruto, :eltype, :nmissing), 8)
8×3 DataFrame
Row variable eltype nmissing
Symbol DataType Int64
1 ID_ABT String7 0
2 Order String31 0
3 Family String31 0
4 Binomial String31 0
5 Body_mass.g. String7 0
6 Wing_length.mm. String7 0
7 Tail_length.mm. String7 0
8 Tarsus_length.mm. String7 0

Massa corporal, comprimento da asa, comprimento da cauda: tudo lido como texto (String), e nenhum valor faltante. O motivo está no próprio arquivo — os valores faltantes estão escritos como as duas letras NA, que o CSV.jl não trata como faltante por padrão. Um único não número em uma coluna faz a coluna inteira virar texto. Diga ao CSV.read qual código significa faltante:

birds = CSV.read(arquivo_aves, DataFrame; missingstring = "NA")
first(describe(birds, :eltype, :nmissing), 8)
8×3 DataFrame
Row variable eltype nmissing
Symbol Type Int64
1 ID_ABT String7 0
2 Order String31 0
3 Family String31 0
4 Binomial String31 0
5 Body_mass.g. Union{Missing, Float64} 11664
6 Wing_length.mm. Union{Missing, Float64} 51134
7 Tail_length.mm. Union{Missing, Float64} 26183
8 Tarsus_length.mm. Union{Missing, Float64} 50742

Em R

O read.csv trata "NA" como faltante por padrão, então em R esse problema passaria despercebido. Aqui ele fica visível: uma coluna numérica que chega como texto é o primeiro sinal de que o arquivo usa um código de faltante que você não declarou. Outros códigos comuns são "", "-", ".", -999 e 99999; missingstring também aceita um vetor deles.

Os nomes das colunas misturam unidades, pontos e maiúsculas, como Body_mass.g.. São incômodos de digitar, e as macros do Tidier não conseguem usá-los diretamente. Renomeie-os uma vez, no início:

rename!(birds,
    "ID_ABT" => :id, "Order" => :order, "Family" => :family, "Binomial" => :species,
    "Body_mass.g." => :mass_g, "Wing_length.mm." => :wing_mm,
    "Tail_length.mm." => :tail_mm, "Tarsus_length.mm." => :tarsus_mm,
    "Bill_length.mm." => :bill_mm, "Age" => :age, "Sex" => :sex, "Status" => :status,
    "Recapture" => :recapture, "Ring" => :ring,
    "Latitude_decimal_degrees" => :lat, "Longitude_decimal_degrees" => :lon,
    "Altitude" => :altitude, "Year" => :year, "Main_researcher" => :source,
    "Outside.range" => :outside_range, "Obs.spp" => :species_note)

names(birds)
21-element Vector{String}:
 "id"
 "order"
 "family"
 "species"
 "mass_g"
 "wing_mm"
 "tail_mm"
 "tarsus_mm"
 "bill_mm"
 "age"
 ⋮
 "recapture"
 "ring"
 "lat"
 "lon"
 "altitude"
 "year"
 "source"
 "outside_range"
 "species_note"

Os nomes novos ficam em inglês, como as colunas dos dados das lagoas, para que o código seja o mesmo nas duas versões do livro.

NotaSua vez 7.1 — Quantas aves foram pesadas?

Quantos registros têm massa corporal? Passe o número para checar("7.1", ...).

count(!ismissing, v) conta os valores não faltantes de um vetor. Isso só faz sentido depois que o arquivo foi lido com missingstring = "NA".

n_massa = count(!ismissing, birds.mass_g)
60819
checar("7.1", n_massa)
✓ Correto!

60 819 de 72 483: a massa corporal é o atributo mais registrado, e ainda assim falta em um de cada seis registros.

7.2 Faltante, desconhecido e inconsistente

Colunas categóricas merecem, antes de tudo, uma tabela dos seus níveis:

(idade = countmap(birds.age), sexo = countmap(birds.sex))
(idade = Dict{Union{Missing, String15}, Int64}(missing => 4935, String15("Juvenile") => 8708, String15("Adult") => 52111, String15("Unknown") => 6727, String15("Nestling") => 2), sexo = Dict{Union{Missing, String7}, Int64}(String7("Female") => 14586, missing => 3072, String7("Male") => 18420, String7("Unknown") => 36405))

Os parênteses com nomes formam uma tupla nomeada (named tuple): uma tupla (Seção 4.1) em que cada valor tem um nome. Ela volta na Seção 10.1.

Há dois tipos de faltante aqui. missing quer dizer que a informação nunca foi anotada; "Unknown" quer dizer que alguém olhou a ave e não conseguiu determinar. Não é a mesma coisa: um estudo de razão sexual deve contar os desconhecidos, mas não os não anotados.

Outras colunas discordam de si mesmas na grafia:

(recaptura = countmap(birds.recapture), fora_distribuicao = countmap(birds.outside_range))
(recaptura = Dict{Union{Missing, String3}, Int64}(missing => 33682, String3("Yes") => 8556, String3("no") => 30245), fora_distribuicao = Dict{String3, Int64}("Yes" => 2725, "no" => 69758))

"no" e "Yes": a capitalização muda entre os níveis. Aqui isso é inofensivo, porque cada nível tem uma única grafia, mas um único "yes" em algum lugar criaria um terceiro nível, que o agrupamento trataria como diferente. Padronizar custa pouco:

birds.recapture     = passmissing(lowercase).(birds.recapture)
birds.outside_range = lowercase.(birds.outside_range)
unique(birds.recapture)
3-element Vector{Union{Missing, String}}:
 "no"
 missing
 "yes"

passmissing(f) aplica f aos valores e deixa missing em paz.

NotaSua vez 7.2 — Com que frequência dá para saber o sexo?

Entre os registros em que o sexo foi anotado (não missing), que proporção foi sexada como "Male" ou "Female"? Passe-a para checar("7.2", ...).

Fique com os valores não faltantes com collect(skipmissing(birds.sex)) e tire a média de in(("Male", "Female")).(...).

sexo_anotado = collect(skipmissing(birds.sex))
p_sexado = mean(in(("Male", "Female")).(sexo_anotado))
0.47551540822059907
checar("7.2", p_sexado)
✓ Correto!

Menos da metade: muitos passeriformes da Mata Atlântica não podem ser sexados na mão. Qualquer análise por sexo é uma análise de uma metade não aleatória dos dados.

7.3 Estrutura: o que é uma réplica?

Uma linha é um registro, mas registros não são observações independentes. Eles estão agrupados de várias formas ao mesmo tempo.

Por espécie. Os registros estão muito desigualmente distribuídos:

por_especie = sort(combine(groupby(birds, :species), nrow => :n), :n, rev = true)

(nomes = nrow(por_especie),
 cinco_mais = first(por_especie, 5),
 com_um_registro = count(==(1), por_especie.n))
(nomes = 780, cinco_mais = 5×2 DataFrame
 Row │ species                   n     
     │ String31                  Int64 
─────┼─────────────────────────────────
   1 │ Chiroxiphia caudata        1857
   2 │ Turdus albicollis          1686
   3 │ Trichothraupis melanops    1493
   4 │ Turdus leucomelas          1475
   5 │ Basileuterus culicivorus   1419, com_um_registro = 68)

780 nomes — alguns identificados só até gênero, como Elaenia sp. —, enquanto o artigo de dados relata 711 espécies. Antes de contar espécies, decida o que conta como uma. As dez espécies mais registradas concentram um quinto de todos os registros; 68 nomes aparecem uma única vez.

Por indivíduo. Uma ave anilhada capturada de novo aparece de novo:

anilhados = dropmissing(birds[:, [:ring, :species]], :ring)
por_anilha = combine(groupby(anilhados, :ring),
                     :species => (s -> length(unique(s))) => :n_species, nrow => :n)
first(sort(por_anilha[por_anilha.n_species .> 1, :], :n, rev = true), 6)
6×3 DataFrame
Row ring n_species n
String31 Int64 Int64
1 BRD 25 31
2 VRD 24 27
3 AZD 21 26
4 AZE 22 24
5 IND1 4 15
6 BRE 14 14

Algumas “anilhas” são compartilhadas por mais de vinte espécies. Não são números de anilha, e sim códigos de anilhas coloridas, como BRD ou BLUE RIGHT, que muitas aves carregam. Uma contagem ingênua de recapturas tomaria todas essas aves por um único indivíduo.

Por fonte e método. Cada registro vem de um de 65 pesquisadores principais, e de aves vivas ou peles de museu, ao longo de dois séculos:

combine(groupby(dropmissing(birds[:, [:status, :year]]), :status),
        nrow => :n, :year => minimum => :primeiro, :year => median => :mediana,
        :year => maximum => :ultimo)
2×5 DataFrame
Row status n primeiro mediana ultimo
String7 Int64 Int64 Float64 Int64
1 live 57862 1990 2010.0 2018
2 museum 11152 1820 2007.0 2017

Cada um desses agrupamentos pode tornar os registros de um mesmo grupo mais parecidos entre si do que registros de grupos diferentes — a dependência que os modelos mistos da Seção 8.7 levam em conta. Explorar a estrutura diz quais termos de agrupamento um modelo precisa.

NotaSua vez 7.3 — Quantas aves foram capturadas duas vezes?

Conte os indivíduos registrados mais de uma vez: anilhas que aparecem em mais de um registro da mesma espécie, ignorando os códigos feitos só de letras (anilhas coloridas). Passe o número para checar("7.3", ...).

Descarte os códigos só de letras com occursin(r"^[A-Za-z]+$", ring), que dá true para eles. Depois agrupe por ring e species, conte as linhas e conte os grupos com mais de uma.

anilhas_reais = anilhados[.!occursin.(r"^[A-Za-z]+$", anilhados.ring), :]
por_ave = combine(groupby(anilhas_reais, [:ring, :species]), nrow => :n)
n_recapturadas = count(>(1), por_ave.n)
4503
checar("7.3", n_recapturadas)
✓ Correto!

Cerca de 4 500 aves contribuem com dois registros ou mais, uma delas com até 43. Tratar esses registros como independentes superestimaria o tamanho amostral — e os códigos de cor teriam acrescentado 27 “indivíduos” falsos à contagem.

7.4 Distribuições

A massa corporal de todas as aves cobre três ordens de grandeza, de beija-flores a grandes aves marinhas. Em escala linear, quase tudo se acumula à esquerda; em escala log, a forma aparece:

massas = collect(skipmissing(birds.mass_g))

fig = Figure(size = (860, 340))
ax1 = Axis(fig[1, 1], xlabel = "Massa corporal (g)", ylabel = "Registros", title = "(a) Linear")
hist!(ax1, massas, bins = 60, color = (:steelblue, 0.7))
ax2 = Axis(fig[1, 2], xlabel = "Massa corporal (g, escala log)", title = "(b) Logarítmica",
           xscale = log10)
hist!(ax2, massas, bins = 10 .^ range(0, 3.6, length = 60), color = (:steelblue, 0.7))
fig
Figura 7.1: Massa corporal de todas as aves pesadas, em escala linear (a) e logarítmica (b).

Tamanhos, áreas e concentrações são limitados em zero e crescem de forma multiplicativa; por isso costumam ser assimétricos à direita e mais simétricos em escala log. É por isso que modelos dessas variáveis usam tão frequentemente o seu logaritmo.

Dentro de uma espécie, o quadro é outro. Para o sabiá-coleira (Turdus albicollis), uma das espécies mais registradas, compare a distribuição com uma curva normal de mesma média e desvio-padrão:

sabia = collect(skipmissing(birds.mass_g[birds.species .== "Turdus albicollis"]))
μ, σ = mean(sabia), std(sabia)

fig = Figure(size = (860, 340))
ax1 = Axis(fig[1, 1], xlabel = "Massa corporal (g)", ylabel = "Densidade",
           title = "(a) Histograma")
hist!(ax1, sabia, bins = 40, normalization = :pdf, color = (:seagreen, 0.6))
xs = range(minimum(sabia), maximum(sabia), length = 200)
lines!(ax1, xs, @.(exp(-(xs - μ)^2 / (2σ^2)) / (σ * sqrt(2π))), color = :black)

ax2 = Axis(fig[1, 2], xlabel = "Quantis da normal", ylabel = "Massa corporal (g)",
           title = "(b) QQ-plot")
n = length(sabia)
z = quantile.(Normal(), ((1:n) .- 0.5) ./ n)
scatter!(ax2, z, sort(sabia), color = (:seagreen, 0.5), markersize = 5)
ablines!(ax2, μ, σ, color = :black, linestyle = :dash)
fig
Figura 7.2: Massa corporal de Turdus albicollis: histograma com uma curva normal de mesma média e desvio-padrão (a), e QQ-plot contra a normal (b).

Perto da normal no meio, com algumas aves bem mais leves ou mais pesadas que as outras — visíveis como os pontos que se afastam da reta nas duas pontas do QQ-plot. Essas caudas são o assunto da próxima seção.

NotaSua vez 7.4 — Média ou mediana?

Calcule a mediana e a média da massa corporal de Turdus albicollis e passe-as, nessa ordem, como um vetor para checar("7.4", ...). Qual das duas é menos afetada pelas aves extremas?

sabia, acima, já tem as massas. [median(x), mean(x)] monta o vetor.

centro_sabia = [median(sabia), mean(sabia)]
2-element Vector{Float64}:
 66.0
 66.74542818610924
checar("7.4", centro_sabia)
✓ Correto!

66 e 66,7 g: quase iguais, como se espera de uma distribuição aproximadamente simétrica. A mediana ignora o quão extremas são as caudas; a média é puxada por elas. Com alguns registros de 26 g ou 114 g, a média se move pouco; com os erros grosseiros da próxima seção, se moveria muito mais.

7.5 Erros e outliers

Um histograma geral não mostra o erro de uma ave de 14 g registrada com 1 240 g: 1 240 g é uma massa perfeitamente comum para uma ave grande. Outliers em dados de atributos são outliers em relação à espécie. Um sinal simples e robusto compara cada massa com a mediana da sua espécie:

pesadas = dropmissing(birds[:, [:species, :mass_g, :age, :status, :source]], :mass_g)
pesadas = transform(groupby(pesadas, :species),
                    :mass_g => median => :species_median, nrow => :n_species)
pesadas.ratio = pesadas.mass_g ./ pesadas.species_median

suspeitas = sort(pesadas[(pesadas.n_species .>= 10) .& (pesadas.ratio .> 3), :],
                 :ratio, rev = true)
first(suspeitas[:, [:species, :mass_g, :species_median, :ratio, :age, :status]], 8)
8×6 DataFrame
Row species mass_g species_median ratio age status
String31 Float64 Float64 Float64 String15? String7?
1 Thlypopsis sordida 1240.0 14.0 88.5714 Unknown museum
2 Thlypopsis sordida 1070.0 14.0 76.4286 Unknown museum
3 Corythopis delalandi 214.0 15.0 14.2667 Adult live
4 Tityra cayana 940.0 73.0 12.8767 Juvenile museum
5 Tityra cayana 840.0 73.0 11.5068 Juvenile museum
6 Platyrinchus mystaceus 102.0 9.0 11.3333 missing missing
7 Thalurania glaucopis 50.0 4.5 11.1111 Adult live
8 Phaethornis eurynome 55.0 5.0 11.0 Adult live

O topo da lista mostra vários tipos de problema:

  • Thlypopsis sordida, uma saíra de 14 g, registrada com mais de 1 kg: quase certamente um erro de digitação, talvez um separador decimal fora do lugar ou uma unidade errada.
  • Várias massas cerca de dez vezes a mediana da espécie — 102 g para um papa-moscas de 9 g, 50 g para um beija-flor de 4,5 g: a assinatura de uma vírgula deslocada.
  • Jovens pesados de Tityra cayana em uma coleção de museu: talvez erros, talvez um espécime com etiqueta trocada. Só a fonte pode dizer.

Agora a outra cauda:

first(sort(pesadas[(pesadas.n_species .>= 10) .& (pesadas.ratio .< 1/3), :], :ratio)[:,
      [:species, :mass_g, :species_median, :ratio, :age, :status]], 6)
6×6 DataFrame
Row species mass_g species_median ratio age status
String31 Float64 Float64 Float64 String15? String7?
1 Crypturellus parvirostris 8.5 185.0 0.0459459 Unknown museum
2 Aramides saracura 22.5 346.0 0.0650289 Unknown museum
3 Aramides saracura 22.5 346.0 0.0650289 Juvenile museum
4 Aramides saracura 24.0 346.0 0.0693642 Unknown museum
5 Aramides saracura 24.0 346.0 0.0693642 Juvenile museum
6 Asio clamator 34.5 470.0 0.0734043 Unknown museum

Aqui alguns dos “outliers” são reais. Uma saracura-do-mato (Aramides saracura, mediana de 346 g) de 24 g marcada como jovem é plausivelmente um filhote. Apagá-la removeria uma ave real; mantê-la em uma análise de tamanho adulto a distorceria.

ImportanteSinalize, não apague

Uma regra estatística só pode dizer que um valor é incomum, nunca que ele está errado. Sinalize os valores suspeitos, olhe-os um a um, volte à fonte quando puder e decida — e registre o que decidiu e por quê, em código, para que a decisão possa ser revisada. Em muitas análises, a escolha certa é restringir os dados ao que a pergunta trata (adultos, digamos) em vez de remover pontos individuais.

Os dados também carregam um problema mais discreto, a precisão. A maioria das massas é um número inteiro:

mean(pesadas.mass_g .== round.(pesadas.mass_g))
0.7217974646080995

72% das massas são inteiras, o que quer dizer que muitas foram pesadas com precisão de 1 grama. Para um beija-flor de 3 g, arredondar ao grama é um erro de até 17%; para um sabiá, menos de 1%. Uma precisão que varia com o tamanho do animal é fácil de não notar, e vale saber disso antes de comparar espécies pequenas.

NotaSua vez 7.5 — Quantas massas suspeitas?

Conte os registros cuja massa é mais de 3 vezes, ou menos de um terço, da mediana da sua espécie, entre as espécies com pelo menos 10 registros pesados. Passe o número para checar("7.5", ...).

pesadas já tem ratio e n_species. Combine as condições com .& e .| e conte os true com count.

n_suspeitas = count((pesadas.n_species .>= 10) .&
                    ((pesadas.ratio .> 3) .| (pesadas.ratio .< 1/3)))
291
checar("7.5", n_suspeitas)
✓ Correto!

291 de cerca de 60 000 registros pesados, 0,5%: poucos, mas suficientes para arruinar a média de uma espécie. O limiar de 3 é uma escolha, não uma regra; com 2, você sinalizaria mais que o dobro.

7.6 Relações e colinearidade

Atributos morfológicos andam juntos: aves grandes têm asas longas, caudas longas, pernas longas e bicos longos. Para ver com que força, resuma cada espécie pela mediana de cada atributo — a mediana, por causa dos outliers acima — e compare as espécies:

atributos = [:mass_g, :wing_mm, :tail_mm, :tarsus_mm, :bill_mm]
mediana_ou_missing(x) = (v = collect(skipmissing(x)); isempty(v) ? missing : median(v))

por_especie_med = dropmissing(combine(groupby(birds, :species),
                                      [t => mediana_ou_missing => t for t in atributos]...))
log_atributos = DataFrame(log.(Matrix(por_especie_med[:, atributos])), atributos)

nrow(por_especie_med)
538
round.(cor(Matrix(log_atributos)); digits = 2)
5×5 Matrix{Float64}:
 1.0   0.93  0.71  0.81  0.72
 0.93  1.0   0.77  0.72  0.64
 0.71  0.77  1.0   0.56  0.5
 0.81  0.72  0.56  1.0   0.64
 0.72  0.64  0.5   0.64  1.0
rotulos = ["log massa", "log asa", "log cauda", "log tarso", "log bico"]
fig = Figure(size = (820, 820))
for i in 1:5, j in 1:5
    ax = Axis(fig[i, j], xlabel = i == 5 ? rotulos[j] : "", ylabel = j == 1 ? rotulos[i] : "",
              xticklabelsvisible = i == 5, yticklabelsvisible = j == 1 && i != 1)
    if i == j                  # diagonal: a distribuição de cada atributo (contagens)
        hist!(ax, log_atributos[:, i], bins = 25, color = (:grey60, 0.8))
    else
        scatter!(ax, log_atributos[:, j], log_atributos[:, i], markersize = 3,
                 color = (:steelblue, 0.5))
    end
end
fig
Figura 7.3: Medianas de cinco atributos por espécie, em escala log, um ponto por espécie. Fora da diagonal, cada painel mostra um par de atributos; na diagonal, a distribuição de cada atributo.

Massa e comprimento da asa são quase uma única variável entre espécies. Se os dois entrassem em um modelo como preditores, o modelo não conseguiria separar os seus efeitos: as estimativas seriam instáveis e os erros-padrão, grandes. O fator de inflação da variância (VIF) mede isso. Para cada preditor, faça a regressão dele em todos os outros; com o \(R^2\) dessa regressão, \(\text{VIF} = 1 / (1 - R^2)\):

function vif(df, col)
    outros = setdiff(propertynames(df), [col])
    f = Term(col) ~ sum(Term.(outros))
    return 1 / (1 - r2(lm(f, df)))
end

DataFrame(atributo = atributos,
          vif = round.([vif(log_atributos, t) for t in atributos]; digits = 1))
5×2 DataFrame
Row atributo vif
Symbol Float64
1 mass_g 11.9
2 wing_mm 9.7
3 tail_mm 2.5
4 tarsus_mm 3.0
5 bill_mm 2.1

Zuur et al. (2010) usam um limite de 3; outros autores usam 5 ou 10. Massa e asa ficam acima de todos eles. As respostas usuais são manter só um de cada grupo colinear, combiná-los (um índice de tamanho) ou fazer uma pergunta que não precise deles separadamente.

NotaSua vez 7.6 — Massa e asa

Qual é a correlação entre log da massa corporal e log do comprimento da asa entre espécies, usando as medianas por espécie acima? Passe-a para checar("7.6", ...).

cor(x, y) em duas colunas de log_atributos.

r_massa_asa = cor(log_atributos.mass_g, log_atributos.wing_mm)
0.9326447165263585
checar("7.6", r_massa_asa)
✓ Correto!

Cerca de 0,93. Entre espécies, esses dois atributos carregam quase a mesma informação. Dentro de uma espécie, a correlação é muito mais fraca — cerca de 0,25 entre indivíduos de Turdus albicollis —, porque indivíduos de uma espécie variam pouco em tamanho: a colinearidade depende da escala da pergunta.

7.7 Por que os gráficos: o quarteto de Anscombe

Resumos não substituem olhar. Anscombe (1973) construiu quatro pequenos conjuntos de dados com as mesmas médias, variâncias, correlações e retas de regressão:

anscombe = dataset("datasets", "anscombe")
DataFrame(conjunto = 1:4,
          media_x = [mean(anscombe[:, "X$i"]) for i in 1:4],
          media_y = [round(mean(anscombe[:, "Y$i"]); digits = 2) for i in 1:4],
          cor_xy = [round(cor(anscombe[:, "X$i"], anscombe[:, "Y$i"]); digits = 3) for i in 1:4])
4×4 DataFrame
Row conjunto media_x media_y cor_xy
Int64 Float64 Float64 Float64
1 1 9.0 7.5 0.816
2 2 9.0 7.5 0.816
3 3 9.0 7.5 0.816
4 4 9.0 7.5 0.817
fig = Figure(size = (760, 560))
for i in 1:4
    x, y = anscombe[:, "X$i"], anscombe[:, "Y$i"]
    ax = Axis(fig[(i - 1) ÷ 2 + 1, (i - 1) % 2 + 1], title = "Conjunto $i",
              limits = (3, 20, 3, 14))
    scatter!(ax, x, y, color = :steelblue)
    b = coef(lm(@formula(y ~ x), DataFrame(; x, y)))
    ablines!(ax, b[1], b[2], color = :darkorange)
end
fig
Figura 7.4: O quarteto de Anscombe: quatro conjuntos de dados com as mesmas estatísticas resumo e a mesma reta de mínimos quadrados.

O primeiro parece o que um modelo linear supõe. O segundo é uma curva. No terceiro, um ponto afasta a reta de uma relação de resto perfeita. No quarto, toda a inclinação depende de uma única observação. Só os gráficos mostram isso.

Desafios

  1. Faça o gráfico das coordenadas de todos os registros (lon, lat) e pinte-os por outside_range. Os registros marcados como fora da distribuição da espécie estão onde você esperaria? Há pontos fora da região da Mata Atlântica?
  2. Machos e fêmeas de Chiroxiphia caudata (tangará) diferem muito na plumagem. Diferem na massa? Compare as distribuições, não só as medianas, e decida o que fazer com os registros de sexo "Unknown".
  3. Repita a regra de outliers da Seção 7.5 para o comprimento da asa. As espécies com asas suspeitas são as mesmas com massas suspeitas? O que isso diria sobre a origem dos erros?