Como Realizar Testes de Hipótese com Python: Guia Prático para Analistas de Dados | Data Driven School

Como Realizar Testes de Hipótese com Python: Guia Prático para Analistas de Dados

Por que os Testes de Hipótese São Fundamentais na Análise de Dados

Durante meus primeiros anos como analista, lembro de uma situação específica onde precisava validar se uma campanha de marketing havia realmente impactado as vendas. Os números pareciam promissores, mas como ter certeza de que não era apenas coincidência? Foi nesse momento que entendi a importância dos testes de hipótese.

Os testes de hipótese são ferramentas estatísticas que nos permitem tomar decisões baseadas em dados, determinando se um efeito observado é estatisticamente significativo ou se pode ser atribuído ao acaso. Para qualquer analista de dados, dominar essa técnica é essencial para:

  • Validar resultados de experimentos A/B
  • Comparar performance entre diferentes grupos
  • Verificar se mudanças implementadas trouxeram resultados significativos
  • Fundamentar decisões de negócio com base estatística sólida

Conceitos Fundamentais dos Testes de Hipótese

As Hipóteses: Nula e Alternativa

Todo teste de hipótese envolve duas proposições:

  • Hipótese Nula (H₀): Assume que não há diferença ou efeito significativo
  • Hipótese Alternativa (H₁): Propõe que existe uma diferença ou efeito significativo

Elementos Chave

  • Nível de Significância (α): Probabilidade de rejeitar a hipótese nula quando ela é verdadeira (geralmente 0,05)
  • P-valor: Probabilidade de obter um resultado igual ou mais extremo, assumindo que H₀ é verdadeira
  • Poder do Teste: Probabilidade de detectar um efeito quando ele realmente existe

Configurando o Ambiente Python

Vamos começar importando as bibliotecas necessárias para nossos testes:

import numpy as np
import pandas as pd
import scipy.stats as stats
from scipy import stats
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.stats.power import ttest_power
from statsmodels.stats.proportion import proportions_ztest
import warnings
warnings.filterwarnings('ignore')

# Configuração para visualizações
plt.style.use('seaborn-v0_8')
sns.set_palette("husl")

Teste T de Student: Comparando Médias

Teste T para Uma Amostra

Usado quando queremos testar se a média de uma amostra difere significativamente de um valor conhecido da população.

# Exemplo prático: Verificando se o tempo médio de carregamento do site é 3 segundos
# Dados simulados de tempo de carregamento (em segundos)
np.random.seed(42)
tempos_carregamento = np.random.normal(3.2, 0.5, 100)

# H₀: μ = 3.0 (tempo médio é 3 segundos)
# H₁: μ ≠ 3.0 (tempo médio é diferente de 3 segundos)

# Realizando o teste t para uma amostra
t_statistic, p_value = stats.ttest_1samp(tempos_carregamento, 3.0)

print(f"Estatística t: {t_statistic:.4f}")
print(f"P-valor: {p_value:.4f}")
print(f"Média da amostra: {np.mean(tempos_carregamento):.4f}")

# Interpretação
alpha = 0.05
if p_value < alpha:
   print(f"Rejeitamos H₀ (p < {alpha}): O tempo médio é significativamente diferente de 3 segundos")
else:
   print(f"Não rejeitamos H₀ (p ≥ {alpha}): Não há evidência suficiente de diferença significativa")

Teste T para Duas Amostras Independentes

Um dos casos mais comuns na prática. Recentemente, usei esse teste para comparar a taxa de conversão entre dois grupos de usuários após uma mudança no design do site:

# Exemplo: Comparando vendas entre duas estratégias de marketing
np.random.seed(42)
vendas_estrategia_a = np.random.normal(150, 30, 50)  # Estratégia A
vendas_estrategia_b = np.random.normal(165, 25, 45)  # Estratégia B

# H₀: μ_A = μ_B (não há diferença entre as estratégias)
# H₁: μ_A ≠ μ_B (há diferença entre as estratégias)

# Primeiro, verificamos a igualdade de variâncias
levene_stat, levene_p = stats.levene(vendas_estrategia_a, vendas_estrategia_b)
print(f"Teste de Levene - P-valor: {levene_p:.4f}")

# Se p > 0.05, assumimos variâncias iguais
equal_var = levene_p > 0.05

# Realizando o teste t
t_stat, p_val = stats.ttest_ind(vendas_estrategia_a, vendas_estrategia_b, equal_var=equal_var)

print(f"\nEstatística t: {t_stat:.4f}")
print(f"P-valor: {p_val:.4f}")
print(f"Média Estratégia A: {np.mean(vendas_estrategia_a):.2f}")
print(f"Média Estratégia B: {np.mean(vendas_estrategia_b):.2f}")

# Interpretação
if p_val < 0.05:
   print("Rejeitamos H₀: Há diferença significativa entre as estratégias")
else:
   print("Não rejeitamos H₀: Não há evidência de diferença significativa")

Teste T Pareado

Útil quando comparamos medições do mesmo grupo em momentos diferentes:

# Exemplo: Comparando performance antes e depois de um treinamento
np.random.seed(42)
n_funcionarios = 30

# Scores antes do treinamento
scores_antes = np.random.normal(70, 10, n_funcionarios)
# Scores depois (com melhoria média de 8 pontos)
scores_depois = scores_antes + np.random.normal(8, 5, n_funcionarios)

# H₀: μ_diferença = 0 (não há melhoria)
# H₁: μ_diferença ≠ 0 (há melhoria)

t_stat, p_val = stats.ttest_rel(scores_depois, scores_antes)

print(f"Estatística t: {t_stat:.4f}")
print(f"P-valor: {p_val:.4f}")
print(f"Diferença média: {np.mean(scores_depois - scores_antes):.2f}")

if p_val < 0.05:
   print("Rejeitamos H₀: O treinamento teve efeito significativo")
else:
   print("Não rejeitamos H₀: Não há evidência de efeito significativo do treinamento")

Testes Qui-Quadrado: Analisando Variáveis Categóricas

Teste de Independência

Extremamente útil para analisar a relação entre variáveis categóricas. Uso muito esse teste para verificar se fatores como região geográfica influenciam preferências de produtos:

# Exemplo: Verificando se há associação entre gênero e preferência de produto
data = {
   'Produto A': [45, 35],  # [Homens, Mulheres]
   'Produto B': [30, 50],
   'Produto C': [25, 15]
}

# Criando a tabela de contingência
df_contingencia = pd.DataFrame(data, index=['Homens', 'Mulheres'])
print("Tabela de Contingência:")
print(df_contingencia)

# H₀: Não há associação entre gênero e preferência de produto
# H₁: Há associação entre gênero e preferência de produto

chi2, p_val, dof, expected = stats.chi2_contingency(df_contingencia)

print(f"\nQui-quadrado: {chi2:.4f}")
print(f"P-valor: {p_val:.4f}")
print(f"Graus de liberdade: {dof}")

print("\nFrequências esperadas:")
print(pd.DataFrame(expected, index=df_contingencia.index, columns=df_contingencia.columns))

if p_val < 0.05:
   print("\nRejeitamos H₀: Há associação significativa entre gênero e preferência")
else:
   print("\nNão rejeitamos H₀: Não há evidência de associação significativa")

Teste de Normalidade: Verificando Pressupostos

Antes de aplicar testes paramétricos, é crucial verificar se os dados seguem distribuição normal:

# Exemplo: Verificando normalidade dos dados de vendas
np.random.seed(42)
vendas_normais = np.random.normal(100, 20, 200)
vendas_nao_normais = np.random.exponential(50, 200)

def teste_normalidade(dados, nome):
   print(f"\n--- Teste de Normalidade: {nome} ---")
   
   # Teste de Shapiro-Wilk (recomendado para n < 5000)
   if len(dados) <= 5000:
       shapiro_stat, shapiro_p = stats.shapiro(dados)
       print(f"Shapiro-Wilk - Estatística: {shapiro_stat:.4f}, P-valor: {shapiro_p:.4f}")
   
   # Teste de Kolmogorov-Smirnov
   ks_stat, ks_p = stats.kstest(dados, 'norm', args=(np.mean(dados), np.std(dados)))
   print(f"Kolmogorov-Smirnov - Estatística: {ks_stat:.4f}, P-valor: {ks_p:.4f}")
   
   # Interpretação
   alpha = 0.05
   if len(dados) <= 5000:
       resultado = "Normal" if shapiro_p > alpha else "Não Normal"
       print(f"Resultado (Shapiro-Wilk): {resultado}")
   
   resultado_ks = "Normal" if ks_p > alpha else "Não Normal"
   print(f"Resultado (KS): {resultado_ks}")

# Testando ambos os conjuntos
teste_normalidade(vendas_normais, "Vendas Normais")
teste_normalidade(vendas_nao_normais, "Vendas Não Normais")

Testes Não Paramétricos: Alternativas Robustas

Teste de Mann-Whitney U

Quando os dados não seguem distribuição normal, utilizamos alternativas não paramétricas:

# Exemplo: Comparando satisfação do cliente entre dois canais de atendimento
np.random.seed(42)
# Dados não normais (escala de 1-10)
satisfacao_chat = np.random.choice(range(1, 11), size=80, p=[0.02, 0.03, 0.05, 0.1, 0.15, 0.2, 0.2, 0.15, 0.08, 0.02])
satisfacao_telefone = np.random.choice(range(1, 11), size=75, p=[0.01, 0.02, 0.03, 0.08, 0.12, 0.18, 0.25, 0.18, 0.1, 0.03])

# H₀: Não há diferença na satisfação entre os canais
# H₁: Há diferença na satisfação entre os canais

u_statistic, p_value = stats.mannwhitneyu(satisfacao_chat, satisfacao_telefone, alternative='two-sided')

print(f"Estatística U: {u_statistic}")
print(f"P-valor: {p_value:.4f}")
print(f"Mediana Chat: {np.median(satisfacao_chat)}")
print(f"Mediana Telefone: {np.median(satisfacao_telefone)}")

if p_value < 0.05:
   print("Rejeitamos H₀: Há diferença significativa na satisfação entre os canais")
else:
   print("Não rejeitamos H₀: Não há evidência de diferença significativa")

Teste de Proporções: Comparando Taxas

Muito útil para comparar taxas de conversão, cliques, ou qualquer métrica percentual:

# Exemplo: Comparando taxa de conversão entre duas versões de uma landing page
# Versão A: 1250 visitantes, 87 conversões
# Versão B: 1180 visitantes, 102 conversões

visitantes_a, conversoes_a = 1250, 87
visitantes_b, conversoes_b = 1180, 102

# Calculando as proporções
prop_a = conversoes_a / visitantes_a
prop_b = conversoes_b / visitantes_b

print(f"Taxa de conversão A: {prop_a:.4f} ({prop_a*100:.2f}%)")
print(f"Taxa de conversão B: {prop_b:.4f} ({prop_b*100:.2f}%)")

# H₀: p_A = p_B (não há diferença nas taxas de conversão)
# H₁: p_A ≠ p_B (há diferença nas taxas de conversão)

# Preparando dados para o teste
count = np.array([conversoes_a, conversoes_b])
nobs = np.array([visitantes_a, visitantes_b])

# Realizando o teste Z para proporções
z_stat, p_val = proportions_ztest(count, nobs)

print(f"\nEstatística Z: {z_stat:.4f}")
print(f"P-valor: {p_val:.4f}")

if p_val < 0.05:
   print("Rejeitamos H₀: Há diferença significativa nas taxas de conversão")
   if prop_a > prop_b:
       print("A versão A tem maior taxa de conversão")
   else:
       print("A versão B tem maior taxa de conversão")
else:
   print("Não rejeitamos H₀: Não há evidência de diferença significativa")

Análise de Poder Estatístico

Uma das lições mais valiosas que aprendi foi a importância de calcular o poder estatístico antes de realizar um experimento. Isso me salvou de várias situações onde os dados coletados seriam insuficientes:

# Calculando o tamanho de amostra necessário para detectar uma diferença
effect_size = 0.5  # Tamanho do efeito esperado (Cohen's d)
alpha = 0.05       # Nível de significância
power = 0.8        # Poder desejado (80%)

# Calculando tamanho da amostra
from statsmodels.stats.power import ttest_power

# Para teste t de duas amostras
sample_size = stats.power.ttest_2samp_size(effect_size, power, alpha)
print(f"Tamanho de amostra necessário (por grupo): {sample_size:.0f}")

# Calculando o poder para um tamanho de amostra específico
actual_power = ttest_power(effect_size, sample_size, alpha, df=None, alternative='two-sided')
print(f"Poder estatístico com n={sample_size:.0f}: {actual_power:.4f}")

# Análise de sensibilidade: como o poder varia com o tamanho da amostra
sample_sizes = np.arange(10, 201, 10)
powers = [ttest_power(effect_size, n, alpha, df=None, alternative='two-sided') for n in sample_sizes]

plt.figure(figsize=(10, 6))
plt.plot(sample_sizes, powers, 'b-', linewidth=2)
plt.axhline(y=0.8, color='r', linestyle='--', label='Poder = 0.8')
plt.xlabel('Tamanho da Amostra (por grupo)')
plt.ylabel('Poder Estatístico')
plt.title('Análise de Poder Estatístico')
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()

Casos Práticos: Aplicações Reais

Caso 1: Teste A/B de Email Marketing

# Cenário: Testando duas versões de email marketing
# Versão A (controle): Taxa de abertura histórica de 22%
# Versão B (nova): Queremos detectar melhoria de pelo menos 3 pontos percentuais

def analise_teste_ab_email():
   # Dados simulados baseados em caso real
   np.random.seed(42)
   
   # Simulando resultados
   emails_enviados_a = 5000
   emails_enviados_b = 5000
   
   # Taxa de abertura real (simulada)
   taxa_real_a = 0.22
   taxa_real_b = 0.26
   
   aberturas_a = np.random.binomial(emails_enviados_a, taxa_real_a)
   aberturas_b = np.random.binomial(emails_enviados_b, taxa_real_b)
   
   print(f"Versão A: {aberturas_a} aberturas de {emails_enviados_a} emails ({aberturas_a/emails_enviados_a*100:.2f}%)")
   print(f"Versão B: {aberturas_b} aberturas de {emails_enviados_b} emails ({aberturas_b/emails_enviados_b*100:.2f}%)")
   
   # Teste de proporções
   count = np.array([aberturas_a, aberturas_b])
   nobs = np.array([emails_enviados_a, emails_enviados_b])
   
   z_stat, p_val = proportions_ztest(count, nobs)
   
   print(f"\nResultados do Teste:")
   print(f"Estatística Z: {z_stat:.4f}")
   print(f"P-valor: {p_val:.4f}")
   
   # Calculando intervalo de confiança para a diferença
   from statsmodels.stats.proportion import confint_proportions_2indep
   
   ci_low, ci_high = confint_proportions_2indep(
       aberturas_a, emails_enviados_a, 
       aberturas_b, emails_enviados_b, 
       method='wald'
   )
   
   diferenca = (aberturas_b/emails_enviados_b) - (aberturas_a/emails_enviados_a)
   print(f"Diferença observada: {diferenca*100:.2f} pontos percentuais")
   print(f"IC 95%: [{ci_low*100:.2f}%, {ci_high*100:.2f}%]")
   
   if p_val < 0.05:
       print("\nConclusão: A versão B tem taxa de abertura significativamente maior")
       print("Recomendação: Implementar a versão B")
   else:
       print("\nConclusão: Não há diferença significativa entre as versões")
       print("Recomendação: Manter versão atual ou coletar mais dados")

analise_teste_ab_email()

Interpretação e Comunicação dos Resultados

Uma das habilidades mais importantes que desenvolvi ao longo dos anos foi a capacidade de comunicar resultados estatísticos de forma clara e acionável. Aqui está um framework que uso:

def relatorio_teste_hipotese(teste_nome, h0, h1, estatistica, p_valor, alpha=0.05, contexto=""):
   """
   Gera um relatório completo de teste de hipótese
   """
   print(f"=== RELATÓRIO: {teste_nome} ===")
   print(f"\nContexto: {contexto}")
   print(f"\nHipóteses:")
   print(f"H₀ (Nula): {h0}")
   print(f"H₁ (Alternativa): {h1}")
   print(f"\nResultados:")
   print(f"Estatística do teste: {estatistica:.4f}")
   print(f"P-valor: {p_valor:.6f}")
   print(f"Nível de significância: {alpha}")
   
   # Decisão
   if p_valor < alpha:
       decisao = "REJEITAMOS H₀"
       significancia = "SIM"
   else:
       decisao = "NÃO REJEITAMOS H₀"
       significancia = "NÃO"
   
   print(f"\nDecisão: {decisao}")
   print(f"Estatisticamente significativo: {significancia}")
   
   # Interpretação prática
   print(f"\nInterpretação:")
   if p_valor < alpha:
       print(f"Com {(1-alpha)*100:.0f}% de confiança, temos evidência suficiente para rejeitar a hipótese nula.")
       print(f"Os dados sugerem que há um efeito real e estatisticamente significativo.")
   else:
       print(f"Com os dados disponíveis, não temos evidência suficiente para rejeitar a hipótese nula.")
       print(f"Isso não prova que H₀ é verdadeira, apenas que não detectamos um efeito significativo.")
   
   # Recomendações
   print(f"\nRecomendações:")
   if p_valor < alpha:
       print("- Implementar as mudanças testadas")
       print("- Monitorar resultados em produção")
       print("- Considerar replicar o teste em outros contextos")
   else:
       print("- Manter status quo ou coletar mais dados")
       print("- Verificar se o tamanho da amostra foi adequado")
       print("- Considerar aumentar o tamanho do efeito procurado")
   
   print("="*50)

# Exemplo de uso
relatorio_teste_hipotese(
   "Teste T para Duas Amostras",
   "Não há diferença na taxa de conversão entre as versões A e B",
   "Há diferença na taxa de conversão entre as versões A e B", 
   -2.45,
   0.0167,
   contexto="Comparação de duas landing pages durante 2 semanas"
)

Armadilhas Comuns e Como Evitá-las

Ao longo da minha carreira, cometi (e vi outros cometerem) vários erros clássicos. Aqui estão os principais:

1. Multiple Testing (Testes Múltiplos)

Quando realizamos múltiplos testes, aumentamos a chance de erro Tipo I. A correção de Bonferroni é uma solução conservadora:

# Exemplo: Testando múltiplas métricas simultaneamente
from statsmodels.stats.multitest import multipletests

# Simulando p-valores de múltiplos testes
p_valores = [0.02, 0.06, 0.01, 0.08, 0.03]
nomes_testes = ['Conversão', 'Tempo na Página', 'Bounce Rate', 'CTR', 'Receita']

print("P-valores originais:")
for nome, p in zip(nomes_testes, p_valores):
   print(f"{nome}: {p:.4f}")

# Aplicando correção de Bonferroni
rejected, p_adjusted, alpha_sidak, alpha_bonf = multipletests(p_valores, method='bonferroni')

print(f"\nAlpha corrigido (Bonferroni): {alpha_bonf:.4f}")
print("\nResultados após correção:")
for nome, p_orig, p_adj, rej in zip(nomes_testes, p_valores, p_adjusted, rejected):
   status = "Significativo" if rej else "Não significativo"
   print(f"{nome}: p_original={p_orig:.4f}, p_ajustado={p_adj:.4f}, {status}")

2. Interpretação Incorreta do P-valor

def interpretar_p_valor(p_valor):
   """
   Interpretações corretas e incorretas do p-valor
   """
   print(f"P-valor: {p_valor:.4f}")
   print("\n✅ INTERPRETAÇÕES CORRETAS:")
   print(f"- Probabilidade de observar dados tão extremos ou mais, assumindo H₀ verdadeira")
   print(f"- Se H₀ fosse verdadeira, esperaríamos resultados assim ou mais extremos em {p_valor*100:.2f}% das vezes")
   
   print("\n❌ INTERPRETAÇÕES INCORRETAS:")
   print("- 'Probabilidade de H₀ ser verdadeira'")
   print("- 'Probabilidade dos resultados serem devido ao acaso'")
   print("- 'Tamanho do efeito ou importância prática'")
   
   if p_valor < 0.05:
       print(f"\n📊 CONCLUSÃO: Rejeitamos H₀ ao nível de 5%")
   else:
       print(f"\n📊 CONCLUSÃO: Não rejeitamos H₀ ao nível de 5%")

interpretar_p_valor(0.023)

Ferramentas Avançadas e Considerações Futuras

Testes Bayesianos: Uma Abordagem Alternativa

Recentemente, tenho explorado métodos bayesianos que oferecem interpretações mais intuitivas:

# Exemplo básico de teste bayesiano usando PyMC
# Nota: Este é um exemplo conceitual
import numpy as np

def teste_bayesiano_exemplo():
   """
   Exemplo conceitual de como estruturar um teste bayesiano
   """
   print("Exemplo de Teste Bayesiano:")
   print("- Define priors baseados no conhecimento anterior")
   print("- Atualiza beliefs com novos dados")
   print("- Fornece probabilidade direta de hipóteses")
   print("- Permite incorporar custos de decisão")
   
   # Simulação simples de credibilidade de diferença
   np.random.seed(42)
   diferenças_simuladas = np.random.normal(0.05, 0.02, 10000)
   prob_diferenca_positiva = np.mean(diferenças_simuladas > 0)
   prob_diferenca_relevante = np.mean(diferenças_simuladas > 0.03)
   
   print(f"\nResultados da Simulação Bayesiana:")
   print(f"Probabilidade de diferença positiva: {prob_diferenca_positiva:.1%}")
   print(f"Probabilidade de diferença relevante (>3%): {prob_diferenca_relevante:.1%}")

teste_bayesiano_exemplo()

Conclusão e Próximos Passos

Dominar testes de hipótese em Python é uma jornada contínua. O que apresentei aqui são as fundações sólidas que uso diariamente. Na minha experiência, o mais importante não é decorar todas as fórmulas, mas entender quando aplicar cada teste e como interpretar os resultados de forma acionável.

Recomendações para Continuar Aprendendo:

  • Pratique com dados reais: Use datasets do seu trabalho ou de competições
  • Estude casos de negócio: Entenda como diferentes empresas aplicam esses conceitos
  • Explore métodos bayesianos: Uma perspectiva complementar valiosa
  • Automatize seus testes: Crie pipelines para análises recorrentes

Lembre-se: estatística não é apenas sobre números, é sobre tomar decisões melhores com base em evidências. Os testes de hipótese são ferramentas poderosas, mas sempre devem ser aplicados com senso crítico e conhecimento do contexto de negócio.

Recursos Adicionais

Para aprofundar seus conhecimentos, recomendo:

  • Documentação oficial do SciPy.stats
  • Livro "Think Stats" de Allen Downey
  • Cursos de estatística bayesiana
  • Prática com datasets do Kaggle

Continue experimentando, questionando e aplicando esses conceitos em projetos reais. A estatística aplicada é uma habilidade que se desenvolve com a prática consistente e reflexão sobre os resultados obtidos.