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) %% 2 sempre 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

  1. 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.

  2. 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 tornaria p = 0,04 claramente insuficiente.

  3. 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.

  4. 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.

  5. 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 p pequeno 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 o p deixa esse julgamento muito mais fácil.

Fontes e leituras recomendadas