Uma pergunta que aparece cedo em qualquer painel usado por muita gente: o botão verde converte mais que o azul? A resposta parece simples — mostre os dois, conte os cliques, compare. Mas a estatística é cruel com essa ingenuidade: com poucos usuários, a diferença observada é quase toda ruído, e quem decide olhando o número bruto vai tomar decisões erradas com absoluta confiança.
Esta aula constrói um simulador de teste A/B dentro do Shiny. Ele serve a dois propósitos: ensinar o mecanismo de dividir usuários entre variantes num app real, e — mais importante — dar ao usuário uma ferramenta para sentir o que significa um teste subdimensionado, vendo com os próprios olhos um teste "vencedor" que era só sorte.
Implementar a divisão de usuários entre variantes numa aplicação Shiny, registrar exposições e conversões, e construir um simulador que mostre por que o tamanho da amostra importa — incluindo o cálculo de significância e a demonstração prática do falso positivo.
O mecanismo: dividir sem enviesar
A parte de engenharia é modesta. Cada sessão que abre o app é sorteada para uma variante, e a escolha precisa ser estável durante a sessão — um usuário que vê o botão verde não pode ver o azul ao navegar para outra aba.
server <- function(input, output, session) {
# sorteado uma vez por sessão; reactiveVal mantém estável
variante <- reactiveVal(sample(c("A", "B"), 1, prob = c(0.5, 0.5)))
output$cta <- renderUI({
if (variante() == "A") {
actionButton("converter", "Começar agora", class = "btn-primary btn-lg")
} else {
actionButton("converter", "Quero experimentar", class = "btn-success btn-lg")
}
})
}
Dois cuidados que fazem diferença num teste de verdade:
- Estabilidade entre sessões. Se o mesmo usuário voltar amanhã e cair na outra variante, você está medindo uma mistura, não um teste. Em app com login, sorteie de forma determinística a partir do identificador —
as.integer(substr(digest::digest(id_usuario), 1, 6), 16L) %% 2sempre devolve o mesmo grupo para o mesmo usuário. Sem login, um cookie resolve. - Registre a exposição, não só a conversão. A taxa é conversões sobre exposições. Contar só quem clicou torna impossível calcular qualquer coisa.
observeEvent(session$clientData, once = TRUE, {
registrar(evento = "exposicao", variante = variante(), em = Sys.time())
})
observeEvent(input$converter, {
registrar(evento = "conversao", variante = variante(), em = Sys.time())
})
O número bruto engana
Suponha 100 visitantes em cada variante: A converte 10, B converte 14. B é 40% melhor?
Quase certamente não. Com taxas próximas de 10%, a variação esperada só por acaso em amostras de 100 é dessa ordem. Repetindo o mesmo experimento com duas variantes idênticas, você veria diferenças assim com frequência desconfortável.
É exatamente isso que o simulador deve mostrar. A função é curta:
#' Simula um teste A/B e devolve a diferença observada e o valor-p.
#'
#' @param n visitantes por variante
#' @param taxa_a taxa real de conversão de A
#' @param taxa_b taxa real de conversão de B
simular_teste <- function(n, taxa_a, taxa_b) {
conv_a <- rbinom(1, n, taxa_a)
conv_b <- rbinom(1, n, taxa_b)
teste <- prop.test(c(conv_a, conv_b), c(n, n), correct = FALSE)
data.frame(
conv_a = conv_a, conv_b = conv_b,
taxa_obs_a = conv_a / n, taxa_obs_b = conv_b / n,
diferenca = conv_b / n - conv_a / n,
p_valor = teste$p.value
)
}
E a demonstração que vale a aula inteira — rodar o teste mil vezes com as duas variantes idênticas e ver quantas vezes ele declara um vencedor:
falsos_positivos <- function(n, taxa, repeticoes = 1000, alfa = 0.05) {
resultados <- replicate(repeticoes, simular_teste(n, taxa, taxa)$p_valor)
mean(resultados < alfa)
}
falsos_positivos(n = 100, taxa = 0.10) # ≈ 0.05
O resultado fica perto de 5%, que é exatamente o alfa escolhido — por definição. Essa é a lição: um em cada vinte testes entre variantes idênticas "encontra" uma diferença significativa. Quem roda vinte experimentos e anuncia o único que deu positivo não descobriu nada; encontrou o falso positivo que a estatística garantia que apareceria.
Tamanho de amostra: calcular antes, não depois
A pergunta correta não é "meu teste deu significativo?" e sim "quantos visitantes eu preciso para detectar a diferença que me interessa?". O R responde com uma função da base:
tamanho_necessario <- function(taxa_base, efeito_minimo, poder = 0.80, alfa = 0.05) {
resultado <- power.prop.test(
p1 = taxa_base,
p2 = taxa_base + efeito_minimo,
power = poder,
sig.level = alfa
)
ceiling(resultado$n)
}
tamanho_necessario(0.10, 0.02) # detectar 10% -> 12%
O número costuma assustar: detectar dois pontos percentuais partindo de 10% exige milhares de visitantes por variante. É informação valiosa — e desconfortável — porque revela que muitos testes rodados por aí nunca tiveram chance de concluir coisa alguma.
Colocar esse cálculo na interface, ao lado do simulador, muda a conversa com quem pede o teste:
output$aviso_amostra <- renderUI({
necessario <- tamanho_necessario(input$taxa_base, input$efeito / 100)
classe <- if (input$n_por_variante >= necessario) "alert-success" else "alert-warning"
div(class = paste("alert", classe),
sprintf("Para detectar %.1f ponto(s) percentual(is) com 80%% de poder você precisa de %s visitantes por variante. Você planejou %s.",
input$efeito, format(necessario, big.mark = "."), format(input$n_por_variante, big.mark = "."))
)
})
O simulador completo
ui <- page_sidebar(
title = "Simulador de Teste A/B",
sidebar = sidebar(
width = 320,
sliderInput("n_por_variante", "Visitantes por variante", 50, 20000, 500, step = 50),
sliderInput("taxa_base", "Taxa real de A", 0.01, 0.50, 0.10, step = 0.01),
sliderInput("efeito", "Ganho real de B (pontos %)", 0, 10, 2, step = 0.5),
numericInput("repeticoes", "Repetições da simulação", 1000, 100, 5000, step = 100),
actionButton("rodar", "Simular", class = "btn-primary w-100")
),
uiOutput("aviso_amostra"),
layout_columns(
value_box("Testes que detectaram o efeito", textOutput("kpi_poder")),
value_box("Testes com variantes idênticas que deram 'vencedor'", textOutput("kpi_falso"))
),
card(plotly::plotlyOutput("distribuicao", height = 360))
)
server <- function(input, output, session) {
simulacao <- eventReactive(input$rodar, {
n <- input$n_por_variante
base <- input$taxa_base
ganho <- input$efeito / 100
list(
com_efeito = replicate(input$repeticoes, simular_teste(n, base, base + ganho)$p_valor),
sem_efeito = replicate(input$repeticoes, simular_teste(n, base, base)$p_valor)
)
})
output$kpi_poder <- renderText(
sprintf("%.1f%%", 100 * mean(simulacao()$com_efeito < 0.05)))
output$kpi_falso <- renderText(
sprintf("%.1f%%", 100 * mean(simulacao()$sem_efeito < 0.05)))
}
Arrastar o slider de visitantes e ver o primeiro KPI subir de 12% para 80% enquanto o segundo permanece teimosamente em 5% ensina, em trinta segundos, o que um capítulo de estatística leva páginas para explicar.
As armadilhas que invalidam testes reais
Três erros derrubam mais testes A/B do que qualquer problema de código:
Espiar e parar cedo. Olhar o resultado todo dia e encerrar assim que o p cruza 0,05 infla o falso positivo muito acima dos 5% nominais — o teste tem várias chances de cruzar o limiar por acaso. Defina o tamanho da amostra antes, e só olhe no fim. Se precisar mesmo monitorar, existem métodos sequenciais próprios para isso.
Testar muita coisa ao mesmo tempo. Cinco variantes comparadas entre si são dez comparações, e a chance de pelo menos um falso positivo sobe para cerca de 40%. Ou se corrige o limiar (Bonferroni e afins), ou se testa uma coisa de cada vez.
Confundir significância com relevância. Com amostra grande o bastante, uma diferença de 0,05 ponto percentual sai significativa — e continua irrelevante para o negócio. Defina o efeito mínimo que justifica a mudança antes de rodar, e trate resultados abaixo dele como "sem diferença que importe", mesmo com p pequeno.
A interface pode ajudar contra os três: mostrar o tamanho planejado e quanto falta, deixar claro que só há uma comparação, e exibir o intervalo de confiança da diferença em vez de só o p.
O sorteio da variante precisa ser estável — por sessão no mínimo, por usuário idealmente, com hash determinístico do identificador ou cookie. E é preciso registrar exposição e conversão: sem a exposição não existe taxa. O número bruto engana com facilidade, porque com n pequeno diferenças de vários pontos percentuais aparecem só por acaso. O jeito mais honesto de ver isso é rodar o teste com variantes idênticas: o falso positivo aparece na prática, em torno do alfa escolhido — por definição, um em cada vinte testes "encontra" o que não existe. O power.prop.test() responde à pergunta certa, que é quantos visitantes seriam necessários para detectar o efeito que interessa, e o número costuma ser maior do que se imagina. Três armadilhas fecham o assunto: espiar e parar cedo, o que infla o falso positivo; comparar muitas variantes, o que multiplica as chances; e confundir significância estatística com relevância de negócio. O antídoto para a terceira é definir o efeito mínimo relevante antes de rodar o teste.
Exercícios
-
Por que a variante precisa ser sorteada de forma estável, e como garantir isso num app com login?
✓ Resposta:Se o mesmo usuário vê a variante A numa visita e a B na seguinte, você deixa de medir o efeito de cada versão e passa a medir uma mistura inconsistente — as conversões não podem ser atribuídas a uma variante, e a comparação perde sentido. Além disso, a mudança de interface entre visitas confunde o usuário e pode ela mesma afetar a conversão. Com login, a solução é sortear de forma determinística a partir do identificador: aplica-se um hash ao id do usuário e toma-se o módulo pelo número de variantes. O mesmo usuário cai sempre no mesmo grupo, sem precisar armazenar a atribuição em lugar nenhum.
-
Um colega rodou 20 experimentos no trimestre e apresenta o único que deu
p < 0,05. Qual o problema?✓ Resposta:Com
alfa = 0,05, espera-se um falso positivo a cada 20 testes entre variantes sem diferença real. Apresentar exatamente o único positivo de 20 testes é indistinguível do resultado que o acaso produziria se nenhuma das mudanças funcionasse — é o problema das comparações múltiplas, agravado por selecionar o vencedor depois de ver os resultados. Para que aquele achado signifique algo, ele precisa ser replicado num teste novo e independente, desenhado especificamente para ele. Alternativamente, corrige-se o limiar para o número de testes realizados, o que tornariap = 0,04claramente insuficiente. -
O que significa, na prática, o KPI de "testes com variantes idênticas que deram vencedor" ficar em 5%?
✓ Resposta:Significa que o procedimento está funcionando exatamente como especificado. O nível de significância
alfa = 0,05é, por definição, a probabilidade de rejeitar a hipótese nula quando ela é verdadeira — ou seja, de declarar diferença onde não há. Com variantes idênticas, a hipótese nula é verdadeira por construção, então a taxa de "vencedores" deve convergir para 5%. O valor didático do KPI é tornar concreto que esses 5% não são um defeito a corrigir, e sim o custo assumido do método: um em cada vinte testes honestos vai apontar um vencedor inexistente, e nenhuma análise do teste isolado distingue esse caso de um efeito real. -
Por que "espiar o resultado todo dia e parar quando der significativo" invalida o teste?
✓ Resposta:Porque o valor-p flutua enquanto os dados chegam, e cada dia de espiada é uma nova oportunidade de ele cruzar 0,05 por acaso. Parar na primeira vez que isso acontece transforma uma decisão com 5% de erro em uma com taxa muito maior — em monitoramento diário prolongado, pode passar de 20% ou 30%. É, na prática, o problema das comparações múltiplas disfarçado de acompanhamento. A conduta correta é fixar o tamanho da amostra antes de começar e analisar uma vez, no fim. Quando o monitoramento é mesmo necessário (para interromper um teste nocivo, por exemplo), existem métodos sequenciais com limiares ajustados justamente para permitir olhadas intermediárias.
-
Um teste com 200 mil visitantes por variante aponta ganho de 0,04 ponto percentual, com
p = 0,01. Vale mudar?✓ Resposta:Provavelmente não, e o
ppequeno não muda isso. Com amostra suficientemente grande, qualquer diferença não-nula fica estatisticamente detectável — significância mede o quanto se pode descartar o acaso, não o quanto o efeito importa. A pergunta a fazer é de negócio: 0,04 ponto percentual sobre o volume real representa quanto em receita, e isso cobre o custo de implementar, manter e arriscar a mudança? Se o efeito mínimo relevante tivesse sido definido antes do teste — digamos, 0,5 ponto —, este resultado seria classificado como "sem diferença que importe" sem qualquer ambiguidade. Relatar o intervalo de confiança da diferença em vez de apenas opdeixa esse julgamento muito mais fácil.