A Parte 1 construiu a máquina e a Parte 2 ensinou ela a alocar com honestidade. Aí chegaram os usuários com a única pergunta que sempre importou para eles: "Meu teste terminou? Posso publicar o vencedor?"
É pergunta justa, com resposta surpreendentemente escorregadia. Bandit nunca termina, ele só segue realocando. Duas variantes equivalentes vão dividir a probabilidade de serem a melhor para sempre. Esta parte final é sobre transformar "terminado" em veredito estatístico, usando arrependimento esperado em vez de confiança, e sobre toda a maquinaria de produto que essa pergunta trouxe junto: promover vencedor e deixar um experimento pronto se graduar em template comum.
A Pergunta Que o P(best) Não Responde
O primeiro instinto é usar limiar de confiança: declare o teste terminado quando o P(best) do líder passar de 95%, por exemplo. Para slot com vencedor de verdade isso funciona bem. A evidência acumula, a posterior fica mais estreita, o P(best) sobe para ~100%, pronto.
Só que lembre do fantasma que assombrou a Parte 2: duas variantes equivalentes nunca se resolvem. O P(best) delas fica oscilando em volta de 50/50, ao sabor do ruído, indefinidamente. Com uma regra de limiar de confiança, esse teste roda para sempre, eternamente "ainda decidindo", mesmo que (e essa é a sacada) não tenha mais nada para decidir. Se dois assuntos performam igual de verdade, escolher qualquer um dos dois não custa nada. O teste terminou. A regra de confiança é que não consegue ver isso.
A pergunta que o usuário está fazendo de verdade não é "você tem certeza de qual é melhor?". É "o que me custaria escolher agora?". São perguntas diferentes, e a segunda tem nome: arrependimento esperado.
Parando por Arrependimento, Não por Confiança
Para cada slot, o otimizador calcula o arrependimento relativo esperado de escolher o líder atual:
$$\text{arrep}_{\text{rel}} = \frac{\mathbb{E}\big[(\max_i \theta_i - \theta_{\text{líder}})_+\big]}{\mathbb{E}[\theta_{\text{líder}}]}$$
Em palavras: ao longo dos sorteios da posterior, quanta taxa de recompensa você deixa na mesa, na média, nos mundos em que o líder não é o melhor. Normalizado pela taxa do próprio líder, para o número ler como percentual. Se o líder domina, o máximo é quase sempre ele mesmo e o numerador dá ~0. Se as duas variantes estão empatadas, o numerador também dá ~0, porque sempre que a outra ganha o sorteio, ela ganha por muito pouco. O arrependimento só fica grande no caso genuinamente indeciso, quando pode existir diferença real e escolher agora significa perder essa diferença.
O arrependimento sai do mesmo sorteio da posterior que produziu os pesos: mesma contagem, mesma semente fixa, mesmos 100.000 sorteios. Assim o número publicado nunca discorda da alocação publicada, por construção. Não tem aquele ticket de suporte esperando para acontecer, do tipo "o peso diz uma coisa e o veredito diz outra". Divergir entre os dois é impossível, não só improvável.
A regra de parada é uma comparação e um subcaso:
- DECIDED quando $\text{arrep}_{\text{rel}} < 1\%$, ou seja, escolher o líder custa menos de 1% da taxa de recompensa dele, em expectativa. Aí:
- WINNER se o peso do líder (o P(best) dele) for ≥ 95%, porque uma variante domina de verdade;
- EQUIVALENT caso contrário, porque os concorrentes performam igual. O P(best) vai se dividir para sempre, mas escolher custa ~nada. Escolha por gosto, por marca ou no cara ou coroa.
- DECIDING para todo o resto: ainda vale esperar.
O veredito EQUIVALENT é a recompensa emocional da série toda. A oscilação da Parte 2 e o teste sem fim com que este post abriu são o mesmo fenômeno, uma pergunta de ranking feita para variantes empatadas, e a resposta é recusar o enquadramento de ranking inteiro: essas duas são iguais, pare de esperar por um vencedor que não existe.
Dois testes, um com vencedor de verdade e um com par genuinamente equivalente, cada um visto duas vezes, pela lente da confiança e pela lente do arrependimento:
O teste com vencedor cruza as duas barras: o P(best) da variante líder passa de 95% (painel 1) e o arrependimento desaba abaixo de 1% (painel 2). O teste equivalente cruza só uma. As duas variantes dele ficam trocando a liderança para sempre (painel 3) e nenhuma chega aos 95%, então uma regra de confiança manteria esse teste "decidindo" para sempre. Mas o arrependimento dele assenta abaixo de 1% (painel 4), porque com as taxas empatadas de verdade escolher qualquer uma custa quase nada. O arrependimento fecha o teste que a confiança não fecha.
Antes de subir os limiares, a gente calibrou eles contra todo teste ativo em produção, algumas dezenas na época. A descoberta que tranquiliza: o epsilon fica num platô. Cortar ele pela metade (1% → 0,5%) concluiu exatamente o mesmo conjunto de testes, porque todo vencedor claro tinha arrependimento ≈ 0 e peso ≥ 95% em qualquer limiar razoável. Quando a saída da regra não depende do valor exato do botão, o botão é honesto. Limiar que só funciona num valor mágico normalmente está superajustado ao dado em que você calibrou ele.
Dois Lados, Um Contrato
Uma decisão pequena de arquitetura aqui pesou muito. O otimizador só reporta o número bruto, o rel_regret, persistido junto de cada versão de pesos. Os limiares (epsilon de 1%, barra de vencedor de 95%) ficam no lado da leitura, como constante na API que serve a tela de monitoramento de testes. Recalibrar limiar é mudança de config no leitor, e o otimizador, que é quem manda no tráfego de produção, não precisa de deploy para isso.
O lado da leitura espelha só o suficiente da lógica do otimizador para não mentir. O ponto importante: peso armazenado durante o cold start é override uniforme, não é P(best), então a tela esconde a confiança até as travas liberarem.
Em cima dos vereditos, o monitor deriva a fase de cada teste, um ciclo de vida calculado a partir do dado e não guardado em lugar nenhum: not running (sem tráfego ainda, ou parou), learning (dentro das travas de cold-start, ainda uniforme), running (otimizador balanceando peso), completed (todo slot decidido) e locked (todo slot promovido, pronto para virar estático).
O monitor também tem um conjunto de sinalizadores de atenção, desenhados em cima de um princípio simples: tela de listagem precisa dizer onde olhar, não só mostrar estado. O ready marca slot decidido em que ninguém mexeu. O no data marca teste calado há uma semana, que normalmente é problema de segmentação ou de volume. E o too few conversions é o interessante, porque é projeção de ritmo e não estado: no ritmo de clique atual, o slot frio mais lento vai passar da trava de 50 cliques em 30 dias? Um detalhe sutil: a janela de ritmo é min(30, idade do teste) dias, então teste de três dias é julgado pelos três dias de ritmo dele, não por um denominador de 30 dias que ele ainda não viveu. Teste novo que está engatando bem não deveria ser sinalizado só por ser novo.
Fechando um Teste: Promoção do Vencedor
Quando um slot lê WINNER (ou lê EQUIVALENT e o usuário simplesmente escolhe), tem um botão: promover. O princípio de design atrás dele é que promoção é atributo, não reescrita. Promover não converte o template, não reescreve a ação, não para o dado. Promover estampa um promoted_variant_id naquele slot da ação, e cada parte do sistema respeita a estampa do jeito dela:
- A hora do envio resolve slot promovido direto do payload da ação, então o vencedor se mantém mesmo se o peso armazenado estiver velho ou faltando.
- O otimizador pula o Thompson naquele slot e emite 100/0 fixo, enquanto os outros slots seguem balanceando normal.
- A tabela de pesos ganha uma versão
source=promotedregistrando quem promoveu o quê e quando, que é a trilha de auditoria da Parte 1 fazendo o trabalho dela.
Como essa é uma das poucas escritas que vêm do usuário num sistema que é da máquina, os casos de borda receberam cuidado. Promover uma variante diferente num slot já promovido devolve erro de conflito, porque mudar vencedor decidido tem que ser reset explícito e nunca sobrescrita silenciosa. Já repromover a mesma variante é idempotente, então cliente que deu timeout e tentou de novo não é punido por isso.
Graduação: De Experimento a Template
O último passo do ciclo de vida fecha o loop. Teste em que todo slot já foi decidido e promovido não precisa mais ser teste, porque virou só um e-mail que carrega cinco bandits de maquinaria. Então a operação final converte ele: a variante de maior peso de cada slot (slot promovido lê 100/0, então o vencedor é o argmax naturalmente) vai gravada num template estático novo, com ação de envio simples, e a ação dinâmica antiga é travada e redireciona para a sucessora.
Tem algo silenciosamente satisfatório no fim dessa pipeline toda ser... um template de e-mail comum e sem graça. A infraestrutura de experimentação existe para se aposentar, um slot decidido por vez.
O Que a Série Nos Ensinou
Três posts, uma feature, mais ou menos quatro meses de um protótipo chumbado até um loop de experimentação que se aposenta sozinho. Os aprendizados que eu levaria para o próximo sistema:
- Decomponha antes de otimizar. A suposição de independência por slot (Parte 1) comprou uma redução de ~125× na necessidade de dado e deixou tudo o que vem depois por slot também: recompensa, veredito, promoção. A estrutura do espaço de decisão foi a escolha de maior alavancagem do projeto.
- Separe o que está congelado do que está vivo. Conteúdo escrito por humano e peso escrito por máquina têm ciclo de vida oposto, então moram separados. O loop é dono do peso e nunca disputa o mesmo campo com a configuração.
- Falhe aberto, e em voz alta. A camada de otimização pode cair para uniforme, mas nunca pode bloquear um envio. E o modo só-cálculo do kill-switch faz mudança de algoritmo se provar em dado real antes de tocar numa caixa de entrada.
- Pergunte que questão a sua estatística responde. O P(best) serve para alocar, o arrependimento esperado serve para parar (Partes 2 e 3). Quase todo problema que a gente teve veio de pedir para um número fazer o trabalho do outro.
- Todo termo tem que se pagar. A penalidade de descadastro, os guardrails, o botão de agressividade: todos pareciam prudentes, todos falharam na medição, todos foram deletados. O sistema que subiu é mais simples que o primeiro rascunho.
- Deixe a destruição explícita. Mudar vencedor decidido tem endpoint próprio, com semântica explícita: erro de conflito em vez de sobrescrita silenciosa, e idempotente em retry.
- Usuário precisa de veredito, não de posterior. A saída mais valiosa dessa maquinaria bayesiana toda é uma palavra (WINNER, EQUIVALENT, DECIDING) e um sinalizador dizendo onde olhar em seguida.
No post sobre Multi-Armed Bandits eu fechei dizendo que ferramentas simples, combinadas com cuidado, ainda ganham. Depois de um ano rodando bandit em produção, eu afinaria isso: a ferramenta nunca foi a parte difícil. A parte difícil é decidir o que você está perguntando para ela, e ter a disciplina de deletar tudo que não ajuda a responder.