Nessa terceira parte da série de projetos completos implementando testes A/B em Python (recomendo dar uma olhada nas Parte 1 e Parte 2), uma universidade busca incentivar os alunos à usarem seus serviços de apoio, principalmente a biblioteca.
Se você é novo aqui, recomendo dar uma olhada no Guia para Planejamento de um Teste A/B, onde explico todas as etapas para criação de um projeto completo.
Vamos usar novamente Jupyter Notebook com Python para desenvolver todo o projeto, e se quiser reproduzir os resultados mostrados aqui, todo o conteúdo da série está disponível no meu GitHub, com os dados usados em cada parte!
Introdução – A Universidade de Montana¶
A universidade de Montana, nos Estados Unidos, possui vários serviços de apoio ao aluno, incluindo uma biblioteca.
A biblioteca da universidade oferece vários serviços para os estudantes, como alocação de salas de estudos, livros, computadores, discussões em grupo, webnários etc. Todos esses serviços e vários outros, ficam disponíveis dentro da página web da própria biblioteca e os alunos podem acessá-la para agendar algum dos serviços disponíveis.
A página possui um banner da universidade, uma barra de busca, três principais categorias de acesso e uma barra lateral direita que exibe as últimas notícias.
Image("img/montana_uni_lib.png")
Durante o período de 3 de Abril de 2013 até 10 de Abril de 2013, a página “home” da biblioteca recebeu 10.819 visitantes. Ao analisar os dados de acesso da página, o time de TI da universidade percebeu uma grande diferença entre os acessos das categorias das páginas. A taxa de click da “Find” foi de 35%, “Request” foi de 6% e “Interact” foi de 2%.
Olhando para as taxas de clicks, o time de TI se perguntou o motivo da conversão da categoria “Interact” estar tão baixa.
Uma das hipóteses do time de TI foi de que o nome “Interact” está confundindo os alunos, pois não deixa claro o propósito daquela categoria. Assim, quatro novos nomes foram propostos para substituir o nome atual da categoria: “Connect”, “Lean”, “Help” e “Service”.
Com as variações do nome da categoria, um teste A/B/n precisa ser definido para validar qual das variações deixa a categoria mais compreensível e atraente para os estudantes, com a expectativa de aumentar a taxa de clicks nessa categoria.
Assim, durante 3 semanas, entre os dias 29 de Maio de 2013 e 18 de Junho de 2013, foi realizado um teste A/B/n. O experimento foi desenhado para garantir que um usuário acessasse qualquer uma das variações com a mesma probabilidade.
Planejamento da Solução¶
Vamos começar descrevendo de forma resumida o processo para encontrarmos as respostas que buscamo. Para isso temos que determinar quais são as informações que entram na nossa análise (input), qual vai ser o resultado final da análise (output) e quais as principais tarefas necessárias para chegarmos no resultado (tasks).
Input – Output – Tasks
Input:
- Conjunto de dados
- Problema de negócio
Output:
Tasks:
- Teste de Hipótese
- Definir o método de inferência estatística (ANOVA, t-test, Chi-Squared)
- Design de Experimento
- Hipóteses
- Tamanho da Amostra
- Efeito esperado
Agora podemos por a mão na massa.
0.1 Imports¶
Começamos importando os pacotes que serão utilizados durante o projeto:
import pandas as pd
import numpy as np
from statsmodels.stats.gof import chisquare_effectsize
from statsmodels.stats.power import TTestIndPower
from statsmodels.sandbox.stats.multicomp import multipletests
from scipy import stats
from itertools import combinations
from IPython.display import Image
0.2 Load data¶
Diferente das análises anteriores, dessa vez as informações estão em arquivos obtidos através do Google Analytics.
Pra que não conhece, Google Analytics é uma ferramenta gratuita de análise de dados desenvolvida pelo Google, utilizada para monitorar e compreender o comportamento dos usuários em websites e aplicativos. Ele coleta, organiza e apresenta dados detalhados sobre o tráfego, como número de visitantes, fontes de acesso, páginas mais visitadas, duração das sessões, taxa de rejeição, conversões e muito mais.
Na pasta datasets do repositório, você encontra os arquivos com informações de performance para cada página.
- Um arquivo CSV com contagem de clicks em diferentes partes da página.
- Um arquivo PDF com *Visualização Confetti*
- Um arquivo JPG com *Mapa de Calor*
A visualização confetti em uma página web é uma representação gráfica utilizada para mostrar onde os usuários clicam em um site, com o objetivo de identificar padrões de comportamento.
Enquanto a visualização heatmap representa os cliques usando cores frias (menos cliques) a quentes (mais cliques), criando uma visualização mais agregada e condensada.
Aqui vamos focar no número de cliques, para isso foi preciso extrair essa informação de cada um dos CSVs:
# informações extraídas dos arquivos obtidos no Google Analytics (Homepage Version 1~5)
d = {'variant': ['interact', 'connect', 'learn', 'help', 'services'],
'visits': [10283, 2742, 2747, 3180, 2064],
'clicks_all': [3714, 1587, 1652, 1717, 1348],
'clicks_link': [42, 53, 21, 38, 45]}
data = pd.DataFrame(d)
data['conversion'] = data['clicks_link']/data['clicks_all']
data
variant | visits | clicks_all | clicks_link | conversion | |
---|---|---|---|---|---|
0 | interact | 10283 | 3714 | 42 | 0.011309 |
1 | connect | 2742 | 1587 | 53 | 0.033396 |
2 | learn | 2747 | 1652 | 21 | 0.012712 |
3 | help | 3180 | 1717 | 38 | 0.022132 |
4 | services | 2064 | 1348 | 45 | 0.033383 |
Já aproveitamos e calculamos a conversão de cada página.
1.0 Escolha do método¶
O método para identificar um efeito dentre as 4 variantes será: A/B/n testing
2.0 Design de Experimentos¶
2.1 Formulação das hipótese¶
Hipótese nula (H0): Não há diferença entre as variantes.
Hipótese alternativa (H1): Há uma diferença entre as variantes.
2.2 Parâmetros do experimento¶
Componentes para o Cálculo do Tamanho da Amostra¶
O tamanho da amostra é influenciado por vários fatores:
- Significância Estatística (α): Nível de confiança desejado, geralmente 5% (0,05).
- Representa a probabilidade de rejeitar a hipótese nula quando ela é verdadeira (falso positivo).
- Poder Estatístico (1-β): Probabilidade de detectar uma diferença quando ela realmente existe.
- Geralmente, um poder de 80% (0,80) é considerado padrão.
- Tamanho do Efeito (Effect Size): Diferença mínima que você deseja detectar entre os grupos.
- Em testes A/B, pode ser uma melhoria percentual em uma métrica. A fórmula varia para cada caso, para saber mais sobre acesse o guia explicando as diferentes formas.
- Variância dos Dados: Quanto maior a variabilidade nos dados, maior deve ser a amostra para compensar a dispersão.
- Proporção de Alocação: A proporção de participantes alocados entre os grupos (geralmente 1:1).
Nível de confiança do experimento: 95%
Métrica de avaliação: CTR (Click-Through Rate) – valor que será analisado no teste
CTR = Total de Clicks no link / Total de Clicks na página
Poder Estatístico: 80%
Baseado nas nossas hipóteses, precisamos calcular as probabilidades para cada variante.
k = len(data['clicks_all'])
# distribuição da probabilidade na hipotese nula
# vamos criar uma lista com a porcentagem de cada variante, se não há diferença, todas são iguais
expected_dist = [1/k]*k # a porcentagem de clicks não é diferente entre as variantes
# lista com o CTR real de cada variante
actual_dist = data['clicks_link']/data['clicks_link'].sum()
Como estamos analisando probabilidades, utilizaremos o teste qui-quadrado. O objetivo é quantificar a magnitude da diferença entre a distribuição esperada e a distribuição observada.
A equação usada é baseada no conceito de divergência de Pearson qui-quadrado, que mede o desvio entre duas distribuições. A fórmula é:
$$ \mathrm{Effect Size} = \sqrt{\sum\frac{(O_i-E_i)^2}{E_i}} $$Onde:¶
- Oi: A probabilidade observada.
- Ei: A probabilidade esperada.
- A soma é calculada sobre todas as categorias i.
Se quiser saber mais sobre Teste Qui-Quadrado, tem um artigo inteiro aplicando o teste para dados categóricos em diferentes situações, e tudo feito em python.
# nível de significância
alpha = 0.05
# poder estatístico
power = 0.80
# tamanho do efeito - Efeito Qui Quadrado
effect_size = chisquare_effectsize(expected_dist, actual_dist)
# Tamanho da amostra para uma variante
sample_size = TTestIndPower().solve_power(
effect_size=effect_size,
power=power,
alpha=alpha
)
sample_size = np.ceil(sample_size).astype(int)
print(f'Tamanho de Amostra Mínimo por Variante: {sample_size}')
print(f'Tamanho de Amostra Total: {sample_size*k}')
Tamanho de Amostra Mínimo por Variante: 222 Tamanho de Amostra Total: 1110
Considerando uma variante, são necessárias 222 amostras, como estamos comparando 4 variantes com a página atual. Precisamos de 5 vezes mais amostras.
3.0 Aplicação do Teste¶
Para escolher o teste de hipóteses, verificamos o comportamentos dos dados. Nesse fluxograma de testes de hipóteses seguimos as etapas para definir qual método será utilizado.
- Qual o tipo de dados?
- Discreto/Categórico
- Tamanho da Amostra
- n > 5
Chegamos na escolha do Teste Qui-Quadrado.
3.1 Teste Qui-Quadrado¶
O teste qui-quadrado é um método estatístico usado para avaliar a associação entre variáveis categóricas em um conjunto de dados. Ele compara as frequências observadas em uma tabela de contingência com as frequências esperadas, calculadas sob a hipótese nula de independência entre as variáveis. Aqui você encontra um artigo completo sobre o teste qui-quadrado, com aplicação em python para todos os casos de uso.
Para aplicarmos o teste, precisamos preparar a tabela de contingência, no link aqui em cima tem uma explicação completa sobre a tabela como ela deve ser calculada.
# calculando clicks que não foram no link
data['no_clicks_link'] = data['clicks_all'] - data['clicks_link']
df = data[['variant', 'no_clicks_link', 'clicks_link']]
# criando a tabela de contingência
df = df.set_index('variant')
# calculando qui-quadrado, p-valor, grau de liberdade, frequência esperada
chi2, pvalue, dof, expected = stats.chi2_contingency(df)
print(f'Chi-Squared: {chi2} - p-value: {pvalue}')
# o p-valor é menor que o nível de significância?
if pvalue < alpha:
print(f'A hipótese nula é rejeitada, há diferença significativa entre as variantes.')
else:
print('Não há dados suficientes para rejeitar a hipótese nula.')
Chi-Squared: 46.33660181942126 - p-value: 2.0959498129984563e-09 A hipótese nula é rejeitada, há diferença significativa entre as variantes.
Há evidências estatísticas para afirmar que a diferença significante entre as taxas de clicks das páginas.
Talvez você já tenha percebido, mas nosso teste não respondeu qual página teve uma taxa com diferença significante. A nossa resposta atual é que há diferença entre as páginas.
Vamos continuar o teste para determinar estatisticamente qual página teve melhor taxa.
4.0 Post-hoc Testing¶
Antes fizemos uma análise com todas as variantes juntas, agora vamos refazer o teste qui-quadrado para cada combinação de duas variantes.
Porém, ao fazermos isso, temos uma preocupação a mais para garantir a veracidade do nosso teste.
Testes de hipóteses, na estatística, baseiam-se na rejeição da hipótese nula quando a probabilidade dos dados observados ocorrerem ao acaso (sob a hipótese nula) é muito baixa. No entanto, ao testar múltiplas hipóteses simultaneamente, a chance de um evento raro aumenta, o que eleva a probabilidade de rejeitar incorretamente uma hipótese nula (erro Tipo I).
A correção de Bonferroni ajusta esse aumento de risco ao testar cada hipótese individualmente em um nível de significância reduzido, calculado como α/m, onde α é o nível de significância global desejado e m é o número de hipóteses testadas.
# criando as combinações entre variantes
all_combinations = list(combinations(df.index, 2))
p_values = []
# Aplicando o teste de qui quadrado para cada combinação possivel
for comb in all_combinations:
new_df = df[(df.index == comb[0]) | (df.index == comb[1])]
chi2, pvalue, dof, ex = stats.chi2_contingency(new_df)
p_values.append(pvalue)
# Corrigindo o p-valor pela Correção de Bonferroni
reject_list, corret_p_values = multipletests(p_values, method='bonferroni')[:2]
frame = {'combinations': all_combinations,
'original_pvalue': p_values,
'correted_pvalue': corret_p_values,
'reject': reject_list}
# tabela com os p-valores originais e corrigidos de todas as combinações
pd.DataFrame(frame)
combinations | original_pvalue | correted_pvalue | reject | |
---|---|---|---|---|
0 | (interact, connect) | 5.367677e-08 | 5.367677e-07 | True |
1 | (interact, learn) | 7.616981e-01 | 1.000000e+00 | False |
2 | (interact, help) | 3.103059e-03 | 3.103059e-02 | True |
3 | (interact, services) | 1.798089e-07 | 1.798089e-06 | True |
4 | (connect, learn) | 1.329287e-04 | 1.329287e-03 | True |
5 | (connect, help) | 6.144184e-02 | 6.144184e-01 | False |
6 | (connect, services) | 1.000000e+00 | 1.000000e+00 | False |
7 | (learn, help) | 5.089582e-02 | 5.089582e-01 | False |
8 | (learn, services) | 2.037404e-04 | 2.037404e-03 | True |
9 | (help, services) | 7.301999e-02 | 7.301999e-01 | False |
Com a tabela acima, observamos que há diferença significativa (rejeitamos a hipótese nula de que não há diferença) entre a página atual (Interact) e as páginas:
- Connect
- Help
- Services
Comparando as variantes que apresentaram diferença, vemos que não há diferença significativa entre as variantes.
Assim, a recomendação para mudança da página segue a ordem de maior conversão e menor p-valor:
- Connect
- Services
- Help
Com os resultados do teste A/B/n podemos indicar qual página variante deve ser implementada. Concluímos nosso objetivo com sucesso!
Gostou deste estudo de caso? Comente abaixo suas dúvidas ou experiências com testes A/B/n! 🚀