customer-journey-analysis-toolbox

O que é a Jatoolbox ?

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.

Usando a Jatoobox em alguns exemplos práticos

Instalação e import da biblioteca

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 dos dados

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

Respondendo algumas perguntas com funções da Jatoolbox

Quais são os canais introdutores?

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

Quais são os canais mais conversores?

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

Qual é a distribuição da quatidade de pontos de contato das jornadas?

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

png

Quais são as principais transições entre canais?

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

Quais são os canais que mais transitam para o canal mais finalizador?

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.

Dado que existem canais pagos e canais orgânicos, as jornadas se iniciam mais por canais pagos ou orgânicos?

Quanto tempo dura cada jornada?

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>

png

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>

png

Além disso, não há relação entre tamanho da jornada, duração e conversão

Há canais em que os usuários demoram mais ou menos até sua próximada etapa da jornada?

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>

png

O canal “Affiliates” é o canal onde os usuários demoram menos tempo, em geral, até sua próxima ação.

Qual é o canal que mais aparece nas jornadas? E nas jornadas que terminaram em transaçã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.

Como é a distribuição de canal ao longo das etapas da jornada?

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>

png

Tabela de funções presentes na Jatoolbox (Função vs. O que ela Faz)

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.

MAM - Marketing Attribution Models

Github da DP6

Blog da DP6