O teste A/B é amplamente utilizado para validar hipóteses de negócio, como identificar a página web com maior taxa de conversão, o cupom de desconto mais eficaz ou o preço promocional ideal. Nessa série de posts, vamos explorar o planejamento, execução e análise de testes A/B, combinando métodos estatísticos tradicionais até abordagens modernas, como os algoritmos Multi-Armed Bandit (MAB).
Antes de começar, recomendo ler o Guia para Planejamento de um Teste A/B, onde explico todas as etapas para criação de um projeto completo.
Na primeira parte vamos desenvolver um projeto para um comercio online, explicando e documentando cada etapa no Jupyter Notebook com Python. 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 Electronic House é um comercio online ( e-commerce ) de produtos de informática para casas e escritórios. Os clientes podem comprar mouses, monitores, teclados, computadores, laptops, cabos HDMI, fones de ouvido, cameras webcam, entre outros, através de um site online e recebem os produtos no conforto de suas casas.
Os produtos não são vendidos somente no Brasil, a Eletronic House está presente em diversos países da Europa e da América do Norte.
O Diretor de Produtos Global pediu ao Head de Design que desenvolvesse uma nova forma de finalizar a compra com cartão de crédito, sem a necessidade do cliente preencher manualmente todas as informações do cartão e que funcionasse em todos os países.
Depois de meses desenvolvendo esse dispositivo, o time de Desenvolvimento Backend entregou uma solução de pagamentos, na qual 90% das informações do formulário eram preenchido automaticamente.
O Head de Designer gostaria de medir a efetividade do novo dispositivo de preenchimento automático dos dados do cartão de crédito na página de vendas e reportar os resultados ao Diretor de Produtos Global, para concluir se a nova forma de pagamento é realmente melhor do que a antiga.
As duas páginas foram colocadas no ar e durante alguns meses e o time de Front- End desenvolveu uma automação que atribui um rótulo para cada cliente, mostrando qual a página de vendas aquele determinado cliente estava visualizando. Todos esses dados foram armazenados em um banco de dados e podem ser acessados pelos times da Electronic House.
Desafio¶
Depois de alguns meses de experimento, o time de Designers da Electronic House, precisa avaliar os resultados do experimento realizado, a fim de concluir qual era a forma de pagamento mais eficaz.
Porém, o time não possui as habilidades necessárias para avaliar os dados e concluir o experimento. Nesse contexto, você foi contratado como Cientista de Dados para ajudar o time de Designers a validar a efetividade do novo meio de pagamento, com mais confiança e rigidez na análise.
Qual a melhor forma de pagamento: Preenchimento Manual ou Automático do formulário de dados do cartão de crédito?
0.1 IMPORTS¶
Etapa inicial de importação dos pacotes que serão usados nesse projeto.
import math
import pandas as pd
import numpy as np
import pingouin as pg
from scipy import stats
from statsmodels.stats import api as sm
0.2 Helper Functions¶
#define F-test function
def f_test(x, y, alpha):
"""
Função para verificar se a variação de duas amostras são próximas.
x: amostra com maior variancia
y: amostra com menor variancia
"""
x = np.array(x)
y = np.array(y)
f = np.var(x, ddof=1)/np.var(y, ddof=1) #calculate F test statistic
dfn = x.size-1 #define degrees of freedom numerator
dfd = y.size-1 #define degrees of freedom denominator
p = 1-stats.f.cdf(f, dfn, dfd) #find p-value of F test statistic
fc_upper = stats.f.ppf(1-(alpha/2), dfn, dfd)
fc_lower = stats.f.ppf(alpha/2, dfn, dfd)
print(f'F estatístico: {f}')
if (f < fc_lower) or (f > fc_upper):
print('Hipótese nula rejeitada: Variâncias não são próximas')
else:
print('Não há evidências suficientes, hipótese nula não pode ser rejeitada')
1.0 Load dataset¶
Com o pacote pandas importamos o arquivo CSV para um DataFrame
.
data_raw = pd.read_csv('../datasets/ab_testing.csv')
data_raw.head()
uid | country | gender | spent | purchases | date | group | device | |
---|---|---|---|---|---|---|---|---|
0 | 11115722 | MEX | F | 1595 | 5 | 2016-03-08 | GRP B | I |
1 | 11122053 | USA | M | 498 | 2 | 2017-07-14 | GRP B | I |
2 | 11128688 | USA | F | 2394 | 6 | 2017-09-17 | GRP A | I |
3 | 11130578 | USA | F | 1197 | 3 | 2017-11-30 | GRP A | I |
4 | 11130759 | ESP | M | 1297 | 3 | 2018-01-10 | GRP B | A |
data_raw.shape
(45883, 8)
Esses dados incluem informações dos clientes, como país, gênero, gasto total (GMV) e o grupo experimental ao qual foram atribuídos: Grupo A (nova página) ou Grupo B (antiga página). Após o carregamento, verificamos que o dataset contém 45.883 registros e está bem estruturado, sem valores ausentes.
2.0 Design de Experimentos¶
A etapa de formulação das hipóteses é essencial em qualquer teste A/B, pois estabelece as bases para a análise estatística e a tomada de decisão. Essa etapa define claramente o que está sendo testado e o que se espera como resultado do experimento.
Importância da Etapa¶
Clareza no Objetivo: A formulação das hipóteses garante que o objetivo do experimento seja claro e focado. Isso evita análises desnecessárias e permite uma comunicação mais precisa entre as equipes.
Planejamento Adequado: Define métricas a serem avaliadas, como taxas de conversão, GMV (Gross Margin Revenue) ou tempo médio de conclusão de compras.
Controle de Erros Estatísticos: Essa etapa é crítica para definir os níveis de significância (α) e poder estatístico (1-β), que ajudam a balancear os riscos de falso positivo (rejeitar H₀ incorretamente) e falso negativo (não rejeitar H₀ quando ela é falsa).
Como Formular Hipóteses Eficazes¶
- Seja Específico: Certifique-se de que as hipóteses estejam alinhadas com uma métrica bem definida.
- Métrica: GMV (Gross Margin Revenue).
- Exemplo de H₀: “O GMV médio da página de preenchimento automático é igual ao da página manual.”
Baseie-se em Dados Preliminares: Utilize benchmarks ou análises exploratórias para definir expectativas realistas de melhoria (ex.: uma diferença de 5% no GMV).
Considere o Contexto do Negócio: Certifique-se de que as hipóteses estejam alinhadas aos objetivos da empresa e às necessidades dos stakeholders.
2.1 Formulação das Hipóteses¶
H0: A média de faturamento não é afetada pela página.
H1: A média de faturamento da página nova é maior.
2.2 Parâmetros do Experimento¶
Nosso próximo passo é verificar a quantidade de dados em cada grupo:
df_control = data_raw[data_raw['group'] == 'GRP B']
df_treatment = data_raw[data_raw['group'] == 'GRP A']
print(f'Control Group Size: {df_control.shape[0]}')
print(f'Treatment Group Size: {df_treatment.shape[0]}')
Control Group Size: 22874 Treatment Group Size: 23009
A métrica definida foi GMV (Gross Margin Revenue), representando o gasto total dos clientes.
Com base no GMV médio do grupo controle (considerado como a média histórica), estima-se uma melhora de 5% com a nova feature desenvolvida para pagamentos.
# Cálculo da média e desvio padrão do grupo controle
init_metric = df_control['spent'].mean()
std_metric = df_control['spent'].std()
# média do grupo controle com acréscimo de 5%
end_metric = init_metric*(1.05)
Tamanho da Amostra¶
Nessa etapa vamos garantir que o experimento seja estatisticamente robusto e capaz de fornecer resultados confiáveis. Amostras inadequadas — sejam muito pequenas ou excessivamente grandes — podem comprometer a validade e a eficiência do teste.
Por que calcular o tamanho da amostra?¶
Evitar Conclusões Erradas: Uma amostra pequena pode levar a erros do tipo II (falso negativo), enquanto uma amostra excessivamente grande pode detectar diferenças irrelevantes, levando a erros do tipo I (falso positivo).
Maximizar Recursos: Experimentos com amostras maiores do que o necessário desperdiçam tempo e recursos financeiros.
Aumentar a Confiabilidade: Um tamanho adequado assegura que as diferenças observadas nos resultados são estatisticamente significativas e não fruto do acaso.
Componentes para o Cálculo¶
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
confidence_level = 0.95
# nível de significância
significance_level = 0.05
# tamanho do efeito - Glass' Delta
effect_size = (end_metric - init_metric)/std_metric
# poder estatístico
power = 0.80
Fórmula Geral¶
A fórmula para calcular o tamanho da amostra varia de acordo com o tipo de teste estatístico a ser realizado. No nosso caso, estamos analisando as médias do GMV, logo a fórmula utilizada será:
Teste t (para médias):¶
$$ n = \frac{2 \cdot \sigma^2 \cdot (Z_{\alpha/2} + Z_\beta)^2}{\Delta^2} $$Onde:¶
- σ²: Variância dos dados.
- Zα/2: Valor crítico da distribuição normal para o nível de significância.
- Zβ: Valor crítico para o poder estatístico.
- Δ: Tamanho do efeito desejado.
# sample size - Tamanho da Amostra: para médias
sample_n = math.ceil(sm.tt_ind_solve_power(
effect_size,
power=power,
alpha=significance_level
))
print(f'O tamanho da amostra do a ser coletado de ambos os grupos é de :{sample_n}')
print(f'Tamanho total da amostra: {2*sample_n}')
O tamanho da amostra do a ser coletado de ambos os grupos é de :3262 Tamanho total da amostra: 6524
Com esses parâmetros, foi calculado que seriam necessários 3.262 participantes por grupo para garantir resultados estatisticamente significativos. Isso resulta em uma amostra total de 6.524 participantes. Temos mais do que o suficiente para realizar nosso teste, por isso será necessário uma amostragem dos grupos.
Desafios e Boas Práticas¶
- Estimativa de Variância: Dados históricos são fundamentais para estimar corretamente a variância.
- Amostras Balanceadas: Garantir que o experimento tenha uma proporção equilibrada entre grupos.
- Coleta de Dados Adicional: Se os dados forem insuficientes para atingir o tamanho mínimo, pode ser necessário estender a duração do experimento.
- Tamanho Mínimo Prático: Definir um limite mínimo para evitar coleta insuficiente mesmo em cenários de baixo tráfego.
3.0 Análise Descritiva dos Dados¶
Antes de iniciarmos nosso teste, vamos fazer uma rápida análise dos dados.
df3 = data_raw.copy()
Analisando os valores mínimos, máximos e média dos valores gastos, e também da quantidade de compras, agrupados por país.
df3.groupby(['group', 'country']).agg({'spent': ['min', 'max'],
'purchases': ['min', 'mean', 'max']})
spent | purchases | |||||
---|---|---|---|---|---|---|
min | max | min | mean | max | ||
group | country | |||||
GRP A | AUS | 99 | 8379 | 1 | 4.711462 | 21 |
BRA | 99 | 10480 | 1 | 4.568172 | 24 | |
CAN | 99 | 9079 | 1 | 4.381089 | 21 | |
DEU | 99 | 10278 | 1 | 4.414286 | 23 | |
ESP | 99 | 8581 | 1 | 4.364948 | 19 | |
FRA | 99 | 7781 | 1 | 4.574320 | 21 | |
GBR | 99 | 8383 | 1 | 4.421356 | 20 | |
MEX | 99 | 9677 | 1 | 4.570686 | 23 | |
TUR | 99 | 11176 | 1 | 4.582730 | 24 | |
USA | 99 | 10675 | 1 | 4.564602 | 25 | |
GRP B | AUS | 99 | 7882 | 1 | 4.379648 | 18 |
BRA | 99 | 10478 | 1 | 4.585393 | 23 | |
CAN | 99 | 7284 | 1 | 4.587730 | 17 | |
DEU | 99 | 9583 | 1 | 4.584718 | 20 | |
ESP | 99 | 8980 | 1 | 4.764523 | 20 | |
FRA | 99 | 8981 | 1 | 4.591199 | 20 | |
GBR | 99 | 10176 | 1 | 4.537597 | 24 | |
MEX | 99 | 9877 | 1 | 4.531794 | 23 | |
TUR | 99 | 8386 | 1 | 4.566016 | 19 | |
USA | 99 | 12170 | 1 | 4.502475 | 30 |
Verificação dos dados faltantes¶
df3.isna().sum()
uid 0 country 0 gender 0 spent 0 purchases 0 date 0 group 0 device 0 dtype: int64
Observações:¶
- O GMV médio entre os grupos parece similar.
- Não há valores ausentes ou inconsistências evidentes nos dados.
Conferir flags¶
Vamos conferir se as flags dos grupos estão consistentes, se não há duplicidade de dados.
df3[['uid', 'group']].groupby(['group']).count().reset_index()
group | uid | |
---|---|---|
0 | GRP A | 23009 |
1 | GRP B | 22874 |
Amostragem dos grupos de tratamento e controle¶
Como o tamanho da amostra calculado é menor do que os dados disponiveis, faremos amostragens aleatórias nos dois grupos. Definindo o random_state
podemos garantir a reprodutibilidade do experimento.
# Control group
df_control_sample = df_control.sample(n=sample_n, random_state=12)
# Treatment group
df_treatment_sample = df_treatment.sample(n=sample_n, random_state=12)
Cálculo da métrica de interesse¶
Após a amostragem, calculamos novamente as médias do GMV dos dois grupos.
print('Control Mean GMV : {:.2f}'.format(df_control_sample['spent'].mean()))
print('Treatment Mean GMV : {:.2f}'.format(df_treatment_sample['spent'].mean()))
Control Mean GMV : 1887.28 Treatment Mean GMV : 1868.16
4.0 Teste de Hipóteses¶
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?
- Contínuo
- Relacional ou Diferença?
- Diferença
- Tipo de Diferença
- Médias
- Quantos grupos?
- 2 grupos
Agora precisamos verificar os pressupostos para determinar se o teste escolhido será paramétrico ou não.
Diferenças entre Paramétricos e Não paramétricos¶
Os testes paramétricos são aqueles que assumem que os dados seguem uma distribuição específica (geralmente a distribuição normal) e que as variâncias dos grupos comparados são semelhantes. Esses testes são mais poderosos quando as suposições são atendidas, ou seja, podem fornecer conclusões mais confiáveis com um menor número de observações.
Características:¶
- Distribuição Normal: Assume que os dados seguem uma distribuição normal (ou aproximadamente normal) para cada grupo.
- Homogeneidade de Variâncias: Assume que as variâncias dos dois grupos comparados são semelhantes.
- Tamanho da Amostra: Requer um tamanho de amostra maior para garantir resultados confiáveis, embora isso dependa de quão fortemente as suposições de normalidade sejam atendidas.
Os testes não paramétricos, também chamados de teste de distribuições livres ou teste de ordem, não assumem uma distribuição específica dos dados. Eles são úteis quando as suposições de normalidade ou homogeneidade de variâncias não são atendidas, ou quando você trabalha com dados ordinais ou de escala nominal.
Características:¶
- Não faz suposições sobre a distribuição: Não requer que os dados sigam uma distribuição normal.
- Menos poderoso (geralmente): São menos eficientes do que os testes paramétricos, pois não utilizam toda a informação dos dados.
- Uso para dados ordinais ou quando a normalidade não é assumida: Usado em dados que são ordinais ou quando a normalidade é duvidosa.
df_control_sample['spent'].hist()
df_treatment_sample['spent'].hist();
Teste de Normalidade – Shapiro-Wilk¶
Primeiro vamos verificar se os dados seguem uma distribuição normal. A verificação de normalidade pode ser feita visualmente, utilizando gráficos como o histograma (gráfico logo acima), o boxplot e o gráfico Q-Q (Quantile-Quantile), ou através de testes formais de normalidade, como o teste de Shapiro-Wilk, o teste de Kolmogorov-Smirnov e o teste de Anderson-Darling.
O teste de Shapiro-Wilk é um dos testes de normalidade mais utilizados, especialmente para amostras pequenas. Ele testa a hipótese nula de que os dados vêm de uma distribuição normal.
H0: Distribuição normalmente distribuída
H1: Distribuição não normal
_, pvalue1 = stats.shapiro(df_control_sample['spent'])
print(f'p-value control : {pvalue1}')
_, pvalue2 = stats.shapiro(df_treatment_sample['spent'])
print(f'p-value treatment: {pvalue2}')
print('\nVar control: {}'.format(np.var(df_control_sample['spent'])))
print('Var treatment: {}'.format(np.var(df_treatment_sample['spent'])))
p-value control : 1.2452401983782016e-38 p-value treatment: 1.2459505165697679e-40 Var control: 1856262.3438713101 Var treatment: 1883054.8635010624
Verificamos que as amostras não são normalmente distribuídas.
Teste de Variância – F-Test¶
O segundo teste a ser verificado é se a variância entre os grupos são significativamente diferentes.Ele é frequentemente utilizado em análises de variância (ANOVA) e também pode ser usado em regressões lineares para testar a adequação de modelos.
H0: Variâncias são iguais
H1: Variâncias são diferentes
f_test(df_control_sample['spent'], df_treatment_sample['spent'], alpha=0.05)
F estatístico: 0.9857717795965125 Não há evidências suficientes, hipótese nula não pode ser rejeitada
Interpretação do Teste F¶
- Valor F alto: Se o valor F calculado for significativamente maior que 1, isso sugere que as variâncias dos grupos são diferentes, e você pode rejeitar a hipótese nula.
- Valor F baixo: Se o valor F calculado for próximo de 1 ou menor, isso sugere que não há diferença significativa entre as variâncias dos grupos, e você não rejeita a hipótese nula.
Logo não há evidências suficientes para afirmar que a variância das amostras é diferente.
Mann-Whitney U-test (Condições paramétricas não satisfeitas)¶
Usaremos um teste não paramétrico, baseado nos resultados dos testes de normalidade e variância.
O teste Mann-Whitney U é um teste estatístico não paramétrico utilizado para comparar duas amostras independentes. Ele é frequentemente usado quando os dados não atendem às suposições de normalidade requeridas por testes paramétricos, como o teste t de Student. O teste avalia se há uma diferença significativa nas distribuições de duas populações independentes.
H0: não há diferença no GMV médio das páginas
H1: o GMV média das páginas é diferente
Cálculo do Teste Mann-Whitney U¶
O teste calcula uma estatística chamada U, que mede a diferença entre as distribuições das duas amostras. A estatística U é baseada em classificar todas as observações de ambas as amostras juntas e, em seguida, calcular a soma das classificações para cada amostra.
- Se as distribuições das amostras forem semelhantes, a soma das classificações de cada amostra será aproximadamente a mesma.
- Se as distribuições forem diferentes, a soma das classificações de uma amostra será maior ou menor do que a da outra, resultando em um valor de U maior ou menor.
statistic, pvalue = stats.mannwhitneyu(df_control_sample['spent'], df_treatment_sample['spent'])
print(f'p-value : {pvalue}')
alpha = 0.05
if pvalue < alpha:
print('Rejeita a hipótese nula')
else:
print('Falha ao rejeitar a hipótese nula')
p-value : 0.4832103974256091 Falha ao rejeitar a hipótese nula
Vemos que o p-valor é maior do que o nível de significância estabelecido.
Com base nos dados analisados, não foi possível confirmar que o novo método de pagamento aumentou o GMV médio significativamente. Possíveis caminhos para o futuro incluem:
- Coletar mais dados: Aumentar o tamanho da amostra pode fornecer mais poder estatístico.
- Aprimorar o dispositivo: Melhorar a experiência de preenchimento automático pode gerar um impacto maior.
- Analisar por região: Avaliar o desempenho do dispositivo por país para identificar padrões específicos.
O experimento mostrou-se uma oportunidade valiosa para aprender e otimizar futuras iterações.
Este estudo reforça a importância de experimentos bem planejados e análise de dados rigorosa.
Continue acompanhando nossa série para aprender mais sobre testes A/B e estratégias para impulsionar resultados em projetos reais. Comenta aqui se você já precisou aplicar testes A/B e como foi!
Imagem de capa por Check Out Vectors via Vecteezy.
Pingback: Teste A/B na Prática – Parte 2: Trabalhando com dados categóricos
Pingback: Teste A/B na Prática – Parte 3: Teste A/B/n para análise de conversão de página