Pandas & NumPy Academy · Aula

ANOVA e testes pós-hoc

Compare médias entre três ou mais grupos com ANOVA de uma via e execute o teste de Tukey HSD para identificar quais pares diferem.

Aula 4 de 413 etapas

ANOVA e testes pós-hoc é uma aula grátis de Pandas & NumPy Academy no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Pandas & NumPy Academy, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Pandas & NumPy Academy inclui 4 aulas no total.

Por que não usar vários testes t?

Ao comparar médias entre três ou mais grupos, executar vários testes t aumenta a taxa de erro do Tipo I (taxa de falsos positivos). Testar os grupos A vs. B, A vs. C e B vs. C, cada um com α=0,05, resulta em uma probabilidade geral de falso positivo de aproximadamente 14%, não 5%. A Análise de Variância (ANOVA) resolve isso testando todos os grupos simultaneamente em um único teste, mantendo a taxa de falsos positivos exatamente em α. Uma ANOVA substitui todos os testes t par a par para a questão geral: “Algum grupo é diferente?”

ANOVA de uma via: visão geral

A ANOVA de uma via testa se as médias de três ou mais grupos diferem significativamente. Ela decompõe a variância total em variância entre grupos (explicada pela associação ao grupo) e variância dentro dos grupos (ruído aleatório). A estatística F é a razão entre elas: F = (variância entre grupos) / (variância dentro dos grupos). Um F grande significa que as diferenças entre os grupos são grandes em relação ao ruído, resultando em um valor-p pequeno. A hipótese nula é H₀: todas as médias dos grupos são iguais.

import numpy as np
from scipy import stats

np.random.seed(0)
# Three product variants with different conversion rates
group_a = np.random.normal(10.0, 2.0, 50)  # baseline
group_b = np.random.normal(11.5, 2.0, 50)  # slightly better
group_c = np.random.normal(13.0, 2.0, 50)  # clearly better

f_stat, p = stats.f_oneway(group_a, group_b, group_c)
print(f'F-statistic: {f_stat:.3f}')
print(f'p-value: {p:.6f}')
print('At least one group differs?', 'Yes' if p < 0.05 else 'No')

ANOVA com DataFrames do Pandas

Na prática, seus dados estão em um DataFrame em formato longo, no qual uma coluna contém o rótulo do grupo e outra contém a medição. Extraia cada grupo como uma Series e passe-os para stats.f_oneway(). Como alternativa, se você tiver um DataFrame em formato largo (uma coluna por grupo), passe cada coluna diretamente. A função aceita qualquer quantidade de argumentos posicionais, facilitando a extensão para 4, 5 ou mais grupos.

import pandas as pd
import numpy as np
from scipy import stats

np.random.seed(1)
df = pd.DataFrame({
    'group': ['A']*40 + ['B']*40 + ['C']*40 + ['D']*40,
    'score': np.concatenate([
        np.random.normal(70, 10, 40),
        np.random.normal(75, 10, 40),
        np.random.normal(80, 10, 40),
        np.random.normal(72, 10, 40)
    ])
})

groups = [group['score'].values
          for _, group in df.groupby('group')]

f, p = stats.f_oneway(*groups)
print(f'F={f:.3f}, p={p:.4f}')
print(df.groupby('group')['score'].mean().round(2))

Pressupostos da ANOVA

A ANOVA de uma via pressupõe: (1) Independência — as observações são independentes umas das outras; (2) Normalidade — os resíduos são aproximadamente normais dentro de cada grupo (robusta para amostras grandes por meio do CLT); (3) Homoscedasticidade — variâncias iguais entre os grupos. Teste o pressuposto de variância com o teste de Levene (stats.levene(*groups)). Se as variâncias forem diferentes, use a ANOVA de Welch, disponível na biblioteca pingouin ou calculada por meio de statsmodels.

import numpy as np
from scipy import stats

np.random.seed(0)
g1 = np.random.normal(10, 2, 40)
g2 = np.random.normal(12, 2, 40)
g3 = np.random.normal(11, 8, 40)   # Much higher variance

# Test equal variances
stat_l, p_l = stats.levene(g1, g2, g3)
print(f'Levene\'s test: stat={stat_l:.3f}, p={p_l:.4f}')
if p_l < 0.05:
    print('Warning: Unequal variances! Standard ANOVA may be unreliable.')
    print('Consider Welch\'s ANOVA or Kruskal-Wallis.')

Kruskal-Wallis: ANOVA não paramétrica

Quando os pressupostos da ANOVA são violados (distribuições não normais, amostras pequenas, variâncias diferentes), o teste de Kruskal-Wallis é a alternativa não paramétrica. Ele testa se as distribuições dos grupos diferem, convertendo os dados em postos e comparando as somas dos postos. Use scipy.stats.kruskal(*groups). A hipótese nula é que todos os grupos têm a mesma distribuição (equivalente a testar medianas iguais quando as distribuições têm o mesmo formato). Ele é sempre válido, mas menos poderoso que a ANOVA quando os pressupostos são atendidos.

import numpy as np
from scipy import stats

np.random.seed(0)
# Non-normal data: customer satisfaction scores (1-10 scale)
g1 = np.random.choice(range(1, 11), 50, p=[0.05]*10)
g2 = np.random.choice(range(1, 11), 50, p=[0.02, 0.03, 0.05, 0.08, 0.12, 0.15, 0.20, 0.15, 0.12, 0.08])
g3 = np.random.choice(range(1, 11), 50, p=[0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.08, 0.05, 0.04, 0.03])

stat, p = stats.kruskal(g1, g2, g3)
print(f'Kruskal-Wallis: H={stat:.3f}, p={p:.4f}')
print('Groups differ?', 'Yes' if p < 0.05 else 'No')

Testes pós-hoc: por que são necessários

Uma ANOVA significativa informa que pelo menos uma média de grupo é diferente, mas não informa quais pares diferem. Os testes pós-hoc realizam todas as comparações par a par, corrigindo-as para comparações múltiplas. O mais popular é a Diferença Honestamente Significativa de Tukey (Tukey HSD), que controla exatamente em α a taxa de erro familiar. Ela está disponível em statsmodels.stats.multicomp.pairwise_tukeyhsd(). Execute testes pós-hoc somente após uma ANOVA significativa — não como uma busca indiscriminada em resultados não significativos.

import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd

np.random.seed(0)
data = np.concatenate([
    np.random.normal(70, 10, 40),
    np.random.normal(80, 10, 40),
    np.random.normal(75, 10, 40)
])
groups = ['A']*40 + ['B']*40 + ['C']*40

# First confirm ANOVA is significant
f, p = stats.f_oneway(*[data[i*40:(i+1)*40] for i in range(3)])
print(f'ANOVA p={p:.4f}')
if p < 0.05:
    print('Post-hoc Tukey HSD:')
    print(pairwise_tukeyhsd(data, groups, alpha=0.05))

Interpretando a saída do Tukey HSD

A tabela do Tukey HSD tem uma linha para cada par de grupos. As colunas principais são: meandiff (diferença entre as médias do par), lower e upper (intervalo de confiança de 95% para a diferença) e reject (True se o par difere significativamente em α). Um IC que não inclui zero significa que o par é significativamente diferente. Grupos cujos ICs se sobrepõem não são significativamente diferentes entre si.

import numpy as np
from statsmodels.stats.multicomp import pairwise_tukeyhsd

np.random.seed(42)
data = np.concatenate([
    np.random.normal(10, 2, 60),  # Group A
    np.random.normal(13, 2, 60),  # Group B (clearly higher)
    np.random.normal(10.5, 2, 60) # Group C (barely different from A)
])
groups = ['A']*60 + ['B']*60 + ['C']*60

result = pairwise_tukeyhsd(data, groups, alpha=0.05)
print(result)
# A-B: reject=True (B is higher)
# A-C: reject=False (barely different)
# B-C: reject=True (B is higher)

Correções de Bonferroni e Benjamini-Hochberg

Como alternativa ao Tukey HSD, você pode aplicar uma correção de Bonferroni aos testes t par a par: execute todos os testes t e depois ajuste o limiar para α/k, em que k é o número de comparações. Essa abordagem é conservadora — pode deixar de detectar diferenças reais. A correção da taxa de falsas descobertas (FDR) de Benjamini-Hochberg é menos conservadora: controla a proporção esperada de descobertas falsas, permitindo mais verdadeiros positivos ao custo de um número ligeiramente maior de falsos positivos. Use statsmodels.stats.multitest.multipletests(p_values, method='fdr_bh').

import numpy as np
from scipy import stats
from statsmodels.stats.multitest import multipletests

np.random.seed(0)
groups = [np.random.normal(10 + i*1.5, 2, 40) for i in range(4)]
names = ['A', 'B', 'C', 'D']

# All pairwise t-tests
p_values = []
pairs = []
for i in range(len(groups)):
    for j in range(i+1, len(groups)):
        _, p = stats.ttest_ind(groups[i], groups[j])
        p_values.append(p)
        pairs.append(f'{names[i]}-{names[j]}')

# Benjamini-Hochberg correction
reject, adj_p, _, _ = multipletests(p_values, method='fdr_bh')
for pair, p, ap, r in zip(pairs, p_values, adj_p, reject):
    print(f'{pair}: raw_p={p:.4f}, adj_p={ap:.4f}, significant={r}')

Tamanho do efeito na ANOVA: eta ao quadrado

Na ANOVA, a medida análoga ao d de Cohen é o eta ao quadrado (η²): a proporção da variância total explicada pela associação ao grupo. η² = SS_between / SS_total. Valores: < 0,01 é pequeno, 0,06 é médio, > 0,14 é grande. O eta ao quadrado parcial (η²_p) é mais comum em pesquisas publicadas: ele divide apenas pela variância do grupo mais a variância do erro, não pela variância total. Calcule sempre η² junto com o valor-p da ANOVA para mostrar se o efeito é relevante na prática, não apenas detectável estatisticamente.

import numpy as np
from scipy import stats

np.random.seed(0)
groups = [
    np.random.normal(10, 2, 50),
    np.random.normal(12, 2, 50),
    np.random.normal(14, 2, 50)
]

all_data = np.concatenate(groups)
grand_mean = all_data.mean()

ss_between = sum(len(g) * (g.mean() - grand_mean)**2 for g in groups)
ss_total = sum((x - grand_mean)**2 for g in groups for x in g)
eta_sq = ss_between / ss_total

f, p = stats.f_oneway(*groups)
print(f'F={f:.3f}, p={p:.4f}')
print(f'Eta-squared: {eta_sq:.4f} ({eta_sq:.1%} of variance explained)')

Visão geral da ANOVA de duas vias

A ANOVA de duas vias estende a ANOVA de uma via a dois fatores categóricos simultaneamente. Por exemplo, testa se as notas dos exames diferem tanto pelo método de ensino quanto pelo nível de experiência dos estudantes. Ela também testa o efeito de interação: o efeito do método de ensino depende do nível de experiência? A ANOVA de duas vias é implementada em statsmodels por meio de ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit() e anova_lm(model). Esta é uma base para projetos experimentais mais avançados.

import pandas as pd
import numpy as np
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm

np.random.seed(0)
df = pd.DataFrame({
    'method': ['trad']*60 + ['active']*60,
    'level': ['beginner']*30 + ['advanced']*30 + ['beginner']*30 + ['advanced']*30,
    'score': (np.random.normal(70, 8, 30).tolist() +
              np.random.normal(80, 8, 30).tolist() +
              np.random.normal(75, 8, 30).tolist() +
              np.random.normal(88, 8, 30).tolist())
})

model = ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit()
print(anova_lm(model, typ=2)[['F', 'PR(>F)']].round(4))

ANOVA de medidas repetidas

A ANOVA de medidas repetidas é usada quando os mesmos participantes são medidos várias vezes — por exemplo, ao testar o tempo de reação nos minutos 0, 30 e 60 após um tratamento. Ela é a generalização para vários grupos do teste t pareado. Ao levar em conta as diferenças individuais, ela tem mais poder que a ANOVA de uma via. Em Python, use pingouin.rm_anova() ou pg.pairwise_tests() para a análise pós-hoc. Quando a esfericidade é violada (testada com o teste de Mauchly), use valores-p corrigidos por Greenhouse-Geisser.

import pandas as pd
import numpy as np

# Example structure for repeated measures (pingouin library)
np.random.seed(0)
subjects = list(range(20)) * 3   # 20 subjects, 3 time points
timepoints = ['T0']*20 + ['T1']*20 + ['T2']*20
scores = np.concatenate([
    np.random.normal(50, 10, 20),   # baseline
    np.random.normal(55, 10, 20),   # improved
    np.random.normal(60, 10, 20)    # further improved
])

df_rm = pd.DataFrame({'subject': subjects, 'time': timepoints, 'score': scores})
print(df_rm.groupby('time')['score'].mean().round(2))
# Use pingouin.rm_anova(data=df_rm, dv='score', within='time', subject='subject')

Verificação rápida

Teste sua compreensão dos conceitos de Análise de Dados desta lição.

Resumo da lição

Nesta lição, você aprendeu que: scipy.stats.f_oneway() testa se algum grupo difere na média, controlando a taxa geral de erro do Tipo I; os testes pós-hoc (Tukey HSD) identificam quais pares específicos diferem após uma ANOVA significativa; e o Kruskal-Wallis é a alternativa não paramétrica quando os pressupostos de normalidade ou de variâncias iguais são violados. Em seguida, começaremos o projeto final, integrando todas as habilidades em um pipeline de dados completo, de ponta a ponta.

Grátis para começar

Aprenda Python com um tutor de IA — grátis

Escreva e execute código real no seu navegador, obtenha ajuda instantânea de um tutor de IA 24/7 e continue de onde parou na web ou no app.

Cursos
30
Aulas
120

Perguntas Frequentes

A aula “ANOVA e testes pós-hoc” é grátis?

Sim — o texto completo de “ANOVA e testes pós-hoc” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Pandas & NumPy Academy, atualize para CoddyKit PRO. O curso de Pandas & NumPy Academy inclui 4 aulas no total.

O que vou aprender em “ANOVA e testes pós-hoc”?

Compare médias entre três ou mais grupos com ANOVA de uma via e execute o teste de Tukey HSD para identificar quais pares diferem. Você pratica Pandas & NumPy Academy com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar Pandas & NumPy Academy?

Nenhuma experiência prévia é necessária. Pandas & NumPy Academy no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.

Quanto tempo leva a aula “ANOVA e testes pós-hoc”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de Pandas & NumPy Academy?

Sim. Cada aula de Pandas & NumPy Academy inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Estatísticas descritivas e teste de normalidade
  2. Testes t para comparar médias
  3. Teste qui-quadrado de independência
  4. ANOVA e testes pós-hoc
← Voltar para Pandas & NumPy Academy