Modelando a incerteza: como decidir com poucos dados

Modelando a incerteza: como decidir com poucos dados

Em marketing digital, growth e otimização de produtos, testes A/B são tratados como instrumentos de precisão cirúrgica. Um botão azul contra um verde. Uma cópia com verbo direto contra uma mais emocional. Basta rodar o experimento, olhar o p-valor e decidir.

Mas quando as taxas de conversão são baixas — e quase sempre são —, essa precisão é ilusória. A estatística frequentista tradicional, com seus testes z e p-valores, frequentemente falha em capturar a verdadeira incerteza envolvida. Um p-valor alto pode esconder um sinal real. Um p-valor baixo pode amplificar um efeito frágil. E nesse ruído, boas decisões morrem na dúvida.

A estatística bayesiana oferece uma alternativa mais realista e, muitas vezes, mais útil. Ela não se limita a rejeitar hipóteses nulas: constrói uma distribuição explícita sobre o que acreditamos ser verdade. Em vez de perguntar “isso é estatisticamente significativo?”, passamos a perguntar:

→ “Com que grau de confiança posso afirmar que a nova versão é melhor? E quanto melhor ela é, exatamente?”

Neste artigo, vamos simular um cenário realista de teste A/B com conversões de 0.3% e 0.5%, aplicando tanto abordagens frequentistas quanto bayesianas. Vamos comparar não apenas os resultados, mas principalmente os modelos mentais que cada abordagem impõe. O objetivo aqui não é defender uma escola estatística, mas explorar onde certos métodos tropeçam quando mais precisamos deles — e o que podemos fazer para enxergar com mais clareza.

Modelagem Inicial: Simulando um Cenário Realista

Começamos com o básico: dois grupos, 5.000 visitantes cada.

A versão A (controle) converte 0.3% dos visitantes. Isso significa que, em média, 15 a cada 5.000 visitantes compram, clicam ou realizam a ação desejada. Já a versão B (variante) tem uma taxa um pouco maior: 0.5%. Parece pouco? Em grandes volumes ou produtos de alto valor, esse 0.2 ponto percentual pode representar milhões em receita.

Para simular, usamos uma distribuição binomial: cada visita é um experimento independente com probabilidade de sucesso conhecida.

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import random

# Simulando 1000 visitantes para cada versão
visitors_A = 5000
visitors_B = 5000

random.seed(56)

def page_conversion(taxa_conversao):
    return int(random.random() < taxa_conversao)

def simulate_experiment(visitors_A, visitors_B, taxa_A, taxa_B):
    conversoes_A_dist = np.array([page_conversion(taxa_A) for _ in range(visitors_A)])
    conversoes_B_dist = np.array([page_conversion(taxa_B) for _ in range(visitors_B)])
    return conversoes_A_dist, conversoes_B_dist

# Taxas reais de conversão
taxa_A = 0.003
taxa_B = 0.005

# Geração binária: 1 = converteu, 0 = não converteu
conversoes_A_dist, conversoes_B_dist = simulate_experiment(visitors_A, visitors_B, taxa_A, taxa_B)

conversoes_A = conversoes_A_dist.sum()
conversoes_B = conversoes_B_dist.sum()

print(f"Conversões A: {conversoes_A} / {visitors_A}")
print(f"Conversões B: {conversoes_B} / {visitors_B}")

Numa execução típica, vemos algo como:

Conversões A: 20 / 5000
Conversões B: 25 / 5000

Diferença de 0.1 ponto percentual. Insignificante? Ruído? Evidência real? Essa é a pergunta que todo profissional de produto precisa responder — e a estatística deve ajudar.

Frequentismo: O Limite do P-Valor

Aplicando o teste z para proporções:

from statsmodels.stats.proportion import proportions_ztest

counts = np.array([conversoes_B, conversoes_A])
nobs = np.array([visitors_B, visitors_A])

stat, pval = proportions_ztest(count=counts, nobs=nobs)
print(f"p-valor: {pval:.4f}")
# p-valor: 0.4550

Um p-valor de 0.4550. Pela convenção, isso nos impediria de rejeitar a hipótese nula. Conclusão? Não houve evidência suficiente para rejeitar a hipótese nula. Mas será que isso significa que não há diferença?

E se repetirmos esse experimento milhares de vezes, mantendo as mesmas taxas verdadeiras (0.3% e 0.5%)? Você verá algo curioso:

  • Em vários testes, A “vence” B — mesmo com a taxa real sendo menor.
  • Em outros, B vence com folga.
  • A variabilidade é enorme.

Isso evidencia o ponto central: com amostras pequenas e eventos raros, o p-valor oscila mais do que gostaríamos. Ele depende da amostra específica e responde a uma pergunta condicional: ‘quão improvável seria observar um resultado tão extremo quanto o que vimos, se a hipótese nula fosse verdadeira?’ — mas não nos diz a probabilidade da hipótese nula ser verdadeira.

from tqdm import tqdm

def run_multiple_experiments(experimentos, visitors_A, visitors_B, taxa_A, taxa_B):
    conversions_A_list = []
    conversions_B_list = []

    for _ in tqdm(range(experimentos)):
        conv_A, conv_B = simulate_experiment(visitors_A, visitors_B, taxa_A, taxa_B)
        conversions_A_list.append(conv_A.sum())
        conversions_B_list.append(conv_B.sum())

    # Convertendo para arrays numpy para facilitar comparações
    conversions_A_arr = np.array(conversions_A_list)
    conversions_B_arr = np.array(conversions_B_list)
    
    plt.figure(figsize=(10,6))
    sns.histplot(conversions_A_arr, label='Página A', alpha=0.5, stat='density')
    sns.histplot(conversions_B_arr, label='Página B', alpha=0.5, stat='density')
    plt.xlabel('Número de conversões')
    plt.ylabel('Densidade')
    plt.title('Distribuição das conversões em 1000 experimentos')
    plt.legend()
    plt.show()
    
    
run_multiple_experiments(100, visitors_A, visitors_B, taxa_A, taxa_B)

Intervalo de Confiança: Uma Lente Parcial

Dentro do paradigma frequentista, uma alternativa ao p-valor é o intervalo de confiança para a diferença entre as taxas de conversão. Em vez de apenas declarar "significativo ou não", ele mostra uma faixa plausível onde a verdadeira diferença pode estar.

No entanto, a interpretação correta do intervalo de confiança costuma ser mal compreendida. Um IC de 95% não significa “95% de chance da diferença estar aqui”. Frequentismo não fala de probabilidade sobre parâmetros — apenas sobre repetições amostrais.

Vamos ilustrar com um caso extremo: comparando 500 mil visitas da versão A (com taxa de 0.3%) com apenas 5.000 visitas da nova versão B.

from statsmodels.stats.proportion import confint_proportions_2indep

ci_low, ci_high = confint_proportions_2indep(
    count1=1441, nobs1=500000,  # A
    count2=19, nobs2=5000,      # B
    method='wald'
)
print(f"IC 95% da diferença B - A: {ci_low:.4%} a {ci_high:.4%}")
# IC 95%: -0.2630% a 0.0794%

O intervalo inclui zero. Isso quer dizer que, se repetíssemos esse experimento diversas vezes, 95% dos intervalos construídos dessa forma conteriam a diferença real — que pode ser positiva, negativa ou nula.

Ele também inclui valores positivos — o que mostra que, mesmo com a incerteza, não podemos ignorar a possibilidade de que a nova versão seja melhor.

O problema aqui não é o método, mas o que ele não responde. O intervalo nos diz onde poderia estar a diferença. Mas não diz quão provável é que essa diferença seja positiva.

Se estamos em um dilema de produto, a pergunta prática não é "o intervalo inclui zero?", mas sim:

→ Dado o que observamos, qual a chance de que a nova versão seja de fato melhor?

Para isso, precisamos de uma abordagem que modele diretamente a incerteza. E é aí que o bayesianismo brilha.

Bayesianismo: Incerteza como Objeto de Estudo

O raciocínio bayesiano começa com uma premissa simples, mas poderosa:

A taxa de conversão é um parâmetro fixo, mas desconhecido. O bayesianismo modela nossa incerteza sobre ela como uma variável aleatória.

Em vez de estimar um ponto ou intervalo, passamos a construir uma distribuição de probabilidade sobre o parâmetro. Essa distribuição é atualizada com os dados observados, usando o Teorema de Bayes.

Para taxas de conversão (proporções entre 0 e 1), a escolha natural é a distribuição Beta, conjugada da binomial.

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import beta

# Priori não informativa para ambas as páginas
alpha_prior = 1
beta_prior = 1

# Posterior A e B
alpha_A = alpha_prior + conversoes_A
beta_A = beta_prior + visitors_A - conversoes_A

alpha_B = alpha_prior + conversoes_B
beta_B = beta_prior + visitors_B - conversoes_B

# Plotando as distribuições posteriores e priori
x = np.linspace(0, 0.02, 1000)
plt.figure(figsize=(10,6))
plt.plot(x, beta.pdf(x, alpha_prior, beta_prior), '--', label='Priori', alpha=0.5)
plt.plot(x, beta.pdf(x, alpha_A, beta_A), label=f'Posterior A ({conversoes_A}/{visitors_A})')
plt.plot(x, beta.pdf(x, alpha_B, beta_B), label=f'Posterior B ({conversoes_B}/{visitors_B})')
plt.xlabel('Taxa de conversão')
plt.ylabel('Densidade')
plt.title('Distribuições Posteriores e Priori (Bayesianas)')
plt.legend()
plt.show()

Começamos com uma priori não informativa (Beta(1,1), equivalente a uma uniforme entre 0 e 1). Após observar os dados, atualizamos os parâmetros com as conversões e os fracassos.

A partir disso, podemos amostrar 100 mil vezes as taxas de conversão plausíveis para A e B, dadas as evidências:

from scipy.stats import beta
import numpy as np

# Amostrando valores das distribuições Beta (posteriores)
samples_A = np.random.beta(alpha_A, beta_A, size=100_000)
samples_B = np.random.beta(alpha_B, beta_B, size=100_000)

Agora temos distribuições completas para nossas crenças sobre as taxas de conversão de A e B. E a diferença entre elas também pode ser tratada como uma distribuição inferida — com média, variância e densidade.

diff = samples_B - samples_A

É essa diferença que importa: o quanto a nova versão supera (ou não) a atual — e com que confiança podemos afirmar isso.

Resultados: Probabilidade, Não Ponto de Corte

Uma das perguntas mais naturais é:

→ “Qual a chance da nova versão ser melhor que a antiga?”

E com a abordagem bayesiana, a resposta é direta:

prob_B_melhor = np.mean(samples_B > samples_A)

Probabilidade de B > A: 77.11%

Isso quer dizer que, com base nos dados e na modelagem que usamos, acreditamos com 77% de confiança que a versão B é melhor que a versão A.

Ao invés de aceitar ou rejeitar, atribuímos probabilidade. Isso permite decisões graduais, ponderadas com base em risco e recompensa. Podemos até definir políticas de rollout do tipo:

  • 80% de chance: lançar para todos
  • 60–80%: lançar para 10% e monitorar
  • <60%: manter controle

Além disso, podemos plotar a distribuição da diferença B - A e observar:

  • Um pico à direita de zero
  • Uma cauda ainda presente no lado negativo
import matplotlib.pyplot as plt
import seaborn as sns

# Diferença entre as amostras
diff_samples = samples_B - samples_A

# Plotando a distribuição da diferença
plt.figure(figsize=(10, 6))
sns.histplot(diff_samples, bins=100, stat='density', kde=True, color='purple', alpha=0.6)
plt.axvline(0, color='black', linestyle='--', label='B = A')
plt.title('Distribuição da Diferença (B - A) nas taxas de conversão')
plt.xlabel('Diferença na taxa de conversão (θ_B - θ_A)')
plt.ylabel('Densidade')
plt.legend()
plt.show()

A densidade maior está no lado positivo. Ainda há incerteza — mas agora ela tem forma, volume e sentido.

Priori Informada: Incorporando Histórico

E se a versão A tiver histórico? Simulamos 50.000 visitas anteriores com taxa de 0.3%, e usamos isso como base da distribuição Beta:

historical = 50000

historical_conversion_A = taxa_A * historical

historical_not_conversion_A = historical - historical_conversion_A

print(historical_conversion_A)
print(historical_not_conversion_A)

# 150.0
# 49850.0
from scipy.stats import beta
import numpy as np
import matplotlib.pyplot as plt

# A tem histórico
alpha_prior = historical_conversion_A
beta_prior = historical_not_conversion_A

print(f"Taxa de conversão A: {alpha_prior / (alpha_prior + beta_prior)}")

# Posterior A e B
alpha_A = alpha_prior + conversoes_A
beta_A = beta_prior + visitors_A - conversoes_A

alpha_B = alpha_prior + conversoes_B
beta_B = beta_prior + visitors_B - conversoes_B

# Plotando as distribuições posteriores e priori
x = np.linspace(0, 0.02, 1000)
plt.figure(figsize=(10,6))
plt.plot(x, beta.pdf(x, alpha_prior, beta_prior), '--', label='Distribuição Priori', color='gray')
plt.plot(x, beta.pdf(x, alpha_A, beta_A), label=f'Posterior A ({conversoes_A}/{visitors_A})')
plt.plot(x, beta.pdf(x, alpha_B, beta_B), label=f'Posterior B ({conversoes_B}/{visitors_B})')
plt.xlabel('Taxa de conversão')
plt.ylabel('Densidade')
plt.title('Distribuições Posteriores (Bayesianas)')
plt.legend()
plt.show()

Isso fortalece a confiança em A — e exige que B ofereça evidência mais robusta para superá-la.

Com as distribuições atualizadas, a probabilidade de B vencer A cai:

from scipy.stats import beta
import numpy as np

# Amostrando valores das distribuições Beta (posteriores)
samples_A = np.random.beta(alpha_A, beta_A, size=100_000)
samples_B = np.random.beta(alpha_B, beta_B, size=100_000)

# Comparando: em quantos casos B > A
prob_B_melhor = np.mean(samples_B > samples_A)

# Resultado
print(f"Probabilidade de que B tenha maior taxa de conversão que A: {prob_B_melhor:.2%}")
Probabilidade de B > A: 60.58%

Em contextos conservadores, talvez isso ainda não seja o bastante. Mas a mensagem do modelo é clara: ainda há incerteza, mas estamos caminhando.

Assimetria de Priori: Um Modelo Mais Realista

No mundo real, B é novidade. A é o padrão, testado e aprovado. Refletimos isso no modelo: A recebe uma prior informada, B começa com Beta(1,1). Resultado:

  • A distribuição de A é estreita e centrada.
  • A de B é larga e mais incerta.
  • A diferença entre elas ganha novo significado: não basta B ter mais conversões — é preciso vencer o histórico.
from scipy.stats import beta
import numpy as np
import matplotlib.pyplot as plt

# A tem histórico
alpha_prior_A = historical_conversion_A
beta_prior_A = historical_not_conversion_A

# B é novo
alpha_prior_B = 1
beta_prior_B = 1


print(f"Taxa de conversão A: {alpha_prior_A / (alpha_prior_A + beta_prior_A)}")
print(f"Taxa de conversão B: {alpha_prior_B / (alpha_prior_B + beta_prior_B)}")

# Posterior A e B
alpha_A = alpha_prior_A + conversoes_A
beta_A = beta_prior_A + visitors_A - conversoes_A

alpha_B = alpha_prior_B + conversoes_B
beta_B = beta_prior_B + visitors_B - conversoes_B

# Plotando as distribuições posteriores e priori
x = np.linspace(0, 0.02, 1000)
plt.figure(figsize=(10,6))
plt.plot(x, beta.pdf(x, alpha_prior_A, beta_prior_A), '--', label='Distribuição Priori', color='gray')
plt.plot(x, beta.pdf(x, alpha_prior_B, beta_prior_B), '--', label='Distribuição Priori', color='gray')
plt.plot(x, beta.pdf(x, alpha_A, beta_A), label=f'Posterior A ({conversoes_A}/{visitors_A})')
plt.plot(x, beta.pdf(x, alpha_B, beta_B), label=f'Posterior B ({conversoes_B}/{visitors_B})')
plt.xlabel('Taxa de conversão')
plt.ylabel('Densidade')
plt.title('Distribuições Posteriores (Bayesianas)')
plt.legend()
plt.show()

Assim, com os dados atuais, o modelo nos dá:

from scipy.stats import beta
import numpy as np

# Amostrando valores das distribuições Beta (posteriores)
samples_A = np.random.beta(alpha_A, beta_A, size=100_000)
samples_B = np.random.beta(alpha_B, beta_B, size=100_000)

# Comparando: em quantos casos B > A
prob_B_melhor = np.mean(samples_B > samples_A)

# Resultado
print(f"Probabilidade de que B tenha maior taxa de conversão que A: {prob_B_melhor:.2%}")
Probabilidade de B > A: 98.89%
Intervalo de credibilidade (B - A): 0.0250% a 0.4319%

Ou seja: evidência forte. A totalidade do intervalo está acima de zero. Mesmo com dados escassos e priori conservadora, a vantagem de B parece real e consistente.

Conclusão: A Incerteza Como Ferramenta

Testes A/B com conversões baixas são traiçoeiros. À primeira vista, parecem simples: dois grupos, duas taxas, um p-valor. Mas sob a superfície, o que temos é um terreno instável — onde decisões de produto, marketing ou negócio podem ser tomadas com base em ruído.

O frequentismo nos oferece uma estrutura de decisão baseada em rejeição ou não rejeição de hipóteses, útil em muitos contextos. Mas quando lidamos com incertezas sutis e decisões graduais, a abordagem bayesiana permite modelar nuances com mais flexibilidade.

O bayesianismo não traz certezas absolutas. Mas nos oferece algo mais precioso: a forma da incerteza. Em vez de perguntar “isso é significativo?”, passamos a perguntar:

→ “Dado o que sei, qual a probabilidade de que essa mudança melhore meu produto?”
→ “Com quanta confiança posso apostar nisso?”
→ “Vale o risco agora — ou espero mais dados?”

Modelar explicitamente a incerteza, incorporar conhecimento prévio, reconhecer assimetrias — tudo isso aproxima a estatística da realidade prática. Ela deixa de ser um veredito mecânico e se torna uma ferramenta estratégica: nos ajuda a raciocinar sobre incerteza, incorporar experiência e tomar decisões mais conscientes — mesmo quando os dados ainda não gritam.

Em tempos onde decisões rápidas valem milhões, talvez a melhor resposta não seja um "sim" ou "não". Mas um "depende — e aqui está o porquê".