Machine Learning: Teste A/B e Design de Experimentos
Última atualização: 2026-08-26
Implantar um modelo sem teste A/B é como caminhar na corda bamba sem rede de segurança — você não tem ideia se ele é realmente melhor ou se teve sorte.
1. O que você vai aprender
- Fundamentos de teste A/B: teste de hipóteses, valor-p, intervalos de confiança, poder estatístico
- Design de experimentos: cálculo de tamanho de amostra, duração do experimento, estratégias de divisão de tráfego
- Teste A/B em modelos de ML: comparando modelos novos vs. antigos online e avaliando métricas de negócio
- Bandits multi-braço (MAB): Thompson Sampling / UCB e a compensação exploração-explotação
- Cenário SaaS da Alice: validando um aumento na taxa de conversão para um novo modelo de recomendação com teste A/B
2. A história real de um gerente de produto
(1) O problema: não saber se um novo modelo é realmente melhor
A plataforma SaaS da Alice lançou um novo modelo de recomendação, e o dashboard mostrou que a conversão subiu de 3,2% para 3,5%. Mas o CEO questionou: "Isso pode ser apenas flutuação natural — como você prova que o modelo causou isso?" Alice não tinha resposta — sem um experimento controlado, nenhuma melhoria pode ser atribuída.
(2) A solução com teste A/B
O teste A/B divide aleatoriamente os usuários em Grupo A (modelo antigo) e Grupo B (modelo novo), executa ambos ao mesmo tempo e usa estatística para determinar se a diferença é significativa.
from scipy import stats
# Grupo A: modelo antigo, Grupo B: modelo novo
conversions_a, n_a = 320, 10000 # 3,2%
conversions_b, n_b = 350, 10000 # 3,5%
z_stat, p_value = stats.proportions_ztest(
[conversions_a, conversions_b], [n_a, n_b]
)
print(f"valor-p: {p_value:.4f}")
print(f"Significativo: {p_value < 0.05}")
(3) O resultado: um experimento de 3 semanas comprovou um aumento de 2,3% na conversão, adicionando 500K USD por ano
Alice usou um teste A/B de 3 semanas para validar cientificamente a melhoria do novo modelo — p=0,03, estatisticamente significativo. Com essa confiança, ela implantou para todos, adicionando cerca de 500 mil USD em receita anual.
3. A estatística por trás do teste A/B
(1) O framework de teste de hipóteses
sequenceDiagram
participant Design as Desenhar Experimento
participant Split as Divisão Aleatória
participant Collect as Coletar Dados
participant Test as Teste Estatístico
participant Decision as Decisão
Design->>Split: Definir H0/H1, tamanho da amostra
Split->>Collect: Grupo A (controle) vs Grupo B (tratamento)
Collect->>Test: Após o período do experimento
Test->>Decision: Cálculo do valor-p
Decision->>Decision: p < α → Rejeitar H0 (B é melhor)
Decision->>Decision: p ≥ α → Não é possível rejeitar H0
(2) Conceitos estatísticos centrais
▶ Exemplo: Cálculo de teste de hipóteses
from scipy import stats
import numpy as np
# Teste z de duas proporções
def ab_test_proportions(conversions_a, n_a, conversions_b, n_b, alpha=0.05):
p_a = conversions_a / n_a
p_b = conversions_b / n_b
p_pool = (conversions_a + conversions_b) / (n_a + n_b)
se = np.sqrt(p_pool * (1 - p_pool) * (1/n_a + 1/n_b))
z_stat = (p_b - p_a) / se
p_value = 1 - stats.norm.cdf(z_stat)
ci_low = (p_b - p_a) - 1.96 * se
ci_high = (p_b - p_a) + 1.96 * se
return {
"conversao_a": f"{p_a:.4f}",
"conversao_b": f"{p_b:.4f}",
"lift": f"{(p_b - p_a) / p_a * 100:.2f}%",
"z_stat": f"{z_stat:.3f}",
"valor_p": f"{p_value:.4f}",
"ic_95": f"({ci_low:.4f}, {ci_high:.4f})",
"significativo": p_value < alpha,
}
result = ab_test_proportions(320, 10000, 350, 10000)
for k, v in result.items():
print(f"{k:15s}: {v}")
Saída:
# Funções definidas com sucesso
| Conceito | Significado | Limite típico |
|---|---|---|
| H0 (hipótese nula) | A e B não são diferentes | — |
| H1 (hipótese alternativa) | B é melhor que A | — |
| α (nível de significância) | Probabilidade de erro tipo I (falso positivo) | 0,05 |
| valor-p | Probabilidade de observar o resultado atual ou mais extremo sob H0 | <0,05 |
| Poder | Probabilidade de rejeitar corretamente H0 (1-β) | 0,8 |
| Intervalo de confiança | Intervalo plausível da verdadeira diferença | IC 95% |
4. Design de Experimentos
(1) Cálculo de tamanho de amostra
▶ Exemplo: Calculando o tamanho de amostra necessário para um teste A/B
from statsmodels.stats.power import zt_ind_solve_power
from statsmodels.stats.proportion import proportion_effectsize
import numpy as np
def calculate_sample_size(p_baseline, mde, alpha=0.05, power=0.8):
"""Calcula o tamanho de amostra necessário por grupo.
Args:
p_baseline: Taxa de conversão baseline
mde: Efeito Mínimo Detectável (lift relativo)
alpha: Nível de significância
power: Poder estatístico
"""
p_new = p_baseline * (1 + mde)
effect_size = proportion_effectsize(p_new, p_baseline)
n = zt_ind_solve_power(effect_size=effect_size, alpha=alpha, power=power)
return int(np.ceil(n))
# SaaS da Alice: baseline 3,2%, quer detectar lift relativo de 10%
baseline = 0.032
for mde in [0.05, 0.10, 0.15, 0.20]:
n = calculate_sample_size(baseline, mde)
print(f"MDE={mde*100:.0f}%: Necessário {n:,} usuários por grupo "
f"(total {n*2:,}, ~{n*2/baseline/1000:.0f}k visitantes)")
Saída:
# Funções definidas com sucesso
| MDE (Efeito Mínimo Detectável) | Tamanho da Amostra por Grupo | Tamanho Total da Amostra | Duração do Experimento (50k DAU) |
|---|---|---|---|
| 5% lift relativo | 152.000 | 304.000 | 6 dias |
| 10% lift relativo | 38.000 | 76.000 | 1,5 dias |
| 15% lift relativo | 17.000 | 34.000 | <1 dia |
| 20% lift relativo | 9.600 | 19.200 | <1 dia |
(2) Estratégias de divisão de tráfego
| Estratégia | Método | Vantagens | Desvantagens |
|---|---|---|---|
| Nível de usuário | Hash por user_id | Experiência consistente do usuário | Requer login |
| Nível de request | Por request_id | Simples | O mesmo usuário pode ver resultados diferentes |
| Nível de dispositivo | Por device_id | Cobre usuários não logados | Usuários com múltiplos dispositivos podem conflitar |
▶ Exemplo: Divisão por hash consistente
import hashlib
def assign_group(user_id, salt="experiment_1", num_groups=2):
"""Atribuição por hash consistente para teste A/B."""
hash_input = f"{salt}_{user_id}".encode()
hash_val = int(hashlib.md5(hash_input).hexdigest(), 16)
return hash_val % num_groups # 0=A, 1=B
# Verificar divisão igual
user_ids = range(100000)
groups = [assign_group(uid) for uid in user_ids]
print(f"Grupo A: {groups.count(0)} ({groups.count(0)/len(groups)*100:.1f}%)")
print(f"Grupo B: {groups.count(1)} ({groups.count(1)/len(groups)*100:.1f}%)")
Saída:
# Funções definidas com sucesso
5. Teste A/B em Modelos de ML
(1) Comparando modelos online
▶ Exemplo: Teste A/B do modelo de recomendação da Alice
import numpy as np
from scipy import stats
rng = np.random.default_rng(42)
# Simular teste A/B de 2 semanas para o modelo de recomendação
n_users = 20000 # Por grupo
# Grupo A: modelo antigo (conversão baseline 3,2%)
conversions_a = rng.binomial(1, 0.032, n_users)
revenue_a = conversions_a * rng.exponential(200, n_users) # Média 200 USD por conversão
# Grupo B: modelo novo (conversão real 3,5%, lift de 9,4%)
conversions_b = rng.binomial(1, 0.035, n_users)
revenue_b = conversions_b * rng.exponential(200, n_users)
# Analisar diferença na taxa de conversão
conv_rate_a = conversions_a.mean()
conv_rate_b = conversions_b.mean()
z_stat, p_value = stats.proportions_ztest(
[conversions_a.sum(), conversions_b.sum()],
[n_users, n_users]
)
# Analisar diferença na receita
rev_per_user_a = revenue_a.mean()
rev_per_user_b = revenue_b.mean()
t_stat, t_pvalue = stats.ttest_ind(revenue_a, revenue_b)
print("=" * 50)
print("RESULTADOS DO TESTE A/B: Modelo de Recomendação")
print("=" * 50)
print(f"Conversão: A={conv_rate_a:.3%} vs B={conv_rate_b:.3%}")
print(f" Lift: {(conv_rate_b - conv_rate_a) / conv_rate_a * 100:.1f}%")
print(f" valor-p: {p_value:.4f} {'✅ Significativo' if p_value < 0.05 else '❌ Não significativo'}")
print(f"\nReceita/Usuário: A={rev_per_user_a:.2f} vs B={rev_per_user_b:.2f} USD")
print(f" Lift: {(rev_per_user_b - rev_per_user_a) / rev_per_user_a * 100:.1f}%")
print(f" valor-p: {t_pvalue:.4f}")
# Impacto de negócio
annual_lift = (rev_per_user_b - rev_per_user_a) * 50000 * 12 # 50k usuários * 12 meses
print(f"\nLift de receita anual projetado: {annual_lift:,.0f} USD")
Saída:
=
RESULTADOS DO TESTE A/B: Modelo de Recomendação
=
(2) Armadilhas comuns em testes A/B
| Armadilha | Descrição | Solução |
|---|---|---|
| Espiar (Peeking) | Declarar significância antes do prazo | Fixar a duração do experimento; só analisar no final |
| Testes múltiplos | Testar muitas métricas / muitos grupos | Correção de Bonferroni |
| Efeito novidade | Uma nova UI performa bem no início | Estender a duração do experimento |
| Contaminação | Usuários do Grupo A veem conteúdo do Grupo B | Divisão por usuário + verificação de logs |
| Tamanho de amostra insuficiente | Poder estatístico não alcançado | Calcular o tamanho da amostra com antecedência |
6. Bandits Multi-Braço (MAB)
(1) Balanceando exploração e explotação
O teste A/B tradicional usa uma divisão fixa de tráfego (50/50), enquanto o MAB ajusta dinamicamente — modelos com melhor desempenho recebem mais tráfego.
▶ Exemplo: Thompson Sampling
import numpy as np
rng = np.random.default_rng(42)
# 3 variantes de modelo com taxas de conversão reais
true_rates = [0.030, 0.035, 0.028] # Modelo B é o melhor
n_arms = len(true_rates)
n_rounds = 10000
# Thompson Sampling
successes = np.zeros(n_arms)
trials = np.zeros(n_arms)
total_reward = 0
for t in range(n_rounds):
# Amostrar da distribuição Beta para cada braço
samples = [rng.beta(successes[i] + 1, trials[i] - successes[i] + 1) for i in range(n_arms)]
chosen = np.argmax(samples)
# Simular interação do usuário
reward = rng.binomial(1, true_rates[chosen])
successes[chosen] += reward
trials[chosen] += 1
total_reward += reward
print(f"Thompson Sampling após {n_rounds} rodadas:")
for i in range(n_arms):
print(f" Modelo {i}: escolhido {int(trials[i]):5d} vezes, "
f"taxa observada={successes[i]/max(trials[i],1):.4f} (real: {true_rates[i]:.4f})")
print(f"Conversões totais: {int(total_reward)}")
Saída:
# Executado com sucesso
| Algoritmo MAB | Estratégia | Método de Exploração | Complexidade |
|---|---|---|---|
| ε-Greedy | Explorar aleatoriamente com probabilidade ε | Proporção fixa | Baixa |
| UCB | Escolher o braço com o maior limite superior de confiança | Direcionada por incerteza | Média |
| Thompson Sampling | Amostrar do posterior para escolher um braço | Direcionada por probabilidade | Média |
| Dimensão | Teste A/B | MAB |
|---|---|---|
| Alocação de tráfego | Fixa (50/50) | Ajustada dinamicamente |
| Custo de exploração | Alto (50% vai para o pior) | Baixo (adaptativo) |
| Rigor estatístico | Alto (teste de hipóteses) | Baixo (assintótico) |
| Caso de uso | Quando é necessária prova estatística | Iteração rápida |
❓ Perguntas Frequentes
P: Por quanto tempo um teste A/B deve rodar? R: Pelo menos 2 semanas (para cobrir efeitos de ciclo completo, como diferenças de fim de semana) e até atingir o tamanho de amostra planejado. Use o que for mais longo. Não pare antes (mesmo se p<0,05).
P: Um valor-p < 0,05 é suficiente? R: Não. Você também precisa — 1) poder estatístico (power ≥ 0,8); 2) tamanho de amostra suficiente; 3) significância de negócio (o lift vale o investimento); 4) efeitos de ciclo descartados.
P: E se o teste A/B e o teste B/A se contradizerem? R: Verifique — 1) se há um efeito de período; 2) se a divisão é uniforme; 3) se há um efeito de interação (o novo modelo é melhor para alguns usuários e pior para outros). Use análise estratificada para investigar mais a fundo.
P: MAB pode substituir o teste A/B? R: Não inteiramente. MAB é adequado para exploração rápida (por exemplo, ranking de recomendação), enquanto o teste A/B é adequado para decisões que precisam de prova estatística (por exemplo, estratégia de preços, rollout de modelo). Eles se complementam.
P: Como lidar com um teste A/A (ambos os grupos usam o modelo antigo)? R: Um teste A/A valida o sistema de divisão — se o teste A/A mostrar uma diferença significativa, a divisão está quebrada. É uma boa ideia executar uma validação A/A antes de cada teste A/B.
P: E quanto a testar várias métricas ao mesmo tempo? R: Use a correção de Bonferroni — com k métricas, use um nível de significância de α/k. Ou pré-designe uma métrica primária e trate as demais como métricas secundárias.
📖 Resumo
- O núcleo do teste A/B: divisão aleatória + execução simultânea + teste estatístico para atribuir cientificamente as melhorias
- Três essenciais estatísticas: α (taxa de falsos positivos) < 0,05, poder ≥ 0,8 e tamanho de amostra suficiente
- O tamanho da amostra depende da taxa de conversão baseline e do MDE (efeito mínimo detectável) — detectar um lift menor requer mais amostras
- Estratégias de divisão: nível de usuário (recomendado) > nível de dispositivo > nível de request; hash consistente garante estabilidade
- Teste A/B em modelos de ML: compare métricas de negócio como conversão / receita, não métricas de modelo (AUC / R²)
- MAB aloca tráfego dinamicamente, reduzindo o custo de exploração e sendo adequado para cenários de iteração rápida
📝 Exercícios
- Básico (Dificuldade ⭐): Use
proportions_ztestpara testar se duas taxas de conversão (3,0% vs 3,5%, 10k usuários cada) diferem significativamente, e calcule o valor-p e o intervalo de confiança de 95%. Dica:stats.proportions_ztest. - Intermediário (Dificuldade ⭐⭐): Calcule o tamanho da amostra para diferentes MDEs e plote uma curva "MDE vs tamanho da amostra". Dica:
zt_ind_solve_power+ matplotlib. - Desafio (Dificuldade ⭐⭐⭐): Construa um simulador de Thompson Sampling — 3 braços com diferentes taxas de conversão reais — e após 10.000 rodadas compare o Thompson Sampling com o ε-Greedy em termos de recompensa total e taxa de seleção do melhor braço. Dica: amostragem da distribuição Beta + comparação contra exploração aleatória.
← Anterior: Implantação de Modelos | Próximo: Monitoramento em Produção e Drift de Modelo →