Nessa segunda parte da série de projetos completos implementando testes A/B em Python, nossa empresa tem um projeto de mudança de uma página para um produto específico, será que essa nova página ajudaria nas vendas?
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 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.
O time de UX designers vem trabalhando em uma nova página de vendas, com o objetivo de aumentar a taxa de conversão de um produto da loja, um teclado bluetooth. O product manager (gerente de produto) disse que a taxa de conversão da página atual é de 13% em média, no último ano.
O objetivo do product manager é aumentar a taxa de conversão em 2%, ou seja, a nova página de vendas, desenvolvida pelo time de UX, seria um sucesso se a sua taxa de conversão fosse de 15%.
O teclado bluetooth possui um preço de venda de R$ 4.500,00 à vista ou parcelado em 12% sem juros no cartão de crédito.
Antes de trocar a página de vendas antiga pela nova, o product manager gostaria de testar a efetividade da nova página em um grupo menor de clientes, a fim de correr menos riscos de queda da conversão, caso a página nova mostre uma conversão pior do que a página atual.
Antes de trocar a página de vendas antiga pela nova, o product manager gostaria de testar a efetividade da nova página em um grupo menor de clientes, a fim de correr menos riscos de queda da conversão, caso a página nova mostre uma conversão pior do que a página atual.
0.1. Imports¶
Começamos importando os pacotes que serão utilizados durante o projeto:
import math
import pandas as pd
import numpy as np
from statsmodels.stats import api as sm
from scipy.stats import chi2_contingency
1.0 Load data¶
Nosso conjunto de dados foi disponibilizado em formato CSV, usando o pacote pandas
importamos os dados em um DataFrame
.
data_raw = pd.read_csv('/datasets/ab_data.csv')
Vamos dar uma rápida olhada nos dados:
data_raw
user_id | timestamp | group | landing_page | converted | |
---|---|---|---|---|---|
0 | 851104 | 2017-01-21 22:11:48.556739 | control | old_page | 0 |
1 | 804228 | 2017-01-12 08:01:45.159739 | control | old_page | 0 |
2 | 661590 | 2017-01-11 16:55:06.154213 | treatment | new_page | 0 |
3 | 853541 | 2017-01-08 18:28:03.143765 | treatment | new_page | 0 |
4 | 864975 | 2017-01-21 01:52:26.210827 | control | old_page | 1 |
… | … | … | … | … | … |
294473 | 751197 | 2017-01-03 22:28:38.630509 | control | old_page | 0 |
294474 | 945152 | 2017-01-12 00:51:57.078372 | control | old_page | 0 |
294475 | 734608 | 2017-01-22 11:45:03.439544 | control | old_page | 0 |
294476 | 697314 | 2017-01-15 01:20:28.957438 | control | old_page | 0 |
294477 | 715931 | 2017-01-16 12:40:24.467417 | treatment | new_page | 0 |
294478 rows × 5 columns
Vemos que os dados incluem o id do usuário, o horário do evento (timestamp), o grupo experimental a qual o usuário faz parte: Grupo Controle (página atual) e Grupo Tratamento (página nova), qual página ele recebeu (qual página foi exibida) e se houve conversão, ou seja, se a página que foi exibida levou a uma compra. No total temos em torno de 294 mil registros.
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. Na primeira parte dessa série explico com mais detalhes a importância dessa etapa e como formular hipóteses eficazes.
2.1 Formulação das Hipóteses¶
Se os resultados do teste mostrarem que a conversão atinge 15%, a nova página será adotada.
H0: A conversão da nova página é de 13%
H1: A conversão da nova página é diferente de 13%
2.2 Parâmetros do Experimento¶
Componentes para o Cálculo do Tamanho da Amomstra¶
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
# conversões da página nova e página atual
p1 = 0.15
p2 = 0.13
# tamanho do efeito
effect_size = sm.proportion_effectsize(p1, p2)
# 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 proporções de conversão, logo a fórmula utilizada será:
Teste de Proporções:¶
$$ n = \frac{(Z_{\alpha/2}\sqrt{2p(1-p)} + Z_{\beta}\sqrt{p_1(1-p_1)+p_2(1-p_2)})^2}{(p_1-P-2)^2} $$Onde:¶
- p: Proporção média esperada.
- 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.
- p1 e p2: Proporções nos grupos controle e tratamento, respectivamente.
# sample size - Tamanho da Amostra: para proporções
sample_n = math.ceil(sm.NormalIndPower().solve_power(
effect_size,
power=power,
alpha=significance_level
))
sample_n
4720
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 :4720 Tamanho total da amostra: 9440
Com esses parâmetros, foi calculado que seriam necessários 4.720 participantes por grupo para garantir resultados estatisticamente significativos. Isso resulta em uma amostra total de 9.440 participantes. Temos mais do que o suficiente para realizar nosso teste, por isso será necessário uma amostragem dos grupos.
3.0 Análise descritiva dos dados¶
Com algumas funções do DataFrame, podemos analisar nosso conjunto de dados, como número de linhas e colunas:
print(f'Number of rows: {data_raw.shape[0]}')
print(f'Number of columns: {data_raw.shape[1]}')
Number of rows: 294478 Number of columns: 5
Verificação dos dados faltantes¶
data_raw.isna().sum()
user_id 0 timestamp 0 group 0 landing_page 0 converted 0 dtype: int64
Nosso conjunto de dados tem boa estrutura, sem dados faltantes.
Conferir as “flags”¶
Vamos conferir se as flags dos grupos estão consistentes, se não há duplicidade de dados. Não queremos que o mesmo usuário esteja em ambos os grupos, isso pode comprometer nosso teste.
data_raw[['user_id', 'group', 'landing_page']].groupby(['group', 'landing_page']).count().reset_index()
group | landing_page | user_id | |
---|---|---|---|
0 | control | new_page | 1928 |
1 | control | old_page | 145274 |
2 | treatment | new_page | 145311 |
3 | treatment | old_page | 1965 |
Removendo flags duplicadas¶
Pela tabela acima podemos ver que não há usuários repetidos entre os grupos, mas por garantia vamos identificar possíveis IDs repetidos e removemos do nosso conjunto de dados.
# grouping users flags and removing the ones with more than one
data_users_delete = data_raw[['user_id',
'group']].groupby('user_id').count().reset_index().query('group > 1')['user_id']
# selecting only users with one flag
df1 = data_raw[~data_raw['user_id'].isin(data_users_delete)]
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 = df1[df1['group'] == 'control'].sample(n=sample_n, random_state=7)
print(f'Size of Control Group :{df_control_sample.shape[0]}')
# Treatment group
df_treatment_sample = df1[df1['group'] == 'treatment'].sample(n=sample_n, random_state=7)
print(f'Size of Treatment Group: {df_treatment_sample.shape[0]}')
# Total sample
df_ab = pd.concat([df_control_sample, df_treatment_sample]).reset_index(drop=True)
Size of Control Group :4720 Size of Treatment Group: 4720
Cálculo da métrica de interesse¶
Como separamos uma amostra do nosso conjunto de dados, vamos calcular novamente nossa conversão média:
# Control group convertion
conversion_rate_control = df_control_sample['converted'].mean()
print(f'Conversion Rate - Control Group : {conversion_rate_control}')
# Treatment group convertion
conversion_rate_treatment = df_treatment_sample['converted'].mean()
print(f'Conversion Rate - Treatment Group : {conversion_rate_treatment}')
Conversion Rate - Control Group : 0.12521186440677967 Conversion Rate - Treatment Group : 0.12033898305084746
Considerando apenas as médias das conversões, nota-se que o Grupo de Controle tem taxa de conversão maior. Nosso teste de hipóteses é exatamente para garantir com uma base estatística se essa variação realmente existe.
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?
- Discreto/Categórico
- Tamanho da Amostra
- n > 5
Chegamos na escolha do Teste Qui-Quadrado.
Por que dados categóricos?¶
Como estamos trabalhando até agora com a porcentagem de conversão, podemos criar a falsa impresão de dados contínuos. Mas estamos lindando com conversões, comprou ou não comprou.
df_table = pd.crosstab(index=df_ab['group'], columns=df_ab['converted'])
df_table.columns = ['not_converted', 'converted']
df_table
not_converted | converted | |
---|---|---|
group | ||
control | 4129 | 591 |
treatment | 4152 | 568 |
Agora temos a tabela com dados categóricos, que serão usados no teste.
4.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.
chi_val, pvalue, dof, expected = chi2_contingency(df_table)
print(f'p-value : {pvalue}')
if pvalue < significance_level:
print('Rejeita a hipótese nula')
else:
print('Falha em rejeitar a hipótese nula')
p-value : 0.49021768956704426 Falha em rejeitar a hipótese nula
Com os dados coletados não é possível afirmar que a diferença é significante.
Não há evidências estatísticas para afirmar que a nova página tenha uma taxa de conversão significativamente maior.
As opções para seguir com o teste são:
- Coletar mais dados
- Melhorar o efeito aprimorando a página
5.0 Conversão da página em faturamento¶
Mesmo com a diferença não significativa, considerando o ticket de R$ 4.500,00, podemos verificar qual seria o faturamento caso o teste da página fosse bem sucedido.
df3 = df1.copy()
Primeiro tratamos a coluna de data, de modo a agruparmos os usuários por dia de registro.
# converting timestamp to year-month-day format
df3['timestamp'] = pd.to_datetime(df3['timestamp']).apply(lambda x: x.strftime('%Y-%m-%d'))
df_date = df3[['user_id', 'timestamp']].groupby('timestamp').count().reset_index()
Com o GMV atual e o esperado com o aumento da conversão, calculamos o aumento esperado:
# Cálculo do GMV atual (com 13% de conversão)
df_date['current_conversions'] = np.ceil(df_date['user_id']*0.13).astype(int)
df_date['current_GMV'] = df_date['current_conversions']*4500
# Cálculo do GMV para a nova página (com 15% de conversão)
df_date['new_conversions'] = np.ceil(df_date['user_id']*0.15).astype(int)
df_date['new_GMV'] = df_date['new_conversions']*4500
# Calcula o GMV total gerado em todo o período para a página atual e para a nova página
current_gmv = df_date['current_GMV'].sum()
new_gmv = df_date['new_GMV'].sum()
print('GMV on period: {}'.format(current_gmv))
print('New GMV on period: {}'.format(new_gmv))
lift_abs = new_gmv-current_gmv
lift = 100*(new_gmv-current_gmv)/current_gmv
print(f'Abs Lift: {lift_abs}')
print(f'Expected Lift: {lift:.2f}%')
GMV on period: 167760000 New GMV on period: 193563000 Abs Lift: 25803000 Expected Lift: 15.38%
Considerando o aumento da conversão para 15% poderíamos esperar um aumento de 15,38% no GMV.
Os resultados do teste A/B mostram que a nova página ainda não apresenta uma vantagem significativa em relação à página atual. No entanto, o potencial de ganho financeiro justifica novos experimentos ou mesmo melhorias no design.
Gostou deste estudo de caso? Comente abaixo suas dúvidas ou experiências com testes A/B! 🚀
Pingback: Teste A/B na Prática – Parte 3: Teste A/B/n para análise de conversão de página