MODELO DE PREDIÇÃO DE CHURN

Victoria Oliveira
6 min readNov 18, 2021

--

Os clientes formam a base de uma empresa, independentemente do setor. Por isso, é tão importante para uma instituição predizer se os seus clientes irão ou não deixar de usar o seu serviço, ou seja, se eles vão ou não entrar em churn. Assim, quanto mais preciso for essa predição, melhor será possível direcionar ações financeiras e de marketing, por exemplo, para evitar que o cliente abandone o serviço.

Conjunto de dados

O dataset escolhido trata de clientes de um banco. As variáveis nele presentes são: RowNumber: número de linhas; CustomerId: Id do cliente; Surname: sobrenome do cliente; CreditScore: pontuação de crédito baseada no histórico do cliente; Geography: localização do cliente (Espanha, Alemanha e França); Gender: gênero (masculino ou feminino); Tenure: tempo de vínculo do cliente com a empresa (suponho que esteja em anos); Balance: montante em dinheiro (dólar) disponível na conta do cliente; NumOfProducts: número de produtos da instituição que o cliente possui;HasCrCard: o cliente tem cartão de crédito, sim ou não?; IsActiveMember: o cliente é um membro ativo, sim ou não?;EstimatedSalary: salário estimado do cliente. Apresentados assim:

df.head()

Análise exploratória

Primeiro, optei por visualizar gráficos em forma de “violino” as variáveis em relação a variável churn de interesse, apresentada aqui como Exited (1 = sim; 0 = não).

for i in df.columns[3:13]:
sns.violinplot(x='Exited', y= i, data = df, palette = 'tab10_r')
plt.show()

Reparem que para a visualização eu descartei as colunas “RowNumber”, “CustomerId” e “Surname” que não trazem nenhuma informação útil para o meu objetivo de prever quais clientes vão entrar ou não em churn. Abaixo apresento os gráficos que mais me chamaram atenção:

Em relação a variável CreditScore vemos que a média está em aproximadamente 650, sem grandes diferenças entre os clientes que deram churn ou não. A variável da idade (Age) explicita uma conclusão interessante: os mais velhos (principalmente as pessoas acima de 50 anos) estão evadindo mais que os mais novos. Outro fato interessante é que, para esse caso, o tempo de vínculo com o banco pouco parece importar (Tenure). Por fim, ao analisarmos o montante dos clientes no banco (Balance), há um dado preocupante: o banco está perdendo clientes com altas quantias em conta corrente.

Considerando agora a correlação entre todas as variáveis:

corr = df.corr()
corr.style.background_gradient(cmap='PiYG')

Focando na nossa variável de interesse (Exited), vemos que ela só tem correlação positiva com as variáveis idade, saldo em conta e salário. Esse resultado me surpreendeu, por isso, resolvi dar uma olhada com mais calma. Para isso, optei por visualizar um “countplot”:

fig, ax = plt.subplots(3, 2, figsize = (18, 15))sns.countplot(x= 'Gender', hue= 'Exited', data = df, palette = 'tab10_r', ax = ax[0][0])
sns.countplot(x= 'Exited', hue= 'Exited', data = df, palette = 'tab10_r', ax = ax[0][1])
sns.countplot(x= 'NumOfProducts', hue= 'Exited', data = df, palette = 'tab10_r', ax = ax[1][0])
sns.countplot(x= 'HasCrCard', hue= 'Exited', data = df, palette = 'tab10_r', ax = ax[1][1])
sns.countplot(x= 'IsActiveMember', hue= 'Exited', data = df, palette = 'tab10_r', ax = ax[2][0])
sns.countplot(x= 'Geography', hue= 'Exited', data = df, palette = 'tab10_r', ax = ax[2][1])
plt.tight_layout()
plt.show()

Em relação ao gênero, vemos que as mulheres estão evadindo ligeiramente mais do que os homens. Outro ponto a ser notado é o quanto a proporção entre os clientes churn (20,37%) e não churn (79,63%) está desbalanceada. Esse fato merece atenção na hora do pré-processamento. Dando continuidade, fica claro que as pessoas que tem apenas um produto evadem mais do que as que possuem mais de um. Os clientes com cartão de crédito também estão evadindo mais (mas quem não tem cartão de crédito hoje em dia?). Naturalmente, os clientes com conta inativa evadem mais. E, por fim, vemos que os clientes da Alemanha estão evadindo mais que os da França e da Espanha.

Pré-processamento

Como eu já tinha notado, as variáveis “RowNumber”, “CustomerId” e “Surname” não possuem relevância para o objetivo da modelagem. Por isso, o primeiro passo foi deletar essas variáveis:

df.drop(["CustomerId","Surname", 'RowNumber'], axis = 1, inplace = True)

O próximo passo foi transformar em dummies as variáveis categóricas restantes:

df['Gender'] = df['Gender'].map({'Female': 1, 'Male': 0})
df = pd.get_dummies(df, columns=['Geography'])

Para deixar todas as variáveis na mesma escala fiz transformações nas variáveis CreditScore, EstimatedSalary e Balance:

df['CreditScore'] = df['CreditScore']/1000 #escalando variável CreditScoremean = np.mean(df['Age'])
dp = np.std(df['Age'])
df['Age'] = (df['Age'] - mean)/dp #padronização (média 0 dp 1)df['EstimatedSalary'] = df['EstimatedSalary']/100000 #escalando variável saláriomean_balance = np.mean(df['Balance'])
dp_balance = np.std(df['Balance'])
df['Balance'] = (df['Balance'] - mean_balance)/dp_balance #padronização (média 0 dp 1)

Assim, temos todas as variáveis na mesma escala:

Machine Learning

Agora que todas as variáveis estão escaladas, posso rodar os modelos de ML. Mas antes, um fator importante não pode ser esquecido: a variável target (Exited) está desbalanceada. Uma alternativa para lidar com essa questão seria aplicar a função RandomUnderSampler() explicada aqui. Porém, optei por escolher uma técnica de validação que já lida muito bem com o problema de classes desbalanceadas: StratifiedKFold. Para entender o funcionamento dessa técnica recomendo a leitura desse artigo!

Resolvido essa questão, defini as variáveis explicativas e target e rodei alguns modelos de classificação:

X = df.drop('Exited', axis = 1) 
y = df['Exited']
lista_de_medidas = ['accuracy', 'recall', 'precision', 'balanced_accuracy', 'f1'] #métricas escolhidas
nome_das_medidas = ['acurácia', 'sensibilidade', 'precisão', 'eficiência', 'f1-score']


lista_de_modelos = [LogisticRegression(),
DecisionTreeClassifier(max_depth = 3),
DecisionTreeClassifier(max_depth = 5),
DecisionTreeClassifier(max_depth = 7),
KNeighborsClassifier(n_neighbors = 5),
KNeighborsClassifier(n_neighbors = 15),
KNeighborsClassifier(n_neighbors = 25),
BaggingClassifier(),
RandomForestClassifier(n_estimators=50, max_depth = 5),
RandomForestClassifier(n_estimators=50, max_depth = 7),
RandomForestClassifier(n_estimators=100, max_depth = 5),
RandomForestClassifier(n_estimators=100, max_depth = 7)]

nome_dos_modelos = ['Regressão Logística',
'Árvore (prof = 3)',
'Árvore (prof = 5)',
'Árvore (prof = 7)',
'5-NN',
'15-NN',
'25-NN',
'Bagging',
'Random Forest (arvs = 50, prof = 5)',
'Random Forest (arvs = 50, prof = 7)',
'Random Forest (arvs = 100, prof = 5)',
'Random Forest (arvs = 100, prof = 7)']

resultados0 = {}

validacao = RepeatedStratifiedKFold(n_splits = 5, random_state= 42)

for i in range(len(lista_de_modelos)):
print('Modelo: ' + nome_dos_modelos[i])
accs_vc = cross_validate(lista_de_modelos[i], X, y, cv = validacao, scoring = lista_de_medidas)

acc = accs_vc['test_accuracy'].mean()
sen = accs_vc['test_recall'].mean()
vpp = accs_vc['test_precision'].mean()
bac = accs_vc['test_balanced_accuracy'].mean()
f1s = accs_vc['test_f1'].mean()

resultados0[nome_dos_modelos[i]] = [acc, sen, vpp, bac, f1s]

resultados = pd.DataFrame(resultados0, index = nome_das_medidas).T

Apresentando os resultados das métricas temos:

resultados.sort_values(by = 'f1-score', ascending = False)

Visão de negócio

Para entender qual o modelo mais adequado para prever quais clientes entrarão ou não em churn, o ideal é entender o problema de negócio. Por exemplo, como estamos em um problema de classificação podemos manter em mente que as métricas acima envolvem em sua fórmula essencialmente quatro itens: os verdadeiros positivos, os verdadeiros negativos, os falsos negativos e os falsos positivos. No problema de churn, o caso verdadeiro positivo seria o cliente sair e eu conseguir prever; o verdadeiro negativo seria o cliente não sair e eu prever que de fato ele não sai; já um caso de falso negativo seria o cliente deixar o serviço e eu não prever, enquanto o falso positivo seria o cliente não sair mas eu prever que iria sair.

Conclusão

Dessa forma, considerando o meu objetivo inicial, o pior cenário seria o cliente sair e eu não prever, ou seja, uma maior presença de falsos negativos no meu modelo. Por isso, dentro das métricas apresentadas acima, dei atenção maior para a sensibilidade e, principalmente, ao f1-score. Pela tabela, fica claro que a árvore de decisão com profundidade 7 seguido do modelo bagging seriam minhas duas melhores opções para melhorar minha previsão.

Os modelos acima foram construídos com algumas suposições. Uma forma de melhorar as previsões seria trabalhar individualmente cada parâmetro dos algoritmos individualmente e até mesmo aplicar técnicas de tunagem de hiperparâmetros. Essa será uma preocupação que eu vou manter daqui em diante.

Você encontra meu código completo aqui. Até a próxima!

--

--