Tabla de contenidos

  • 1  Pre-análisis de datos
    • 1.1  Importación de librerías
    • 1.2  Carga de datos
    • 1.3  Previsualización de datos
    • 1.4  Optimización de datos
  • 2  Análisis de datos
    • 2.1  Estudiar y comprobar los datos
      • 2.1.1  ¿Cuántos eventos hay en los registros?
      • 2.1.2  ¿Cuántos usuarios y usuarias hay en los registros?
      • 2.1.3  ¿Cuál es el promedio de eventos por usuario?
      • 2.1.4  ¿Qué periodo de tiempo cubren los datos?
      • 2.1.5  ¿Hay una cantidad suficiente de registros por grupo?
    • 2.2  Estudiar el embudo de eventos
      • 2.2.1  ¿Qué eventos hay en los registros? y ¿Cuál su frecuencia de suceso?
      • 2.2.2  ¿Cual la cantidad de usuarios y usuarias que realizaron cada una de las acciones registradas?
      • 2.2.3  ¿En qué orden ocurrieron las acciones?
      • 2.2.4  ¿Cual es la proporción de usuarios y usuarias que pasan de una etapa a la siguiente?
    • 2.3  Estudiar los resultados del experimento
      • 2.3.1  ¿Cuántos usuarios y usuarias hay en cada grupo?
      • 2.3.2  ¿Hay una diferencia estadísticamente significativa entre los grupos de control?
      • 2.3.3  ¿Qué conclusiones se pueden sacar del experimento?
      • 2.3.4  ¿El nivel de significación ha sido el correcto?
  • 3  Conclusiones
    • 3.1  Estudio y comprobación de los datos
    • 3.2  Embudo de eventos
    • 3.3  Experimento

Introducción¶

Se debe investigar el comportamiento de usuario para la aplicación de una empresa que comercializa productos alimentarios.

Primero, se estudiará el embudo de ventas para descubrir cómo los usuarios y las usuarias llegan a la etapa de compra. Para ello se formularán preguntas relacionadas al comportamiento de los clientes durante su paso por el embudo.

Luego se analizarán los resultados de un test A/A/B, realizado debiido a que al equipo de diseño le gustaría cambiar las fuentes de toda la aplicación, pero la gerencia teme que los usuarios y las usuarias piensen que el nuevo diseño es intimidante. Por ello, deciden tomar una decisión basada en los resultados del test A/A/B.

Los usuarios se dividen en tres grupos: dos grupos de control obtienen las fuentes antiguas y un grupo de prueba obtiene las nuevas. Debemos descubrir qué conjunto de fuentes produce mejores resultados.

Crear dos grupos A tiene ciertas ventajas. Podemos establecer el principio de que solo confiaremos en la exactitud de nuestras pruebas cuando los dos grupos de control sean similares. Si hay diferencias significativas entre los grupos A, esto puede ayudarnos a descubrir factores que pueden estar distorsionando los resultados. La comparación de grupos de control también nos dice cuánto tiempo y datos necesitaremos cuando realicemos más tests.

La información para el análisis general y para el análisis A/A/B está en el mismo dataset.

Descripción de datos¶

Cada entrada de registro es una acción de usuario o un evento. La ruta del archivo es /datasets/logs_exp_us.csv.

  • EventName: nombre del evento.
  • DeviceIDHash: identificador de usuario unívoco.
  • EventTimestamp: hora del evento.
  • ExpId: número de experimento: 246 y 247 son los grupos de control, 248 es el grupo de prueba.

Pauta de trabajo¶

Este informe estará dividido en tres partes, cada una de ellas tendrá diferentos objetivos:

Preanálisis: Etapa en la cual se cargan los datos y se optimizan en caso de ser necesario, se trabajan los valores duplicados y ausentes, y finalmente, se modifican los datos para facilitar el posterior análisis.

  • Optimizar el nombre de las columnas de manera que sea conveniente para el análisis.
  • Comprobar si hay tipos de datos y valores ausentes y corregir los datos si es necesario.
  • Agregar una columna de fecha y hora y una columna separada para las fechas (transformar las fechas del formato timestamp).

Análisis: Etapa en la cual se responden las preguntas establecidas y se trazan los gráficos correspondientes.

  • Estudiar y comprobar los datos.

    • ¿Cuántos eventos hay en los registros?
    • ¿Cuántos usuarios y usuarias hay en los registros?
    • ¿Cuál es el promedio de eventos por usuario?
    • ¿Qué periodo de tiempo cubren los datos?
    • ¿Hay una cantidad suficiente de registros por grupo?
  • Estudiar el embudo de eventos.

    • ¿Qué eventos hay en los registros? y ¿Cuál su frecuencia de suceso?
    • ¿Cual la cantidad de usuarios y usuarias que realizaron cada una de las acciones registradas?
    • ¿En qué orden ocurrieron las acciones?
    • ¿Cual es la proporción de usuarios y usuarias que pasan de una etapa a la siguiente?
    • ¿En qué etapa pierdes más usuarios y usuarias?
    • ¿Qué porcentaje de usuarios y usuarias hace todo el viaje desde su primer evento hasta el pago?
  • Estudiar los resultados del experimento.

    • ¿Cuántos usuarios y usuarias hay en cada grupo?
    • ¿Hay una diferencia estadísticamente significativa entre los grupos de control?
    • ¿Los grupos se dividieron correctamente?
    • ¿Qué conclusiones se pueden sacar del experimento?
    • ¿El nivel de significación ha sido el correcto?

Conclusiones: Etapa en la cual se resume la totalidad del informe y se destacan los hallazgos mas importantes del análisis.

Pre-análisis de datos¶

Se realizarán todos los preparativos, importación de librerías, limpieza, etc. que se deben realizar antes de empezar con el análisis.

Importación de librerías¶

Se importarán las librerías necesarias para el análisis, pruebas estadísticas y visualizaciones.

In [1]:
import pandas as pd
import math as mt
import numpy as np
from scipy import stats as st
from plotly import graph_objects as go
from matplotlib import pyplot as plt
import seaborn as sns

Importación exitosa.

Carga de datos¶

Usando .read_csv() se importará el dataset que tiene la información que necesitamos.

In [2]:
df = pd.read_csv('/datasets/logs_exp_us.csv', sep='\t')

Se descubre que el dataset no está separado por comas (como se esperaría), sino que está separado por \t, una vez corregido esto se importa correctamente el conjunto de datos.

Previsualización de datos¶

Se busca dar una revisión general a los datos para observar su tipo, nombres de columnas y si es que hay valores ausentes (entre otros).

In [3]:
df.info()
df.head()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 4 columns):
 #   Column          Non-Null Count   Dtype 
---  ------          --------------   ----- 
 0   EventName       244126 non-null  object
 1   DeviceIDHash    244126 non-null  int64 
 2   EventTimestamp  244126 non-null  int64 
 3   ExpId           244126 non-null  int64 
dtypes: int64(3), object(1)
memory usage: 7.5+ MB
Out[3]:
EventName DeviceIDHash EventTimestamp ExpId
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248
3 CartScreenAppear 3518123091307005509 1564054127 248
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248

Se puede apreciar que no hay valores ausentes, las fechas están en formato timestamp (deberá ser modificado), el resto de tipos de datos está en su tipo correcto.

Optimización de datos¶

En esta fase se harán las correcciones mencionadas anteriormente, además de hacer otros cambios como: optimizar el nombre de las columnas y buscar valores duplicados.

In [4]:
df.columns = ['event', 'device_id', 'ts', 'group']

Se modifican exitosamente los nombres de las columnas, estos nombres facilitan el análisis al ser más cortos que los anteriores y al estar en minúsculas, reduciendo la posibilidad de errores. Ahora se crearán columnas de fecha y hora a partir de la columna ts.

In [5]:
df['date'] = pd.to_datetime(df['ts'], unit='s').dt.strftime('%Y-%m-%d')
df['hour'] = pd.to_datetime(df['ts'], unit='s').dt.strftime('%H:%M')
df.head()
Out[5]:
event device_id ts group date hour
0 MainScreenAppear 4575588528974610257 1564029816 246 2019-07-25 04:43
1 MainScreenAppear 7416695313311560658 1564053102 246 2019-07-25 11:11
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248 2019-07-25 11:28
3 CartScreenAppear 3518123091307005509 1564054127 248 2019-07-25 11:28
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248 2019-07-25 11:48

Se puede observar el dataset con los nuevos nombres de columnas y las nuevas columnas. Ahora se buscarán valores duplicados.

In [6]:
print(df.duplicated().sum())
df[df.duplicated()].head()
413
Out[6]:
event device_id ts group date hour
453 MainScreenAppear 5613408041324010552 1564474784 248 2019-07-30 08:19
2350 CartScreenAppear 1694940645335807244 1564609899 248 2019-07-31 21:51
3573 MainScreenAppear 434103746454591587 1564628377 248 2019-08-01 02:59
4076 MainScreenAppear 3761373764179762633 1564631266 247 2019-08-01 03:47
4803 MainScreenAppear 2835328739789306622 1564634641 248 2019-08-01 04:44

Se encuntran 413 valores, lo cual es muy bajo teniendo en cuenta que el dataset tiene 244126 registros. Se visualizan los datos duplicados para ver si tienen algo en común o si sólo son errores. No se encuentran cosas en común por lo que serán eliminados.

In [7]:
df.drop_duplicates(inplace=True)

Duplicados eliminados exitosamente

Análisis de datos¶

En esta fase se responderán las preguntas establecidas, se trazarán los gráficos correspondientes y se analizarán los resultados del test A/A/B.

Estudiar y comprobar los datos¶

Antes de entrar a sacar conclusiones se debe comprobar la integridad de los datos, verificar si están dentro de los límites establecidos y si son suficientes para poder obtener un análisis correcto.

¿Cuántos eventos hay en los registros?¶

Para responder esta pregunta basta con simplemente contar las filas del dataset.

In [8]:
len(df)
Out[8]:
243713

Se encuentran 243.713 eventos registrados.

¿Cuántos usuarios y usuarias hay en los registros?¶

Se contará cuantos dispositivos únicos hay registrados, esto proporciona un valor estimado de la cantidad de usuarios que generó la información del dataset.

In [9]:
df['device_id'].nunique()
Out[9]:
7551

Se identifican aproximadamente 7551 usuarios y usuarias.

¿Cuál es el promedio de eventos por usuario?¶

Se contará el número de eventos en promedio por dispositivo y se analizará la distribución.

In [10]:
print(df.groupby('device_id')['event'].count().mean().round(0))
sns.displot(df.groupby('device_id')['event'].count())
plt.xlabel('Eventos por usuario')
plt.ylabel('Cantidad de usuarios');
32.0
No description has been provided for this image

Se descubre que la media es 32, sin embargo no estamos en presencia de una distribución normal, lo que significa que debemos usar otra medida como la moda.

In [11]:
print(df.groupby('device_id')['event'].count().mode())
print()
sns.histplot(df.groupby('device_id')['event'].count())
plt.xlim(0, 100)
plt.grid(True)
plt.xlabel('Eventos por usuario')
plt.ylabel('Cantidad de usuarios');
0    5
dtype: int64

No description has been provided for this image

Obtenemos la moda y verificamos mediante el nuevo histograma, se puede concluir que un usuario promedio tiende a realizar 5 eventos en la web de la empresa.

¿Qué periodo de tiempo cubren los datos?¶

Esta pregunta puede abordarse de varias maneras, se comenzará con la más simple.

In [12]:
print(df['date'].min())
print(df['date'].max())
2019-07-25
2019-08-07

Se identifican las fechas límite de los datos, cubren un rango de dos semanas. Ahora se analizará la distribución de los datos dentro de estas dos semanas estudiadas.

In [13]:
sns.set(style='white')
sns.histplot(data=df, x='date')
plt.grid(True)
plt.xticks(rotation=80)
plt.xlabel('Fecha')
plt.ylabel('Registros');
No description has been provided for this image

Se observa que tenemos datos completos desde el 2019-08-01 en adelante, un rango de una semana. Se eliminarán los datos de antes de la fecha indicada.

In [14]:
new_df = df[df['date'] >= '2019-08-01']

Se crea un nuevo dataset con la información filtrada por fecha el análisis, se comprobará si es significativa la cantidad de datos perdidos.

In [15]:
print('Total de eventos:' ,len(df))
print('Eventos perdidos:' ,len(df) - len(new_df))
print('Eventos perdidos:' ,((len(df) - len(new_df)) / len(df))*100, '%')
print()
print('Total de clientes:', df['device_id'].nunique())
print('Clientes perdidos:' ,df['device_id'].nunique() - new_df['device_id'].nunique())
print('Clientes perdidos:' ,(df['device_id'].nunique() - new_df['device_id'].nunique()) / df['device_id'].nunique(), '%')
Total de eventos: 243713
Eventos perdidos: 2826
Eventos perdidos: 1.159560630741897 %

Total de clientes: 7551
Clientes perdidos: 17
Clientes perdidos: 0.0022513574361011784 %

Se comprueba que las pérdidas de información son mínimas, sobre todo en cuanto a registros de clientes (que es en lo que más se trabajará).

¿Hay una cantidad suficiente de registros por grupo?¶

Mediante un gráfico de barras se comprobará si hay una cantidad suficiente de registros por grupo.

In [16]:
ax = new_df['group'].value_counts().plot(kind='bar', rot=0, grid=True, xlabel='Grupo', ylabel='Cantidad de registros', title='Registros por grupo')
for p in ax.patches:
    ax.annotate(str(int(p.get_height())), (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points', fontsize=8, color='blue');
No description has been provided for this image

Efectivamente hay una cantidad significativa de registros por grupo.

Estudiar el embudo de eventos¶

En esta fase se estudiará el embudo de eventos para analizar el comportamiento de los usuarios en la web de la compañía.

¿Qué eventos hay en los registros? y ¿Cuál su frecuencia de suceso?¶

Mediante un filtrado y un gráfico de barras con etiquetas se graficarán los registros y su determinada frecuencia.

In [17]:
ax = new_df['event'].value_counts().plot(kind='bar', rot=75, grid=True, xlabel='Evento', ylabel='Cantidad de registros', title='Tipos de eventos registrados')
for p in ax.patches:
    ax.annotate(str(int(p.get_height())), (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points', fontsize=8, color='blue');
No description has been provided for this image

Hay 5 tipos de evento registrados:

  • Aparición de pantalla principal.
  • Aparición de pantalla de ofertas.
  • Aparición de pantalla de carrito.
  • Aparición de pantalla de Pago exitoso.
  • Tutorial.

Están distribuídas en orden lógico exceptuando la etapa Tutorial la cual debe ser una guía de como hacer una compra, esto tiene sentido debido a que esto se suele omitir, lo que explicaría una cantidad tan baja de registros de este evento. Se probará más adelante esta teoría.

In [18]:
bar = new_df.groupby(['event', 'group'])['device_id'].count().reset_index()
sns.barplot(data=bar, x='event', y='device_id', hue='group', order=['MainScreenAppear', 'OffersScreenAppear', 'CartScreenAppear', 'PaymentScreenSuccessful', 'Tutorial'])
plt.xticks(rotation=75)
plt.title('Número de eventos registrados (división por grupos)')
plt.grid(True)
No description has been provided for this image

En cuanto al gráfico desglosado por grupos, se observa al grupo 248 (grupo B) levemente superior a los otros dos grupos.

¿Cual la cantidad de usuarios y usuarias que realizaron cada una de las acciones registradas?¶

De la misma manera que en el paso anterior (filtrando y graficando) se obtendrá la cantidad de usuarios que realizaron cada acción.

In [19]:
ax = new_df.groupby('event')['device_id'].nunique().sort_values(ascending=False).plot(kind='bar', rot=75, grid=True, xlabel='Evento', ylabel='Cantidad de usuarios', title='Usuarios registrados por evento')
for p in ax.patches:
    ax.annotate(str(int(p.get_height())), (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points', fontsize=8, color='blue');
No description has been provided for this image

Las proporciones de usuarios por evento coinciden con el gráfico anterior, sin embargo, la diferencia entre la pantalla principal y la pantalla de oferta es menor que en el gráfico de eventos, esto significa que los usuarios tienden a visitar la pantalla principal muchas veces antes de concretar una compra (se puede seguir profundizando en esto).

In [20]:
bar = new_df.groupby(['event', 'group'])['device_id'].nunique().reset_index()
sns.barplot(data=bar, x='event', y='device_id', hue='group', order=['MainScreenAppear', 'OffersScreenAppear', 'CartScreenAppear', 'PaymentScreenSuccessful', 'Tutorial'])
plt.xticks(rotation=75)
plt.title('Usuarios registrados por evento (división por grupos)')
plt.grid(True)
No description has been provided for this image

En cuanto al desglose por grupos, podemos darnos cuenta de que la cantidad de usuarios por grupo esta muy pareja.

¿En qué orden ocurrieron las acciones?¶

Ahora se comprobará el orden de las acciones, para ello, se extraerán dos id de dispositivo que hayan completado el tutorial y se observará la cronología de eventos que completaron en la web.

In [21]:
new_df.query('event == "Tutorial"')
Out[21]:
event device_id ts group date hour
2828 Tutorial 3737462046622621720 1564618048 246 2019-08-01 00:07
2841 Tutorial 3737462046622621720 1564618338 246 2019-08-01 00:12
3353 Tutorial 5587388800188073787 1564625981 247 2019-08-01 02:19
3433 Tutorial 2988069914968447512 1564626904 246 2019-08-01 02:35
3590 Tutorial 1884616937397676465 1564628471 247 2019-08-01 03:01
... ... ... ... ... ... ...
241781 Tutorial 1309234519709630135 1565204899 247 2019-08-07 19:08
241869 Tutorial 5523029501947287934 1565205070 247 2019-08-07 19:11
242673 Tutorial 4069063897900929368 1565206705 248 2019-08-07 19:38
242695 Tutorial 5862484110925354625 1565206785 247 2019-08-07 19:39
243542 Tutorial 5365227480683749189 1565209432 248 2019-08-07 20:23

1005 rows × 6 columns

Se seleccionarán las id de dispositivo 3737462046622621720 y 1309234519709630135.

In [22]:
new_df.query('device_id == 3737462046622621720')
Out[22]:
event device_id ts group date hour
2828 Tutorial 3737462046622621720 1564618048 246 2019-08-01 00:07
2829 MainScreenAppear 3737462046622621720 1564618080 246 2019-08-01 00:08
2830 MainScreenAppear 3737462046622621720 1564618135 246 2019-08-01 00:08
2831 OffersScreenAppear 3737462046622621720 1564618138 246 2019-08-01 00:08
2834 OffersScreenAppear 3737462046622621720 1564618226 246 2019-08-01 00:10
2835 MainScreenAppear 3737462046622621720 1564618247 246 2019-08-01 00:10
2836 MainScreenAppear 3737462046622621720 1564618270 246 2019-08-01 00:11
2837 MainScreenAppear 3737462046622621720 1564618280 246 2019-08-01 00:11
2841 Tutorial 3737462046622621720 1564618338 246 2019-08-01 00:12
2842 MainScreenAppear 3737462046622621720 1564618354 246 2019-08-01 00:12
14835 MainScreenAppear 3737462046622621720 1564656354 246 2019-08-01 10:45
21428 MainScreenAppear 3737462046622621720 1564666204 246 2019-08-01 13:30
21433 OffersScreenAppear 3737462046622621720 1564666212 246 2019-08-01 13:30
21437 CartScreenAppear 3737462046622621720 1564666220 246 2019-08-01 13:30
21456 PaymentScreenSuccessful 3737462046622621720 1564666237 246 2019-08-01 13:30
21461 OffersScreenAppear 3737462046622621720 1564666241 246 2019-08-01 13:30
21481 PaymentScreenSuccessful 3737462046622621720 1564666261 246 2019-08-01 13:31
21483 CartScreenAppear 3737462046622621720 1564666262 246 2019-08-01 13:31
21500 OffersScreenAppear 3737462046622621720 1564666287 246 2019-08-01 13:31
21506 CartScreenAppear 3737462046622621720 1564666298 246 2019-08-01 13:31
21515 CartScreenAppear 3737462046622621720 1564666325 246 2019-08-01 13:32
21535 PaymentScreenSuccessful 3737462046622621720 1564666349 246 2019-08-01 13:32
21536 CartScreenAppear 3737462046622621720 1564666350 246 2019-08-01 13:32
26563 MainScreenAppear 3737462046622621720 1564673455 246 2019-08-01 15:30
35881 MainScreenAppear 3737462046622621720 1564687439 246 2019-08-01 19:23
35883 OffersScreenAppear 3737462046622621720 1564687443 246 2019-08-01 19:24
54720 MainScreenAppear 3737462046622621720 1564746202 246 2019-08-02 11:43
54724 OffersScreenAppear 3737462046622621720 1564746206 246 2019-08-02 11:43
54786 OffersScreenAppear 3737462046622621720 1564746286 246 2019-08-02 11:44
85397 MainScreenAppear 3737462046622621720 1564827238 246 2019-08-03 10:13
95115 MainScreenAppear 3737462046622621720 1564842157 246 2019-08-03 14:22
95133 PaymentScreenSuccessful 3737462046622621720 1564842169 246 2019-08-03 14:22
95134 CartScreenAppear 3737462046622621720 1564842169 246 2019-08-03 14:22
95149 OffersScreenAppear 3737462046622621720 1564842185 246 2019-08-03 14:23
95981 OffersScreenAppear 3737462046622621720 1564843420 246 2019-08-03 14:43
155623 MainScreenAppear 3737462046622621720 1565005996 246 2019-08-05 11:53
155629 OffersScreenAppear 3737462046622621720 1565005999 246 2019-08-05 11:53
155635 OffersScreenAppear 3737462046622621720 1565006008 246 2019-08-05 11:53

En este caso el Tutorial es efectivamente el primer evento completado, vamos a analizar ahora la segunda id que es de una fecha diferente.

In [23]:
new_df.query('device_id == 1309234519709630135')
Out[23]:
event device_id ts group date hour
241781 Tutorial 1309234519709630135 1565204899 247 2019-08-07 19:08
241823 MainScreenAppear 1309234519709630135 1565204960 247 2019-08-07 19:09
241843 MainScreenAppear 1309234519709630135 1565205001 247 2019-08-07 19:10
241844 OffersScreenAppear 1309234519709630135 1565205005 247 2019-08-07 19:10
241877 CartScreenAppear 1309234519709630135 1565205090 247 2019-08-07 19:11
242317 PaymentScreenSuccessful 1309234519709630135 1565205997 247 2019-08-07 19:26
242341 OffersScreenAppear 1309234519709630135 1565206016 247 2019-08-07 19:26
242361 OffersScreenAppear 1309234519709630135 1565206049 247 2019-08-07 19:27
242366 OffersScreenAppear 1309234519709630135 1565206057 247 2019-08-07 19:27
242558 OffersScreenAppear 1309234519709630135 1565206420 247 2019-08-07 19:33

En el segundo caso el también el tutorial es el primer evento completado, por lo que el orden de eventos sería el siguiente:

  • Tutorial (opcional).
  • Pantalla principal.
  • Pantalla de oferta.
  • Pantalla de carrito.
  • Pago exitoso.

¿Cual es la proporción de usuarios y usuarias que pasan de una etapa a la siguiente?¶

Para trabajar con esta métrica se debe, primero, tener presente el número total de usuarios registrados en el nuevo dataset filtrado.

In [24]:
new_df['device_id'].nunique()
Out[24]:
7534

Hay 7534 usuarios y registrados en el nuevo dataset. Ahora se construirá un gráfico de embudo con los porcentajes visibles para cada evento respecto del total y respecto de la etapa anterior.

In [25]:
fig = go.Figure(go.Funnel(
    y = ["MainScreenAppear", "OffersScreenAppear", "CartScreenAppear", "PaymentScreenSuccessful"],
    x = [7419, 4593, 3734, 3539],
    textposition = "inside",
    textinfo = "value+percent previous+percent initial"
    ))

(Se adjunta una foto debido a que la librería Figure de gráficos interactivos, necesita tener el kernel funcionando para mostrar la visualización, debido a eso, una imagen adjunta es más práctico.)

Funnel.png

Comparandolo con otros embudos de ventas, este se ve muy bien, ya que un 48% de los usuarios completó el Funnel hasta el pago. Analizando las pérdidas se puede apreciar que donde más se pierden usuarios es en la transición de la pantalla inicial a la pantalla de oferta, donde abandona el 38% de los usuarios que ven la pantalla inicial.

Ahora visualizaremos el mismo funnel, pero con divisiones por grupos, para esto debemos comenzar crando un dataset agrupado por evento, por cada grupo de prueba.

In [40]:
g246 = new_df[new_df['group'] == 246].groupby('event')['device_id'].nunique().sort_values(ascending=False).reset_index()
g247 = new_df[new_df['group'] == 247].groupby('event')['device_id'].nunique().sort_values(ascending=False).reset_index()
g248 = new_df[new_df['group'] == 248].groupby('event')['device_id'].nunique().sort_values(ascending=False).reset_index()
g246
Out[40]:
event device_id
0 MainScreenAppear 2450
1 OffersScreenAppear 1542
2 CartScreenAppear 1266
3 PaymentScreenSuccessful 1200
4 Tutorial 278

Una vez listos los datasets, podemos generar la visualización.

In [43]:
fig = go.Figure()

fig.add_trace(go.Funnel(
    name = '246',
    meta = g246,
    y = g246['event'],
    x = g246['device_id'],
    textposition = "inside",
    textinfo = "value+percent previous+percent initial"
    ))

fig.add_trace(go.Funnel(
    name = '247',
    meta = g247,
    y = g247['event'],
    x = g247['device_id'],
    textposition = "inside",
    textinfo = "value+percent previous+percent initial"
    ))

fig.add_trace(go.Funnel(
    name = '248',
    meta = g248,
    y = g248['event'],
    x = g248['device_id'],
    textposition = "inside",
    textinfo = "value+percent previous+percent initial"
    ))

plt.close()

(Se adjunta una foto debido a que la librería Figure de gráficos interactivos, necesita tener el kernel funcionando para mostrar la visualización, debido a eso, una imagen adjunta es más práctico.)

newplot%20%281%29.png

El nuevo gráfico nos muestra lo mismo que habíamos visto en los gráficos de barras anteriores, las tasas de conversión son muy parecidas entre los grupos. Las diferencias observadas oscilan entre un 1% y un 2% lo que, probablemente, no sea suficiente para que sean estadísticamente significativas, de todas maneras, se realizarán las pruebas correspondientes.

Estudiar los resultados del experimento¶

En esta fase se estudiarán los resultados del test A/A/B que se mencionó al principio del informe, mediante los grupos de control se comprobará si la muestra fue aislada correctamente y en el caso que así sea, mediante el grupo de prueba se comprobará si los cambios en la fuente de la aplicación modifican el comportamiento de los usuarios.

¿Cuántos usuarios y usuarias hay en cada grupo?¶

Antes de hacer pruebas estadísticas necesitamos saber cuántos usuarios hay en cada grupo.

In [28]:
ax = new_df.groupby('group')['device_id'].nunique().sort_values(ascending=False).plot(kind='bar', rot=0, grid=True, xlabel='Grupo', ylabel='Cantidad de usuarios', title='Usuarios registrados por grupo')
for p in ax.patches:
    ax.annotate(str(int(p.get_height())), (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points', fontsize=8, color='blue');
No description has been provided for this image

Las cantidades son muy similares entre los grupos y hay suficientes usuarios para poder tener resultados significativos.

¿Hay una diferencia estadísticamente significativa entre los grupos de control?¶

Ahora se comprobará, mediante una prueba estadística sobre las proporciones de usuarios que hacen compras y los que no, si es que hay diferencias entre los grupos de control (246 y 247).

In [29]:
conversions = new_df[['device_id', 'group']].drop_duplicates() #Se crea una matriz de id de dispositivo y grupos, para evitar duplicados
converted = pd.DataFrame(data={"device_id": new_df[new_df["event"] == "PaymentScreenSuccessful"]["device_id"].unique(), "converted": 1}) #Se crea un dataframe donde cada usuario que hizo al menos una compra tiene un booleano de 1
conversions = conversions.merge(converted, on="device_id", how="left")
conversions["converted"] = conversions["converted"].fillna(0)
conversions.head()
Out[29]:
device_id group converted
0 3737462046622621720 246 1.0
1 1433840883824088890 247 0.0
2 4899590676214355127 247 1.0
3 1182179323890311443 246 0.0
4 4613461174774205834 248 1.0

Lo primero que se hizo aquí es crear una matriz de device_id y grupos, para hacer la division por grupos entre los usuarios y eliminar la posibilidad de que un mismo id esté en más de un grupo, luego se crea un dataset donde cada device_id que hizo al menos una compra, tiene en la columna converted un booleano 1, finalmente se combina con la matriz quedando todos los id que no hicieron compras como valores ausentes, estos se rellenan con ceros quedando lista la clasificación por compra por cliente.

Ahora solo queda aplicar la formula para la prueba estadística.

In [30]:
alpha = .05 #  nivel de significación

purchases = np.array([len(conversions.query('group == 246 and converted == 1')), len(conversions.query('group == 247 and converted == 1'))])
leads = np.array([len(conversions.query('group == 246')), len(conversions.query('group == 247'))])

print(purchases)
print(leads)

p1 = purchases[0]/leads[0]
p2 = purchases[1]/leads[1]
p_combined = ((purchases[0] + purchases[1]) / (leads[0] + leads[1]))
difference = p1 - p2

print('246:', format(p1, '.2%'), '%  ','247:',format(p2, '.2%'), '%.')

z_value = difference / mt.sqrt(p_combined * (1 - p_combined) * (1/leads[0] + 1/leads[1]))

distr = st.norm(0, 1) 

p_value = (1 - distr.cdf(abs(z_value))) * 2 

print('p-value: ', p_value)

if (p_value < alpha):
    print("Rechazar la hipótesis nula: hay una diferencia significativa entre las proporciones")
else:
    print("No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes")
[1200 1158]
[2484 2513]
246: 48.31% %   247: 46.08% %.
p-value:  0.11456679313141849
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

La prueba estadística concluye que no hay diferencias significativas entre la tasa de conversión de los grupos de control.

¿Qué conclusiones se pueden sacar del experimento?¶

Para poder sacar conclusiones del experimiento, se definirá una función para calcular las diferencias estadísticas entre los eventos y los grupos del dataset, el cálculo anterior ayudará a corroborar que la función trabaje adecuadamente.

In [31]:
def prop_test(group_1, group_2, event):
    
    conversions = new_df[['device_id', 'group']].drop_duplicates()
    converted = pd.DataFrame(data={"device_id": new_df[new_df["event"] == event]["device_id"].unique(), "converted": 1})
    conversions = conversions.merge(converted, on="device_id", how="left")
    conversions["converted"] = conversions["converted"].fillna(0)
    
    purchases = np.array([len(conversions[(conversions['group'] == group_1) & (conversions['converted'] == 1)]), 
                          len(conversions[(conversions['group'] == group_2) & (conversions['converted'] == 1)])])
                          
    leads = np.array([len(conversions[conversions['group'] == group_1]), 
                      len(conversions[conversions['group'] == group_2])])
    
    print(event, ':',purchases)
    print('Total :',leads)
    
    alpha = .05 / 5 #Corrección de Bonferroni
    
    p1 = purchases[0]/leads[0]
    p2 = purchases[1]/leads[1]
    p_combined = ((purchases[0] + purchases[1]) / (leads[0] + leads[1]))
    difference = p1 - p2
    
    print(group_1, ':', format(p1, '.2%'), '%  ', group_2, ':', format(p2, '.2%'), '%.')

    z_value = difference / mt.sqrt(p_combined * (1 - p_combined) * (1/leads[0] + 1/leads[1]))

    distr = st.norm(0, 1) 

    p_value = (1 - distr.cdf(abs(z_value))) * 2
    
    print('p-value: ', p_value)

    if (p_value < alpha):
        print("Rechazar la hipótesis nula: hay una diferencia significativa entre las proporciones")
    else:
        print("No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes")

La función está lista, incorpora todas las salidas y los pasos de la formula, se establece un valor alpha de .05 / 5 ya que estamos aplicando la correción de Bonferroni para disminuir la probabilidad de error en las múltiples pruebas estadísticas ( se divide por cinco porque son cinco eventos registrados). Funciona proporcionando los grupos a comparar y el nombre del evento, para verificar que funcione bien se observará el resultado del evento PaymentScreenSuccessful.

Se aplicará la función, primero, entre los grupos de control (246 y 247)

In [32]:
prop_test(246, 247, 'MainScreenAppear')
print()
prop_test(246, 247, 'OffersScreenAppear')
print()
prop_test(246, 247, 'CartScreenAppear')
print()
prop_test(246, 247, 'PaymentScreenSuccessful')
print()
prop_test(246, 247, 'Tutorial')
MainScreenAppear : [2450 2476]
Total : [2484 2513]
246 : 98.63% %   247 : 98.53% %.
p-value:  0.7570597232046099
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

OffersScreenAppear : [1542 1520]
Total : [2484 2513]
246 : 62.08% %   247 : 60.49% %.
p-value:  0.2480954578522181
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

CartScreenAppear : [1266 1238]
Total : [2484 2513]
246 : 50.97% %   247 : 49.26% %.
p-value:  0.22883372237997213
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

PaymentScreenSuccessful : [1200 1158]
Total : [2484 2513]
246 : 48.31% %   247 : 46.08% %.
p-value:  0.11456679313141849
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

Tutorial : [278 283]
Total : [2484 2513]
246 : 11.19% %   247 : 11.26% %.
p-value:  0.9376996189257114
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

La función opera correctamente, ya que el resultado de PaymentScreenSuccessful coincide con el del ejercicio anterior.

Además se observa que los grupos de control están bien divididos, que no hay diferencias estadísticas en ninguno de los eventos, se observan valores p relativamente altos y variaciones bajas en los porcentajes.

Ahora se hará la comparación con el grupo de prueba (248) para determinar si los cambios de fuente en la aplicación generan resultados positivos, neutrales o negativos. Se mantendrá la funcion con el valor alpha de .05 / 5 ya que, aunque ahora sean más pruebas estadisticas, los grupos a comparar se repiten, de igual manera, cinco veces (una por cada evento)

In [33]:
prop_test(246, 248, 'MainScreenAppear')
print()
prop_test(247, 248, 'MainScreenAppear')
MainScreenAppear : [2450 2493]
Total : [2484 2537]
246 : 98.63% %   248 : 98.27% %.
p-value:  0.2949721933554552
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

MainScreenAppear : [2476 2493]
Total : [2513 2537]
247 : 98.53% %   248 : 98.27% %.
p-value:  0.4587053616621515
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes
  • No hay diferencia significativa en el evento de muestra de pantalla principal.
In [34]:
prop_test(246, 248, 'OffersScreenAppear')
print()
prop_test(247, 248, 'OffersScreenAppear')
OffersScreenAppear : [1542 1531]
Total : [2484 2537]
246 : 62.08% %   248 : 60.35% %.
p-value:  0.20836205402738917
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

OffersScreenAppear : [1520 1531]
Total : [2513 2537]
247 : 60.49% %   248 : 60.35% %.
p-value:  0.9197817830592261
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes
  • No hay diferencia significativa en el evento de muestra de pantalla de oferta.
In [35]:
prop_test(246, 248, 'CartScreenAppear')
print()
prop_test(247, 248, 'CartScreenAppear')
CartScreenAppear : [1266 1230]
Total : [2484 2537]
246 : 50.97% %   248 : 48.48% %.
p-value:  0.07842923237520116
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

CartScreenAppear : [1238 1230]
Total : [2513 2537]
247 : 49.26% %   248 : 48.48% %.
p-value:  0.5786197879539783
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes
  • No hay diferencia significativa en el evento de muestra de pantalla de carrito.
In [36]:
prop_test(246, 248, 'PaymentScreenSuccessful')
print()
prop_test(247, 248, 'PaymentScreenSuccessful')
PaymentScreenSuccessful : [1200 1181]
Total : [2484 2537]
246 : 48.31% %   248 : 46.55% %.
p-value:  0.2122553275697796
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

PaymentScreenSuccessful : [1158 1181]
Total : [2513 2537]
247 : 46.08% %   248 : 46.55% %.
p-value:  0.7373415053803964
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes
  • No hay diferencia significativa en el evento de muestra de pago exitoso.
In [37]:
prop_test(246, 248, 'Tutorial')
print()
prop_test(247, 248, 'Tutorial')
Tutorial : [278 279]
Total : [2484 2537]
246 : 11.19% %   248 : 11.00% %.
p-value:  0.8264294010087645
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes

Tutorial : [283 279]
Total : [2513 2537]
247 : 11.26% %   248 : 11.00% %.
p-value:  0.765323922474501
No se pudo rechazar la hipótesis nula: no hay razón para pensar que las proporciones son diferentes
  • No hay diferencia significativa en el evento de muestra de tutorial.

Del experimento se concluye que los cambios en la fuente de la aplicación propuestos por el equipo de diseño no dieron resultados positivos ni tampoco negativos, estadísticamente hablando, mirando los porcentajes se puede apreciar que estuvieron levemente por debajo de los grupos de control, por lo cual, no se recomienda hacer los cambios propuestos.

¿El nivel de significación ha sido el correcto?¶

Si, en una primera instancia se realizaron todas las pruebas con un nivel de significación de .05 y los resultados fueron los mismos, debido a que los valores p son altos (la probabilidad de que sean iguales es muy alta) no habrá variaciones en los resultados al bajar el nivel de significación, de igual manera se aplica la corrección de Bonferroni, ya que es la manera correcta de aplicar las pruebas estadísticas en estos casos por la manera en que disminuye las probabilidades de un falso positivo.

In [38]:
print('Probabilidad de falso positivo sin corrección:' ,format(1 - (1 - 0.05)**5, '.2%'))
print()
print('Probabilidad de falso positivo con corrección:',format(1 - (1 - (0.05 / 5))**5, '.2%'))
Probabilidad de falso positivo sin corrección: 22.62%

Probabilidad de falso positivo con corrección: 4.90%

Una drástica disminución (17%) de las probabilidades de falla se observa en este caso.

Conclusiones¶

En este apartado se desarollarán ampliamente todas las conclusiones obtenidas en el desarrollo del informe, haciendo hincapié en los hallazgos fundamentales del trabajo, como lo son: hallazgos sobre el registro de los datos, información sobre el embudo de eventos, relevancia de los hallazgos y conclusiones sobre el experimento.

Estudio y comprobación de los datos¶

  • Se calculó la cantidad de datos por día y se determinó que hay una cantidad suficiente de datos (eventos, registros de grupo y usuario) para realizar un análisis y pruebas estadísiticas.
  • Se observó que hay datos completos desde el 2019-08-01 en adelante, un rango de una semana. Se eliminaron los datos de antes de la fecha indicada.

Embudo de eventos¶

Hay 5 tipos de evento registrados:

  • Aparición de pantalla principal.
  • Aparición de pantalla de ofertas.
  • Aparición de pantalla de carrito.
  • Aparición de pantalla de Pago exitoso.
  • Tutorial.

El tutorial es un opcional que está en la parte superior del embudo y que la gran mayoría elige saltar (no quita que pueda ser muy útil para la gente que elige verlo). En cuanto a las hallazgos sobre el paso de los clientes por el embudo, hemos detectado que:

  • Los usuarios suelen visitar más de una vez la pantalla principal antes de hacer una compra.
  • En donde más se pierden usuarios es en la transición de pantalla principal a pantalla de oferta (un 38%).

Experimento¶

En cuanto al experimento, podemos rescatar que el segmento A/A del test, fue realizado correctamente, ya que no se apreciaron diferencias significativas entre estos grupos. En cuanto al resultado final (como se menciona anteriormente) se concluye que:

No se recomienda implementar el cambio del grupo B, ya que no se rechaza ninguna hipótesis nula en favor de este grupo y, en porcentajes está ligeramente por debajo de los grupos A.