library(palmerpenguins) # Dados dos pinguins
library(ggplot2) # Gráficos
library(performance) # check_model()
library(see) # Gráficos bonitos para o performance
library(car) # leveneTest()
library(WRS2) # Testes robustos
library(robustbase) # lmrob() - regressão robusta
library(afex) # Para ANOVAs
options(contrasts = c("contr.sum", "contr.poly"))Pressupostos e Estatística Robusta
0 Introdução
Nos tutoriais anteriores, ajustámos modelos como se fossem sempre perfeitos. Spoiler: não são. Os modelos lineares (lm(), aov(), mixed()) têm pressupostos, ou seja, assumem coisas sobre os dados. Quando os pressupostos falham, os valores-p (que são computados a partir da distribuição da estatística de teste, em H0, assumindo os pressupostos do modelo) começam a não ser tão fiáveis.
Neste tutorial vamos aprender:
Como testar os principais pressupostos (de forma visual e formal).
Como usar estatística robusta quando os pressupostos falham.
Em vez de testar pressupostos e depois decidir, uma abordagem mais moderna consiste em:
Ajustar o modelo OLS normal.
Ajustar a versão robusta.
Se os resultados forem semelhantes → reportar o OLS (mais familiar).
Se diferirem → reportar o robusto (e mencionar que pressupostos foram violados e de que forma).
Field e Wilcox (2017) vão mais longe e sugerem mesmo que os modelos robustos deveriam ser o default na psicologia.
Vamos começar carregando as ferramentas:
1 Os Pressupostos Principais
Para modelos lineares, precisamos de verificar:
Linearidade: A relação entre X e Y é linear (só faz sentido para VIs contínuas).
Normalidade dos Resíduos: Os erros seguem a distribuição normal.
Independência: As observações não estão correlacionadas (já falámos disto no contexto das medidas repetidas) e/ou os erros são independentes (saber o erro para a observação x não me ajuda a prever o erro da observação x+1; o que é violado em algumas progressões temporais e será melhor modelado por séries temporais.
Homocedasticidade: A variância dos erros é constante (não se aplica a ANOVAs de medidas repetidas que não tenham fatores entre-pp)
Esferecidade: A variância das diferenças entre todos os momentos de medição (condições intra-pp) é constante (não se aplica a ANOVAs sem medições repetidas nem a ANOVAs em que o/s fator/es intra-pp só tenham dois níveis, porque aí só há uma diferença e a variância dessa diferença tem de ser igual a ela própria…como os jogadores de futebol).
Ausência de Multicolinearidade: Os preditores/VIs não estão correlacionados entre si. Acaba por ser garantido pelo delineamento experimental, mas pode facilmente ser violado em dados observacionais (e.g., se quero ver se as percepções subjectivas de ansiedade e auto-confiança preveêm a percepção subjectiva de bem-estar, havendo uma correlação forte entre ansiedade e auto-confiança, os declives de cada uma destas variáveis não serão bem estimados porque uma variável “rouba” variância à outra).
Ausência de Outliers: Nenhum ponto “sequestra” o modelo. Este não é necessariamente um pressuposto formal, dado que a existência de linearidade, normalidade e homocedasticidade perfeitas (enquanto conceito matemático formal) impediriam a existência de outliers.
2 Atalho: check_model()
A forma mais rápida de inspeccionar TUDO é usar performance::check_model():
library(palmerpenguins)
library(performance)
# Modelo: Prever massa corporal com comprimento do bico
modelo <- lm(body_mass_g ~ bill_length_mm, penguins)
check_model(modelo)Este dashboard mostra 6 painéis: - Posterior Predictive Check: Os dados simulados pelo modelo parecem-se com os reais? - Linearity: A linha vermelha deve ser horizontal. - Homogeneity of Variance: A linha deve ser horizontal (sem funil). - Influential Observations: Pontos fora das linhas tracejadas são problemáticos. - Normality of Residuals: Os pontos devem seguir a linha diagonal. - Collinearity: VIF < 5 é bom (só relevante com múltiplas VIs, observadas).
Se tudo parecer “razoável”, pode avançar. Se algo parecer muito errado, convém tentar perceber o que se passa e testar a robustez das conclusões.
3 Linearidade (Só para IVs Contínuas)
Verificar Visualmente
ggplot(penguins, aes(x = bill_length_mm, y = body_mass_g)) +
geom_point(alpha = 0.5) +
geom_smooth(method = "lm", color = "blue") +
geom_smooth(method = "loess", color = "red", linetype = "dashed") +
theme_classic() +
labs(title = "Linearidade: Azul = Linear, Vermelho = Loess")- Se as duas linhas forem semelhantes→ relação é linear.
- Se a linha vermelha (loess) com curvas descrever muito melhor os dados→ não-linearidade.
A linha azul será sempre linear, mesmo que os dados não sejam e a linha do loess terá quase sempre algum tipo de curva mesmo que a relação seja bastante linear. Foquem-se no padrão dos pontos.
Soluções
Se a violação da linearidade sugerir um padrão que pode ser bem descrito matematicamente (e.g., quadrático, exponencial, etc…) então o que teremos de fazer é modelar esse padrão. Se o padrão não parecer propriamente linear, mas também não for fácil descrevê-lo com outra função matemática, assumir linearidade pode ser o que implica menos pressupostos teóricos (mas devemos ter cuidado para não interpretar o modelo com o melhor modelo à face da Terra).
4 Normalidade dos Resíduos
Visual: Q-Q Plot (Preferido!)
residuos <- data.frame(resid = rstandard(modelo))
ggplot(residuos, aes(sample = resid)) +
stat_qq() +
stat_qq_line(color = "red") +
theme_classic() +
labs(title = "Q-Q Plot: Normalidade dos Resíduos")Como interpretar: - Pontos perto da linha vermelha → Normal. - Caudas que se afastam → Desvios nas extremidades (comum e geralmente OK). - “S” shape → Assimetria forte.
Testes Formais
- N pequeno → O teste tem pouco poder (não detecta violações).
- N grande → O teste é demasiado sensível (detecta desvios irrelevantes).
Prefira sempre a inspeção visual.
Shapiro-Wilk
shapiro.test(resid(modelo))
Shapiro-Wilk normality test
data: resid(modelo)
W = 0.99144, p-value = 0.04502
Kormogorov-Smirnov com Correcção de Lilliefors
library(nortest)
lillie.test(resid(modelo))
Lilliefors (Kolmogorov-Smirnov) normality test
data: resid(modelo)
D = 0.040585, p-value = 0.1859
Soluções
Se a normalidade falhar:
Transformar Y (e.g.,
log(Y)para skewness positiva).Usar estatística robusta (ver abaixo).
Não fazer nada e confiar no teorema do limite central (TLC; tendo um N grande) e o facto dos modelos tenderem a ser robustos a violações da normalidade (mas isto só se a violação não for muito grave e soubermos mesmo o que TLC quer dize, nomeadamente ter em conta que a convergência para a normal pode necessitar de Ns maiores/ou demorar mais que o esperado em alguns casos).
5 Homoscedasticidade
Visual: Resíduos vs Previsões
residuos$fitted <- fitted(modelo)
ggplot(residuos, aes(x = fitted, y = resid)) +
geom_point(alpha = 0.5) +
geom_hline(yintercept = 0, color = "red") +
geom_smooth(color = "blue") +
theme_classic() +
labs(title = "Homoscedasticidade: Procurar Padrão de Funil",
x = "Valores Ajustados", y = "Resíduos")# ou com o performance e o see
plot(check_heteroscedasticity(modelo))Como interpretar:
Dispersão constante ao longo do eixo X → Bom.
Funil (aumenta ou diminui) → Heteroscedasticidade.
Teste Formal: Teste de Levene e Brown-Forsythe
# Modelo com IV categórica
modelo_cat <- lm(body_mass_g ~ species, penguins)
library(car)
# Levene (SPSS chama Levene baseado na média)
leveneTest(modelo_cat, center = "mean")| Df | F value | Pr(>F) | |
|---|---|---|---|
| group | 2 | 5.335495 | 0.0052305 |
| 339 | NA | NA |
# Brown-Forsythe (SPSS chama Levene baseado na mediana)
leveneTest(modelo_cat, center = "median")| Df | F value | Pr(>F) | |
|---|---|---|---|
| group | 2 | 5.120251 | 0.0064451 |
| 339 | NA | NA |
# Ou sem argumento center, porque median é o default
leveneTest(modelo_cat)| Df | F value | Pr(>F) | |
|---|---|---|---|
| group | 2 | 5.120251 | 0.0064451 |
| 339 | NA | NA |
# ou com o performance
# (o `[1]` é preciso para previnir um bug que o `check_homogeneity()`
# tem com o método "levene")
check_homogeneity(modelo_cat, method = "levene", center = "mean")[1][1] 0.005230535
check_homogeneity(modelo_cat, method = "levene", center = "median")[1][1] 0.006445083
check_homogeneity(modelo_cat, method = "levene")[1][1] 0.006445083
Se p < α → Variâncias desiguais.
Soluções
Transformar Y (e.g.,
log(Y)ousqrt(Y)).Para ANOVA: Usar correção de Welch (
oneway.test(Y ~ grupo, var.equal = FALSE)).Usar testes robustos (ver abaixo).
6 Esfericidade
Usar o check_sphericity() do performance e aplicar a correcção de Greenhouse-Geisser (ou outra)—o afex::aov_4()/aov_car() já faz isso por nós.
7 Outliers
A palavra outlier parece simples, mas na prática há várias definições.
Na psicologia (e em modelos lineares), as definições baseadas no modelo costumam ser mais úteis do que métodos univariados (tipo “pontos fora do IQR” numa variável isolada), porque aquilo que nos interessa é: este caso está a distorcer a conclusão do modelo?
Há várias definições: resíduos padronizados, leverage, Cook’s distance, etc. Cada uma responde a uma pergunta diferente.
Outliers podem ser multivariados: um ponto pode não ser “extremo” em nenhuma variável isolada, mas ser estranho na combinação (pensem em “perfil raro”).
Os critérios podem ser enviesados pela presença de outliers: se os próprios outliers influenciam médias/variâncias/ajustes, eles também podem influenciar a régua que estamos a usar para os detectar.
O pacote performance tem um tutorial excelente sobre check_outliers() e os métodos de detecção disponíveis.
Verificar com check_outliers()
out <- check_outliers(modelo)
outOK: No outliers detected.
- Based on the following method and threshold: cook (0.7).
- For variable: (Whole model)
Se quiser ver os casos (linhas) que foram sinalizados, a forma mais segura é pegar nos dados que o modelo realmente usou:
# Dados usados pelo modelo (já sem NAs, etc.)
dados_modelo <- model.frame(modelo)
# Ver as linhas marcadas como influentes/outliers
# (Nota: 'out' funciona como índice)
#View(dados_modelo[out, ])Visual: Cook’s Distance
residuos$cooksd <- cooks.distance(modelo)
residuos$id <- 1:nrow(residuos)
ggplot(residuos, aes(x = id, y = cooksd)) +
geom_col() +
geom_hline(yintercept = 4/nrow(penguins), color = "red", linetype = "dashed") +
theme_classic() +
labs(title = "Cook's Distance: Outliers Influentes",
subtitle = "Acima da linha vermelha = Problemático")# ou com performance e see
plot(check_outliers(modelo))Mas eu não queria remover outliers…
Tudo bem.
Só não confundam duas coisas:
- Detecção de casos influentes (diagnóstico) vs.
- Remoção de casos (decisão).
E lembrem-se: muitos métodos robustos acabam por “aparar”/reduzir o peso dos outliers quase por definição, sem precisarmos de apagar linhas à mão.
Recomendação prática (sem cair em loops)
Um workflow razoável (e muito comum) é:
- Ajustar o modelo no dataset completo.
- Identificar casos influentes.
- Re-ajustar o modelo sem esses casos.
- Ver se a história muda.
# 1) Modelo no dataset completo
modelo <- lm(body_mass_g ~ bill_length_mm, penguins)
# 2) Detectar casos influentes/outliers (índices no dataset usado pelo modelo)
out <- check_outliers(modelo)
# 3) Reajustar sem esses casos (uma vez)
dados_sem_out <- model.frame(modelo)[-out, ]
modelo_sem_out <- lm(body_mass_g ~ bill_length_mm, penguins)
# 4) Comparar (o que muda?)
summary(modelo)
Call:
lm(formula = body_mass_g ~ bill_length_mm, data = penguins)
Residuals:
Min 1Q Median 3Q Max
-1762.08 -446.98 32.59 462.31 1636.86
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 362.307 283.345 1.279 0.202
bill_length_mm 87.415 6.402 13.654 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 645.4 on 340 degrees of freedom
(2 observations deleted due to missingness)
Multiple R-squared: 0.3542, Adjusted R-squared: 0.3523
F-statistic: 186.4 on 1 and 340 DF, p-value: < 2.2e-16
summary(modelo_sem_out)
Call:
lm(formula = body_mass_g ~ bill_length_mm, data = penguins)
Residuals:
Min 1Q Median 3Q Max
-1762.08 -446.98 32.59 462.31 1636.86
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 362.307 283.345 1.279 0.202
bill_length_mm 87.415 6.402 13.654 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 645.4 on 340 degrees of freedom
(2 observations deleted due to missingness)
Multiple R-squared: 0.3542, Adjusted R-squared: 0.3523
F-statistic: 186.4 on 1 and 340 DF, p-value: < 2.2e-16
Se removermos outliers e voltarmos a calcular os critérios, pode acontecer que:
- casos que não eram outliers antes passem a ser agora;
- removemos esses também;
- e isto repete-se…
Por isso, neste tutorial prático, a recomendação é simples: façam no máximo uma ronda (modelo completo → modelo sem outliers) e usem estatística robusta quando o problema é estrutural.
Soluções
Investigar os casos (erro de medição? entrada mal feita? caso genuíno?).
Comparar modelo completo vs. modelo sem outliers (uma ronda, sem loops).
Usar modelos robustos (dão menos peso aos outliers e evitam “apagar pessoas”).
8 Estatística Robusta: As Ferramentas
Quando as assunções falham, usamos métodos que são menos sensíveis a violações.
Regressão -> robustbase::lmrob()
library(robustbase)
# Modelo OLS normal
modelo_ols <- lm(body_mass_g ~ bill_length_mm, penguins)
# Modelo robusto (estimador MM)
modelo_robusto <- lmrob(body_mass_g ~ bill_length_mm, penguins)
# Comparar
summary(modelo_ols)
Call:
lm(formula = body_mass_g ~ bill_length_mm, data = penguins)
Residuals:
Min 1Q Median 3Q Max
-1762.08 -446.98 32.59 462.31 1636.86
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 362.307 283.345 1.279 0.202
bill_length_mm 87.415 6.402 13.654 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 645.4 on 340 degrees of freedom
(2 observations deleted due to missingness)
Multiple R-squared: 0.3542, Adjusted R-squared: 0.3523
F-statistic: 186.4 on 1 and 340 DF, p-value: < 2.2e-16
summary(modelo_robusto)
Call:
lmrob(formula = body_mass_g ~ bill_length_mm, data = penguins)
\--> method = "MM"
Residuals:
Min 1Q Median 3Q Max
-1892.83 -445.17 29.46 416.78 1593.07
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -7.327 382.138 -0.019 0.985
bill_length_mm 96.554 9.555 10.105 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Robust residual standard error: 563.7
(2 observations deleted due to missingness)
Multiple R-squared: 0.4028, Adjusted R-squared: 0.4011
Convergence in 21 IRWLS iterations
Robustness weights:
28 weights are ~= 1. The remaining 314 ones are summarized as
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.2366 0.8229 0.9368 0.8806 0.9813 0.9990
Algorithmic parameters:
tuning.chi bb tuning.psi refine.tol
1.548e+00 5.000e-01 4.685e+00 1.000e-07
rel.tol scale.tol solve.tol zero.tol
1.000e-07 1.000e-10 1.000e-07 1.000e-10
eps.outlier eps.x warn.limit.reject warn.limit.meanrw
2.924e-04 1.084e-10 5.000e-01 5.000e-01
nResample max.it best.r.s k.fast.s k.max
500 50 2 1 200
maxit.scale trace.lev mts compute.rd fast.s.large.n
200 0 1000 0 2000
psi subsampling cov
"bisquare" "nonsingular" ".vcov.avar1"
compute.outlier.stats
"SM"
seed : int(0)
O que muda:
Os coeficientes podem ser ligeiramente diferentes.
Os erros-padrão (e valores-p) são mais fiáveis com outliers.
ANOVA -> WRS2::t1way(); WRS2::t2way() (etc…)
Df Sum Sq Mean Sq F value Pr(>F)
species 2 146864214 73432107 343.6 <2e-16 ***
Residuals 339 72443483 213698
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
2 observations deleted due to missingness
# ANOVA robusta (trimmed means)
t1way(body_mass_g ~ species, penguins)Call:
t1way(formula = body_mass_g ~ species, data = penguins)
Test statistic: F = 247.5234
Degrees of freedom 1: 2
Degrees of freedom 2: 118.49
p-value: 0
Explanatory measure of effect size: 0.91
Bootstrap CI: [0.85; 0.98]
t1way(body_mass_g ~ species, penguins, nboot = 5000)Call:
t1way(formula = body_mass_g ~ species, data = penguins, nboot = 5000)
Test statistic: F = 247.5234
Degrees of freedom 1: 2
Degrees of freedom 2: 118.49
p-value: 0
Explanatory measure of effect size: 0.91
Bootstrap CI: [0.86; 0.98]
O que faz:
Usa médias aparadas (ignora os 20% extremos).
Não assume homoscedasticidade.
Pode usar bootstrapping.
Medidas Repetidas -> robustlmm::rlmer()
9 Fluxo de Trabalho Recomendado
Passo 1: Ajustar Modelo OLS
modelo <- lm(VD ~ IV, dados)Passo 2: Diagnosticar
check_model(modelo)Passo 3: Decidir
Tudo OK? → Reportar o modelo OLS.
Violações menores? → Reportar OLS + mencionar (ex: “pequenos desvios na normalidade foram observados”).
Violações graves? → Ajustar versão robusta.
Passo 4: Comparar
Se os valores-p mudarem substancialmente (e.g: de 0.03 para 0.45, ou vice-versa), considerar reportar o robusto e citar Field & Wilcox (2017).
10 Exemplo Completo
Vamos prever a massa corporal dos pinguins com comprimento do bico E espécie.
library(palmerpenguins)
library(performance)
library(robustbase)
# 1. Ajustar modelo OLS
modelo_completo <- lm(body_mass_g ~ bill_length_mm * species, penguins)
# 2. Diagnosticar
check_model(modelo_completo)# 3. Verificar outliers
check_outliers(modelo_completo)OK: No outliers detected.
- Based on the following method and threshold: cook (0.7).
- For variable: (Whole model)
Call:
lm(formula = body_mass_g ~ bill_length_mm * species, data = penguins)
Residuals:
Min 1Q Median 3Q Max
-918.76 -245.89 -8.65 238.44 1126.27
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 252.399 317.986 0.794 0.42790
bill_length_mm 87.692 6.946 12.625 < 2e-16 ***
species1 -217.516 408.146 -0.533 0.59443
species2 593.744 498.875 1.190 0.23482
bill_length_mm:species1 6.808 9.568 0.711 0.47729
bill_length_mm:species2 -28.575 10.485 -2.725 0.00676 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 371.8 on 336 degrees of freedom
(2 observations deleted due to missingness)
Multiple R-squared: 0.7882, Adjusted R-squared: 0.7851
F-statistic: 250.1 on 5 and 336 DF, p-value: < 2.2e-16
summary(modelo_robusto)
Call:
lmrob(formula = body_mass_g ~ bill_length_mm * species, data = penguins)
\--> method = "MM"
Residuals:
Min 1Q Median 3Q Max
-921.2416 -234.0475 0.8195 241.9822 1130.1167
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) 273.129 286.068 0.955 0.3404
bill_length_mm 87.293 6.338 13.774 <2e-16 ***
species1 -356.282 371.500 -0.959 0.3382
species2 677.378 446.428 1.517 0.1301
bill_length_mm:species1 9.945 8.806 1.129 0.2596
bill_length_mm:species2 -30.348 9.590 -3.164 0.0017 **
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Robust residual standard error: 368.3
(2 observations deleted due to missingness)
Multiple R-squared: 0.7894, Adjusted R-squared: 0.7863
Convergence in 10 IRWLS iterations
Robustness weights:
39 weights are ~= 1. The remaining 303 ones are summarized as
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.3261 0.8615 0.9500 0.9041 0.9835 0.9989
Algorithmic parameters:
tuning.chi bb tuning.psi refine.tol
1.548e+00 5.000e-01 4.685e+00 1.000e-07
rel.tol scale.tol solve.tol zero.tol
1.000e-07 1.000e-10 1.000e-07 1.000e-10
eps.outlier eps.x warn.limit.reject warn.limit.meanrw
2.924e-04 1.084e-10 5.000e-01 5.000e-01
nResample max.it best.r.s k.fast.s k.max
500 50 2 1 200
maxit.scale trace.lev mts compute.rd fast.s.large.n
200 0 1000 0 2000
psi subsampling cov
"bisquare" "nonsingular" ".vcov.avar1"
compute.outlier.stats
"SM"
seed : int(0)
Interpretação:
Se os coeficientes forem semelhantes → reportar OLS.
Se diferirem → reportar robusto e mencionar: “Devido a violações de homoscedasticidade, reportamos estimativas robustas (Field & Wilcox, 2017).”
Neste caso, as conclusões são muito semelhantes (a decisão em relação aos efeitos significativos vs não-significativos não muda; não há inversões de sinais nos declives; o decive de
bill_length_mmé semelhante; as maiores diferenças estão nos efeitos não significativos da espécie, os declives da interacção até aumentam a magnitude). Logo, não parece que as conclusões que retirámos estejam a ser enviesadas por outliers.
11 Tabela Resumo: Soluções Rápidas
| Violação | Sintoma Visual | Teste Formal | Solução |
|---|---|---|---|
| Não-Linearidade | Loess ≠ Linear | NA | GLM ou transformação |
| Não-Normalidade | Q-Q Plot desviado | Shapiro-Wilk |
lmrob(), WRS2::t1way() (ou equivalente) |
| Heteroscedasticidade | Funil nos resíduos | Levene’s Test |
lmrob(), Welch ANOVA, correcção de Brown-Forsythe, WRS2::t1way() (ou equivalente) |
| Outliers | Cook’s D alto | check_outliers() |
lmrob(), rlmer()
|
12 Recursos Adicionais
13 Conclusão
Testar pressupostos não é uma “checklist” burocrática. É uma forma de entender os nossos dados. Quando as assunções falham, não entrem em pânico. A estatística robusta existe para isso.
Recomendações
Olhar para
check_model().Se algo parecer errado, ajustar a versão robusta (ou ajustá-la anyway…).
Comparar resultados.
Reportar o mais apropriado (e justificar).
E lembrem-se desvios consideráveis aos pressupostos dos modelos podem ter relevância teórica. Não pensem neles apenas como “burocracia” matemática.