Gradiente Impulsionador para Previsão de Clientes Churn

Rafael Fabri Chimidt
11 min readOct 2, 2021

--

Olá a todos!!!

Estamos de volta a nossa análise a um conjunto de dados de uma empresa de telecomunicações, com a finalidade de prever quais futuros consumidores que desistiram de seus planos de assinatura, ou melhor dizendo um cliente Churn.

Para você que não viu o artigo anterior, que envolve a análise exploratória, pode clicar aqui para poder ler e entender melhor sobre essa segunda parte. Caso queira ver com maior detalhe a parte de código, pode entrar nesse link que irá lhe direcionar para meu GitHub onde encontra-se o projeto completo com os códigos em linguagem de programação Python.

No entanto, tenho certeza que nesse artigo estará a parte mais legal do nosso desenvolvimento. Vamos criar um algoritmo de machine learning para classificação de clientes, em linguagem de programação Python, que identificará consumidores que estão desapontados com o serviço prestado pela empresa de telecomunicações e que provavelmente cancelaram suas assinaturas.

Este artigo será dívidido em algumas etapas:

  • Transformando variáveis categóricas;
  • Separação do conjunto de dados;
  • Balanceamento de dados;
  • Grid Search;
  • Treino e teste do modelo;
  • Gráfico ROC
  • Previsão e melhoria do modelo.

Para criar o modelo preditivo utilizaremos a biblioteca XGBoost, que é um algoritmo baseado no Gradient Boosting. A XGBoost é um pouco semelhante a um algoritmo de floresta aleatória (como se fosse várias árvores de decisões), porém com algumas diferenças importantes. (Pathak. M, 2019) (XGBoost).

Já para a preparação dos dados (transformação), validação cruzada, Grid Search etc utilizaremos a biblioteca Scikit Learn.

Nesse segundo artigo irei colocar os códigos para um melhor entendimento do que realizaremos passo a passo.

Partiu para o desenvolvimento do nosso algoritmo!!!

Abaixo temos a imagem das 5 primeiras linhas de como é o nosso conjunto de dados.

5 primeiras linhas — 1° parte da Base de Dados
5 primeiras linhas — 2°parte da Base de Dados

A etapa de transformação é fundamental para não ocorrer nenhum erro no momento do treino do modelo e também para alcançar os melhores resultados possíveis na predição.

Vamos importar as bibliotecas necessárias para o projeto.

#IMPORTANDO BIBLIOTECAS#Pré processamento - Tranformação dos dados
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler
#Separação do conjunto de dados
from sklearn.model_selection import train_test_split
#Validação do modelo
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import GridSearchCV
#Modelo de previsão
import xgboost as xgb
#Métricas do modelo de previsão
from sklearn.metrics import confusion_matrix, roc_curve

Transformando variáveis categóricas!!!

Primeiramente, sabemos que o código de aprendizado de máquina não aceita variáveis categóricas do tipo texto (string). Por exemplo:

Print do conjunto de dados: Antes de utilizar o LabelEnconding

Portanto, temos que converter PaperlessBilling e as outras colunas para números inteiros(int), exemplo (Yes = 1, No = 0).

Print do conjunto de dados: Transformação utilizando LabelEnconding

Para isso vamos utilizar a função LabelEnconding() da biblioteca Scikit Learn.

#Criando listas com colunas que serão transformadas
columns_label_enc = []
#separando colunas/variáveis do tipo string
for i in range(0, len(df_copy.dtypes), 1):
if (df_copy.dtypes[i] == 'object'):
columns_label_enc.append(df_copy.dtypes.keys()[i])
#instanciândo LabelEnconder
label = LabelEncoder()
#condição de repetição
for i in columns_label_enc:
#Tranformando dados colunas categóricas (yes, no) em LabelEnconding (0,1)
df_copy[i] = label.fit_transform(df_copy[i])

Pronto ,todo o conjunto foi modificado e está com colunas do tipo numérico.

Agora temos uma outra tarefa de transformação muito importante.

Separação do conjunto de dados!!!

Neste momento iremos realizar a separação do conjunto de dados em treino e teste.

Os dados de treino são utilizados para que o algoritmo de machine learning saiba distinguir as duas classes (positiva e negativa), através do treino o modelo irá aprender a previsão de novos dados, assim no conjunto de teste poderemos aplicar o modelo para que seja realizada a previsão.

Após o teste podemos comparar os resultados previstos com os resultados reais para obtenção de algumas métricas para análise.

#separando o conjunto em variáveis independentes X  e variável dependente y
X, y = df_copy.drop(labels = 'Churn', axis = 1), df_copy['Churn']
#Separando conjunto em dados de treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y,
random_state=123, test_size=0.40, stratify=y)

Também, vamos separar o conjunto teste em dois tipos.

  • test_val: Conjunto para validação e melhoria do modelo depois da primeira previsão de dados.
  • test_final: Conjunto de teste final depois do modelo já otimizado e com os melhores parâmetros.

A parte de código é bem semelhante ao que foi visto acima na separação do conjunto de treino e teste.

Dimensão dos conjuntos de treino, teste val e teste final

Balanceamento de dados!!!

Caso você não se lembre do artigo anterior, existe um desbalanceamento em nossa variável alvo, pois 26% dos clientes são Churn e 74% dos Clientes não são Churn.

Isso pode acarretar que o modelo faça uma boa previsão da classe majoritária (clientes não Churn) e não prever tão bem a classe minoritária (clientes Churn). A pouca quantidade de clientes Churn pode fazer com que o algoritmo não consiga aprender a prever essa classe por causa de sua menor quantidade se comparado com a classe não Churn.

Realizaremos uma validação cruzada, para vermos o resultado do nosso modelo desbalanceado e balanceado. Abaixo está o código.

#instanciando modelo
model_xg_1 = xgb.XGBClassifier()
#validação cruzada
score_xg_1_accuracy = cross_val_score(model_xg_1, X_train, y_train,
scoring='accuracy', cv=5)
score_xg_1_recall = cross_val_score(model_xg_1, X_train, y_train,
scoring='recall', cv=5)
print('RESULTADOS DAS MÉTRICAS DE DESEMPENHO DO MODELO SEM O BALANCEAMENTO')
print('Acurácia média = ', round(score_xg_1_accuracy.mean(),3), "+/-" , round(score_xg_1_accuracy.std(),3))
print('Recall médio = ', round(score_xg_1_recall.mean(),3), "+/-" , round(score_xg_1_recall.std(),3))

Para balancear o conjunto de dados utilizaremos a biblioteca imblearn. Realizaremos o balanceamento no conjunto minoritário, criando novas entradas utilizando o OverSampling. Assim o conjunto terá mais clientes Churn.

#importando biblioteca para o balancemento dos dados
from imblearn.over_sampling import RandomOverSampler
#instanciando modelo
ros = RandomOverSampler()
#utilizando o método OverSampling para o balancemento
X_train_ros, y_train_ros = ros.fit_sample(X_train, y_train)
#tranformando em tipo Series
y_train_ros = pd.Series(y_train_ros)

Como o código é bem semelhante ao anterior, mostrarei apenas o resultado.

Chegou o momento de discutirmos os dois resultados.

Observa-se que o valor da acurácia (percentual total de acertos na classificação) para os dois modelos, balanceado e não balanceado, é bem semelhante. No entanto, nota-se que houve uma diferença significativa no resultado do Recall(Agarwal. R).

A métrica Recall apenas verifica os dados positivos, onde o cliente é Churn. O recall significa, de todos os valores positivos (1), quantos o modelo previu corretamente como sendo positivo.

Pode-se notar que no modelo desbalanceado, que temos menos clientes Churn, o modelo teve um percentual de acertos de 53,0%.

Um valor muito baixo, tendo a mesma chance de uma adivinhação aleatória de 50%.

No entanto, olhando o resultado Recall do modelo balanceado, onde os dados são 50% clientes Churn (1) e 50% clientes não Churn(0), houve uma melhora muito significativa no treino do modelo, o percentual de acerto foi para 83,6%.

Essa comparação evidencia a importância do conjunto de dados estar balanceado para a realização do treino.

obs: o balanceamento foi apenas realizado no conjunto treino, o conjunto teste não será alterado. Assim, quando realizarmos o teste do resultado será o mais real possível.

Grid Search!!!

Chegou a hora de treinarmos nosso modelo. Através da função Grid Search vamos encontrar os melhores parâmetros para previsão de novos dados.

Os parâmetros que vamos verificar são:

Learning rate: Taxa de aprendizado do modelo;

n_estimators: Número de árvores que será construídas;

max_depth: Profundidade da árvore.

Para isso foi utilizado o código abaixo:

#instanciando modelo
xg_best_params = xgb.XGBClassifier()
#definindo parâmetros para otimização
params = {'eta' : [0.0001, 0.001, 0.01],
'max_depth' : [5,7,9,10],
'n_estimators': [200,250,400,450],
'clf__tree_method': ["gpu_hist"]
}
#instanciando GridSearchCV
grid = GridSearchCV(xg_best_params, params, scoring='recall')
#aplicando fit para encontrar melhores parâmetros
grid.fit(X_train_ros, y_train_ros)
print('\t\tATRAVÉS DO GRID SEARCH OS MELHORES PARÂMETROS E SCORE FORAM:')
print('melhor parametro', grid.best_params_)
print('\t\t\tmelhor score', grid.best_score_)

Agora que encontramos os melhores parâmetros pela função Grid Search, podemos realizar o treino do algoritmo e seu teste.

Vale ressaltar que o resultado do GridSearch provavelmente não será o resultado real na hora de prevermos os valores do conjunto teste.

Treino e teste do modelo!!!

Abaixo está o código com o treinamento do algoritmo de machine learning.

#instanciando modelo modelo com melhores parâmetros
xg_best_params = xgb.XGBClassifier(n_estimators = 450, max_depth =
10, learning_rate = 0.0001)
#treinando modelo
xg_best_params.fit(X_train_ros, y_train_ros)
#fazendo a previsão do modelo
y_pred_val = xg_best_params.predict(np.asarray(X_test_val))
y_pred_proba = xg_best_params.predict_proba(np.asarray(X_test_val))

Agora para avaliarmos o resultado utilizaremos uma matriz de confusão.

A matriz de confusão tem a ideia de mostrar as previsões corretas e erradas do modelo de machine learning.

Utilizando a variável y_pred_val que é a previsão obtida e comparando com y_test_val que é nosso verdadeiro valor, que foi separado anteriormente, iremos construir a matriz. A matriz tem 4 partes:

  • Verdadeiro Negativo: Quando o valor real é negativo e o modelo preve como sendo negativo, previsão correta “Verdadeiro”;
  • Falso Positivo: Quando o valor real é negativo, porém o modelo preve como positivo, previsão errada “Falsa”;
  • Falso Negativo: Quando o valor real é positivo, mas o modelo preve como negativo, previsão errada “Falsa”;
  • Verdadeiro Positivo: Quando o valor real é positivo e o modelo preve como sendo positivo, previsão correta “Verdadeiro”.

A partir disso temos uma ideia básica do que é uma matriz de confusão, assim abaixo está o código para criação da mesma.

#matriz de confusão
skplt.metrics.plot_confusion_matrix(y_test_val, y_pred_val, normalize=True);

Para conhecimento, o valor de 0.65 é o Recall. Isso significa que hipoteticamente separando uma amostra, do conjunto teste, que possui somente a classe positiva, o modelo previu corretamente 65% delas, isso somente considerando os clientes Churn.

Já o valor de 0.75 é chamado de especialidade, tem o significado que hipoteticamente separando uma amostra, do conjunto teste, com apenas a classe negativa, o modelo previu corretamente 75%, levando em conta somente os cliente não Churn.

Não temos um um resultado tão bom, mas para nosso primeiro modelo criado está ótimo.

Claramente podemos tentar melhorar esses valores, talvez com um tratamento diferente de tranformações de dados, criando novas variáveis, utilizando outro algoritmo de machine learning, etc. No entanto, que tal utilizarmos a curva ROC para alterarmos os limites e vermos se os resultados mudam.

A teoria da curva ROC é muito interessante e importante, mas não irei explicar ela toda, pois levaria todo um artigo para demonstrá-la. Porém, acessem esse link, nele está uma ótima tese que li para entender a teoria básica por trás da curva ROC.

Sendo breve, a curva ROC tem em seu eixo x a TFP (Taxa de Falsos Positivos ou 1-Especialidade) e no eixo y aTVP (Taxa de Verdadeiros Positivos ou Recall).

Nossa matriz anterior foi realizada a partir de um limiar de 50%, o que é isso:

Significa que se a probabilidade de um cliente ser Churn, for maior do que 50% o resultado previsto será positivo (1) e se menor do que 50% será negativo (0).

Porém, podemos alterar esse limiar para um valor diferente de 50%. Na verdade a curva ROC são vários pontos plotados com os valores de TFP e TVP com diferentes limiares que resultam em diferentes matrizes de confusão e resultados (Matsubara. E).

UAU, muito legal isso não é?!?

Achei sensacional quando entendi!!!

Abaixo segue a parte principal do código para fazer o gráfico ROC.

#transformar y_pred_proba_val em uma dimensão apenas
predict = y_pred_proba[:,1]
#tranformar y_test_ros_val e uma array com uma dimensão
test = np.asarray(y_test_val)
#copilar valores
fpr, tpr, thresholds = roc_curve(test, predict)
#estilo do gráfico
sns.set_style('whitegrid')
#criando figure, axes e definindo tamanho do gráfico
fig, ax = plt.subplots(figsize=(12,8))
#plotando gráfico
sns.lineplot([0,1],[0,1], linestyle='--')
sns.scatterplot(fpr, tpr, ax=ax)
sns.lineplot(fpr, tpr, ax=ax)
#colocando legenda dos limiares dentro do gráfico
for i in range(1, len(thresholds), 20):
ax.text(x = fpr[i]-0.03, y = tpr[i]+0.03, s =
str(round(thresholds[i],3)), size=14)
#otimizando gráfico
fig.tight_layout();

Olhando o gráfico, nota-se que a curva ROC obtida não é tão boa, pois invés da curva crescer de forma mais paralela ao eixo y, na verdade ela começa inclinar cada vez mais para o eixo x. Uma boa curva ROC é mostrada abaixo:

Contudo, podemos modificar nossos resultados e a matriz de confusão a partir do gráfico ROC de uma maneira que os ganhos de uma companhia de telecomunicação sejam maiores.

Pensando em nosso problema, o que desejamos?

Previsão e melhoria do modelo.

Queremos prever quais são os clientes Churn, não é? Então precisamos melhorar nossa Taxa de Verdadeiro Positivo. No entanto, olhando no gráfico não podemos melhorar nosso verdadeiro positivo sem piorar o falso positivo.

Com uma análise macro da situação, não seria tão ruim prever um falso positivo, pois na pior das situações daríamos um mês gratís de um serviço da empresa ou algum outro benefício para conquistar aquele cliente que na verdade está satisfeito com a empresa. Já perder um cliente que não foi previsto corretamente como Churn seria uma situação ruim.

Claro, acima foi levantada apenas uma hipótese, cada empresa e companhia terá uma situação financeira, ganhos, objetivos entre outros aspectos estratégicos que podem nortear as decisões importantes do modelo de machine learning.

Então que tal modificar os limites de probabilidade para obter uma taxa 0.80 de verdadeiros positivos. Olhando no gráfico acima para obtermos este Recall é necessário seguir em linha reta do eixo y do valor de 0.80 e ver qual é o ponto em que intersecta a curva ROC.

A partir disso encontramos que esse ponto é aproximadamente 0.492 e verificando qual é valor x (TFP) para esse limite percebemos que é aproximadamente 0.38.

Assim, com o código abaixo podemos verificar com melhor exatidão qual é esse limiar de probabilidade.

for i in range(0, len(fpr), 1):
if((tpr[i]>0.796) & (fpr[i]<0.378)):
print(thresholds[i])

O valor que retorna do código é 0.48765573.

Esse resultado é o limite de probabilidade que utilizaremos na nova predição. Resumindo esse limite significa que se a probabilidade de um cliente ser da classe positiva (Cliente Churn) for maior que 0.48765573 este é considerado como da classe positiva.

P(x≥0.48765573) = 1

P(x<0.48765573) = 0

Agora vamos realizar novamente a previsão do conjunto de dados teste, porém agora utilizaremos o test_final. Como o código é muito semelhante aos anteriores, não irei colocá-lo abaixo, apenas o resultado da matriz de confusão.

Uau, conseguimos um melhor resultado para previsão dos clientes que são Churn. Nosso modelo de machine learning consegue acertar uma taxa de 79% os clientes Churn e 61% os clientes não Churn.

Claro, temos uma perda de acertos de clientes da classe negativa e também caso verificarmos a precisão que é uma outra métrica muito importante que não irei discuti-la neste artigo, provavelmente essa métrica também foi afetada. Porém, para um modelo inicial conseguimos fazer e entender coisas muito interessantes.

Infelizmente, estamos chegando ao fim do nosso artigo, mas antes de terminarmos podemos concluir algumas coisas.

Conclusão

Podemos concluir que o resultado do nosso algoritmo de machine learning está aceitável dentro de nossas hipóteses. Claro, que pode ser melhorado e modificado caso em alguma análise mais apurada não traga tantos ganhos a uma companhia.

Contudo, podemos entender algumas coisas bem interessantes com a construção deste modelo de machine learning, como por exemplo:

  • Pré processamento de dados;
  • Validação Cruzada;
  • Grid Search;
  • Treino e teste do modelo;
  • Matrizes de confusão;
  • Gráfico ROC.

Dessa forma, ficaremos por aqui nesse artigo. Caso tenha lido e gostado, por favor curta o artigo :)

Agradeço.

--

--

No responses yet