Aumento do Faturamento da Empresa com Previsão de Churn

Antes de mais nada, vou explicar de forma breve o que é Churn, e todos seus conceitos envolvidos. Vamos lá!

O que é churn?

Churn é a medida de quantos clientes param de usar um produto. Isso pode ser medido com base no uso real ou falha na renovação (quando o produto é vendido usando um modelo de assinatura). Frequentemente avaliada por um período de tempo específico, pode haver uma taxa de churn mensal, trimestral ou anual.

Quando novos clientes começam a comprar e/ou usar um produto, cada novo usuário contribui para a taxa de crescimento do produto. Inevitavelmente, alguns desses clientes acabarão por descontinuar a sua utilização ou cancelar a sua subscrição; ou porque mudaram para um concorrente ou solução alternativa, não precisam mais das funções do produto, estão insatisfeitos com a experiência do usuário ou não podem mais arcar ou justificar o custo. Os clientes que param de usar/pagar são o “churn” por um determinado período de tempo.

Como é calculado o Churn?

Em sua forma mais simplista, a taxa de cancelamento é a porcentagem do total de clientes que param de usar/pagar durante um período de tempo. Assim, se houvesse 10.000 clientes totais em março e 1.000 deles deixassem de ser clientes, a taxa de churn mensal seria de 10%.

churn-rate_calculation

Por que o Churn ocorre?

Muitos fatores influenciam as razões de um cliente para o Churn. Pode ser o fato de haver um novo concorrente no mercado oferecendo preços melhores ou talvez o serviço que eles estão recebendo não esteja à altura, e assim por diante. Portanto, não há uma resposta correta sobre por que exatamente o cliente deseja churn, porque, como você pode ver, há muitos fatores que influenciam.

Prós e Contras da Taxa de Churn

Prós Contras
Fornece clareza sobre a qualidade do negócio Não fornece clareza sobre os tipos de clientes que saem: novos versus antigos
Indica se os clientes estão satisfeitos ou insatisfeitos com o produto ou serviço Não diferencia os tipos de empresas na comparação do setor: startups, em crescimento e consolidadas
Permite a comparação com concorrentes para medir um nível aceitável de churn
Fácil de calcular

O trabalho de um cientista de dados é encontrar esses padrões nos dados fornecidos e ver quais fatos são produzidos durante a análise de dados.

Diante disso, vamos para o problema de negócio e contextuar a situação ao Churn Rate.

Empresa TopBank

a descrição a seguir é totalmente fictícia, apenas contextualização para um problema de negócio.

A TopBank é uma grande empresa de serviços bancários. Ela atua principalmente nos países da Europa oferecendo produtos financeiros, desde contas bancárias até investimentos, passando por alguns tipos de seguros e produto de investimento.

O modelo de negócio da empresa é do tipo serviço, ou seja, ela comercializa serviços bancários para seus clientes através de agências físicas e um portal online.

O principal produto da empresa é uma conta bancária, na qual o cliente pode depositar seu salário, fazer saques, depósitos e transferência para outras contas. Essa conta bancária não tem custo para o cliente e tem uma vigência de 12 meses, ou seja, o cliente precisa renovar o contrato dessa conta para continuar utilizando pelos próximos 12 meses.

Segundo o time de Analytics da TopBank, cada cliente que possui essa conta bancária retorna um valor monetário de 15% do valor do seu salário estimado, se esse for menor que a média e 20% se esse salário for maior que a média, durante o período vigente de sua conta. Esse valor é calculado anualmente.

Por exemplo, se o salário mensal de um cliente é de 1.000,00 e a média de todos os salários do banco é de 800,00. A empresa, portanto, fatura 200,00 anualmente com esse cliente. Se esse cliente está no banco há 10 anos, a empresa já faturou 2.000,00 com suas transações e utilização da conta. (Valores em reais).

Nos últimos meses, o time de Analytics percebeu que a taxa de clientes cancelando suas contas e deixando o banco, atingiu números inéditos na empresa. Preocupados com o aumento dessa taxa, o time planejou um plano de ação para diminuir taxa de evasão de clientes.

Preocupados com a queda dessa métrica, o time de Analytics da TopBottom, contratou você como consultor de Data Science para criar um plano de ação, com o objetivo de reduzir a evasão de clientes, ou seja, impedir que o cliente cancele seu contrato e não o renove por mais 12 meses. Essa evasão, nas métricas de negócio, é conhecida como Churn.

De maneira geral, Churn é uma métrica que indica o número de clientes que cancelaram o contrato ou pararam de comprar seu produto em um determinado período de tempo. Por exemplo, clientes que cancelaram o contrato de serviço ou após o vencimento do mesmo, não renovaram, são clientes considerados em churn.

Outro exemplo seria os clientes que não fazem uma compra à mais de 60 dias. Esse clientes podem ser considerados clientes em churn até que uma compra seja realizada. O período de 60 dias é totalmente arbitrário e varia entre empresas.

O Desafio

Como um Consultor de Ciência de Dados, criarei um plano de ação para diminuir o número de clientes em churn e mostrar o retorno financeiro da sua solução.

Ao final da consultoria, entregarei ao CEO da TopBottom um modelo em produção, que receberá uma base de clientes via API e retornará essa mesma base “scorada”, ou seja, um coluna à mais com a probabilidade de cada cliente entrar em churn.

Além disso, você precisará fornecer um relatório reportando a performance do seu modelo e o impacto financeiro da sua solução. Questões que o CEO e o time de Analytics gostariam de ver em seu relatório:

  1. Qual a taxa atual de Churn da TopBank? Como ela varia mensalmente?
  2. Qual a Performance do modelo em classificar os clientes como churns?
  3. Qual o retorno esperado, em termos de faturamento, se a empresa utilizar seu modelo para evitar o churn dos clientes?
  4. Uma possível ação para evitar que o cliente entre em churn é oferecer um cupom de desconto, ou alguma outro incentivo financeiro para ele renovar seu contrato por mais 12 meses.

Dicionário de dados

Cada linha representa um cliente e cada coluna contém alguns atributos que descrevem esse cliente. O conjunto de dados inclui informações sobre:

  • RowNumber: O número da coluna
  • CustomerID: Identificador único do cliente
  • Surname: Sobrenome do cliente.
  • CreditScore: A pontuação de Crédito do cliente para o mercado de consumo.
  • Geography: O país onde o cliente reside.
  • Gender: O gênero do cliente.
  • Age: A idade do cliente.
  • Tenure: Número de anos que o cliente permaneceu ativo.
  • Balance: Valor monetário que o cliente tem em sua conta bancária.
  • NumOfProducts: O número de produtos comprado pelo cliente no banco.
  • HasCrCard: Indica se o cliente possui ou não cartão de crédito.
  • IsActiveMember: Indica se o cliente fez pelo menos uma movimentação na conta bancário dentro de 12 meses.
  • EstimateSalary: Estimativa do salário mensal do cliente.
  • Exited: Indica se o cliente está ou não em Churn.
In [ ]:
# Instala pacotes
!pip install -q pycaret --user
!pip install -q sweetviz --user
!pip install -q autoviz --user
!pip install -q gradio --user
In [ ]:
# Carregando as bibliotecas

import time
import sklearn
import datetime
import pandas as pd
import numpy as np
import matplotlib as m
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
from pycaret.classification import *
warnings.filterwarnings("ignore")
C:\Users\mathe\Anaconda3\lib\site-packages\dask\config.py:168: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
  data = yaml.load(f.read()) or {}
C:\Users\mathe\Anaconda3\lib\site-packages\dask\dataframe\utils.py:13: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
  import pandas.util.testing as tm
C:\Users\mathe\Anaconda3\lib\site-packages\distributed\config.py:20: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
  defaults = yaml.load(f)
In [ ]:
# Carrega o dataset
df1 = pd.read_csv("churn.csv", encoding = 'utf-8')
In [ ]:
# Verifica o shape
df1.shape
Out[ ]:
(10000, 14)
In [ ]:
# Visualiza os dados
df1.head()
Out[ ]:
RowNumber CustomerId Surname CreditScore Geography Gender Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
0 1 15634602 Hargrave 619 France Female 42 2 0.00 1 1 1 101348.88 1
1 2 15647311 Hill 608 Spain Female 41 1 83807.86 1 0 1 112542.58 0
2 3 15619304 Onio 502 France Female 42 8 159660.80 3 1 0 113931.57 1
3 4 15701354 Boni 699 France Female 39 1 0.00 2 0 0 93826.63 0
4 5 15737888 Mitchell 850 Spain Female 43 2 125510.82 1 1 1 79084.10 0

Análise exploratória

Vamos explorar os dados para compreender as relações e influências ou não entre as variáveis dependentes e alvo.

In [ ]:
# Verifica valores unicos
df1.nunique()
Out[ ]:
RowNumber          10000
CustomerId         10000
Surname             2932
CreditScore          460
Geography              3
Gender                 2
Age                   70
Tenure                11
Balance             6382
NumOfProducts          4
HasCrCard              2
IsActiveMember         2
EstimatedSalary     9999
Exited                 2
dtype: int64
In [ ]:
df1.isnull().sum()
Out[ ]:
RowNumber          0
CustomerId         0
Surname            0
CreditScore        0
Geography          0
Gender             0
Age                0
Tenure             0
Balance            0
NumOfProducts      0
HasCrCard          0
IsActiveMember     0
EstimatedSalary    0
Exited             0
dtype: int64
In [ ]:
# Tipos de dados
df1.dtypes
Out[ ]:
RowNumber            int64
CustomerId           int64
Surname             object
CreditScore          int64
Geography           object
Gender              object
Age                  int64
Tenure               int64
Balance            float64
NumOfProducts        int64
HasCrCard            int64
IsActiveMember       int64
EstimatedSalary    float64
Exited               int64
dtype: object
In [ ]:
# Resumo das colunas numéricas
df1.describe()
Out[ ]:
RowNumber CustomerId CreditScore Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
count 10000.00000 1.000000e+04 10000.000000 10000.000000 10000.000000 10000.000000 10000.000000 10000.00000 10000.000000 10000.000000 10000.000000
mean 5000.50000 1.569094e+07 650.528800 38.921800 5.012800 76485.889288 1.530200 0.70550 0.515100 100090.239881 0.203700
std 2886.89568 7.193619e+04 96.653299 10.487806 2.892174 62397.405202 0.581654 0.45584 0.499797 57510.492818 0.402769
min 1.00000 1.556570e+07 350.000000 18.000000 0.000000 0.000000 1.000000 0.00000 0.000000 11.580000 0.000000
25% 2500.75000 1.562853e+07 584.000000 32.000000 3.000000 0.000000 1.000000 0.00000 0.000000 51002.110000 0.000000
50% 5000.50000 1.569074e+07 652.000000 37.000000 5.000000 97198.540000 1.000000 1.00000 1.000000 100193.915000 0.000000
75% 7500.25000 1.575323e+07 718.000000 44.000000 7.000000 127644.240000 2.000000 1.00000 1.000000 149388.247500 0.000000
max 10000.00000 1.581569e+07 850.000000 92.000000 10.000000 250898.090000 4.000000 1.00000 1.000000 199992.480000 1.000000
In [ ]:
# Deletando colunas irrelevantes para análise
df1 = df1.drop(columns = ['RowNumber', 'CustomerId', 'Surname'])
df1.head()
Out[ ]:
CreditScore Geography Gender Age Tenure Balance NumOfProducts HasCrCard IsActiveMember EstimatedSalary Exited
0 619 France Female 42 2 0.00 1 1 1 101348.88 1
1 608 Spain Female 41 1 83807.86 1 0 1 112542.58 0
2 502 France Female 42 8 159660.80 3 1 0 113931.57 1
3 699 France Female 39 1 0.00 2 0 0 93826.63 0
4 850 Spain Female 43 2 125510.82 1 1 1 79084.10 0
In [ ]:
# Proporção entre clientes em churn ou não
(df1['Exited'].value_counts()/len(df1['Exited']))*100
Out[ ]:
0    79.63
1    20.37
Name: Exited, dtype: float64

Vimos que a taxa de churn está em torno dos 20,3%. Traduzindo para números reais se em um mês entram 10 mil clientes, 2 mil deles irão sair até o fim do mês. Vamos fazer algumas análises para poder entender o que está acontencendo com nossos clientes e possíveis motivos que ocasionam o Churn, além de criar um modelo que consiga atrair mais clientes e diminuir essa taxa.

Análise Descritiva

Overview das variáveis numéricas

In [ ]:
# Plot

# Tamanho da figura
plt.figure(1, figsize = (20, 12))

# Inicializa contador
n = 0

# Loop pelas colunas
for x in ['CreditScore', 'Age', 'Tenure', 'Balance', 'NumOfProducts', 'HasCrCard', "EstimatedSalary"]:
    n += 1
    plt.subplot(3, 4, n)
    plt.subplots_adjust(hspace = 0.5, wspace = 0.5)
    sns.histplot(df1[x], bins = 15)
    plt.title("Histplot de {}".format(x))
plt.show()

Vamos criar um gráfico de estimativa de densidade do Kernel (KDE) pelo valor do alvo. KDE é uma maneira de identificar se existe uma correlação entra a variável dependente e alvo, nesse caso a correlação entre idade e churn.

In [ ]:
# KDE plot

plt.figure(figsize = (15,6))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.kdeplot(df1.loc[df1['Exited'] == 1, 'Age'], label = 'Clientes em Churn')
sns.kdeplot(df1.loc[df1['Exited'] == 0, 'Age'], label = 'Clientes fora do Churn')
plt.legend()
plt.xlim(left = 18, right = 90)
plt.xlabel('Idade')
plt.ylabel('Densidade')
plt.title('Distribuição de idade em porcentagem com a variável Churn');
In [ ]:
# Plot do Valor monetário em conta dos clientes dentro e fora do churn

plt.figure(figsize = (15,6))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.histplot(data = df1, x = 'Balance', hue = 'Exited', kde = True, multiple = 'dodge')
plt.legend(['Em churn', 'Fora do churn'])
plt.xlabel('Valor monetário em conta')
plt.ylabel('Contagem')
plt.title('Valor monetário em conta X Distribuição total');
In [ ]:
# Plot do estimativa salarial mensal dos clientes dentro e fora do churn

plt.figure(figsize = (15,8))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.histplot(data = df1, x = 'EstimatedSalary', hue = 'Exited', kde = True, multiple = 'dodge')
plt.legend(['Em churn', 'Fora do churn'], loc = 'upper right')
plt.xlabel('Estimativo Salarial Mensal')
plt.ylabel('Contagem')
plt.title('Estimativa Salarial Mensal dos clientes em churn e fora do churn');
In [ ]:
# Plot do Score de Crédito dos clientes dentro e fora do churn

plt.figure(figsize = (15,6))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.histplot(data = df1, x = 'CreditScore', hue = 'Exited', kde = True, multiple = 'dodge')
plt.legend(['Em churn', 'Fora do churn'])
plt.xlabel('Pontuação de Crédito')
plt.ylabel('Contagem')
plt.title('Pontuação de Crédito dos clientes');
In [ ]:
# Contagem de clientes por região

plt.figure(figsize = (15,6))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.countplot(x = 'Geography', hue = 'Exited', data = df1)
plt.legend(labels = ['Fora do Churn', 'Em Churn'])
plt.xlabel("Região")
plt.ylabel("Contagem")
plt.title("Distribuição de clientes por região")
Out[ ]:
Text(0.5, 1.0, 'Distribuição de clientes por região')
In [ ]:
# Contagem de clientes por produtos

plt.figure(figsize = (15,6))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.countplot(x = 'NumOfProducts', hue = 'Exited', data = df1)
plt.legend(labels = ['Fora do Churn', 'Em Churn'], loc = 'upper right')
plt.xlabel("Número de Produtos")
plt.ylabel("Contagem")
plt.title("Distribuição de clientes por produto")
Out[ ]:
Text(0.5, 1.0, 'Distribuição de clientes por produto')
In [ ]:
from matplotlib.cm import get_cmap
from matplotlib.patches import Patch

# Criando um dataframe secundário para analisar as variáveis categoricas Geography, NumOfProducts e Exited
df2 = df1[['Geography', 'NumOfProducts', 'Exited']]
df2['Dummy'] = np.ones(len(df2))

# Agrupando pelas categorias desejadas
grouped = df2.groupby(by=['Geography','Exited','NumOfProducts' ]).count().unstack()

# Lista dos clientes em churn ou fora, para usar como categoria depois
kinds = grouped.columns.levels[1]

# Cores para o grafico
colors = [get_cmap('viridis')(v) for v in np.linspace(0,1,len(kinds))]

sns.set(context="talk")
nxplots = len(grouped.index.levels[0])
nyplots = len(grouped.index.levels[1])
fig, axes = plt.subplots(nxplots,
                         nyplots,
                         sharey=True,
                         sharex=True,
                         figsize=(15,10))

fig.suptitle('Agrupamento de Clientes por Região e \nQuantidade de Produtos')

# plot
for a, b in enumerate(grouped.index.levels[0]):
    for i, j in enumerate(grouped.index.levels[1]):
        axes[a,i].bar(kinds,grouped.loc[b,j],color=colors)
        axes[a,i].xaxis.set_ticks([])

axeslabels = fig.add_subplot(111, frameon=False)
plt.tick_params(labelcolor='none', top=False, bottom=False, left=False, right=False)
plt.grid(False)
axeslabels.set_ylabel('Localidade',rotation='horizontal',y=1,weight="bold")
axeslabels.set_xlabel('Em churn ou não',weight="bold")

# Rotulos dos eixo X e Y
for i, j in enumerate(grouped.index.levels[1]):
    axes[nyplots,i].set_xlabel(j)
for i, j in enumerate(grouped.index.levels[0]):
    axes[i,0].set_ylabel(j)

# Ajuste manual para abrir espaço para a legenda
fig.subplots_adjust(right=0.78)

fig.legend([Patch(facecolor = i) for i in colors],
           kinds,
           title="Quantidade de Produtos",
           loc="upper right")
Out[ ]:
<matplotlib.legend.Legend at 0x26607f7c9b0>
In [ ]:
# Contagem por clientes que tem ou não cartao de credito

plt.figure(figsize = (15,6))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.countplot(x = 'HasCrCard', hue = 'Exited', data = df1)
plt.legend(labels = ['Fora do Churn' , 'Em Churn'])
plt.xlabel("Clientes que possuem ou não cartao de credito")
plt.ylabel("Contagem")
plt.title("Distribuição de clientes que possuem ou não cartão de crédito")
Out[ ]:
Text(0.5, 1.0, 'Distribuição de clientes que possuem ou não cartão de crédito')
In [ ]:
# Distribuição de clientes por tempo de empresa fora do churn

plt.figure(figsize = (15,8))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.countplot(x = 'Tenure', hue = 'Exited', data = df1)
plt.legend(labels = ['Fora do Churn' , 'Em Churn'])
plt.xlabel("Anos na empresa")
plt.ylabel("Contagem")
plt.title("Distribuição de clientes em churn ou não por anos de permanência na empresa")
Out[ ]:
Text(0.5, 1.0, 'Distribuição de clientes em churn ou não por anos de permanência na empresa')
In [ ]:
# Distribuição de clientes que fazem movimentação na conta num período de 12 meses em ou fora do churn

plt.figure(figsize = (15,6))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.countplot(x = 'IsActiveMember', hue = 'Exited',data = df1)
plt.legend(['Fora do Churn' , 'Em Churn'])
plt.xlabel("Membro Ativo")
plt.ylabel("Contagem")
plt.title("Distribuição de membros ativos em ou fora do churn")
Out[ ]:
Text(0.5, 1.0, 'Distribuição de membros ativos em ou fora do churn')
In [ ]:
# Distribuição de clientes por genero em ou fora do churn

plt.figure(figsize = (15,6))
plt.style.use('seaborn-colorblind')
plt.grid(True, alpha = 0.5)
sns.countplot(x = 'Gender', hue = 'Exited',data = df1)
plt.legend(labels = ['Fora do Churn' , 'Em Churn'])
plt.xlabel("Gênero")
plt.ylabel("Contagem")
plt.title("Distribuição por gênero em ou fora do churn")
Out[ ]:
Text(0.5, 1.0, 'Distribuição por gênero em ou fora do churn')

Após a Análise Exploratória dos dados, vamos verificar abaixo alguns insights para o problema proposto.

Insights do problema de negócio

insight-01

insight-02

insight-03

In [ ]:
# Gerando plot de correlação entre as variáveis

# Armazenando correlação do dataframe
corr = df1.corr()

# Gerando corrplot
plt.figure(figsize = (15,10))
sns.heatmap(corr, annot = True, fmt = '.3f')
plt.title("Correlação entre as variáveis")
plt.show()
In [ ]:
# Proporção entre clientes em churn ou não
(df1['Exited'].value_counts()/len(df1['Exited']))*100
Out[ ]:
0    79.63
1    20.37
Name: Exited, dtype: float64

Através do código acima vimos que há um desbalanceamento dos dados na variável alvo (Exited, o que pode enviesar o modelo e atrapalhar nas decisões futuras). Vamos Utilizar o PyCaret, uma ferramenta completa que consegue treinar o dataset com vários modelos, mostrar as diferentes métricas de desempenho para cada um deles, tunar o modelo escolhido e mostrar todo esse resultado.

In [ ]:
# Configurando o modelo para o dataset
s = setup(df1, target = "Exited", fix_imbalance = True)
Description Value
0 session_id 1523
1 Target Exited
2 Target Type Binary
3 Label Encoded None
4 Original Data (10000, 11)
5 Missing Values False
6 Numeric Features 4
7 Categorical Features 6
8 Ordinal Features False
9 High Cardinality Features False
10 High Cardinality Method None
11 Transformed Train Set (6999, 25)
12 Transformed Test Set (3001, 25)
13 Shuffle Train-Test True
14 Stratify Train-Test False
15 Fold Generator StratifiedKFold
16 Fold Number 10
17 CPU Jobs -1
18 Use GPU False
19 Log Experiment False
20 Experiment Name clf-default-name
21 USI 4afd
22 Imputation Type simple
23 Iterative Imputation Iteration None
24 Numeric Imputer mean
25 Iterative Imputation Numeric Model None
26 Categorical Imputer constant
27 Iterative Imputation Categorical Model None
28 Unknown Categoricals Handling least_frequent
29 Normalize False
30 Normalize Method None
31 Transformation False
32 Transformation Method None
33 PCA False
34 PCA Method None
35 PCA Components None
36 Ignore Low Variance False
37 Combine Rare Levels False
38 Rare Level Threshold None
39 Numeric Binning False
40 Remove Outliers False
41 Outliers Threshold None
42 Remove Multicollinearity False
43 Multicollinearity Threshold None
44 Remove Perfect Collinearity True
45 Clustering False
46 Clustering Iteration None
47 Polynomial Features False
48 Polynomial Degree None
49 Trignometry Features False
50 Polynomial Threshold None
51 Group Features False
52 Feature Selection False
53 Feature Selection Method classic
54 Features Selection Threshold None
55 Feature Interaction False
56 Feature Ratio False
57 Interaction Threshold None
58 Fix Imbalance True
59 Fix Imbalance Method SMOTE

Vamos rankear e selecionar o melhor modelo de acordo com a métrica AUC.

In [ ]:
# Comparando todos os modelos
best_model = compare_models(sort = 'AUC')
Model Accuracy AUC Recall Prec. F1 Kappa MCC Profit TT (Sec)
gbc Gradient Boosting Classifier 0.8600 0.8652 0.5260 0.7025 0.6009 0.5182 0.5265 264200.0000 1.9600
lightgbm Light Gradient Boosting Machine 0.8650 0.8588 0.5110 0.7357 0.6020 0.5241 0.5373 261400.0000 0.6540
rf Random Forest Classifier 0.8614 0.8558 0.5017 0.7236 0.5917 0.5117 0.5246 255000.0000 1.1890
xgboost Extreme Gradient Boosting 0.8577 0.8511 0.5110 0.7003 0.5900 0.5065 0.5162 256300.0000 2.2870
ada Ada Boost Classifier 0.8463 0.8431 0.5409 0.6400 0.5852 0.4918 0.4951 260900.0000 0.6640
et Extra Trees Classifier 0.8520 0.8430 0.4840 0.6860 0.5669 0.4810 0.4920 240900.0000 1.2100
lda Linear Discriminant Analysis 0.7664 0.8326 0.7409 0.4507 0.5600 0.4137 0.4378 289300.0000 0.1250
nb Naive Bayes 0.6817 0.7565 0.7302 0.3576 0.4799 0.2878 0.3258 225500.0000 0.0460
lr Logistic Regression 0.6661 0.7172 0.6740 0.3369 0.4486 0.2466 0.2770 190900.0000 1.4030
dt Decision Tree Classifier 0.8023 0.7021 0.5346 0.5070 0.5196 0.3954 0.3962 227400.0000 0.0910
knn K Neighbors Classifier 0.5687 0.5204 0.4150 0.2096 0.2784 0.0162 0.0182 13500.0000 0.2020
dummy Dummy Classifier 0.7993 0.5000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0960
svm SVM - Linear Kernel 0.5047 0.0000 0.5776 0.1563 0.2446 0.0346 0.0589 37500.0000 0.3370
ridge Ridge Classifier 0.7663 0.0000 0.7409 0.4505 0.5599 0.4135 0.4376 289200.0000 0.0580
qda Quadratic Discriminant Analysis 0.7993 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.1300
In [ ]:
# Print dos parametros do melhor modelo
print(best_model)
GradientBoostingClassifier(ccp_alpha=0.0, criterion='friedman_mse', init=None,
                           learning_rate=0.1, loss='deviance', max_depth=3,
                           max_features=None, max_leaf_nodes=None,
                           min_impurity_decrease=0.0, min_impurity_split=None,
                           min_samples_leaf=1, min_samples_split=2,
                           min_weight_fraction_leaf=0.0, n_estimators=100,
                           n_iter_no_change=None, presort='deprecated',
                           random_state=1523, subsample=1.0, tol=0.0001,
                           validation_fraction=0.1, verbose=0,
                           warm_start=False)
In [ ]:
# Confusion matrix do modelo sem melhorias
plot_model(best_model, plot = 'confusion_matrix')

A Confusion Matrix acompanha o conjunto de teste, um split de 70/30: 70% do dataset utilizado para treino e 30% para teste.

  • Das 3000 linhas separadas para treino, ou seja, dos 3000 clientes Temos 307 Verdadeiros Positivo (10%) - Os quais podemos oferecer alguma promoção;
  • 4,2% dos clientes, 127 para ser exato, seria onde perderíamos dinheiro por se tratarem de Falsos Positivo, identificados como clientes em Churn porém não são verdadeiramente, ocasionando um custo extra;
  • 325 clientes (11%), seriam Falsos Negativo, clientes que entrariam em Churn porém não seriam identificados como tal, ocasionando na perda real deles sem nenhuma tentativa de incentivo para permanência destes.
In [ ]:
# plot AUC do melhor modelo original
plot_model(best_model, plot = 'auc')
In [ ]:
# Plot das variáveis mais relevantes
plot_model(best_model, plot = 'feature')

Nós vamos utilizar de uma função do PyCaret para automaticamente tunar os hiperparâmetros do modelo, afim de trazer um melhor resultado diante as métricas observadas.

In [ ]:
# Tunando o melhor modelo
tuned_best_model = tune_model(best_model)
Accuracy AUC Recall Prec. F1 Kappa MCC Profit
0 0.8514 0.8468 0.4857 0.6800 0.5667 0.4800 0.4899 240000.0000
1 0.8743 0.8840 0.5500 0.7549 0.6364 0.5626 0.5729 283000.0000
2 0.8529 0.8571 0.4786 0.6907 0.5654 0.4803 0.4920 238000.0000
3 0.8814 0.8754 0.6071 0.7522 0.6719 0.6006 0.6057 312000.0000
4 0.8514 0.8697 0.4894 0.6832 0.5702 0.4834 0.4932 244000.0000
5 0.8586 0.8540 0.5887 0.6694 0.6264 0.5396 0.5413 291000.0000
6 0.8700 0.8911 0.5106 0.7660 0.6128 0.5384 0.5544 266000.0000
7 0.8671 0.8589 0.5674 0.7143 0.6324 0.5526 0.5581 288000.0000
8 0.8643 0.8509 0.5674 0.7018 0.6275 0.5456 0.5502 286000.0000
9 0.8555 0.8654 0.5571 0.6667 0.6070 0.5194 0.5225 273000.0000
Mean 0.8627 0.8653 0.5402 0.7079 0.6117 0.5303 0.5380 272100.0000
SD 0.0099 0.0139 0.0435 0.0353 0.0332 0.0377 0.0365 23526.3682

O modelo teve uma melhoria de apenas 0,01%, porém muda na quantidade de Verdadeiros Positivos de forma razoável, influenciando no resultado final.

In [ ]:
# Plot AUC do melhor modelo tunado
plot_model(tuned_best_model, plot = 'auc')
In [ ]:
# Confusion matrix do modelo sem melhorias
plot_model(tuned_best_model, plot = 'confusion_matrix')

Com a melhoria do modelo temos as seguintes informações:

  • Dos 3000 clientes temos agora 311 Verdadeiros Positivo - Para os quais podemos oferecer alguma promoção;
  • Manteve a quantidade de Falsos positivos, onde seria o público para quem perderíamos dinheiro, identificados como clientes em Churn porém não são verdadeiramente;
  • E uma diminuição para 321 clientes Falsos Negativo, clientes que entrariam em Churn porém não seriam identificados como tal.

Num problema de negócio como esse de rotatividade de cliente, o retorno financeiro dos verdadeiros positivos é diferente do custo dos falsos positivos. Vamos concretizar essa teoria com as seguintes posições:

  • Oferecer um voucher de R$1000,00 a todos os clientes identificados como churn (Verdadeiro Positivo + Falso Positivo);

  • Se interrompermos o churn, ganhamos R$5000,00 em valor de permanência do cliente na empresa.

Com a suposições acima e os valores da Confusion Matrix, vamos calcular o impacto financeiro desse modelo:

tabela_profit_v1

Com o PyCaret conseguimos adicionar uma nova métrica que vai de acordo com o problema proposto. Para isso vamos criar uma métrica de lucro, que é o que queremos aumentar no final de avaliação de churn.

In [ ]:
# Cria uma função personalizada
def calcula_profit(y, y_pred):
    tp = np.where((y_pred == 1) & (y == 1), (5000-1000), 0)
    fp = np.where((y_pred == 1) & (y == 0), -1000, 0)
    return np.sum([tp,fp])

# Adiciona metrica ao PyCaret
add_metric('profit', 'Profit', calcula_profit)
Out[ ]:
Name                                                          Profit
Display Name                                                  Profit
Score Function       <function calcula_profit at 0x0000026664AFFB70>
Scorer                                   make_scorer(calcula_profit)
Target                                                          pred
Args                                                              {}
Greater is Better                                               True
Multiclass                                                      True
Custom                                                          True
Name: profit, dtype: object
In [ ]:
# Agora vamos comparar os modelos com a nova métrica
best_model_profit = compare_models(sort = 'Profit')
Model Accuracy AUC Recall Prec. F1 Kappa MCC Profit TT (Sec)
lda Linear Discriminant Analysis 0.7664 0.8326 0.7409 0.4507 0.5600 0.4137 0.4378 289300.0000 0.0830
ridge Ridge Classifier 0.7663 0.0000 0.7409 0.4505 0.5599 0.4135 0.4376 289200.0000 0.0350
gbc Gradient Boosting Classifier 0.8600 0.8652 0.5260 0.7025 0.6009 0.5182 0.5265 264200.0000 1.2550
lightgbm Light Gradient Boosting Machine 0.8650 0.8588 0.5110 0.7357 0.6020 0.5241 0.5373 261400.0000 0.5660
ada Ada Boost Classifier 0.8463 0.8431 0.5409 0.6400 0.5852 0.4918 0.4951 260900.0000 0.3740
xgboost Extreme Gradient Boosting 0.8577 0.8511 0.5110 0.7003 0.5900 0.5065 0.5162 256300.0000 2.2940
rf Random Forest Classifier 0.8614 0.8558 0.5017 0.7236 0.5917 0.5117 0.5246 255000.0000 0.8360
et Extra Trees Classifier 0.8520 0.8430 0.4840 0.6860 0.5669 0.4810 0.4920 240900.0000 0.9450
dt Decision Tree Classifier 0.8023 0.7021 0.5346 0.5070 0.5196 0.3954 0.3962 227400.0000 0.0600
nb Naive Bayes 0.6817 0.7565 0.7302 0.3576 0.4799 0.2878 0.3258 225500.0000 0.0340
lr Logistic Regression 0.6661 0.7172 0.6740 0.3369 0.4486 0.2466 0.2770 190900.0000 1.0590
svm SVM - Linear Kernel 0.5047 0.0000 0.5776 0.1563 0.2446 0.0346 0.0589 37500.0000 0.2220
knn K Neighbors Classifier 0.5687 0.5204 0.4150 0.2096 0.2784 0.0162 0.0182 13500.0000 0.1590
qda Quadratic Discriminant Analysis 0.7993 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0470
dummy Dummy Classifier 0.7993 0.5000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0410

Podemos verificar que na coluna Profit o modelo com melhor desempenho em questões financeiras é o LDA (Análise Discriminante Linear), que está longe de ser o melhor modelo através da métrica AUC, com valor de 83.08%. Agora vamos conferir a Confusion Matrix do modelo.

In [ ]:
# Confusion Matrix LDA
plot_model(best_model_profit, plot = 'confusion_matrix')

Acontece que agora o modelo aumentou tanto os valores de Verdadeiros Positivos e Falsos Positivos, e isso é relevante para nosso caso porque estamos focados em entregar produtos para quem o modelo irá prever que estará em Churn, tanto os Verdadeiros Positivos como Falsos Positivos. Agora vamos aplicar as mesmas suposições que comentei logo antes:

tabela_profit_v2

  • No modelo tunado que utilizamos para verificação de lucro que a empresa teria resgatando clientes em Churn, teríamos um lucro total de R$ 1.101.000,00;
  • No modelo selecionado a partir da métrica criada conseguimos um retorno financeiro de R$ 1.326.000,00;
  • Isso significa 17% de aumento ou R$ 225.000,00 em valores monetários, apenas na escolha de uma métrica que melhor se posiciona para o caso

Na versão 2.3.6 do PyCaret houve também muita coisa nova, como o deploy do modelo de forma bastante prática.

É possível criar um app apenas com uma linha de código. Com base na biblioteca Gradio, cria-se um web app em que você pode inserir os valores das variáveis independentes e o app informa o resultado previsto. Vamos conferir abaixo.

In [ ]:
# Cria o app
create_app(best_model_profit)

web_app

Chegamos ao fim de um Projeto de Churn de clientes, com insights, avaliações de modelos preditivos, métricas, resultado financeiro e aplicação web de todo esse estudo de caso.