No contexto da DP6 é comum lidarmos com uma base de dados para atribuição que apresentas jornadas no formato ‘A > B > C’, em que as jornadas são representadas por uma string em que os pontos de contato de um usuário são representados por substrings separados por ‘ > ‘, na ordem em que aconteceram, da esquerda para a direita.
Análisar essas funções pode se tornar tedioso e trabalhoso, uma vez que para extrair informações relevantes das mesmas é necessário criar uma combinação de funções muitas vezes repetitivas, baseadas em splits, contagens de elementos, combinações, etc.
Nesse cenário, a Jatoolbox (Journey Analisys Toolbox) é uma classe implementada em Python, que deve ser encarada como um conjunto de ferramentas úteis para análise de jornadas. Nos métodos da classe o usuário irá encontrar funções prontas e frequentemente úteis para extrair informações das jornadas. Na sessão a seguir serão apresentados alguns exemplos práticos.
As seguintes linhas de código irão instalar e importar a Jatoolbox
#instalação
!pip install Jatoolbox
Collecting Jatoolbox
Downloading JATOOLBOX-0.0.2-py3-none-any.whl (6.1 kB)
Installing collected packages: Jatoolbox
Successfully installed Jatoolbox-0.0.2
#import
from jatoolbox import jatoolbox as jt
Com a linha abaixo, o usuário irá instanciar um objeto da classe Jatoolbox, e a partir dele terá acesso a todos os métodos (funções) da classe:
#instanciando um objeto analisador
an = jt.JAToolbox()
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
base = 'https://raw.githubusercontent.com/DP6/customer-journey-analysis-toolbox/main/base_demonstracao.csv?token=ATC3AU2LXPVSV6B5F6G3GU3BRF35G'
df = pd.read_csv(base, index_col=0)
df.reset_index(inplace = True)
df.head()
id_jornada | id_usuario | horario_ultima_sessao | jornada | tempo_para_conversao | transacoes | receita | has_transaction | |
---|---|---|---|---|---|---|---|---|
0 | 00576442052177483560_0 | 0057644205217748356 | 2017-07-19T00:09:34Z | Display | 0 | 0 | 0.0 | 0 |
1 | 00666113575347467730_0 | 0066611357534746773 | 2017-07-19T17:37:50Z | Direct > Organic Search | 240 > 0 | 0 | 0.0 | 0 |
2 | 03252031254674218300_0 | 0325203125467421830 | 2017-08-01T02:19:08Z | Direct > Organic Search > Paid Search | 346 > 200 > 0 | 0 | 0.0 | 0 |
3 | 03383384021862613320_0 | 0338338402186261332 | 2017-07-21T12:03:32Z | Direct > Direct > Direct | 140 > 16 > 0 | 0 | 0.0 | 0 |
4 | 0720215518020975470_0 | 072021551802097547 | 2017-07-28T04:53:24Z | Display > Organic Search > Referral > Referral... | 245 > 215 > 155 > 151 > 143 > 132 > 3 > 0 | 0 | 0.0 | 0 |
intro_df = df.copy()
intro_df['first_touch_point'] = intro_df['jornada'].apply(lambda x: an.get_first_tp(x))
intro_df = intro_df.groupby('first_touch_point').size().reset_index()
intro_df.columns = ['first_touch_point', 'journeys']
intro_df = intro_df.sort_values(by='journeys', ascending=False)
intro_df
first_touch_point | journeys | |
---|---|---|
3 | Organic Search | 27156 |
1 | Direct | 9340 |
6 | Social | 6997 |
5 | Referral | 5128 |
4 | Paid Search | 1524 |
0 | Affiliates | 1325 |
2 | Display | 325 |
conv_df = df.copy()
conv_df = conv_df[conv_df['transacoes'] != 0]
conv_df['last_touch_point'] = conv_df['jornada'].apply(lambda x: an.get_last_tp(x))
conv_df = conv_df.groupby('last_touch_point').size().reset_index()
conv_df.columns = ['last_touch_point', 'journeys']
conv_df = conv_df.sort_values(by='journeys', ascending=False)
/usr/local/lib/python3.7/dist-packages/ipykernel_launcher.py:3: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
This is separate from the ipykernel package so we can avoid doing imports until
conv_df
last_touch_point | journeys | |
---|---|---|
5 | Referral | 384 |
3 | Organic Search | 243 |
1 | Direct | 108 |
4 | Paid Search | 61 |
2 | Display | 27 |
0 | Affiliates | 4 |
6 | Social | 3 |
dist_df = df.copy()
dist_df = dist_df['jornada'].apply(lambda x: an.get_size(x))
print("Menor jornada:",dist_df.min())
print("Maior jornada:", dist_df.max())
sns.histplot(data=dist_df, discrete=True)
plt.show()
Menor jornada: 1
Maior jornada: 46
A jatoolbox possui uma função ‘get_transitions’ que retorna quais as transações ocorreram em uma dada jornada
j_example = 'A > A > A > B > A > C > B > C'
an.get_transitions(j_example,count=True)
(A, A) 2
(B, A) 1
(A, C) 1
(C, B) 1
(B, C) 1
(A, B) 1
dtype: int64
Então podemos utiliza-la para descobrir quais as principais transições aconteceram na nossa base:
transitions=df['jornada'].apply(an.get_transitions,count=True)
transitions_counts = transitions.sum()
transitions_counts.sort_values(ascending=False)
(Organic Search, Organic Search) 2736.0
(Referral, Referral) 1933.0
(Direct, Direct) 1850.0
(Paid Search, Paid Search) 540.0
(Social, Social) 539.0
(Organic Search, Referral) 340.0
(Affiliates, Affiliates) 322.0
(Paid Search, Organic Search) 271.0
(Direct, Organic Search) 227.0
(Referral, Organic Search) 219.0
(Display, Display) 192.0
(Direct, Referral) 179.0
(Organic Search, Paid Search) 166.0
(Organic Search, Display) 119.0
(Organic Search, Affiliates) 111.0
(Organic Search, Social) 65.0
(Affiliates, Organic Search) 54.0
(Affiliates, Referral) 54.0
(Social, Organic Search) 52.0
(Display, Organic Search) 42.0
(Social, Referral) 29.0
(Display, Referral) 29.0
(Direct, Display) 28.0
(Referral, Display) 27.0
(Paid Search, Display) 25.0
(Direct, Social) 25.0
(Direct, Paid Search) 24.0
(Direct, Affiliates) 23.0
(Referral, Social) 22.0
(Paid Search, Referral) 15.0
(Display, Paid Search) 11.0
(Referral, Paid Search) 9.0
(Social, Display) 9.0
(Social, Paid Search) 8.0
(Referral, Affiliates) 8.0
(Affiliates, Paid Search) 4.0
(Paid Search, Affiliates) 4.0
(Social, Affiliates) 3.0
(Paid Search, Social) 3.0
(Affiliates, Social) 3.0
(Display, Affiliates) 1.0
(Organic Search, Direct) 1.0
(Display, Social) 1.0
(Direct, (Other)) 1.0
(Affiliates, Display) 1.0
dtype: float64
Podemos ver que três transações se destacam: Organic Search para si mesmo, Referral para si mesmo, e Direct para si mesmo. A transição entre canais distintos que mais ocorreu foi de Organic Search para Referral
Foi visto que o canal que mais finaliza jornadas em conversões foi Referral. Então podemos utilizar o resultado obtido no item anterior para obter qual canal mais transiciona para Referral
trans_df = pd.DataFrame({'from':[x[0] for x in transitions_counts.index],
'to':[x[1] for x in transitions_counts.index],
'counts':transitions_counts.values})
trans_df.loc[trans_df['to']=='Referral'].sort_values(by='counts',ascending=False)
from | to | counts | |
---|---|---|---|
37 | Referral | Referral | 1933.0 |
25 | Organic Search | Referral | 340.0 |
12 | Direct | Referral | 179.0 |
4 | Affiliates | Referral | 54.0 |
18 | Display | Referral | 29.0 |
43 | Social | Referral | 29.0 |
31 | Paid Search | Referral | 15.0 |
Podemos ver que os canais que mais transicionaram para Referral foram o próprio Referral, seguido por Organic Search e Direct.
def tempo_do_canal(lista):
for itens in range(len(lista)):
if itens != len(lista)-1:
lista[itens] = lista[itens] - lista[itens+1]
return lista
df_tempo_dos_canais = df.copy()
df_tempo_dos_canais['tempo'] = df_tempo_dos_canais['tempo_para_conversao'].apply(lambda x: x.split(" > "))
df_tempo_dos_canais['tamanho'] = df_tempo_dos_canais['tempo'].apply(lambda x: len(x))
df_tempo_dos_canais['tempo'] = df_tempo_dos_canais['tempo'].apply(lambda x: [int(item) for item in x])
df_tempo_dos_canais['duracao'] = df_tempo_dos_canais['tempo'].apply(lambda x: an.get_duration(x,(-1,0)))
df_tempo_dos_canais['duracao_canal'] = df_tempo_dos_canais['tempo'].apply(lambda x: tempo_do_canal(x))
df_tempo_dos_canais['lista_canais'] = df_tempo_dos_canais['jornada'].apply(lambda x: x.split(" > "))
df_tempo_dos_canais = df_tempo_dos_canais[df_tempo_dos_canais['duracao']!=0]
df_aux = df_tempo_dos_canais[['lista_canais','tempo']].apply(pd.Series.explode)
df_aux = df_aux[df_aux['tempo']!=0]
print(df_tempo_dos_canais['duracao'].describe())
sns.boxplot(x = 'duracao', data = df_tempo_dos_canais)
count 3097.000000
mean 223.189538
std 218.322967
min 1.000000
25% 27.000000
50% 147.000000
75% 389.000000
max 719.000000
Name: duracao, dtype: float64
<matplotlib.axes._subplots.AxesSubplot at 0x7f4eeac63e50>
Pelo boxplot podemos ver que as jornadas duram em torno de 2h
sns.scatterplot(x = 'duracao', y = 'tamanho' , data = df_tempo_dos_canais, hue = 'has_transaction')
<matplotlib.axes._subplots.AxesSubplot at 0x7f4ee94644d0>
Além disso, não há relação entre tamanho da jornada, duração e conversão
sns.boxplot(data = df_aux , y = 'tempo', x ='lista_canais')
lista_canais tempo
count 9034 9034
unique 7 686
top Organic Search 1
freq 3165 620
<matplotlib.axes._subplots.AxesSubplot at 0x7f4eeae9e710>
O canal “Affiliates” é o canal onde os usuários demoram menos tempo, em geral, até sua próxima ação.
df_agrupado = df[['jornada','has_transaction','transacoes']].copy()
df_agrupado = df_agrupado[['jornada','has_transaction']].groupby(['jornada','has_transaction']).size().reset_index(name='ocurrencies').merge(df_agrupado.groupby(['jornada','has_transaction']).sum().reset_index(),on=['jornada','has_transaction'])
jornada | has_transaction | ocurrencies | transacoes | |
---|---|---|---|---|
0 | Affiliates | 0 | 1090 | 0 |
1 | Affiliates | 1 | 2 | 2 |
2 | Affiliates > Affiliates | 0 | 104 | 0 |
3 | Affiliates > Affiliates | 1 | 1 | 1 |
4 | Affiliates > Affiliates > Affiliates | 0 | 27 | 0 |
qtd_canais = an.tps_by_channel(df = df_agrupado, j = 'jornada', ocurrencies = 'transacoes').merge(an.tps_by_channel(df = df_agrupado, j = 'jornada', ocurrencies = 'ocurrencies'), on='Channel')
qtd_canais.columns = ['Channel','transacoes','ocurrencies']
qtd_canais
Channel | transacoes | ocurrencies | |
---|---|---|---|
0 | Affiliates | 7 | 1797 |
1 | Organic Search | 522 | 30757 |
2 | Direct | 247 | 11191 |
3 | Referral | 841 | 7707 |
4 | Display | 71 | 726 |
5 | Paid Search | 129 | 2286 |
6 | Social | 6 | 7655 |
Apesar do canal Orgânico ser o quie mais aparece de forma geral, o Referral foi o que mais apareceu em jornadas conversoras.
df_heatmap = an.channels_by_tp(df_agrupado, j = 'jornada', ocurrencies = 'ocurrencies', max_journey_size=10)
fig, ax = plt.subplots(figsize=(14,10))
df_heatmap.set_index('channels',inplace=True)
sns.heatmap(data = df_heatmap, annot = True, fmt=".10g", cmap="YlGnBu")
<matplotlib.axes._subplots.AxesSubplot at 0x7f4ee8e5f550>
Nome da Função ⚙ | Entradas 🚪 | Ação 🦾 |
---|---|---|
get_size | <li> journey </li><li> separator </li> | Calculates the number of touch points in a given journey. |
get_last_tp | <li> journey </li><li> separator </li> | Returns the last touch point of the journey. |
get_first_tp | <li> journey </li><li> separator </li> | Returns the first touch point of the journey. |
get_nth_tp | <li> journey </li><li> separator </li> | Returns the n_th touch point of the journey. |
get_intermediate_tp | <li> journey </li> <li> range </li> <li> separator </li> | Returns a subjourney formed by the touch points between the points indicated in the range parameter. |
get_tps_counts | <li> journey </li> <li> norm </li> <li> separator </li> | Returns how many times each distinct touch point occurres in the journey. |
skip_tp | <li> journey </li> <li> tp_to_skip </li> <li> separator </li> | Returns a transformed version of the journey, where all occurencies of a given touch point is skipped |
skip_tp_group | <li> journey </li> <li> tps_to_skip </li> <li> separator </li> | Returns a transformed version of the journey, where all occurencies of any element from a given group of touch points is skipped |
check_tp | <li> journey </li> <li> tp_to_check </li> <li> separator </li> | Checks if there is any occurency of a indicated touch point in the journey. |
check_tp_group | <li> journey </li> <li> tp_group_to_check </li> <li> separator </li> | Checks if there is any occurency of each touch indicated in a group of touch points. |
get_tp_counts | <li> journey </li> <li> tp </li> <li> norm </li> <li> separator </li> | Returns how many occurrecies of a given touch point there is in the journey |
get_duration | <li> timestamps </li> <li> range </li> | Returns the time interval between two indicated touch points in the journey. By default the interval between the first and the last touch point. |
translate_tp | <li> journey </li> <li> translation_dict </li> <li> separator </li> | Returns a transformed version of the journey, where indicated touch points are replaced by other values according to a traslation dictionary. |
get_transitions | <li> self </li> <li> journey </li> <li> count </li> <li> norm </li> <li> separator </li> | Returns all the transitions between two touch points that occured in the journey. If the optional parameter count is set to True, the occurency counts for each transition is also returned. |
channels_by_tp | <li> dataframe </li> <li> journey </li> <li> ocurrencies </li> <li> max_journey_size </li> <li> separator </li> | How many times did each channel appear at each stage of the journeys. |
tps_by_channel | <li> self </li> <li> dataframe </li> <li> journey </li> <li> ocurrencies </li> <li> separator </li> | How many times each channel appeared on the journeys. |