coorte.png


Empresas que enfrentam incertezas do mercado ou mudanças frequentes em seus produtos, serviços e processos precisar de métricas específicas que avaliem os diferentes momentos do ciclo de vida. E uma destas ferramentas é a Análise de Coorte (ou Análise Cohort).

Presente em muitas plataformas como o Google Analytics, o princípio por trás da métrica é de que clientes de períodos diferentes vão ter experiências diferentes, por mais sutis que aparentam ser.

Por definição, um coorte é um grupo de indivíduos que compartilham a mesma característica, e a métrica permite avaliar o comportamento do coorte durante o tempo e compará-lo com outros coortes. Em uma de suas aplicações no setor de marketing/comercial, a análise divide os clientes de acordo com o período de aquisição do produto ou serviço e o seu ciclo de vida em um intervalo definido.

Assim, é possível analisar a retenção dos clientes durante o ciclo de vida do produto, e mais importante: durante o ciclo de vida do cliente, agrupado de acordo com o primeiro contato. Tudo isso de forma visual e intuitiva.

Desta forma, as métricas ficam bastante granulares e permitem avaliar com precisão o impacto das alterações, investimentos e campanhas, por exemplo.

Além da análise temporal, os coortes permitem dividir em outros grupos como:

  • Produto ou plano adquirido;
  • Quantidade ou valor gasto;

E quaisquer agrupamentos que possam representar diferentes comportamentos ou necessidades dos clientes.


Roteiro de Análise Coorte - Tratamento e visualização de dados

Para este exemplo será utilizado um conjunto de dados do repositório da UCI.

Os dados estão em uma planilha do excel (.xlsx) e representam vendas de uma loja on-line no Reino Unido. As colunas presentes no dataset são:

  • Número do pedido;
  • Código do produto;
  • Descrição do produto;
  • Quantidade;
  • Data do pedido;
  • Preço unitário;
  • ID do cliente;
  • País.

Contudo, para elaboração da análise de coorte só serão utilizadas as 3 colunas em destaque.

Pacotes e bibliotecas Python utilizadas

In [1]:
import numpy as np
import pandas as pd
import datetime as dt
import matplotlib.pyplot as plt
import seaborn as sns

Leitura dos dados

In [52]:
df = pd.read_excel('Online Retail.xlsx')
In [53]:
df.head(5)
Out[53]:
InvoiceNo StockCode Description Quantity InvoiceDate UnitPrice CustomerID Country
0 536365 85123A WHITE HANGING HEART T-LIGHT HOLDER 6 2010-12-01 08:26:00 2.55 17850.0 United Kingdom
1 536365 71053 WHITE METAL LANTERN 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom
2 536365 84406B CREAM CUPID HEARTS COAT HANGER 8 2010-12-01 08:26:00 2.75 17850.0 United Kingdom
3 536365 84029G KNITTED UNION FLAG HOT WATER BOTTLE 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom
4 536365 84029E RED WOOLLY HOTTIE WHITE HEART. 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom

Tratamento dos dados

Contagem de linhas e identificação do tipo

In [54]:
df.columns = ['PedidoNum', 'ProdutoCod', 'ProdutoDesc', 'Qtd', 'PedidoData', 'PrecoUnit', 'ClienteID', 'Pais']
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541909 entries, 0 to 541908
Data columns (total 8 columns):
 #   Column       Non-Null Count   Dtype         
---  ------       --------------   -----         
 0   PedidoNum    541909 non-null  object        
 1   ProdutoCod   541909 non-null  object        
 2   ProdutoDesc  540455 non-null  object        
 3   Qtd          541909 non-null  int64         
 4   PedidoData   541909 non-null  datetime64[ns]
 5   PrecoUnit    541909 non-null  float64       
 6   ClienteID    406829 non-null  float64       
 7   Pais         541909 non-null  object        
dtypes: datetime64[ns](1), float64(2), int64(1), object(4)
memory usage: 33.1+ MB

Limpeza dos dados

Na descrição do conjunto de dados é informado que os pedidos que se iniciam com C representam cancelamentos.

Portanto, estes registros serão eliminados do conjunto.

In [55]:
df= df[df['PedidoNum'].str[0] != 'C']

Contabilizando linhas com valores não preenchidos

In [56]:
df.isnull().sum()
Out[56]:
PedidoNum           0
ProdutoCod          0
ProdutoDesc      1454
Qtd                 0
PedidoData          0
PrecoUnit           0
ClienteID      134697
Pais                0
dtype: int64

Eliminando os registros que possuem valores não preenchidos, pois distorcem as contagems e cálculos utilizados.

In [57]:
df = df[df['ClienteID'].notnull()]

Verificando se existem vendas duplicadas

In [58]:
df.duplicated().sum()
Out[58]:
5192

Eliminando duplicadas

In [59]:
df = df.drop_duplicates()
In [60]:
df.duplicated().sum()
Out[60]:
0

Conferindo estatísticas para ver se existe algum valor fora do padrão

In [61]:
df.describe()
Out[61]:
Qtd PrecoUnit ClienteID
count 392732.000000 392732.000000 392732.000000
mean 13.153718 3.125596 15287.734822
std 181.588420 22.240725 1713.567773
min 1.000000 0.000000 12346.000000
25% 2.000000 1.250000 13955.000000
50% 6.000000 1.950000 15150.000000
75% 12.000000 3.750000 16791.000000
max 80995.000000 8142.750000 18287.000000

Existem preços unitários com valor 0,00. Será puxado um exemplo dos registros:

In [62]:
df[df['PrecoUnit']==0].head()
Out[62]:
PedidoNum ProdutoCod ProdutoDesc Qtd PedidoData PrecoUnit ClienteID Pais
9302 537197 22841 ROUND CAKE TIN VINTAGE GREEN 1 2010-12-05 14:02:00 0.0 12647.0 Germany
33576 539263 22580 ADVENT CALENDAR GINGHAM SACK 4 2010-12-16 14:36:00 0.0 16560.0 United Kingdom
40089 539722 22423 REGENCY CAKESTAND 3 TIER 10 2010-12-21 13:45:00 0.0 14911.0 EIRE
47068 540372 22090 PAPER BUNTING RETROSPOT 24 2011-01-06 16:41:00 0.0 13081.0 United Kingdom
47070 540372 22553 PLASTERS IN TIN SKULLS 24 2011-01-06 16:41:00 0.0 13081.0 United Kingdom

Aparentemente são registros comuns, pode ser brindes ou promoções. Serão mantidos na análise.

Venda mais antiga

In [63]:
df['PedidoData'].min()
Out[63]:
Timestamp('2010-12-01 08:26:00')

Venda mais recente

In [64]:
df['PedidoData'].max()
Out[64]:
Timestamp('2011-12-09 12:50:00')

Criando a Matriz

Como citado, serão utilzadas apenas 3 colunas:

  • Número do pedido (PedidoNum);
  • Data do pedido (PedidoData);
  • ID do cliente (ClienteID);

A partir destas serão extraídos novos dados:

  • Mês daquele pedido em específico (já existe a coluna de data, mas será extraída a coluna do mês para poder agrupar os clientes);
  • Mês do primeiro pedido daquele cliente;
  • Lifetime: intervalo de tempo da análise (mêses de diferença daquele pedido para o primeiro pedido daquele cliente).

Neste caso a divisão dos clientes é por mês e o intervalo observado também. Em outros casos pode ser que intervalos mais curtos sejam melhores como aplicativos, sites, serviços ou campanhas, os quais necessitam uma análise mais granular.


Criação das novas colunas

In [117]:
def mes_ano(x) : return dt.datetime(x.year,x.month,1)
df['PedidoMes'] = df['PedidoData'].apply(mes_ano)
df['PrimeiroMes'] = df.groupby('ClienteID')['PedidoMes'].transform('min')

def get_month_int (dframe,column):
    year = dframe[column].dt.year
    month = dframe[column].dt.month
    day = dframe[column].dt.day
    return year, month , day 

pedido_ano,pedido_mes,_ = get_month_int(df,'PedidoMes')
primeiro_ano,primeiro_mes,_ = get_month_int(df,'PrimeiroMes')

dif_ano = pedido_ano - primeiro_ano
dif_mes = pedido_mes - primeiro_mes

df['lifetime'] = dif_ano * 12 + dif_mes + 1 
df
Out[117]:
PedidoNum ProdutoCod ProdutoDesc Qtd PedidoData PrecoUnit ClienteID Pais PedidoMes PrimeiroMes lifetime
0 536365 85123A WHITE HANGING HEART T-LIGHT HOLDER 6 2010-12-01 08:26:00 2.55 17850.0 United Kingdom 2010-12-01 2010-12-01 1
1 536365 71053 WHITE METAL LANTERN 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom 2010-12-01 2010-12-01 1
2 536365 84406B CREAM CUPID HEARTS COAT HANGER 8 2010-12-01 08:26:00 2.75 17850.0 United Kingdom 2010-12-01 2010-12-01 1
3 536365 84029G KNITTED UNION FLAG HOT WATER BOTTLE 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom 2010-12-01 2010-12-01 1
4 536365 84029E RED WOOLLY HOTTIE WHITE HEART. 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom 2010-12-01 2010-12-01 1
... ... ... ... ... ... ... ... ... ... ... ...
541904 581587 22613 PACK OF 20 SPACEBOY NAPKINS 12 2011-12-09 12:50:00 0.85 12680.0 France 2011-12-01 2011-08-01 5
541905 581587 22899 CHILDREN'S APRON DOLLY GIRL 6 2011-12-09 12:50:00 2.10 12680.0 France 2011-12-01 2011-08-01 5
541906 581587 23254 CHILDRENS CUTLERY DOLLY GIRL 4 2011-12-09 12:50:00 4.15 12680.0 France 2011-12-01 2011-08-01 5
541907 581587 23255 CHILDRENS CUTLERY CIRCUS PARADE 4 2011-12-09 12:50:00 4.15 12680.0 France 2011-12-01 2011-08-01 5
541908 581587 22138 BAKING SET 9 PIECE RETROSPOT 3 2011-12-09 12:50:00 4.95 12680.0 France 2011-12-01 2011-08-01 5

392732 rows × 11 columns

Agrupamento dos clientes por primeiro mês e ciclo de vida

In [75]:
agrup = df.groupby(['PrimeiroMes', 'lifetime'])
cohort = agrup['ClienteID'].apply(pd.Series.nunique)
cohort = cohort.reset_index()
cohort
Out[75]:
PrimeiroMes lifetime ClienteID
0 2010-12-01 1 885
1 2010-12-01 2 324
2 2010-12-01 3 286
3 2010-12-01 4 340
4 2010-12-01 5 321
... ... ... ...
86 2011-10-01 2 86
87 2011-10-01 3 41
88 2011-11-01 1 324
89 2011-11-01 2 36
90 2011-12-01 1 41

91 rows × 3 columns

A partir daqui é só pivotar (transpor) a matriz e plotar em esquema heatmap para ficar mais visual.


Análise de coorte - Quantidade de clientes

In [121]:
matriz_cohort = cohort.pivot(index='PrimeiroMes',columns='lifetime',values='ClienteID')
matriz_cohort.index = matriz_cohort.index.date
plt.figure(figsize=(15, 8))
plt.style.use('default')
plt.title('Análise Cohort - Quantidade de Clientes', pad=20)
sns.heatmap(data=matriz_cohort,annot = True,fmt = '.0f',cmap="cool", linecolor='white', linewidth=1)
plt.show()

Esta matriz diz respeito à quantidade de clientes.

Uma forma mais eficaz de visualizar a retenção é aplicando a taxa: dividindo a quantidade de clientes pelo total daquele coorte.


Análise de coorte - Retenção

In [116]:
totaldeclientes = matriz_cohort.iloc[:,0]
retencao = matriz_cohort.divide(totaldeclientes,axis=0)
retencao.round(3) * 100
plt.figure(figsize=(15, 8))
plt.style.use('default')
plt.title('Análise Cohort - Retenção de clientes', pad=20)
sns.heatmap(data=retencao,annot = True,fmt = '.0%',cmap="BuPu", linecolor='white', linewidth=1, vmin=0, vmax=0.8)
plt.show()