Índice

[75.06 / 95.58] Organización de Datos
Trabajo Práctico 1: Análisis Exploratorio de Datos

Grupo 30: Datatouille

  • 101055 - Bojman, Camila
  • 100029 - del Mazo, Federico
  • 100687 - Hortas, Cecilia
  • 97649 - Souto, Rodrigo

https://github.com/FdelMazo/7506-Datos

https://kaggle.com/datatouille2018/7506-TP1/

https://kaggle.com/datatouille2018/7506-TP1-anexo/

Presentamos aca un análisis de datos obtenido de usuarios que visitaron www.trocafone.com, un sitio de e-commerce de compra y venta de celulares reacondicionados, con operaciones principalmente en Brasil.


Set up inicial y curado de datos

In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import squarify # pip install squarify
import geopandas as gpd #  conda install -c conda-forge geopandas
from wordcloud import WordCloud # conda install -c conda-forge wordcloud
from pySankey import sankey# pip install pySankey
from shapely.geometry import Point
from time import strptime
from math import pi
from PIL import Image
import calendar

import plotly
import plotly.plotly as py # conda install -c conda-forge plotly
import plotly.graph_objs as go
from __future__ import division
plotly.tools.set_credentials_file(username='datatouille', api_key='GJ7Foc8nFWf23VZfzFSK')

%matplotlib inline

df = pd.read_csv('data/events.csv', low_memory=False)
In [2]:
sns.set(style="darkgrid")
plt.rcParams['axes.titlesize'] = 30
plt.rcParams['axes.labelsize'] = 24
plt.rcParams['axes.labelweight'] = 'bold'
plt.rcParams['figure.figsize'] = (25,15)
sns.set(font_scale=2)
In [3]:
with pd.option_context('display.max_column',0):
  display(df.sample(n=5))
timestamp event person url sku model condition storage color skus search_term staticpage campaign_source search_engine channel new_vs_returning city region country device_type screen_resolution operating_system_version browser_version
897335 2018-04-20 06:19:30 searched products e39426e9 NaN NaN NaN NaN NaN NaN 2796 Galaxy NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
175376 2018-05-14 17:17:56 viewed product 2e342477 NaN 8343.0 Samsung Galaxy J2 4G Duos Excelente 8GB Dourado NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
863449 2018-06-10 18:48:07 viewed product db94f19b NaN 9902.0 iPhone 7 Bom 128GB Preto Brilhante NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
346486 2018-05-16 01:17:58 brand listing 5af7e2bc NaN NaN NaN NaN NaN NaN 2718,2821,2787,2741 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
492740 2018-06-03 00:18:37 visited site 7e0fa2b0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN Paid New São Paulo Sao Paulo Brazil Computer 1024x600 Windows 7 Chrome 66.0

Información general del dataset

In [4]:
bytes_used = df.memory_usage().sum()
print('Memoria usada: {:.2f}MB'.format(bytes_used/1000000))
print('{} atributos y {} registros en el dataframe.\n'.format(df.shape[1],df.shape[0]))
print('Primer registro: {} \nÚltimo registro: {}.'.format(df['timestamp'].min(),df['timestamp'].max()))
Memoria usada: 186.08MB
23 atributos y 1011288 registros en el dataframe.

Primer registro: 2018-01-01 07:32:26 
Último registro: 2018-06-15 23:59:31.
In [5]:
describe = df.describe().T
descripcion = pd.read_csv('data/columns-desc.csv',index_col='column')
data = pd.merge(descripcion,describe,left_index=True,right_index=True)
data['null count'] = df.isnull().sum()
data['dtype'] = df.dtypes
with pd.option_context('display.max_colwidth',-1):
    display(data)
description count unique top freq null count dtype
column
timestamp Fecha y hora cuando ocurrió el evento. 1011288 793805 2018-05-15 15:56:06 11 0 object
event Tipo de evento. 1011288 11 viewed product 528931 0 object
person Identificador de cliente que realizó el evento. 1011288 27624 71492f2b 2771 0 object
url Url visitada por el usuario. 82756 227 / 28323 928532 object
sku Identificador de producto relacionado al evento. 563838 3574 2830.0 4282 447450 object
model Nombre descriptivo del producto incluyendo marca y modelo. 564284 202 iPhone 6 50916 447004 object
condition Condición de venta del producto. 563836 5 Bom 243014 447452 object
storage Cantidad de almacenamiento del producto. 563836 8 16GB 190833 447452 object
color Color del producto. 563836 63 Preto 132960 447452 object
skus Identificadores de productos visualizados en el evento. 221699 35310 2820,6706,6720,2750,6649,7251,6663,12604,7224,2774,2773,2999 594 789589 object
search_term Términos de búsqueda utilizados en el evento. 48967 5851 Iphone 1207 962321 object
staticpage Identificador de página estática visitada. 3598 14 CustomerService 1528 1007690 object
campaign_source Origen de campaña si el tráfico se originó de una campaña de marketing. 82796 24 google 58153 928492 object
search_engine Motor de búsqueda desde donde se originó el evento si aplica. 50957 4 Google 50240 960331 object
channel Tipo de canal desde donde se originó el evento. 87378 7 Paid 44193 923910 object
new_vs_returning Indicador de si el evento fue generado por un usuario nuevo o por un usuario que regresa al sitio. 87378 2 Returning 60480 923910 object
city Ciudad desde donde se originó el evento. 87378 1939 Unknown 15819 923910 object
region Región desde donde se originó el evento. 87378 93 Sao Paulo 24996 923910 object
country País desde donde se originó el evento. 87378 46 Brazil 84308 923910 object
device_type Tipo de dispositivo desde donde se genero el evento. 87378 4 Smartphone 44239 923910 object
screen_resolution Resolución de pantalla que se está utilizando en el dispositivo desde donde se genero el evento. 87378 282 360x640 30009 923910 object
operating_system_version Version de sistema operativo desde donde se origino el evento. 87378 121 Windows 7 19675 923910 object
browser_version Versión del browser utilizado en el evento. 87378 343 Chrome 66.0 22611 923910 object

Se sabe que los datos proporcionados son un mero subconjunto de todos los datos de la empresa.

¿Cómo se decidió el truncamiento de la base original?

In [6]:
print('Cantidad de usuarios: {}'.format(df['person'].nunique()))

by_person = df[['event','person']].groupby('person')
con_checkouts = by_person.agg({'event':lambda x: any(y == 'checkout' for y in x)}).sum()['event']

print('Cantidad de usuarios con checkouts: {}'.format(con_checkouts))
Cantidad de usuarios: 27624
Cantidad de usuarios con checkouts: 27624

Después de varias teorías y sus respectivas pruebas, encontramos que todos los usuarios presentes en el set tienen al menos un evento checkout, mostrando que efectivamente la base de datos original se truncó (ya que sería inocente creerse que el 100% de los usuarios que entran al sitio tienen un checkout).

Este dato parece menor pero es muy importante; nos muestra que las conclusiones de este análisis no podrán hacerse sobre el sitio en su totalidad. Por los limites de los datos de entrada todas las conclusiones serán sobre los usuarios que hayan tenido al menos un checkout, en vez de acerca de todos los usuarios del sitio.

Selección de datos

Pasado el vistazo general a los datos, se pueden transformar datos en sus tipos correspondientes para mejor manejo de estos y para ahorro de memoria.

In [7]:
# Los atributos con pocos valores posibles se pasan a variables categoricas para ahorrar memoria
df['event'] = df['event'].astype('category')
df['condition'] = df['condition'].astype('category')
df['storage'] = df['storage'].astype('category')
df['search_engine'] = df['search_engine'].astype('category')
df['channel'] = df['channel'].astype('category')
df['device_type'] = df['device_type'].astype('category')

# Se pasan los sku a números, para evitar conflictos entre skus iguales pero registrados como 1001 vs 1001.0
df['sku'] = df['sku'].replace({np.nan:0.0, 'undefined':0.0})
df['sku'] = df['sku'].astype('float64')

# El tiempo es mejor manejarlo como tal
df['timestamp'] = pd.to_datetime(df['timestamp'])

# Chequeo
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1011288 entries, 0 to 1011287
Data columns (total 23 columns):
timestamp                   1011288 non-null datetime64[ns]
event                       1011288 non-null category
person                      1011288 non-null object
url                         82756 non-null object
sku                         1011288 non-null float64
model                       564284 non-null object
condition                   563836 non-null category
storage                     563836 non-null category
color                       563836 non-null object
skus                        221699 non-null object
search_term                 48967 non-null object
staticpage                  3598 non-null object
campaign_source             82796 non-null object
search_engine               50957 non-null category
channel                     87378 non-null category
new_vs_returning            87378 non-null object
city                        87378 non-null object
region                      87378 non-null object
country                     87378 non-null object
device_type                 87378 non-null category
screen_resolution           87378 non-null object
operating_system_version    87378 non-null object
browser_version             87378 non-null object
dtypes: category(6), datetime64[ns](1), float64(1), object(15)
memory usage: 137.0+ MB
In [8]:
ahorro = (bytes_used - df.memory_usage().sum())
porcentaje = (ahorro/bytes_used) * 100
print("Memoria ahorrada: {:.4f}MB ({:.2f}%)".format(ahorro/1000000,porcentaje))
Memoria ahorrada: 42.4723MB (22.83%)

Data Mining

Se agregó el concepto de sesiones. Definimos una sesión como una serie de eventos por usuario, los cuales están todos con menos de 30 minutos de inactividad entre el actual y el anterior.

Todo el código respecto a la generación de sesiones está ubicado en anexo.ipynb.

In [9]:
df_sessions = pd.read_csv('data/sessions.csv')
df = df.merge(df_sessions, how='left', left_index=True, right_index=True)
In [10]:
session_cols = ['person', 'timestamp', 'time_diff_min', \
        'session_id', 'session_total_events', \
        'session_cumno', 'session_first', 'session_last', \
        'session_conversion', 'session_checkout', 'session_ad']
df[session_cols].head(15)
Out[10]:
person timestamp time_diff_min session_id session_total_events session_cumno session_first session_last session_conversion session_checkout session_ad
0 0004b0a2 2018-05-31 23:38:05 0.000000 0 4 0 True False False True True
1 0004b0a2 2018-05-31 23:38:05 0.000000 0 4 1 False False False True True
2 0004b0a2 2018-05-31 23:38:09 0.066667 0 4 2 False False False True True
3 0004b0a2 2018-05-31 23:38:40 0.516667 0 4 3 False True False True True
4 0006a21a 2018-05-29 13:29:25 0.000000 0 4 0 True False False True False
5 0006a21a 2018-05-29 13:29:26 0.016667 0 4 1 False False False True False
6 0006a21a 2018-05-29 13:29:27 0.016667 0 4 2 False False False True False
7 0006a21a 2018-05-29 13:29:35 0.133333 0 4 3 False True False True False
8 000a54b2 2018-04-09 20:12:31 0.000000 0 7 0 True False False False False
9 000a54b2 2018-04-09 20:12:31 0.000000 0 7 1 False False False False False
10 000a54b2 2018-04-09 20:12:31 0.000000 0 7 2 False False False False False
11 000a54b2 2018-04-09 20:12:31 0.000000 0 7 3 False False False False False
12 000a54b2 2018-04-09 20:12:53 0.366667 0 7 4 False False False False False
13 000a54b2 2018-04-09 20:13:14 0.350000 0 7 5 False False False False False
14 000a54b2 2018-04-09 20:13:20 0.100000 0 7 6 False True False False False

De las columnas model, operating_system_version y browser_version se pueden extraer datos de la marca, el sistema operativo y el explorador. Estos se usan para generar nuevos dataframes y luego se hace un left join de los datos obtenidos.

Todo el código respecto a la creación de nuevos dataframes está ubicado el notebook anexo.

In [11]:
df_brands = pd.read_csv('data/brands.csv')
df = df.merge(df_brands, how='left', on='model')
df['brand'] = df['brand'].astype('category')
sample = df[df['model'].notnull()]
sample[['model','brand']].head()
Out[11]:
model brand
2 iPhone 5s iphone
3 iPhone 5s iphone
4 Samsung Galaxy S8 samsung
7 Samsung Galaxy S8 samsung
13 Motorola Moto Z Play motorola
In [12]:
df_os = pd.read_csv('data/os.csv')
df = df.merge(df_os, how='left', on='operating_system_version')
df['operating_system'] = df['operating_system'].astype('category')
sample = df[df['operating_system_version'].notnull()]
sample[['operating_system_version', 'operating_system']].head()
Out[12]:
operating_system_version operating_system
1 Android 6 android
5 Android 5.1.1 android
9 Windows 10 windows
16 Windows 10 windows
45 Windows 10 windows
In [13]:
df_browsers = pd.read_csv('data/browsers.csv')
df = df.merge(df_browsers, how='left', on='browser_version')
df['browser'] = df['browser'].astype('category')
sample = df[df['browser_version'].notnull()]
sample[['browser_version','browser']].head()
Out[13]:
browser_version browser
1 Chrome Mobile 39 chrome mobile
5 Android 5.1 android
9 Chrome 65.0 chrome
16 Chrome 66.0 chrome
45 Chrome 65.0 chrome

Extracción de información de las fechas.

In [14]:
df['month_number'] = df['timestamp'].dt.month
df['month_name'] = df['month_number'].apply(lambda x: calendar.month_abbr[x])
df['week_day'] = df['timestamp'].dt.weekday
df['week_number'] = df['timestamp'].dt.week
df['week_day_name'] = df['timestamp'].dt.weekday_name
df['day_date'] = df['timestamp'].dt.to_period('D')
df['day_dom'] = df['timestamp'].dt.day
df['hour_count'] = df['timestamp'].dt.hour
df['day_doy'] = df['timestamp'].dt.dayofyear

Finalmente, sabiendo que los SKUs se refieren a la combinación única entre modelo, condición de compra, almacenamiento y color, se agrega la columna 'sku_name' para saber específicamente a que dispositivo se refiere.

In [15]:
df['sku_name'] = df['model'] + ' ' + df['storage'].astype(str) + ' ' + df['color'] + ' (' + df['condition'].astype(str) + ')'
df[['sku','sku_name']].head()
Out[15]:
sku sku_name
0 0.0 NaN
1 0.0 NaN
2 2694.0 iPhone 5s 32GB Cinza espacial (Bom)
3 2694.0 iPhone 5s 32GB Cinza espacial (Bom)
4 15338.0 Samsung Galaxy S8 64GB Dourado (Bom)

Limpieza de datos

Tomando como un error de tracking (double tracking en este caso) cuando un usuario compra el mismo celular varías veces en un día, se limpian estos datos duplicados.

In [16]:
df2 = df[df['event'] == 'conversion'][['day_date','person','event','model']]
df2.groupby('person').head()
double_tracking_rows = df2.duplicated(keep='last')
double_tracking_rows = double_tracking_rows[double_tracking_rows]
print('Se limpian {} registros producto de double tracking.'.format(double_tracking_rows.count()))
display(df2[df2.index.isin(double_tracking_rows.index)].sort_values('person').head())

df = df[~df.index.isin(double_tracking_rows.index)]
Se limpian 205 registros producto de double tracking.
day_date person event model
20273 2018-04-30 0562e9d2 conversion iPhone 4G
36203 2018-05-10 0a37e81a conversion iPhone 6
36213 2018-05-10 0a37e81a conversion iPhone 6
36218 2018-05-10 0a37e81a conversion iPhone 6
47591 2018-03-19 0da63237 conversion Samsung Galaxy S5 New Edition Duos

Se limpian los registros en los que el usuario copró un producto sin antes haber pasado por ningún otra etapa previa a la conversión (en una sesión).

In [17]:
df2 = df[(df['session_total_events'] == 1) & (df['event'] == 'conversion')]
print('Se limpian {} registros de conversión directa.'.format(df2['event'].count()))
df = df[~df.index.isin(df2.index)]
Se limpian 67 registros de conversión directa.

Análisis de eventos

In [18]:
event = df['event']
descripcion = pd.read_csv('data/events-desc.csv',index_col='event')
descripcion['value_counts'] = event.value_counts()
with pd.option_context('display.max_colwidth',0):
    display(descripcion)
description value_counts
event
viewed product El usuario visita una página de producto. 528931
brand listing El usuario visita un listado específico de una marca viendo un conjunto de productos. 98635
visited site El usuario ingresa al sitio a una determinada url. 87378
ad campaign hit El usuario ingresa al sitio mediante una campana de marketing online. 82827
generic listing El usuario visita la homepage. 67534
searched products El usuario realiza una búsqueda de productos en la interfaz de búsqueda del site. 56073
search engine hit El usuario ingresa al sitio mediante un motor de búsqueda web. 50957
checkout El usuario ingresa al checkout de compra de un producto. 33735
staticpage El usuario visita una página 3598
conversion El usuario realiza una conversión comprando un producto. 900
lead El usuario se registra para recibir una notificación de disponibilidad de stock para un producto que no se encontraba disponible en ese momento. 448

Conversion Rate

La primer métrica a analizar y la más importante en un negocio de e-commerce es el conversion rate. Siendo una conversión un usuario tomando la acción deseada, en este caso comprar un producto, queremos saber como es la razón entre conversiones y el total de eventos.

In [19]:
conversion_rate_total = (df.loc[df['event']=='conversion'].shape[0] / df.shape[0] )*100
print('Overall conversion rate, from 2018-01-01 to 2018-06-15: {:.3f}%'.format(conversion_rate_total))
Overall conversion rate, from 2018-01-01 to 2018-06-15: 0.089%

Este dato, suelto, no sirve. ¿Cuando es una buena taza de conversión? Cuando es mejor que la anterior!

In [20]:
data = df[['event','week_number']]
data = data.groupby(['week_number','event']).agg({'event':'count'})
data = data.rename(columns={'event':'count'})
data = data.reset_index()
data = data.pivot_table(index='week_number', values='count', columns='event')
data = pd.DataFrame(data.to_records())
data['conversion rate'] = ( data['conversion'] / data.sum(axis=1) ) * 100

plt.plot(data['conversion rate'])
plt.ylabel('Conversion rate')
plt.xlabel('Semana del año')
plt.xticks(data['week_number'])
plt.title('Conversion rate por semana')

plt.savefig('informe/figures/010-conversion_rate_semana-lineplot.png')
In [21]:
data = df[['event','month_number']]
data = data.groupby(['month_number','event']).agg({'event':'count'})
data = data.rename(columns={'event':'count'})
data = data.reset_index()
data = data.pivot_table(index='month_number', values='count', columns='event')
data = pd.DataFrame(data.to_records())
data['conversion rate'] = ( data['conversion'] / data.sum(axis=1) ) * 100

visu = sns.barplot(x=data['month_number'],y=data['conversion rate'])
visu.set_title('Conversion rate según mes')
visu.set_ylabel('Conversion Rate')
visu.set_xlabel('Mes')
visu.set_xticklabels(['Jan', 'Feb', 'Mar', 'Apr', 'May', '(First half) Jun'])

plt.savefig('informe/figures/011-conversion_rate_mes-barplot.png')

Lo que se puede ver en ambos gráficos es que los primeros 4 meses del año hubo un conversion rate bastante estable, y luego una súbita baja en mayo. En las semanas de mayo a junio se ve que hay indicios de un incremento. Al no haber datos de todo el mes de junio, no se pueden hacer más conclusiones pasada la semana 24.

¿Cuál es el evento más frecuente?

In [22]:
orden = event.value_counts().head(7).index
visu = sns.countplot(x='event',data=df,order=orden)
visu.axes.set_title('Frecuencia de eventos')
visu.set_xlabel("Evento")
visu.set_ylabel("Cantidad")

plt.savefig('informe/figures/02-eventos-barplot.png')

El evento con mas hits es el de ver un producto. Esto tiene sentido ya que trocafone es una plataforma de e-commerce y ver productos es su principal función como sitio.

¿Cómo evolucionan los eventos a través el tiempo?

Lo siguiente que nos preguntamos es si aumentaron los productos vistos, los checkouts y las compras en el transcurso del primer semestre del 2018.

In [23]:
data = df.pivot_table(index='day_dom',columns='month_number', values='event', aggfunc='count')
data = ((data-data.min()) / (data.max() - data.min()))

visu = sns.heatmap(data.T,  cmap="OrRd")
visu.set_title("Tráfico (normalizado) en sitio según mes y día")
visu.set_xlabel("Día")
visu.set_ylabel("Mes")

plt.savefig('informe/figures/030-eventos_segun_mes-heatmap.png')

Como se puede ver, no hay relación directa entre dia del mes y mayores visitas al sitio. Se podría decir que la segunda quincena tiene más visitas que la primera, pero las magnitudes no son lo suficientemente distintas como para argumentar esto.

Es importante que este gráfico este normalizado para poder encontrar patrones y no simplemente ver como el mes con más eventos es simplemente el mes con más eventos por día.

In [24]:
data = df.pivot_table(index='week_day',columns='month_number', values='event', aggfunc='count')
data.index = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
data = ((data-data.min()) / (data.max() - data.min()))

visu = sns.heatmap(data.T,  cmap="OrRd")
visu.set_title('Tráfico (normalizado) en sitio según mes y día de la semana')
visu.set_xlabel('Día de la semana')
visu.set_ylabel('Mes')

plt.savefig('informe/figures/031-eventos_segun_dow-heatmap.png')

Se puede observar en el gráfico que los días de mayor tráfico son los días laborales mientras que el fin de semana los usuarios visitan notoriamente menos la página.

In [25]:
month_name = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
month_counts = df.groupby('month_name').count()
month_counts = month_counts.loc[month_name]
visu = month_counts['event'].plot(kind='bar')
visu.axes.set_title('Cantidad de eventos segun mes')
visu.set_xlabel('Mes')
visu.set_ylabel("Cantidad")

plt.savefig('informe/figures/032-eventos_segun_mes-barplot.png')

Como se puede ver, mayo y junio tienen considerablemente más visitas que el resto de los meses. Y esto es incluso teniendo en cuenta que en junio se registraron la mitad de los días (el último registro del set de datos es el 15 de junio).

Es importante notar como esta evolución es casi inversa a la del conversion rate, mostrando que en mayo si bien no hubo tantas ventas, si aumento mucho la cantidad de visitas.

¿Por que mayo y junio son los meses de mayor tráfico? ¿Eso implica una mayor cantidad de ventas?

In [26]:
df_top = df.loc[(df['month_name'] == 'May') | (df['month_name'] == 'Jun')]

df_temporal = df_top[['event', 'day_dom']]
df_temporal = df_temporal.loc[(df_temporal['event'] == 'conversion') | (df_temporal['event'] == 'checkout') | (df_temporal['event'] == 'viewed product')]
df_temporal = df_temporal.groupby('day_dom')['event'].value_counts().unstack('event')

visu = plt.plot(np.log(df_temporal))
plt.legend(iter(visu), ('checkout', 'conversion', 'viewed_products'))
plt.title("Cantidad de eventos según día de los meses de junio y mayo")
plt.xlabel("Día")
plt.ylabel("Cantidad")
plt.savefig('informe/figures/033-eventos_mayo_junio_lineplot.png')

Es posible concluir que hay un pico entre los días 13 y 16 de los distintos tipos de eventos. Como Trocafone es del país de Brasil y el mayor tráfico proviene de allí, lo cual será verificado posteriormente, se infiere que puede deberse a alguna promoción lanzada en la plataforma o en el mismo país. Esto no puede concluirse con certeza debido a la escasez de información en internet y en la plataforma de promociones pasadas.

¿A qué hora se registró la mayor cantidad de conversiones?

In [27]:
df_conversion = df.loc[df['event'] == 'conversion']['hour_count'].value_counts().to_frame().sort_index()
df_checkout = df.loc[df['event'] == 'checkout']['hour_count'].value_counts().to_frame().sort_index()

labels = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
angles = [i / float(24) * 2 * pi for i in range(24)]
angles += angles[:1]  #cerrar el círculo

gray = '#999999'
black = '#000000'
orange = '#FD7120'
blue = '#00BFFF'
In [79]:
fig=plt.figure(figsize=(7,7))
series = plt.subplot(1, 1, 1, polar=True)
    
series.set_theta_offset(pi / 2)
series.set_theta_direction(-1)

plt.xticks(angles, labels, color=black, size=20)

plt.yticks([20, 40, 60, 80], ['20', '40', '60', '80'], color=gray, size=2)

plt.ylim(0,100),

series_values = df_conversion.values.flatten().tolist()
series_values += series_values[:1]  

series.set_rlabel_position(0)
series.plot(angles, series_values, color=orange, linestyle='solid', linewidth=1)
series.fill(angles, series_values, color=orange, alpha=0.5)
series.set_title('Cantidad de conversiones por hora', y=1.08)
#plt.savefig('informe/figures/040-hours-conversion-radarchart.png')

Como es esperable, a la mañana y a la madrugada se registra una muy baja cantidad de compras. Se produce un pico a las 19 hs, hora en la que la mayoría de la gente vuelve del trabajo.

¿A qué hora se registró la mayor cantidad de checkouts?

In [80]:
fig=plt.figure(figsize=(7,7))
series = plt.subplot(1, 1, 1, polar=True)
    
series.set_theta_offset(pi / 2)
series.set_theta_direction(-1)

plt.xticks(angles, labels, color=black, size=20)

#plt.yticks([1000, 1250, 1500, 1750, 2000], ['1000', '1250', '1500', '1750', '2000'], color=gray, size=7)
    
plt.ylim(0,2250)

series_values = df_checkout.values.flatten().tolist()
series_values += series_values[:1]  

series.set_rlabel_position(0)
series.plot(angles, series_values, color=blue, linestyle='solid', linewidth=1)
series.fill(angles, series_values, color=blue, alpha=0.5)
series.set_title('Cantidad de checkouts por hora', y=1.08)
#plt.savefig('informe/figures/041-hours-checkout-radarchart.png')

En las primeras 12 hs del día la cantidad de conversiones realizadas es muy baja a contraste de las restantes 12 hs. A diferencia de las conversiones, la cantidad de checkouts realizados mantiene su valor más alto a lo largo de la tarde y la noche. Igualmente se produce un pico a las 19 hs.

¿Cuál es la distribución de la cantidad de eventos producidos por usuario en función de los eventos?

In [87]:
usuarios = df.groupby(['person', 'event'])
usuarios = usuarios.size().unstack(level = 'event')
usuarios = usuarios.fillna(usuarios.mean())
p = sns.boxplot(data=usuarios)
sns.set(font_scale=1.5)
plt.xticks(rotation=45)

#plt.savefig('informe/figures/190-usuarios_eventos-boxplot.png')

Como se puede observar el gráfico no es ilustrativo de la respuesta que se desea obtener. Por lo tanto, se propone truncar la éscala del eje y a un valor en el cual se puedan observar las figuras de manera apropiada y representativa.

In [88]:
usuarios = df.groupby(['person', 'event'])
usuarios = usuarios.size().unstack(level = 'event')
usuarios = usuarios.fillna(usuarios.mean())
p = sns.boxplot(data=usuarios)
p.axes.set_ylim((0,25))
plt.xticks(rotation=45)

#plt.savefig('informe/figures/191-usuarios_eventos_truncado-boxplot.png')

Se propone analizar la cantidad de eventos producidos por usuario en un gráfico de tipo boxplot. Se puede observar que los usuarios suelen más comúnmente ver los productos antes que comprarlos, algo que podía predecirse anteriormente. Lo que puede resultar llamativo es que la cantidad de usuarios que ven productos es mayor a los que los buscan, pero esto se puede explicar por el hecho de que en una búsqueda pueden verse varios productos a la vez y eso cuenta como un solo evento. En cambio, ante el evento viewed products al ver un producto se contabiliza como un solo evento.


Análisis geográfico

¿Desde que países se accede más al sitio?

In [32]:
countries = df['country'].value_counts()
countries = countries.drop('Unknown')
data = countries.head(3)

visu = squarify.plot(data, label=data.index, alpha=.5, color=['green','red','cyan'])
visu.set_title('Países con más visitas')

plt.savefig('informe/figures/050-paises_visitas-treemap.png')

Al ser Trocafone una empresa que inicialmente radicó en Brasil y llegó a Argentina en 2016 era esperable que Brasil sea el país de mayor tráfico.

Por la amplia diferencia, se separa a Brasil del gráfico y se analiza el tráfico en el resto de los países, para pdoer ver su diferencia en orden y magnitud.

In [33]:
countries = df['country'].value_counts()
countries = countries.drop('Unknown')
countries = countries.drop('Brazil')
data = countries.head(7)

visu = squarify.plot(data, label=data.index, alpha=.5, color=['red','cyan','yellow','grey','purple','orange','blue'])
visu.set_title('Países con más visitas, exceptuando Brazil')

plt.savefig('informe/figures/051-paises_visitas_sin_brazil-treemap.png')

¿Desde que regiones y ciudades de Brazil se registran más visitas?

Sacando las longitudes y latitudes de distintas ciudades del mundo, podemos ver que ciudades de Brazil son las que más visitas tienen.

Con ayuda del módulo geopandas, podemos plotear directamente sobre un mapa.

Las bases de datos adicionales fueron sacadas de http://www.geonames.org/.

Primero, se grafica las regiones, para tener un vistazo general de donde nos encontraremos más visitas. Luego, se ve por ciudad.

In [34]:
regiones_brasil = df.loc[(df['country'] == 'Brazil')]['region']
data = regiones_brasil.value_counts()
data = data.drop('Unknown')

fig = data.head(7).plot(kind='bar')
fig.axes.set_title('Regiones de Brazil con más visitas')
fig.axes.set_ylabel('Visitas')
fig.axes.set_xlabel('Región')

#plt.savefig('informe/figures/060-regiones_brazil-barplot.png')

Viendo que la mayoría de los eventos se registran en la region de San Pablo, Minas Gerais y Rio de Janeiro, se ve que la mayor concentración esta sobre la costa del sur este.

In [35]:
BR = pd.read_csv('data/BR.csv', low_memory=False, sep='\t')
BR = BR[['name','latitude','longitude']]

# Para que geopandas pueda leer bien las latitudes y longitudes, deben ser de la clase Point
BR['coordinates'] = list(zip(BR['longitude'],BR['latitude']))
BR['coordinates'] = BR['coordinates'].apply(Point)
BR.head()
Out[35]:
name latitude longitude coordinates
0 Tawaribar Rapids 2.66667 -59.96667 POINT (-59.96666999999999 2.66667)
1 Machpawa Falls 2.90000 -59.95000 POINT (-59.95 2.9)
2 Kulutuik Fall 5.08333 -59.96667 POINT (-59.96666999999999 5.08333)
3 Kukuipawa Falls 2.82548 -59.95024 POINT (-59.95024 2.82548)
4 Kenukawai Mountain 1.46667 -58.43333 POINT (-58.43333000000001 1.46667)
In [36]:
# Se debe hacer un join de los datos que se tienen (nombre de ciudad, cantidad de eventos) y los datos de geonames (nombre de ciudad, punto en el mapa), y esto plotearlo sobre los datos de geopandas (nombre de pais, punto en el mapa mundial)

# Se preparan los datos para el join (inner join de nombre de ciudad (columna name))
ciudades_brazil = df.loc[(df['country'] == 'Brazil')]
data = ciudades_brazil['city'].value_counts()
data = data.drop('Unknown')
data = data.to_frame()
data.reset_index(inplace=True)
data = data.rename(columns={'index':'name','city':'count'})

# Se pasa de un dataframe normal de pandas a uno de geopandas
BRA = gpd.GeoDataFrame(BR, geometry='coordinates')

# Se hace el inner join de ambos sets. Siendo que geonames daba mucha más información de la necesaria, duplicando valores por ciudades, se borran los duplicados
data = BRA.merge(data, on='name')
data = data.drop_duplicates('name')

# Se prepara el 'fondo' del gráfico, siendo este nomás el país. Para esto se usan los datos por defecto de geopandas
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
ax = world[world.name=='Brazil'].plot(color='white',edgecolor='black')

visu = data.plot(ax=ax,cmap='OrRd', legend=True)
visu.axes.set_title('Ciudades de Brazil con más visitas')

plt.savefig('informe/figures/061-ciudades_brazil-choropleth.png')

Como se preveía en el gráfico anterior, se ve como la mayoría de las visitas se producen sobre la costa sudeste de Brazil.


Análisis de busquedas

Para lo que respecta a busquedas, hay varias cosas para analizar y encontrar:

  1. La mas obvia, ¿qué terminos ingresan los usuarios en el buscador? (columna search term)
    • Esto nos da una visualización rápida de los gustos en crudo de los usuarios. Lo que antojan.
  2. ¿Qué productos buscan específicamente, usando la interfaz? (evento searched product)
    • Esta busqueda es un poco más refinada, permitiendonos saber específicamente los productos buscados, en vez de la crudeza de escribir cualquier termino que nos puede plantear el punto anterior.

Si bien tienen un objetivo en comun, difieren sustancialmente los analisis. En el primero se puede ver una vista rápida de marcas/modelos, mientras que en la segunda habrá una idea un poco mas profunda.

¿Qué terminos ingresan los usuarios en el buscador?

In [37]:
# Buscamos las palabras más buscadas, con un mínimo de 300 busquedas

search_terms = df['search_term'].dropna()
search_terms = search_terms.apply(lambda x: x.lower())
search_terms = search_terms.value_counts()
search_terms = search_terms[search_terms >= 300]

# Para que funcione correctamente el módulo de wordcloud, hay que juntar todas las palabras en el mismo texto.
text = ''
for w,q in zip(search_terms.index,search_terms):
    text += ' '.join([w for x in range(q)])

text = ' '.join([s for s in text.split() if len(s)>2])    

wordcloud = WordCloud(width=2000, height=800, margin=0,collocations=False).generate(text)
 
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.margins(x=0, y=0)
plt.title('Keywords más buscadas')
plt.show()

wordcloud.to_image().save('informe/figures/07-search_terms-wordcloud.png')

Se puede ver que los términos más buscados estan relacionados a las marcas de Motorola, Apple y Samsung.

¿Qué productos buscan usando la interfaz?

In [38]:
# Sabemos que el evento searched products refiere a varios SKUs. Spearemos y busquemos especificamente cuales son los más buscados.
searched = df[df['event']=='searched products']
skus_codiciados = searched['skus'].str.split(',').to_frame()
skus_codiciados = skus_codiciados['skus'].apply(pd.Series).unstack().reset_index()
skus_codiciados = skus_codiciados[['level_1',0]]
skus_codiciados.rename(columns={'level_1':'id',0:'sku'}, inplace=True)
skus_codiciados.sample(5)
Out[38]:
id sku
298162 319335 NaN
652049 641109 10954
121969 178506 6357
551338 847361 5015
664999 875015 NaN
In [39]:
top_5 = skus_codiciados['sku'].value_counts().head(5)
sku_names = df[df['sku'].isin(top_5.index)][['sku','sku_name']].drop_duplicates()

visu = top_5.plot(kind='bar', title ='5 productos más buscados por interfaz')
visu.set_title('5 productos más buscados por interfaz')
visu.set_xlabel('SKU')
visu.set_ylabel('Busquedas')

display(sku_names)
plt.savefig('informe/figures/08-skus_buscados-barplot.png')
sku sku_name
388 6357.0 Samsung Galaxy J5 16GB Preto (Bom)
577 3371.0 Samsung Galaxy S6 Flat 32GB Dourado (Bom)
1123 6371.0 Samsung Galaxy J5 16GB Dourado (Bom)
2997 6413.0 Samsung Galaxy J7 16GB Dourado (Bom)
5328 2777.0 Samsung Galaxy S4 i9505 16GB Preto (Bom)

Análisis de modelos

Queremos encontrar algun patrón de visita y compra desde el punto de vista del dispositivo y luego ver si en esos patrones hay diferencias según marca o modelo

¿Qué eventos tenemos asociados a un modelo?

In [40]:
cronologia = df.loc[df['model'].notnull(), ['model', 'event','condition']]
cronologia['event'].value_counts()
Out[40]:
viewed product       528931
checkout              33733
conversion              900
lead                    448
visited site              0
staticpage                0
searched products         0
search engine hit         0
generic listing           0
brand listing             0
ad campaign hit           0
Name: event, dtype: int64

Tomamos como cronología de eventos sobre un modelo:

  1. Visitarlo (viewed product)
  2. Mandar a comprarlo (checkout)
  3. Efectivamente comprarlo (conversion)

¿Es el modelo más comprado el más buscado? ¿Es el modelo mas buscado el más llevado al carrito?

In [41]:
# Se descartan los eventos 'lead' porque no influyen en el análisis
cronologia = cronologia.loc[cronologia['event'] != 'lead'] 

# Comparemos los modelos por los cuales más se opera segun el evento
vistos = cronologia.loc[cronologia['event']=='viewed product']['model'].value_counts().head()
checkouts = cronologia.loc[cronologia['event']=='checkout']['model'].value_counts().head()
comprados = cronologia.loc[cronologia['event']=='conversion']['model'].value_counts().head()

print("Más vistas") ; display(vistos)
print("Más checkouts") ; display(checkouts)
print("Más compras") ; display(comprados)
Más vistas
iPhone 6                  47538
iPhone 5s                 42045
iPhone 6S                 36930
iPhone 7                  26330
Samsung Galaxy S7 Edge    23062
Name: model, dtype: int64
Más checkouts
iPhone 6             3295
iPhone 5s            2744
iPhone 6S            2308
Samsung Galaxy J5    1918
Samsung Galaxy S7    1234
Name: model, dtype: int64
Más compras
Samsung Galaxy J5        69
iPhone 5s                59
iPhone 6                 52
iPhone 6S                26
Motorola Moto G4 Plus    25
Name: model, dtype: int64
In [42]:
# Definimos un set de X modelos como los más prominentes, haciendo una combinación de los 7 más vistos, los 5 más por comprar  y los 5 más comprados
modelos_prominentes = set(vistos.index)
modelos_prominentes.update(checkouts.index)
modelos_prominentes.update(comprados.index)

eventos = cronologia.groupby('model')['event'].value_counts().unstack('event') 
eventos = eventos[['viewed product', 'checkout', 'conversion']]
eventos = eventos.loc[eventos.index.isin(modelos_prominentes)]
eventos
Out[42]:
event viewed product checkout conversion
model
Motorola Moto G4 Plus 7522.0 508.0 25.0
Samsung Galaxy J5 11036.0 1918.0 69.0
Samsung Galaxy S7 18085.0 1234.0 14.0
Samsung Galaxy S7 Edge 23062.0 843.0 12.0
iPhone 5s 42045.0 2744.0 59.0
iPhone 6 47538.0 3295.0 52.0
iPhone 6S 36930.0 2308.0 26.0
iPhone 7 26330.0 1205.0 13.0
In [81]:
# Paso a porcentajes las columnas
eventos['total'] = eventos.sum(axis=1)
for c in eventos:
    eventos[c+'%'] = ( eventos[c] / eventos['total'] ) * 100

vistos = eventos['viewed product%']
checkouts = eventos['checkout%']
comprados = eventos['conversion%']

plt.bar(vistos.index, vistos, color=sns.xkcd_rgb["muted blue"],log=True)
plt.bar(checkouts.index, checkouts, bottom=vistos, color=sns.xkcd_rgb["muted green"],log=True)
plt.bar(comprados.index, comprados, bottom=vistos+checkouts, color=sns.xkcd_rgb["muted pink"],log=True)
plt.legend(['viewed product','checkout', 'conversion'])
plt.ylabel('Cantidad de eventos')
plt.xlabel('Modelos de celular')
plt.xticks(rotation=45)
plt.title('Cantidad de eventos en función de modelo (escala logarítmica)')
#plt.savefig('informe/figures/090-modelos_eventos-stackedbarplot.png')
In [44]:
for c in eventos[['viewed product','checkout','conversion']]:
    eventos[c+' ranking'] = eventos[c].rank('index', ascending=False)

rankings = (eventos.filter(regex='ranking')).T
#display(eventos[['viewed product','viewed product ranking','checkout','checkout ranking','conversion','conversion ranking']])
orden = (eventos.sort_values('viewed product ranking', ascending=True)).index
orden = [(str(i+1) + ' - ' + x) for i,x in enumerate(orden)]

orden2 = (eventos.sort_values('conversion ranking')).index
orden2 = [(str(i+1) + ' - ' + x ) for i,x in enumerate(orden2)]

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_ylim(8)
ax.plot(rankings)
ax.set_yticklabels(orden)

ax2 = ax.twinx()
ax2.set_ylim(8)
ax2.set_yticklabels(orden2)

plt.title('Rankings de modelos para distintos eventos')
#plt.savefig('informe/figures/091-rankings_celulares-rank.png')

Es interesante ver como los celulares Samsung tienden a tener el comportamiento inverso al de los celulares Apple. Los Samsung son menos buscados pero más comprados mientras que los Apple son muy buscados pero poco comprados (¿será por 'espantarse' al ver el precio?).

¿Varían los eventos del modelo según su condición? ¿Están los usuarios dispuestos a comprar mejores modelos a menor condición?

Buscamos ahora alguna diferencia en compras de celulares re-acondicionados segun la condición de estos.

In [76]:
condiciones_analizadas = ['Bom', 'Muito Bom', 'Excelente']

condicion = cronologia.groupby('model')['condition'].value_counts().unstack('condition')
condicion = condicion[condiciones_analizadas]
condicion = condicion.loc[condicion.index.isin(modelos_prominentes)]

condicion['total'] = condicion.sum(axis=1)
for c in condicion:
    condicion[c] = ( condicion[c] / condicion['total'] ) * 100

bom = condicion['Bom']
muito_bom = condicion['Muito Bom']
excelente = condicion['Excelente']

plt.bar(bom.index, bom, color=sns.xkcd_rgb["muted blue"],log=True)
plt.bar(muito_bom.index, muito_bom, bottom=bom, color=sns.xkcd_rgb["muted green"],log=True)
plt.bar(excelente.index, excelente, bottom=bom+muito_bom, color=sns.xkcd_rgb["muted pink"],log=True)
plt.legend(condiciones_analizadas)
plt.ylabel('Cantidad')
plt.xlabel('Modelo')
plt.xticks(rotation=45)
plt.title('Cantidad de vistas y compras segun condición de modelo')
ax.set_yticklabels(orden)


#plt.savefig('informe/figures/10-condicion-stackedbarplot.png')

Vemos como nuevamente hay una diferencia substancial entre celulares marca Apple y celulares marca Samsung. Para los Samsungs, los usuarios estan más dispuestos a ver o comprar celulares en peor condición, mientras que para los Apple lo que más se buscan son celulares en mejor condición.

¿Como varían los colores de los modelos más prominentes?

In [46]:
sns_colors = {'Preto':'black', 'Dourado':'gold', 'Branco':'white', 'Prata':'silver', 'Rosa':'pink', 'Azul':'blue',
       'Black Piano':'midnightblue', 'Olympic Edition':'dodgerblue', 'Cinza espacial':'grey', 'Prateado':'darkgray',
       'Ouro Rosa':'hotpink', 'Preto Matte':'darkslategray', 'Preto Brilhante':'k', 'Vermelho':'red', 'Branco Vermelho':'salmon', 'Bambu':'maroon', 'Preto Vermelho':'crimson'} 

data = df.loc[df['model'].isin(modelos_prominentes)][['model','color']].groupby('model')['color'].value_counts().to_frame()
data = data.rename(columns={'color':'count'})
data = data.reset_index()
visu = plt.scatter(data['model'], data['count'], s=3000, alpha=0.5,c=data['color'].apply(lambda x:sns_colors[x]))
visu.axes.set_title("Visitas a modelos prominentes segun color")
visu.axes.set_xlabel("Modelo")
visu.axes.set_ylabel("Visitas")

plt.savefig('informe/figures/11-colores-bubbleplot.png')

Análisis de páginas estáticas

In [47]:
df['staticpage'].value_counts()
Out[47]:
CustomerService                      1528
AboutUs                               443
FaqEcommerce                          347
Quiosks                               326
trust-trocafone                       243
galaxy-s8                             167
TermsAndConditionsReturnEcommerce     156
how-to-sell                            96
Conditions                             89
how-to-buy                             86
TermsAndConditionsEcommerce            59
club-trocafone                         46
PrivacyEcommerce                        9
black_friday                            3
Name: staticpage, dtype: int64

Podemos ver que la cantidad de visitas a Customer Service es mucho mayor a la página de FAQ. Para optimizar recursos sería más eficiente intentar de redireccionar el tráfico a FAQ, haciendo más visible los links a la página y de ser necesario mejorándola.

In [48]:
faq_and_service = df[(df['staticpage'] == 'CustomerService') | (df['staticpage'] == 'FaqEcommerce')]['staticpage']
visu = sns.countplot(faq_and_service)
visu.set(xlabel='Page', ylabel='Visits')
visu.axes.set_title('Cantidad de eventos según staticpage')

plt.savefig('informe/figures/12-static_pages-barplot.png')

Análisis de New vs Returning

La intención de este análisis es mostrar que cantidad de usuarios solo ingresan una vez al site. El primer gráfico no nos aporta información, porque todos los usuarios que volvieron a ingresar, en su primera vez fueron New.

In [49]:
order = df['new_vs_returning'].value_counts().index
visu = sns.countplot(df['new_vs_returning'].dropna(), order=order)
visu.set(xlabel='Tipo de usario', ylabel='Visitas al home')
visu.axes.set_title('Cantidad de eventos según marca')

plt.savefig('informe/figures/130-eventos_new_returning-barplot.png')

En este gráfico sí se puede apreciar los valores reales de usuarios que regresan con los que sólo entraron una vez. Para aumentar la tasa de personas que regresan a ĺa página proponemos aumentar el presupuesto en publicidad y mejorar la experiencia de usuario de la home para que provea al usuario una experiencia más amena.

In [50]:
gb = df.groupby(['person', 'new_vs_returning']).agg({'new_vs_returning' : 'size'})
gb = gb.unstack(level='new_vs_returning')
gb = gb['new_vs_returning']
returners_count = gb['Returning'].count()
only_once_count = gb['New'].count() - returners_count

objects = ('Regresan', 'Ingresan sólo una vez')
y_pos = np.arange(len(objects))
type_users_count = pd.Series([returners_count, only_once_count])
 
plt.bar(y_pos, type_users_count, align='center', alpha=0.5)
plt.xticks(y_pos, objects)
plt.ylabel('Cantidad')
plt.title('Tipos de usuarios')
 
plt.savefig('informe/figures/131-tipos_usuarios-barplot.png')
plt.show()

Análisis de Brands

In [51]:
order = df['brand'].value_counts().index
visu = sns.countplot(df['brand'].dropna(), order=order)
visu.axes.set_title('Cantidad de eventos segun marca')
visu.axes.set_xlabel('Cantidad')
visu.axes.set_ylabel('Marca')
sns.set(font_scale=2)

plt.savefig('informe/figures/141-eventos_marca-barplot.png')
In [52]:
df_conv_by_brand = df.loc[(df['event'] == 'conversion') | (df['event'] == 'checkout')][['event', 'brand']]
# Es necesario para que no me muestre las otras categorías que ya fueron filtradas
df_conv_by_brand['event'] = df_conv_by_brand['event'].astype('object').astype('category')
ax = sns.countplot(x='brand', hue='event', data=df_conv_by_brand)
ax.set_yscale('log')

ax.legend(loc='upper left',bbox_to_anchor=(0, 1.1))
ax.set_xlabel('Cantidad (log)');
ax.set_ylabel('Marca');
ax.set_title('Relación de conversiones y checkouts en escala logarítmica');
sns.set(font_scale=3)

plt.savefig('informe/figures/142-conversiones_checkouts_marca-barplot.png')

Análisis de tipos de dispositivos

Cómo la mayoría del tráfico proviene de smartphones y computadoras, debería dedicarse una mayor cantidad de recursos a desarrollar la aplicación para estos dispositivos y no dedicar mucho tiempo y desarrolladores a las aplicaciones para tablets.

In [53]:
order = df['device_type'].value_counts().index
visu = sns.countplot(df['device_type'].dropna(), order=order)
visu.set(xlabel='Device Type', ylabel='Visits')
visu.axes.set_title('Cantidad de eventos segun tipo de dispositivo')

plt.savefig('informe/figures/150-eventos_tipo-barplot.png')

Otra pregunta que nos hacemos es la de si un usuario esta dispuesto a cambiar de marca o no a la hora de ver dispositivos nuevos, es decir, cuan fiel es a su sistema operativo y/o marca.

Por ejemplo, queremos ver si visitantes que entran desde un celular con iOS (sistema operativo de Apple) es mas propenso a ver celulares de Apple y ser fiel a su previa marca o si esta dispuesto a cambiar y ve otro celular.

Esto lo visualizamos con diagramas de flujo llamados diagramas Sankey (por un capitan irlandes en 1989), un ejemplo muy famoso de un diagrama Sankey es el mapa de Charles Minard de la campaña a Rusia de Napoleon.

In [54]:
data = df[['operating_system','brand','person']].groupby('person').first()
data = data.dropna()

data['operating_system'].replace({'mac':'iOS/mac', 'ios':'iOS/mac', 'ubuntu':'linux'},inplace=True)
oss = ['android','windows','iOS/mac','linux']
brands = ['samsung','motorola','iphone']

data = data[(data['brand'].isin(brands)) & (data['operating_system'].isin(oss))]
data.head()
Out[54]:
operating_system brand
person
0004b0a2 android iphone
0006a21a android samsung
000a54b2 windows motorola
00184bf9 windows iphone
0019c395 android samsung
In [55]:
display('Diagrama de flujo de fidelidad de usuarios')
sankey.sankey(data['operating_system'],data['brand'],figure_name='informe/figures/151-os_brands-sankey')
'Diagrama de flujo de fidelidad de usuarios'

Lo que nos encontramos es que los usuarios de iOS/Mac prefieren en su mayoría seguir con su misma marca, mientras que los de Android se dividen bastante uniformemente entre iPhone (y cambiar) y Samsung/Motorola (y ser fieles)

Análisis de publicidad

¿Aumentó Trocafone el presupuesto en publicidad en algún período específico?

In [56]:
publicitados = df.loc[df['campaign_source'].notnull()][['campaign_source','month_number']] 

month_counts = publicitados.groupby('month_number').count()
month_counts = month_counts['campaign_source']
visu = sns.barplot(x=month_counts.index, y=month_counts)
visu.set_title('Cantidad de visitas por mes por una publicidad')
visu.set_ylabel('Visitas')
visu.set_xlabel('Mes')
visu.set_xticklabels(['Jan', 'Feb', 'Mar', 'Apr', 'May', '(First half) Jun'])

plt.savefig('informe/figures/16-presupuesto-barplot.png')

No se halló la respuesta buscada ya que la distribución presentada de la cantidad de visitas de campaign_hit remite a la misma distribución de eventos por usuario. Mayo es el mes que notoriamente tiene una mayor cantidad de eventos generalmente pero también de campaign_hits. Por lo tanto concluimos que el gráfico es irrelevante para este tipo de análisis.

¿Cuáles son los métodos de publicidad más usados?

In [57]:
ranking = df['campaign_source'].value_counts()
ranking_visu = ranking.head(5)

visu = squarify.plot(ranking_visu, label=ranking_visu.index, alpha=.5, color=['red','cyan','yellow','grey','purple','orange','blue'])
visu.axes.set_title('5 metodos de publicidad más clikeados')

plt.savefig('informe/figures/170-publicidad_clickeada-barplot.png')

Por su importancia mundial era esperable que Google sea el método de publicidad con más alcance. Se procede a analizar los otros motores de b

In [58]:
ranking = ranking.drop('google')
ranking_visu2 = ranking.head(10)

ranking_visu2
visu = squarify.plot(ranking_visu2, label=ranking.index, alpha=.5, color=['red','cyan','yellow','grey','purple','orange','blue'])
visu.axes.set_title('10 metodos de publicidad más clikeados sin Google')

plt.savefig('informe/figures/171-publicidad_sin_google-barplot.png')

Análisis de canales de tráfico

Canales de Tráfico:

  • Paid: Usuarios que llegan mediante una campaña de marketing.

  • Direct: Usuarios que llegan directamente al sitio, sin ayuda externa (por ejemplo, escribiendo directamente la url en el explorador web, un marcador, un link de un documento sin tracking)

  • Email: Usuarios que llegan desde un link en un email.

  • Organic: Usuarios que llegan desde motores de búsqueda.

  • Referral: Usuarios que llegan al sitio desde otro sitio web.

  • Social: Usuarios que llegan desde redes sociales.

Revenue by Traffic Source

Otra importante métrica a analizar del e-commerce es la del revenue by traffic source. Con esto nos referimos a que channel de trafico atrae más consumidores que efectivamente compran.

In [59]:
channels = df[(df['channel'].notnull()) & (df['channel'] != 'Unknown')][['channel','event','person']].drop_duplicates()
channels['total persons'] = channels.groupby('channel')['person'].transform('count')

users = channels[['channel','person']]
total_persons = channels[['channel','total persons']].drop_duplicates()
conversions = df[df['event']=='conversion'][['person']]
total_conversions = users.merge(conversions, how='right').groupby('channel').agg({'person':'count'}).rename(columns={'person':'total conversions'})

channels_conversions = channels.merge(total_conversions,left_on='channel',right_index=True)
channels_conversions = channels_conversions[['channel','total persons','total conversions']].drop_duplicates()
channels_conversions['revenue'] = ( channels_conversions['total conversions'] / channels_conversions['total persons'] ) * 100
channels_conversions
Out[59]:
channel total persons total conversions revenue
1 Paid 20025 701 3.500624
45 Organic 7420 480 6.469003
87 Direct 5347 634 11.857116
118 Social 684 66 9.649123
127 Referral 4167 586 14.062875
10786 Email 52 17 32.692308
In [60]:
revenue_label = list("{:.2f}%".format(x) for x in channels_conversions['revenue'])

barra_total = sns.barplot(x='channel', y='total persons', data=channels_conversions, color=sns.xkcd_rgb["muted blue"])
barra_conversiones = sns.barplot(x='channel', y='total conversions', data=channels_conversions, color=sns.xkcd_rgb["muted green"])
barra_total.set_yscale('log')
barra_conversiones.set_yscale('log')

for i,barra in enumerate(barra_total.patches[:6]):
    height = barra.get_height()
    barra_conversiones.text(barra.get_x() + barra.get_width()/2., height + 10, revenue_label[i], ha='center')

barra_total.set_title('Revenue by Traffic Source (escala logarítmica)')
barra_total.set_ylabel('Cantidad de eventos (log)')
barra_total.set_xlabel('Canal de tráfico')
barra_total.legend(['conversions','total'])

plt.savefig('informe/figures/180-revenue_traffic-boxplot.png')

Como se puede ver si bien el canal pago es el que más usuarios atrae, son otros canales con menos usuarios, como las redes sociales, donde más ganancia hay, y por ende donde mayor foco e inversión hay que hacer.

Funnels de usuarios

Teniendo en cuenta si una sesión comienza por advertising o no, se genera un funnel que muestre la proporción de conversiones entre cada paso hasta una conversion, y la proporción de los eventos que provienen por publicidad.

In [61]:
dfunnel = df[\
    (df['event'] == 'generic listing') | \
    (df['event'] == 'brand listing') | \
    (df['event'] == 'searched products') | \
    (df['event'] == 'viewed product') | \
    (df['event'] == 'checkout') | \
    (df['event'] == 'conversion') \
]

# color for each segment
colors = ['rgb(63,92,128)', 'rgb(90,131,182)', 'rgb(255,255,255)', 'rgb(127,127,127)', 'rgb(84,73,75)']
colors = ['rgb(63,92,128)', 'rgb(84,73,75)']
In [62]:
dfunnel = dfunnel.groupby(['event', 'session_ad']).size().unstack(level='session_ad')
dfunnel.head()
Out[62]:
session_ad False True
event
brand listing 84620 14015
checkout 27278 6457
conversion 796 104
generic listing 58727 8807
searched products 48060 8013
In [63]:
# Se cambió de nombre a las columnas, el orden de las filas y se pasó los valores a escala logarítmica

dfunnel.columns = ['Other', 'Ad']
dfunnel = dfunnel.reindex([\
                 'viewed product', 'checkout', 'conversion'])
dfunnel = np.log(dfunnel)
In [64]:
dfunnel
Out[64]:
Other Ad
event
viewed product 13.009192 11.319729
checkout 10.213836 8.772920
conversion 6.679599 4.644391
In [65]:
total = [sum(row[1]) for row in dfunnel.iterrows()]
In [66]:
n_phase, n_seg = dfunnel.shape
In [67]:
plot_width = 500
unit_width = plot_width / total[0]
 
phase_w = [int(value * unit_width) for value in total]
 
# height of a section and difference between sections 
section_h = 100
section_d = 10

# shapes of the plot
shapes = []
 
# plot traces data
data = []
 
# height of the phase labels
label_y = []
In [68]:
height = section_h * n_phase + section_d * (n_phase-1)

# rows of the DataFrame
df_rows = list(dfunnel.iterrows())

# iteration over all the phases
for i in range(n_phase):
    # phase name
    row_name = dfunnel.index[i]
    
    # width of each segment (smaller rectangles) will be calculated
    # according to their contribution in the total users of phase
    seg_unit_width = phase_w[i] / total[i]
    seg_w = [int(df_rows[i][1][j] * seg_unit_width) for j in range(n_seg)]
    
    # starting point of segment (the rectangle shape) on the X-axis
    xl = -1 * (phase_w[i] / 2)
    
    # iteration over all the segments
    for j in range(n_seg):
        # name of the segment
        seg_name = dfunnel.columns[j]
        
        # corner points of a segment used in the SVG path
        points = [xl, height, xl + seg_w[j], height, xl + seg_w[j], height - section_h, xl, height - section_h]
        path = 'M {0} {1} L {2} {3} L {4} {5} L {6} {7} Z'.format(*points)
        
        shape = {
                'type': 'path',
                'path': path,
                'fillcolor': colors[j],
                'line': {
                    'width': 1,
                    'color': colors[j]
                }
        }
        shapes.append(shape)
        
        # to support hover on shapes
        hover_trace = go.Scatter(
            x=[xl + (seg_w[j] / 2)],
            y=[height - (section_h / 2)],
            mode='markers',
            marker=dict(
                size=min(seg_w[j]/2, (section_h / 2)),
                color='rgba(255,255,255,1)'
            ),
            text="Segment : %s" % (seg_name),
            name="Value : %d" % (dfunnel[seg_name][row_name])
        )
        data.append(hover_trace)
        
        xl = xl + seg_w[j]

    label_y.append(height - (section_h / 2))

    height = height - (section_h + section_d)
In [69]:
# For phase names
label_trace = go.Scatter(
    x=[-350]*n_phase,
    y=label_y,
    mode='text',
    text=dfunnel.index.tolist(),
    textfont=dict(
        color='rgb(200,200,200)',
        size=9
    )
)

data.append(label_trace)
 
# For phase values (total)
value_trace = go.Scatter(
    x=[350]*n_phase,
    y=label_y,
    mode='text',
    text=total,
    textfont=dict(
        color='rgb(200,200,200)',
        size=9
    )
)
In [83]:
layout = go.Layout(
    title="<b>Funnel (log) [Otro:Azul | Ad:Marrón]</b>",
    titlefont=dict(
        size=20,
        color='rgb(230,230,230)'
    ),
    hovermode='closest',
    shapes=shapes,
    showlegend=False,
    paper_bgcolor='rgba(44,58,71,1)',
    plot_bgcolor='rgba(44,58,71,1)',
    xaxis=dict(
        showticklabels=False,
        zeroline=False,
    ),
    yaxis=dict(
        showticklabels=False,
        zeroline=False
    )
)

fig = go.Figure(data=data, layout=layout)
py.iplot(fig)
#plotly.io.write_image(fig,'informe/figures/200-advertisement-funnel.png')
Out[83]: