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


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.

PYTHON
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

100%
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

PYTHON
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:

TEXT 📖 Somente leitura
# 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

PYTHON
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:

TEXT 📖 Somente leitura
# 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
📌 Ponto-chave: Quanto menor o MDE (menor o lift que você quer detectar), maior a amostra necessária. Com um baseline de apenas 3,2%, detectar um lift relativo de 5% (ou seja, 3,2%→3,36%) exige mais de 300.000 usuários.

(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

PYTHON
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:

TEXT 📖 Somente leitura
# 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

PYTHON
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:

TEXT 📖 Somente leitura
=
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

PYTHON
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:

TEXT 📖 Somente leitura
# 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


📝 Exercícios

  1. Básico (Dificuldade ⭐): Use proportions_ztest para 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.
  2. 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.
  3. 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 →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%