A análise RFM é um modelo de clusterização de clientes que visa dividi-los em grupos para 3 características:
Última vez que o cliente fez uma determinada ação (precisa ser definido pelo negócio), geralmente uma compra ou um engajamento específico;
Quantidade de ações (compras, engajamentos, etc) realizados pelo cliente em um intevalo de tempo definido.
Quantia total gasta ou alguma quantificação de valor recebido do cliente.
A primeira questão importante a se atentar é a definição de como serão divididos estes grupos. Pode ser uma definição personalizada da empresa seguindo alguma diretriz específica do negócio ou seguir algumas regras quantitativa como a divisão 80/20 de Pareto ou a partir dos percentis.
Neste estudo será exemplificado o modo a partir dos percentis, onde os clientes são dividios em grupos de mesmo tamanho a partir da quantidade de níveis escolhida para cada variável (R, F e M).
Se o negócio definir o R, F e o M em 5 níveis para cada cliente, o processo manual para o R (recency) seria:
Note que estamos encontrado O CLIENTE que divide a distribuição no meio e não a data do meio do período, ou seja, estamos dividindo os clientes em 2 grupos de mesma quantidade. Tecnicamente, esté cliente é a mediana ou o percentil de 50%.
Perceba que esta divisão cria 5 grupos de mesmo tamanho. Portanto se entrarem 2 clientes no grupo dos que tem nota (3) em Recência, um cliente precisará passar para nota (2) ou (4) a depender dos valores.
De forma técnica, para uma distribuição de 5 níveis, valores que dividem os clientes são o percentil de 25% (segundo quartil), 50% (mediana) e 75% (terceiro quartil);
Mas ATENÇÃO!
Um ponto positivo é que a clusterização acontece de maneira relativa, sendo pouco influenciada por outliers e se auto-ajustando de acordo com as tendências gerais. Contudo, ela não fornece insumos para avaliações globais como taxas de retorno, valores gastos, entre outros.
Como assim?
Imagine que uma empresa X tenha péssimos indicadores de faturamento, quantidade de vendas e retorno. Ainda assim, a análise dividirá os clientes igualitáriamente entre os níveis, havendo o mesmo números de indivíduos no nível 1 e no 5. Ou seja, os clientes do nível 5 podem estar gastado pouco em relação ao esperado pelo negócio, porém relativamente eles estão no nível mais alto de (M).
Os níveis RFM são relativos aos demais individuos da amostra.
Como utilizar a matriz RFM
Alguns portais sugerem classificar as diferentes porções da matriz em cluster e traçar diretrizes para cada cluster de cliente de acordo com suas necessidades e expectativas.
Um exemplo de matriz é a seguinte, o crédito do design é do portal Vida de Produto.
Para este exemplo será utilizado um conjunto de dados do repositório da UCI.
Se você já viu nosso artigo sobre Análise Coorte, pode pular a etapa de leitura e tratamento dos dados, pois são as mesmas etapas já realizadas no artigo da Análise Coorte.
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
;Quantidade
;Data do pedido
;Preço unitário
;ID do cliente
;Contudo, para elaboração da análise RFM só serão utilizadas as 5 colunas em destaque.
Pacotes e bibliotecas Python utilizadas
import numpy as np
import pandas as pd
import datetime as dt
import matplotlib.pyplot as plt
import seaborn as sns
Leitura dos dados
df = pd.read_excel('Online Retail.xlsx')
df.head(5)
# Traduzindo os títulos e puxando as informações
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
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.
df= df[df['PedidoNum'].str[0] != 'C']
Contabilizando linhas com valores não preenchidos
df.isnull().sum()
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.
df = df[df['ClienteID'].notnull()]
Verificando se existem vendas duplicadas
df.duplicated().sum()
5192
Eliminando duplicadas
df = df.drop_duplicates()
df.duplicated().sum()
0
Conferindo estatísticas para ver se existe algum valor fora do padrão
df.describe()
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:
df[df['PrecoUnit']==0].head()
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
df['PedidoData'].min()
Timestamp('2010-12-01 08:26:00')
Venda mais recente
df['PedidoData'].max()
Timestamp('2011-12-09 12:50:00')
Como o conjunto veio com valor unitário e quantidade, precisamos criar uma coluna com o valor total de cada venda, depois agrupar o valor da venda por cliente (soma dos valores) e teremos o Valor Monetário para cada cliente.
# Criando coluna preço total (PreçoUnitario x Quantidade)
df['PrecoTotal'] = df['PrecoUnit']* df['Qtd']
df.head()
PedidoNum | ProdutoCod | ProdutoDesc | Qtd | PedidoData | PrecoUnit | ClienteID | Pais | PrecoTotal | |
---|---|---|---|---|---|---|---|---|---|
0 | 536365 | 85123A | WHITE HANGING HEART T-LIGHT HOLDER | 6 | 2010-12-01 08:26:00 | 2.55 | 17850.0 | United Kingdom | 15.30 |
1 | 536365 | 71053 | WHITE METAL LANTERN | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom | 20.34 |
2 | 536365 | 84406B | CREAM CUPID HEARTS COAT HANGER | 8 | 2010-12-01 08:26:00 | 2.75 | 17850.0 | United Kingdom | 22.00 |
3 | 536365 | 84029G | KNITTED UNION FLAG HOT WATER BOTTLE | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom | 20.34 |
4 | 536365 | 84029E | RED WOOLLY HOTTIE WHITE HEART. | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom | 20.34 |
Uma segunda manipulação que precisamos fazer é o referencial para a análise da Recência (R). Normalmente a recência de uma compra é calculada em relação ao dia da análise ou dia do hit, que é o momento (dia, mês, ano e hora) em que os dados são extraídos da fonte de dados (caso não seja streaming em tempo real).
Entretanto, como os dados são de compras realizadas entre 01/12/2010 e 09/12/2011, vamos definir uma variável dia_do_hit
para simular que a análise está sendo feita um dia depois da última compra.
dia_do_hit = df['PedidoData'].max() + dt.timedelta(days=1)
dia_do_hit
Timestamp('2011-12-10 12:50:00')
Pronto, as diferenças de tempo para calcular a Recência serão em relação ao dia 10/12/2011. Em situações normais podemos definir um dia_do_hit
ou utilizar o módulo today
quando as análises são em relação ao próprio dia de elaboração.
Primeiro deve ser calculado os valores dos índices R, F e M. Observe que no momento ainda não são os níveis, os níveis serão divididos a partir dos quartis destes índices.
# Agrupando por cliente e aplicando os cálculos às colunas data (diferença de dias para Recência), número do pedido (contagem para frequência) e Preço total (soma para Valor Monetário)
rfm = df.groupby(['ClienteID']).agg({'PedidoData': lambda x : (dia_do_hit - x.max()).days, 'PedidoNum':'count','PrecoTotal': 'sum'})
# Renomeando as colunas, pois elas são criadas com os nomes originais e não das métricas
rfm = rfm.rename(columns={'PedidoData':'Recência','PedidoNum':'Frequência','PrecoTotal':'ValorMonetario'})
rfm.head()
Recência | Frequência | ValorMonetario | |
---|---|---|---|
ClienteID | |||
12346.0 | 326 | 1 | 77183.60 |
12347.0 | 2 | 182 | 4310.00 |
12348.0 | 75 | 31 | 1797.24 |
12349.0 | 19 | 73 | 1757.55 |
12350.0 | 310 | 17 | 334.40 |
Observação
# Criando os níveis de R, F e M - 5 níveis
niveis_r =range(5,0,-1)
niveis_f=range(1,6)
niveis_m=range(1,6)
# Dividindo a lista de clientes em 5 quintis (dividindo nos quartis)
r_quintis = pd.qcut(rfm['Recência'], q=5, labels = niveis_r)
f_quintis = pd.qcut(rfm['Frequência'],q=5, labels = niveis_f)
m_quintis = pd.qcut(rfm['ValorMonetario'],q=5,labels = niveis_m)
rfm = rfm.assign(R=r_quintis,F=f_quintis,M=m_quintis)
# Criando tabela atribuindo níveis RFM e pontuação RFM (soma dos níveis)
def add_rfm(x) : return str(x['R']) + str(x['F']) + str(x['M'])
rfm['RFM_cluster'] = rfm.apply(add_rfm,axis=1 )
rfm['RFM_score'] = rfm[['R','F','M']].sum(axis=1)
rfm['FM_media'] = rfm[['F','M']].mean(axis=1).round()
rfm = rfm.reset_index()
rfm.head()
ClienteID | Recência | Frequência | ValorMonetario | R | F | M | RFM_cluster | RFM_score | FM_media | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 12346.0 | 326 | 1 | 77183.60 | 1 | 1 | 5 | 115 | 7.0 | 3.0 |
1 | 12347.0 | 2 | 182 | 4310.00 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
2 | 12348.0 | 75 | 31 | 1797.24 | 2 | 3 | 4 | 234 | 9.0 | 4.0 |
3 | 12349.0 | 19 | 73 | 1757.55 | 4 | 4 | 4 | 444 | 12.0 | 4.0 |
4 | 12350.0 | 310 | 17 | 334.40 | 1 | 2 | 2 | 122 | 5.0 | 2.0 |
Pronto, este dataframe será a base para todas as variações da análise RFM.
Para criação da matriz sugerida no início do estudo é necessário apenas pivotar
ou transpor
, e preenche-la com a contagem de clientes.
pivot_rfm = rfm.pivot_table(values='ClienteID', index='FM_media', columns='R', aggfunc = 'count', fill_value=0)
pivot_rfm = pivot_rfm.loc[[5.0, 4.0, 3.0, 2.0, 1.0],[1, 2, 3, 4, 5]]
pivot_rfm
R | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
FM_media | |||||
5.0 | 8 | 34 | 83 | 170 | 306 |
4.0 | 75 | 218 | 279 | 344 | 332 |
3.0 | 80 | 152 | 130 | 115 | 89 |
2.0 | 432 | 316 | 276 | 205 | 106 |
1.0 | 270 | 123 | 90 | 71 | 35 |
plt.figure(figsize=(10,7))
plot = sns.heatmap(pivot_rfm,cmap='YlOrRd',linewidth=1, linecolor="white", annot=True, fmt ='.0f')
plot.set_xlabel('Recência', fontsize=15, labelpad=15)
plot.set_ylabel('Frequência e Valor Monetário (média)', fontsize=15, labelpad=15)
plot.set_title('Matriz RFM', fontsize=25, fontweight="bold", pad=20)
Text(0.5, 1, 'Matriz RFM')
A matriz permite recuperar os clientes com qualquer filtro desejado, como por exemplo:
Consultar clientes campeões (R nível 5 e média de F e M nível 5)
#Consultar segmento '555'
rfm[rfm['RFM_cluster']=='555']
ClienteID | Recência | Frequência | ValorMonetario | R | F | M | RFM_cluster | RFM_score | FM_media | |
---|---|---|---|---|---|---|---|---|---|---|
1 | 12347.0 | 2 | 182 | 4310.00 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
15 | 12362.0 | 3 | 266 | 5226.23 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
56 | 12417.0 | 3 | 192 | 3649.10 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
71 | 12433.0 | 1 | 420 | 13375.87 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
75 | 12437.0 | 2 | 200 | 4951.41 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
4292 | 18223.0 | 5 | 272 | 6484.54 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
4294 | 18225.0 | 3 | 269 | 5504.96 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
4298 | 18229.0 | 12 | 164 | 7276.90 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
4310 | 18245.0 | 7 | 175 | 2567.06 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
4328 | 18272.0 | 3 | 166 | 3078.58 | 5 | 5 | 5 | 555 | 15.0 | 5.0 |
306 rows × 10 columns
É possível atribuir as classes pré-definidas a cada setor da matriz de acordo com as diretrizes da empresa. Neste caso iremos atribuir as classes sugeridas no início do estudo.
rfm['R'] = rfm['R'].astype('int64')
rfm['FM_media'] = rfm['FM_media'].astype('int64')
def classificar(df):
if (df['FM_media'] == 5) and (df['R'] == 1):
return 'Não posso perdê-lo'
elif (df['FM_media'] == 5) and ((df['R'] == 3) or (df['R'] == 4)):
return 'Cliente leal'
elif (df['FM_media'] == 5) and (df['R'] == 5):
return 'Campeão'
elif (df['FM_media'] == 4) and (df['R'] >= 3):
return 'Cliente leal'
elif (df['FM_media'] == 3) and (df['R'] == 3):
return 'Precisa de atenção'
elif ((df['FM_media'] == 3) or (df['FM_media'] == 2)) and (df['R'] > 3):
return 'Lealdade potencial'
elif ((df['FM_media'] == 2) or (df['FM_media'] == 1)) and (df['R'] == 1):
return 'Perdido'
elif (df['FM_media'] == 2) and (df['R'] == 2):
return 'Hibernando'
elif ((df['FM_media'] == 2) or (df['R'] == 1)) and (df['R'] == 3):
return 'Prestes a hibernar'
elif (df['FM_media'] == 1) and (df['R'] == 2):
return 'Perdido'
elif (df['FM_media'] == 1) and (df['R'] == 4):
return 'Promissor'
elif (df['FM_media'] == 1) and (df['R'] == 5):
return 'Recentes'
else:
return 'Em risco'
rfm['Classe'] = rfm.apply(classificar,axis=1)
rfm
ClienteID | Recência | Frequência | ValorMonetario | R | F | M | RFM_cluster | RFM_score | FM_media | Classe | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 12346.0 | 326 | 1 | 77183.60 | 1 | 1 | 5 | 115 | 7.0 | 3 | Em risco |
1 | 12347.0 | 2 | 182 | 4310.00 | 5 | 5 | 5 | 555 | 15.0 | 5 | Campeão |
2 | 12348.0 | 75 | 31 | 1797.24 | 2 | 3 | 4 | 234 | 9.0 | 4 | Em risco |
3 | 12349.0 | 19 | 73 | 1757.55 | 4 | 4 | 4 | 444 | 12.0 | 4 | Cliente leal |
4 | 12350.0 | 310 | 17 | 334.40 | 1 | 2 | 2 | 122 | 5.0 | 2 | Perdido |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
4334 | 18280.0 | 278 | 10 | 180.60 | 1 | 1 | 1 | 111 | 3.0 | 1 | Perdido |
4335 | 18281.0 | 181 | 7 | 80.82 | 1 | 1 | 1 | 111 | 3.0 | 1 | Perdido |
4336 | 18282.0 | 8 | 12 | 178.05 | 5 | 1 | 1 | 511 | 7.0 | 1 | Recentes |
4337 | 18283.0 | 4 | 721 | 2045.53 | 5 | 5 | 4 | 554 | 14.0 | 4 | Cliente leal |
4338 | 18287.0 | 43 | 70 | 1837.28 | 3 | 4 | 4 | 344 | 11.0 | 4 | Cliente leal |
4339 rows × 11 columns
Pronto, essa tabela por exeplo pode ser exportada para excel e encaminhada para outras estratégias direcionadas para cada cluster como e-mail marketing e CRM. o comando para exportar em excel é:
rfm.to_excel("nomedaplanilha.xlsx", index=False)
Isso cria uma planilha .xlsx na mesma pasta em que está o notebook. Caso queira especificar um local diferente para salvar é só escrever o endereço no disco:
rfm.to_excel(C:endereco/nomedaplanilha.xlsx')