![](https://crypto4nerd.com/wp-content/uploads/2023/07/0HmNz-8-FEVPzpcfP-1024x683.jpeg)
Um dos desafios que as empresas de telecomunicação enfrentam é de tentar manter os seus atuais clientes e evitar que eles deixem a empresa. Isto ocorre porque é bem mais barato manter um cliente do que conquistar clientes novos. Alguns estudos mostram a diferença entre manter e adquirir um cliente novo chega a 25 vezes.
Por isso as empresas tentam fidelizar seus clientes através de técnicas conhecidas como Customer Success (sucesso do cliente). Estas técnicas abrangem:
- Investir em programas de fidelidade;
- Criar canais para obter feedback do cliente;
- Aperfeiçoar o atendimento ao cliente;
- Melhorar os processos de pós-venda;
- Identificar os clientes que estão mais propensos a cancelar.
Para o último item acima, pode se usar modelos de machine learning para identificar os clientes mais propensos a cancelar. É o que veremos neste artigo em que iremos utilizar diversos modelos de ML e algumas técnicas para melhorar o desempenho delas
Os dados que iremos utilizar neste artigo foram obtidos através da plataforma Kaggle. Tratam-se de dados oriundos de uma plataforma de ensino da IBM e que se referem uma empresa de fictícia de telecomunicações chamada Telco.
Este conjunto de dados possui 7043 registros, sendo que pouco mais que um quarto deles são de clientes que deixaram a empresa. Há também 21 colunas, cujas descrições são vistas a seguir, entre parêntese, o tipo de dados, ou os valores possíveis:
Dicionário de dados
CustomerID
: ID do cliente (texto);gender
: se o cliente é homem ou mulher (Male, Female);SeniorCitizen
: Se o cliente é idoso ou não (1, 0);Partner
: Se o cliente tem um parceiro ou não (Yes, No);Dependents
: Se o cliente tem dependentes ou não (Yes, No);tenure
: Número de meses que o cliente permaneceu na empresa (numérico);PhoneService
: Se o cliente possui atendimento telefônico ou não (Yes, No);MultipleLines
: Se o cliente tem várias linhas ou não (Yes, No, No phone service);InternetService
: Provedor de serviços de Internet do cliente (DSL, Fibra ótica, Não);OnlineSecurity
: Se o cliente tem segurança online ou não (Yes, No, No internet service);OnlineBackup
: Se o cliente tem backup online ou não (Yes, No, No internet service);DeviceProtection
: Se o cliente tem proteção de dispositivo ou não (Yes, No, No internet service);TechSupport
: Se o cliente tem suporte técnico ou não (Yes, No, No internet service);StreamingTV
: Se o cliente tem streaming de TV ou não (Yes, No, No internet service);StreamingMovies
: Se o cliente tem streaming de filmes ou não (Yes, No, No internet service);Contract
: O prazo do contrato do cliente (Month-to-month, One year, Two year);PaperlessBilling
: Se o cliente tem faturamento sem papel ou não (Yes, No);PaymentMethod
: O método de pagamento do cliente (Electronic check, Mailed check, Bank transfer (automatic), Credit card (automatic));MonthlyCharges
: O valor cobrado mensalmente do cliente (numérico);TotalCharges
: O valor total cobrado do cliente (texto);Churn
: Se o cliente cancelou ou não (Yes or No).
Estranhamente a coluna TotalCharges
é do tipo texto:
Ao se analisar o arquivo CSV que contém os dados, verificou-se que algumas linhas não traziam este valor preenchido. Isto ocorre quando o cliente novo e ainda não fez nenhum pagamento. Então teremos de transformar estes valores vazios em zeros:
df['TotalCharges']= df['TotalCharges'].apply(lambda x: 0.0 if x == ' ' else float(x))
Feito isso a coluna TotalCharges
se torna numérica como se pode verificar ao executar o o comando df.dtypes
:
Verificando se existem dados ausentes:
Antes de iniciarmos nossa predição de cancelamentos, precisamos efetuar alguns procedimentos. Primeiro, vamos verificar se existem dados ausentes, e em seguida verificar se os dados estão balanceados, ou seja, se a nossa variável alvo, no caso os cancelamentos possuem um equilíbrio entre sim e não.
Como se pode notar acima, o conjunto de dados não possui valores nulos:
Verificando o balanceamento dos dados:
Como se pode notar, o conjunto de dados encontra-se desbalanceado, sendo que os cancelamentos representam pouco mais de 26,5% dos registros.
Traçando um perfil dos clientes da Telco
Vamos agora mostrar alguns gráficos que mostram um perfil dos clientes da companhia de telecomunicações. São informações referentes ao tempo de contrato e seus valores médios, diferenças entre clientes que cancelaram e não cancelaram
Por outro lado, apenas uma minoria de clientes é idosa e possui dependentes. Quanto aos serviços, a maioria possui serviços de telefonia e de Internet. A forma de contrato mais comum é de mês a mês e a forma de pagamento mais utilizada é cheque eletrônico.
Vamos agora analisar o perfil de quem está mais propenso a cancelar os serviços com a operadora:
Há uma diferença entre o perfil dos usuários que cancelaram os serviços com a a empresa e os que não cancelaram. Os que cancelaram, tem em média, as mensalidades mais caras e menos tempo de empresa, consequentemente, os valores totais pagos por estes clietes (gráfico mais à direita) são menores.
As características dos clientes mais propensos a cancelar são estas:
- cliente mais idoso;
- possui serviço de Internet por fibra óptica;
- não possui serviço de suporte técnico, nem de proteção de dispositivo, mas possui serviço de backup;
- contrato na modalidade mês a mês;
- faz pagamento via cheque eletrônico.
Por outro lado, os que têm a menor possibilidade de cancelar, apresentam o seguinte perfil:
- possui cônjuge;
- possui dependentes;
- não tem serviço de Internet;
- possui contrato de 2 anos;
- recebe cobrança em papel;
- efetua pagamento via cartão de crédito.
Transformação das colunas
Antes de executar nossos modelos de machine learning temos de fazer alguns preparativos. Primeiro, vamos remover a coluna customerId
, que não é relevante para nossos propósitos. A seguir, iremos transformar todos os dados em numéricos utilizando métodos de codificação abaixo:
- OneHotEncoder: para colunas que aceitam 3 ou mais valores
- OrdinalEncoder: para colunas com apenas valores do tipo sim ou não.
- LabelEncoder: com um funcionamento semelhante ao de cima, porém recomendado apenas para as colunas alvo´, no caso
Churn
.
Além disso vamos fazer uma padronização nos dados numéricos usando a classe StandardScaler
É possível executar os procedimentos acima, com exceção da codificação LabelEncoder
, usando o método make_column_transformer da biblioteca do Scikit-learn. O código abaixo executa todos os procedimentos acima.
#1ª etapa: remover a coluna customer_id
df.drop("customerID",axis=1,inplace=True)#2ª etapa: transformar a coluna 'Churn' para numérica com possiveis valores usando 0 e 1
#O LabelEncoder não pode ser executado usando o método make_column_transfer
le = LabelEncoder()
df["Churn"] = le.fit_transform(df["Churn"])
#3º etapa: efetuar o tramamento em todas as colunas, com exceção da coluna Churn
transform = make_column_transformer(
(StandardScaler(), ["tenure","MonthlyCharges","TotalCharges"]), #Aplicando StandardScaler nas colunas numéricas
(OneHotEncoder(), ['gender','MultipleLines','InternetService','OnlineSecurity','DeviceProtection','TechSupport','StreamingTV','StreamingMovies',
'Contract','PaymentMethod','OnlineBackup']), #Codificação OneHotEncoder p/ colunas categóricas com mais de dois valores
(OrdinalEncoder(),['Dependents','Partner','PhoneService','PaperlessBilling']) , #Codificação OrdinalEncoder p/ colunas com apenas dois valores possíveis.
remainder="passthrough"
)
transformed = transform.fit_transform(df)
transformed_df = pd.DataFrame(transformed, columns=transform.get_feature_names_out())
transformed_df.head()
Quando o método make_column_transformer
é executado, as colunas são renomeadas, adicionando um prefixo indicando qual o operação foi executada, se for um StandardScaler, a coluna é prefixada com standarscaler_
, se for OneHotEncoder, o prefixo é onehotencoder_
, e assim por diante. Caso não tenha sofrido nenhum pré-processamento e a opção remainder
for definida como passthrough
, o prefixo é remainder_
. No nosso caso, as colunas ficarão assim:
Criando conjuntos de treino e de validação
Iremos separar o conjunto de dados atual separando 15% dos registros para a validação final.
Efetuando o balanceamento dos dados
O conjunto de dados encontra-se desbalanceado, pois um pouco mais de um quarto dos registros correspondem a cancelamentos. Vamos balancear utilizando a técnica de oversampling, que gera dados da classe minoritária. A biblioteca escolhida é a classe SMOTE da biblioteca imblearn.
Pontos importantes
Iremos fazer um comparativo com diversos modelos de machine learning e mostrar no final um comparativo resumido com os modelos testados. Os modelos a serem testados serão:
- Regressão logística
- Máquinas de vetor de suporte (SVM)
- Árvore de decisão
- Naive Bayes
- Gradiente descendente estocástico
- XGBoost
- Floresta aleatória (Random forest)
- AdaBoost
- Classificador de votação
#definindo as métricas de desempenho a serem utilizadas
scoring = {"AUC": "roc_auc", "Accuracy": "accuracy","Precision":"precision","Recall":"recall"}
Iremos utilizar validação cruzada e faremos testes com diversos hiperparâmetros usando gridsearch, que executa teste exaustivos com todos as combinações de hiperparâmetros passados. Serão mostradas as métricas mais importantes: acurácia, recall, precisão e AUC, porém a que será levada em conta para definir o melhor desempenho é o recall, (ou o termo aportuguesado revocação), que mede a capacidade de se detectar corretamente fraudes.
As únicas exceções serão o modelo XGBoost e o Classificador de votação. Vamos mostrar um resumo com cada um dos métodos
Regressão Logística
Parâmetros testados: C
e solver
# 2. Instanciar e escolher os hyperparameters
model = LogisticRegression(max_iter=300)
parameters = {
'C': [0.1,1,10,100,1000],
'solver':['lbfgs', 'liblinear', 'newton-cg', 'newton-cholesky']
} clf = GridSearchCV(model, parameters, scoring=scoring,refit="Recall")
clf.fit(X_smt, y_smt)
Melhor desempenho: 81,3% usando C=100
e, solver=lbfgs
SVM
Parâmetros testados: C
e kernel
model = SVC()
kernel = ['poly', 'rbf']
C = [100, 50, 10, 1.0]
# define grid search
parameters = dict(kernel=kernel,C=C)
clf = GridSearchCV(model, parameters, scoring=scoring,refit="Recall")
clf.fit(X_smt, y_smt)
Melhor desempenho: 88,0% usando C=50
e kernel='rbf'
Árvore de decisão
Parâmetros testados: criterion
e max_depth
model=DecisionTreeClassifier()
parameters ={
"max_depth":[2,3,4,5],
"criterion":["gini", "entropy", "log_loss"]
}
clf = GridSearchCV(model, parameters, scoring=scoring,refit="Recall")
clf.fit(X_smt, y_smt)
Melhor desempenho: 83,2% usando criterion='entropy’
e max_depth=5
Naive Bayes
Parâmetro testado: var_smoothing
model = GaussianNB()
parameters = {
'var_smoothing': [1e-9,1e-5,0.001,0.01,0.1],
}clf = GridSearchCV(model, parameters, scoring=scoring,refit="Recall")
clf.fit(X_smt, y_smt)
Melhor desempenho: 87,8% usando var_smoothing=0.01
Gradiente descendente estocástico
Parâmetros testados: loss
, learning_rate
, alpha
e eta0
model = SGDClassifier()parameters={
'loss': ['hinge', 'log_loss', 'perceptron'],
'learning_rate':['optimal','constant','invscaling'],
'alpha':[0.0001,0.001, 0.01],
'eta0':[0.01,0.1,1.0]
}
clf = GridSearchCV(model, parameters, scoring=scoring,refit="Recall")
clf.fit(X_smt, y_smt)
Melhor desempenho: 87,8% usando: loss='perceptron'
, learning_rate='optimal'
, alpha=0.001
e eta0=0.1
XGBoost
Obs.: Este foi o único método em que não foi usado o grid search, mas sim o random search, configurado para 10 testes. Pois o primeiro estava demorando mais de uma hora para executar.
Parâmetros testados: learning_rate
, n_estimators
, max_depth
e subsample
# 2. Instanciar e escolher os hyperparameters
model = XGBClassifier()parameters = {
'n_estimators': [10,100,1000],
'learning_rate': [0.001, 0.01, 0.1],
'max_depth': [2,3,7,9],
'subsample': [0.5,0.7,0.8,0.9,1.0],
}
clf = RandomizedSearchCV(model, parameters, scoring=scoring,refit="Recall")
clf.fit(X_smt, y_smt)
Melhor desempenho: 85,6% com learning_rate=0.1
, n_estimators=100
, max_depth=2
e subsample=1.0
Floresta aleatória
Parâmetros testados: n_estimators
e max_features
# define models and parameters
model = RandomForestClassifier()
n_estimators = [10, 100, 1000]
max_features = ['sqrt', 'log2']
parameters = dict(n_estimators=n_estimators,max_features=max_features)clf = GridSearchCV(model, parameters, scoring=scoring,refit="Recall")
clf.fit(X_smt, y_smt)
Melhor desempenho: 86,9% com n_estimators=1000
e max_features='log2'
AdaBoost
Parâmetros testados: learning_rate
e n_estimators
parameters = {
'n_estimators': [10,100,1000],
'learning_rate': [0.001, 0.01, 0.1,1],
}
model=AdaBoostClassifier()
clf = GridSearchCV(model, parameters, scoring=scoring,refit="Recall")
clf.fit(X_smt, y_smt)
Melhor desempenho: 90,6% com learning_rate=0.001
e n_estimators=10
Classificador de votação
Foi utilizado um classificador combinando 3 modelos de machine learning: regressão logística, Naive Bayes e SVM e testes foram feitos usando validação cruzada
#inicializando modelos de ML
lr = LogisticRegression(max_iter=300,C= 1000, solver ='lbfgs')
nb = GaussianNB(var_smoothing = 0.1)
svm = SVC(C=10, kernel='poly')
vtc = VotingClassifier(estimators=[
('lr', lr), ('svm', svm), ('gnb', nb)], voting='hard')
#Se incluir a métrica 'roc_auc' dá erro de execução.
scoring = ["accuracy","precision","recall"]
scv = cross_validate(vtc, X_smt, y_smt, cv=5, scoring=scoring)
Com este modelo obtemos um recall de 85,6%
Comparativo entre os modelos de machine learning empregados neste projeto:
Para finalizar iremos testar o modelo vencedor, que foi o AdaBoost nos dados de validação, que foram separados na etapa inicial do projeto, e os quais o modelo não conhece:
# Criando o modelo de ML
model = AdaBoostClassifier(learning_rate=0.001, n_estimators= 10)#treinando o modelo
model.fit(X_smt, y_smt)
## Preparndo os dados de teste
X = test.drop("remainder__Churn",axis=1)
y= test["remainder__Churn"]
y_pred = model.predict(X)
Abaixo a matriz de confusão obtida ao aplicar o AdaBoost nos dados de teste:
As métricas de desempenho pioram sensivelmente ao executar o modelo sobre dados de teste, nunca vistos anteriormente. Ainda assim, o AdaBoost
foi capaz de identificar mais 85% dos cancelamentos (226 de 275).
Vários testes foram realizados, com ajustes de hiperparâmetros e no geral não há grandes diferenças entre os métodos testados. O AdaBoostClassifier
se saiu melhor, seguido de perto pelo modelo de Máquina de Vetor de Suporte e do Gradiente Descendente. Nota-se também que há uma queda do desempenho dos modelos de ML no processo de validação, quando se faz predições em dados não vistos anteriormente e não balanceados, ainda assim, o modelo AdaBoost
se saiu bem com índice de acertos de 85%. Porém a precisão cai significativamente, indicando que o modelo incorre em muitos falsos positivos. Serão necessárias técnicas mais avançadas de machine learning para melhorar a precisão.
Clique aqui para baixar o código fonte completo do notebook utilizado neste projeto