import numpy as np
import pandas as pd
import scipy.stats as stats
import statsmodels.stats.api as sms
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
from math import ceil
%matplotlib inline
Objetivo: determinar como afecta cambiar el landing page en el CTR
Nivel de Confianza (α)
Poder Estadístico (1 - β)
Tamaño del Efecto (Diferencia Mínima Detectable)
Tasa de Conversión Base (p1)
Proporción Esperada en la Variante (p2)
Varianza de los Datos
La fórmula para estimar el tamaño de muestra por grupo en un A/B test es:
$$n = \frac{2 \cdot \left( Z_{\alpha/2} + Z_{\beta} \right)^2 \cdot \hat{p} \cdot (1 - \hat{p})}{\Delta^2}$$Donde:
import statsmodels.stats.api as sms
# Parámetros
p_0 = 0.10 # Tasa de conversión del grupo control
p_1 = 0.12 # Tasa de conversión esperada en el grupo tratamiento
alpha = 0.05 # Nivel de significancia
power = 0.8 # Poder estadístico
# Calcular tamaño del efecto: h-Cohen
effect_size = sms.proportion_effectsize(p_0, p_1)
# Calcular tamaño de muestra
sample_size = sms.NormalIndPower().solve_power(effect_size, power=power, alpha=alpha)
print(f"Tamaño de muestra requerido por grupo bajo el h-Cohen: {int(sample_size)}")
#D-Risk
DR = p_1 - p_0
effect_size = DR
# Calcular tamaño de muestra
sample_size = sms.NormalIndPower().solve_power(effect_size, power=power, alpha=alpha)
print(f"Tamaño de muestra requerido por grupo bajo el DR: {int(sample_size)}")
required_n = sample_size
Tamaño de muestra requerido por grupo bajo el h-Cohen: 3834 Tamaño de muestra requerido por grupo bajo el DR: 39244
Discusión sobre medidas para calcular el tamaño del efecto
Fórmula:
$$RD = p_1 - p_0$$
Donde $p_0$ y $p_1$ son las tasas de éxito en los grupos A (control) y B (tratamiento).
Fórmula:
$$RR = \frac{p_1}{p_0}$$
Aplicación en A/B testing:
Útil para comunicar el aumento o reducción proporcional en una métrica clave.
Ejemplo: Si $p_0 = 0.10$ (10%) y $p_1 = 0.12$ (12%),
$$RR = \frac{0.12}{0.10} = 1.2$$
Esto significa que la versión B tiene un 20% más de probabilidad de éxito comparada con la versión A.
Fórmula:
$$OR = \frac{p_1 / (1 - p_1)}{p_0 / (1 - p_0)}$$
Aplicación en A/B testing:
Es útil cuando se trabaja con datos binarios y se quiere comparar probabilidades relativas. Aunque se usa más en estudios médicos, también es válido para análisis de tasas de conversión en A/B testing.
Fórmula:
$$h = 2 \times \left( \arcsin(\sqrt{p_1}) - \arcsin(\sqrt{p_0}) \right)$$
Aplicación en A/B testing:
Especialmente útil para pruebas con tamaños de muestra pequeños o cuando las proporciones están cerca de 0 o 1, lo que puede causar problemas de varianza.
Fórmula:
$$d = \frac{p_1 - p_0}{\sqrt{\frac{(p_1(1 - p_1) + p_0(1 - p_0)}{2}}}$$
Aplicación en A/B testing:
Permite comparar tamaños de efecto entre diferentes experimentos cuando se trabaja con tasas de conversión.
Método | A/B Testing Aplicación | Uso Común |
---|---|---|
Diferencia de Riesgo (RD) | Comparar el cambio absoluto en tasas de conversión. | Tasa de conversión |
Riesgo Relativo (RR) | Comparar el aumento/reducción proporcional del éxito. | Análisis porcentual |
Razón de Momios (OR) | Comparar probabilidades relativas entre grupos. | Datos binarios |
h de Cohen | Comparar proporciones pequeñas o cercanas a 0/1. | Psicología, A/B CTR |
d de Cohen | Estandarizar diferencias entre proporciones. | Comparación general |
Para demostrar que el $h$ de Cohen estabiliza la varianza, necesitamos analizar cómo esta transformación afecta la varianza de las proporciones.
Varianza en proporciones sin transformación
Dada una proporción $p$ (e.g., click through rate), la varianza de una proporción binaria en una muestra de tamaño $n$ se define como:
$$\text{Var}(p) = \frac{p(1-p)}{n}$$Transformación de arcoseno del h de Cohen
La transformación de arcoseno aplicada a una proporción $p$ es:
$$\theta = \arcsin(\sqrt{p})$$La ventaja de esta transformación es que estabiliza la varianza de las proporciones, especialmente cuando $p$ está cerca de 0 o 1.
Demostración Analítica: Estabilización de la Varianza
Podemos usar el método delta para derivar la varianza de $\theta = \arcsin(\sqrt{p})$:
$$\text{Var}(\theta) \approx \left( \frac{d\theta}{dp} \right)^2 \cdot \text{Var}(p)$$La derivada de $\arcsin(\sqrt{p})$ respecto a $p$ es:
$$\frac{d}{dp} \arcsin(\sqrt{p}) = \frac{1}{2 \sqrt{p(1-p)}}$$Por lo tanto, sustituyendo los términos:
$$\text{Var}(\theta) \approx \left( \frac{1}{2\sqrt{p(1-p)}} \right)^2 \cdot \frac{p(1-p)}{n} \approx \frac{1}{4n}$$Observar:
La varianza transformada no depende de $p$, lo que significa que ahora es constante y solo depende del tamaño de la muestra $n$.
Esto contrasta con la varianza original $\frac{p(1-p)}{n}$, que depende de $p$ y se comporta de manera desigual cuando $p$ se acerca a 0 o 1.
Demostración en la obtención del calculo de la muestra
1. Cálculo del Tamaño de Muestra con Varianza Original (RD)
La fórmula para el tamaño de muestra $n$ para detectar una diferencia de riesgo $\Delta = p_1 - p_0$ es:
$$n = \frac{(Z_{\alpha/2} + Z_{\beta})^2 \cdot (p_1(1 - p_1) + p_0(1 - p_0))}{\Delta^2}$$Donde:
2. Cálculo del Tamaño de Muestra con Varianza Estabilizada (h de Cohen)
Con la transformación de arcoseno, la varianza estabilizada se aproxima a:
$$\text{Var}(\theta) \approx \frac{1}{4n}$$La fórmula del tamaño de muestra usando el h de Cohen es:
$$n = \frac{(Z_{\alpha/2} + Z_{\beta})^2}{h^2 / 4}$$Donde:
Tamaño de muestra con la Diferencia de Riesgo (RD):
La muestra requerida será mayor debido a la dependencia directa de la varianza $p(1-p)$, que es más alta cuando las proporciones están en valores intermedios.
Tamaño de muestra con $h$ de Cohen:
La muestra será menor porque la transformación de arcoseno estabiliza la varianza, eliminando la dependencia no lineal de $p$.
Explicación
La varianza original depende de $p$, lo que causa una mayor dispersión en los datos cuando las proporciones son pequeñas o moderadas. Esto eleva el tamaño de muestra necesario para detectar diferencias.
La transformación de arcoseno estabiliza la varianza, lo que facilita la comparación entre proporciones, especialmente cuando estas son cercanas a 0 o 1.
Al utilizar h de Cohen, se logra un cálculo más eficiente del tamaño de muestra, ya que la varianza constante permite detectar diferencias pequeñas con menos observaciones.
# Realización del experimento: obtención de los datos
df = pd.read_csv('ab_data.csv')
control_sample = df[df['group'] == 'control'].sample(n=int(required_n), random_state=22)
treatment_sample = df[df['group'] == 'treatment'].sample(n=int(required_n), random_state=22)
ab_test = pd.concat([control_sample, treatment_sample], axis=0)
ab_test.reset_index(drop=True, inplace=True)
ab_test.head()
user_id | timestamp | group | landing_page | converted | |
---|---|---|---|---|---|
0 | 644179 | 2017-01-16 04:15:36.663685 | control | old_page | 0 |
1 | 729672 | 2017-01-20 19:04:10.409185 | control | old_page | 0 |
2 | 866186 | 2017-01-09 02:56:47.675707 | control | old_page | 0 |
3 | 884303 | 2017-01-18 04:49:04.225284 | control | old_page | 0 |
4 | 882576 | 2017-01-15 13:36:49.854723 | control | old_page | 0 |
# Ejemplo de Como se asigna aleatoriamente un experimeto
# Sample data
data = ab_test.copy()
# Random assignment
np.random.seed(42)
data['grupo_experim'] = np.random.choice(['A', 'B'], size=len(data), p=[0.5, 0.5])
data.head()
user_id | timestamp | group | landing_page | converted | grupo_experim | |
---|---|---|---|---|---|---|
0 | 644179 | 2017-01-16 04:15:36.663685 | control | old_page | 0 | A |
1 | 729672 | 2017-01-20 19:04:10.409185 | control | old_page | 0 | B |
2 | 866186 | 2017-01-09 02:56:47.675707 | control | old_page | 0 | B |
3 | 884303 | 2017-01-18 04:49:04.225284 | control | old_page | 0 | B |
4 | 882576 | 2017-01-15 13:36:49.854723 | control | old_page | 0 | A |
data["grupo_experim"].value_counts()
B 39280 A 39208 Name: grupo_experim, dtype: int64
# Que datos tenemos del experimento
ab_test.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 78488 entries, 0 to 78487 Data columns (total 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 78488 non-null int64 1 timestamp 78488 non-null object 2 group 78488 non-null object 3 landing_page 78488 non-null object 4 converted 78488 non-null int64 dtypes: int64(2), object(3) memory usage: 3.0+ MB
# Vemos si hay usuarios que repiten
session_counts = ab_test['user_id'].value_counts(ascending=False)
multi_users = session_counts[session_counts > 1].count()
print(f'Hay {multi_users} usuarios que aparecen varias veces en el dataset')
Hay 272 usuarios que aparecen varias veces en el dataset
# Los eliminamos
users_to_drop = session_counts[session_counts > 1].index
ab_test = ab_test[~ab_test['user_id'].isin(users_to_drop)]
print(f'El dataset tiene ahora {ab_test.shape[0]} observaciones')
El dataset tiene ahora 77944 observaciones
# El control: ¿sólo vio la página vieja?
pd.crosstab(ab_test['group'], ab_test['landing_page'])
landing_page | new_page | old_page |
---|---|---|
group | ||
control | 388 | 38587 |
treatment | 38571 | 398 |
# elimino los usarios que han visto los dos
# Filtro lógico para datos correctos:
ab_test_clean = ab_test[((ab_test['group'] == 'control') & (ab_test['landing_page'] == 'old_page')) |
((ab_test['group'] == 'treatment') & (ab_test['landing_page'] == 'new_page'))]
# Mostrar la tabla de contingencia limpia
pd.crosstab(ab_test_clean['group'], ab_test_clean['landing_page'])
landing_page | new_page | old_page |
---|---|---|
group | ||
control | 0 | 38587 |
treatment | 38571 | 0 |
ab_test = ab_test_clean.copy()
ab_test.head()
user_id | timestamp | group | landing_page | converted | |
---|---|---|---|---|---|
0 | 644179 | 2017-01-16 04:15:36.663685 | control | old_page | 0 |
1 | 729672 | 2017-01-20 19:04:10.409185 | control | old_page | 0 |
2 | 866186 | 2017-01-09 02:56:47.675707 | control | old_page | 0 |
3 | 884303 | 2017-01-18 04:49:04.225284 | control | old_page | 0 |
4 | 882576 | 2017-01-15 13:36:49.854723 | control | old_page | 0 |
# Analisis de resultados
conversion_rates = ab_test.groupby('group')['converted']
std_p = lambda x: np.std(x, ddof=0) # Std. deviation of the proportion
se_p = lambda x: stats.sem(x, ddof=0) # Std. error of the proportion (std / sqrt(n))
conversion_rates = conversion_rates.agg([np.mean, std_p, se_p])
conversion_rates.columns = ['conversion_rate', 'std_deviation', 'std_error']
conversion_rates.style.format('{:.3f}')
conversion_rate | std_deviation | std_error | |
---|---|---|---|
group | |||
control | 0.122 | 0.327 | 0.002 |
treatment | 0.118 | 0.323 | 0.002 |
plt.figure(figsize=(8,6))
sns.barplot(x=ab_test['group'], y=ab_test['converted'])
plt.ylim(0, 0.17)
plt.title('Conversion rate por grupo', pad=20)
plt.xlabel('Grup0', labelpad=15)
plt.ylabel('Converted (proportion)', labelpad=15);
plt.show()
# Testear la hipótesis
from statsmodels.stats.proportion import proportions_ztest, proportion_confint
control_results = ab_test[ab_test['group'] == 'control']['converted']
treatment_results = ab_test[ab_test['group'] == 'treatment']['converted']
n_con = control_results.count()
n_treat = treatment_results.count()
successes = [control_results.sum(), treatment_results.sum()]
nobs = [n_con, n_treat]
z_stat, pval = proportions_ztest(successes, nobs=nobs)
(lower_con, lower_treat), (upper_con, upper_treat) = proportion_confint(successes, nobs=nobs, alpha=0.05)
print(f'z statistic: {z_stat:.2f}')
print(f'p-value: {pval:.3f}')
print(f'ci 95% for control group: [{lower_con:.3f}, {upper_con:.3f}]')
print(f'ci 95% for treatment group: [{lower_treat:.3f}, {upper_treat:.3f}]')
z statistic: 1.80 p-value: 0.073 ci 95% for control group: [0.119, 0.125] ci 95% for treatment group: [0.115, 0.121]