É cada vez mais comum encontrarmos empresas que se baseiam no modelo de negócio chamado de Saas ou Software as a Service que, incialmente, era o termo usado para definir, por exemplo, programas que eram vendidos no formato de licensa temporária, mas que, com o advento de serviços de assinatura nos mais diversos segmentos, passou a ser utilizado num sentido mais amplo. Como se trata de uma espécia de aluguel de um produto ou serviço, a saúde da empresa estará diretamente ligada ao chamado Customer Churn, conceito este que poderia ser explicado como sendo a taxa de cancelamento por parte dos clientes associada a um determinado período de tempo.
Neste estudo, vamos utilizar o dataset referente a uma empresa do ramo de Telecom fornecido pela Data Science Academy dentro do programa "Formação Cientista de Dados", como sendo um dos projetos propostos para serem desenvolvidos pelos alunos da plataforma ao longo da formação. O objetivo é criar um modelo de aprendizado de máquina capaz de prever ser um cliente pode ou não cancelar o seu plano e qual é a probabilidade relativa a ambas as possibilidades.
O conjunto de dados fornecido apresenta 20 variáveis, sendo elas:
Baseado nos dados fornecidos, também tentaremos responder a alguns questionamentos que podem nos trazer insights interessantes sobre a situação dos clientes desta empresa de Telecom:
# Pacotes utilizados:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
import warnings
warnings.filterwarnings('ignore')
from sklearn.preprocessing import MinMaxScaler
from imblearn.over_sampling import SMOTE
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix, plot_confusion_matrix
from sklearn.linear_model import LogisticRegression
from xgboost import XGBClassifier
from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV
# Leitura do arquivo:
cust_churn = pd.read_csv('projeto4_telecom_treino.csv')
cust_churn.head()
cust_churn.dtypes
Vamos excluir a primeira variável (sem nome), pois ela não será necessária:
# Excluindo a variável "unnamed":
cust_churn = cust_churn.drop(cust_churn.iloc[:, [0]], axis = 1)
Com os dados carregados, daremos início ao processo de análise exploratória com intuito de extrair informações do conjunto de dados fornecidos. Vamos focar em responder as questões apresentadas na introdução deste estudo.
Antes disso, faremos um resumo estatístico das variáveis para termos uma ideia da variação destes atributos:
cust_churn.iloc[:, 0:12].describe()
cust_churn.iloc[:, 12:].describe()
Vamos criar subsets, dividindo o conjunto de dados original entre as observações de cancelamento e não cancelamento para, então, criarmos um gráfico de contagem do número de ocorrências de cancelamento ou não cancelamento por estado:
# Criando o subset de observações de cancelamento:
churn_y = cust_churn[cust_churn['churn'] == 'yes']
# Criando o gráfico de contagem de cancelamentos por estado:
fig, ax = plt.subplots(nrows = 1, ncols = 1, figsize = (16, 6))
plt.yticks([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])
sns.countplot(x = 'state', order = churn_y['state'].value_counts().index, data = churn_y, palette = 'Reds_r').set(
xlabel = 'Estado',
ylabel = 'Número de Cancelamentos',
title = 'Contagem de Cancelamentos por Estado')
fig.show()
Podemos observar que, em números absolutos, os estados do Texas e New Jersey empatam com o maior número de ocorrências de cancelamento neste conjunto de dados. Maryland, Michigan e, empatados, Minnesota e New York completam o ranking de estados com maior número absoluto de clientes que cancelaram seus planos com a empresa.
# Criando o subset de observações de não cancelamento:
churn_n = cust_churn[cust_churn['churn'] == 'no']
# Criando o gráfico de contagem de não cancelamentos por estado:
fig, ax = plt.subplots(nrows = 1, ncols = 1, figsize = (16, 6))
plt.yticks([5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100])
sns.countplot(x = 'state', order = churn_n['state'].value_counts().index, data = churn_n, palette = 'Blues_r').set(
xlabel = 'Estado',
ylabel = 'Número de Clientes Mantidos',
title = 'Contagem de Clientes Mantidos por Estado')
fig.show()
Entre os clientes que foram mantidos no período de abrangência do conjunto de dados, o estado de West Virginia se destaca muito em relação aos demais estados. O ranking positivo se completa com os estados de Virginia, Alabama, Wisconsin e Minnesota, sendo que estes estados apresentam números muito semelhantes.
Agora vamos avaliar a relação do gasto dos clientes com o cancelamento da conta:
# Selecionando as colunas que indicam tempo de contrato, gasto do cliente e se o cancelamento ocorreu:
cust_charge = cust_churn[['account_length', 'total_day_charge', 'total_eve_charge', 'total_night_charge', \
'total_intl_charge', 'churn']]
# Transformando as variáveis para obtermos o gasto médio diário dos clientes:
cust_charge['avg_day_charge'] = cust_charge['total_day_charge'] / cust_charge['account_length']
cust_charge['avg_eve_charge'] = cust_charge['total_eve_charge'] / cust_charge['account_length']
cust_charge['avg_night_charge'] = cust_charge['total_night_charge'] / cust_charge['account_length']
cust_charge['avg_intl_charge'] = cust_charge['total_intl_charge'] / cust_charge['account_length']
# Selecionando as novas variáveis e agrupando-as pelo "churn" e calculando os gastos médios dos clientes:
cust_charge_mean = cust_charge[['churn', 'avg_day_charge', 'avg_eve_charge', 'avg_night_charge', \
'avg_intl_charge']].groupby(['churn'], as_index = False).mean()
cust_charge_mean['avg_total'] = cust_charge_mean['avg_day_charge'] + cust_charge_mean['avg_eve_charge'] + \
cust_charge_mean['avg_night_charge'] + cust_charge_mean['avg_intl_charge']
# Visualizando o dataset tranformado:
cust_charge_mean
Para melhor visualização, vamos criar gráficos que mostram o gasto médio diário discriminado por clientes que cancelaram e que não cancelaram suas contas com a empresa. Vamos visualizar o gasto por horário do dia bem como o gasto diário total:
# Criando o gráfico:
fig, ax = plt.subplots(nrows = 1, ncols = 2, figsize = (15, 5))
sns.barplot(x = 'churn', y = 'avg_day_charge', data = cust_charge_mean, ci = None,
palette = 'autumn_r', ax = ax[0]).set_title('Gasto Médio Horário Comercial')
ax[0].set_xlabel('cancelamento')
ax[0].set_ylabel('gasto médio diário')
sns.barplot(x = 'churn', y = 'avg_eve_charge', data = cust_charge_mean, ci = None,
palette = 'autumn_r', ax = ax[1]).set_title('Gasto Médio Pós-Horário Comercial')
ax[1].set_xlabel('cancelamento')
ax[1].set_ylabel('gasto médio diário')
fig.show()
# Criando o gráfico:
fig, ax = plt.subplots(nrows = 1, ncols = 2, figsize = (15, 5))
sns.barplot(x = 'churn', y = 'avg_night_charge', data = cust_charge_mean, ci = None,
palette = 'autumn_r', ax = ax[0]).set_title('Gasto Médio Noturno')
ax[0].set_xlabel('cancelamento')
ax[0].set_ylabel('gasto médio diário')
sns.barplot(x = 'churn', y = 'avg_intl_charge', data = cust_charge_mean, ci = None,
palette = 'autumn_r', ax = ax[1]).set_title('Gasto Médio Internacional')
ax[1].set_xlabel('cancelamento')
ax[1].set_ylabel('gasto médio diário')
fig.show()
# Criando o gráfico:
fig, ax = plt.subplots(nrows = 1, ncols = 1, figsize = (7, 5))
sns.barplot(x = 'churn', y = 'avg_total', data = cust_charge_mean, ci = None,
palette = 'autumn_r').set_title('Gasto Médio Total')
ax.set_xlabel('cancelamento')
ax.set_ylabel('gasto médio diário')
fig.show()
Podemos observar que os clientes que cancelaram têm um gasto médio mais elevado em horário comercial e com ligações internacionais. O gasto médio diário é levemente menor nos demais horários. Somando os gastos relativos a todos os períodos, pode-se observar que os clientes que solicitaram o cancelamento dos serviços têm um gasto médio maior que os clientes que permaneceram.
Embora não se possa afirmar que existe uma relação de causalidade entre gasto médio e cancelamento, é possível ter uma ideia do perfil dos clientes com potencial para cancelar a relação com a empresa, sendo que este perfil tem a tendência de ser caracterizado pelo maior gasto com ligações em horário comercial e com ligações internacionais.
O próximo passo é analisar as ligações para o SAC - Serviço de Atendimento ao Consumidor:
# Selecionando as colunas que indicam se o cancelamento ocorreu, o tempo de contrato, e o número de ligações ao SAC:
cust_serv_calls = cust_churn[['churn', 'account_length', 'number_customer_service_calls']]
# Criando uma variável que indica o número de ligação para um período de 100 dias:
cust_serv_calls['calls_per_100_days'] = (cust_serv_calls['number_customer_service_calls'] / \
cust_serv_calls['account_length'])*100
# Agrupando o subset pelo "churn" e retornando a média geral de ligações ao SAC e a média de ligações ao SAC por 100 dias:
cust_serv_calls = cust_serv_calls[['churn', 'number_customer_service_calls', \
'calls_per_100_days']].groupby(['churn'], as_index = False).mean()
# Arredondando valores:
cust_serv_calls['number_customer_service_calls'] = cust_serv_calls['number_customer_service_calls'].round(decimals = 2)
cust_serv_calls['calls_per_100_days'] = cust_serv_calls['calls_per_100_days'].round(decimals = 2)
# Visualizando o subset transformando:
cust_serv_calls
# Criando um gráfico para diferença entre o número de ligações para clientes que cancelaram e não cancelaram seus planos:
fig, ax = plt.subplots(nrows = 1, ncols = 2, figsize = (15, 5))
sns.barplot(x = 'churn', y = 'number_customer_service_calls', data = cust_serv_calls, ci = None,
palette = 'autumn_r', ax = ax[0]).set_title('Número Médio de Ligações para o SAC')
ax[0].set_xlabel('cancelamento')
ax[0].set_ylabel('nº médio de ligações')
sns.barplot(x = 'churn', y = 'calls_per_100_days', data = cust_serv_calls, ci = None,
palette = 'autumn_r', ax = ax[1]).set_title('Número Médio de Ligações para o SAC por 100 Dias')
ax[1].set_xlabel('cancelamento')
ax[1].set_ylabel('nº médio de ligações a cada 100 dias')
fig.show()
Através desta análise, fica claro que os clientes que cancelaram seus planos ligaram mais vezes para o Serviço de Atendimento ao Consumidor em comparação com os clientes que não fizeram o cancelamento. Na comparação de número de ligações ao SAC por 100 dias, pode-se observar que a proporção de ligações praticamente dobra para o caso dos clientes que efetuaram o cancelamento.
Este fato pode dar um indicativo à empresa de Telecom que o Serviço de Atendimento ao Consumidor pode ser peça chave para evitar que um cliente, de fato, efetue o cancelamento do seu plano, seja melhorando este serviço de atendimento ou fazendo uso deste contato para propor vantagens que possam deixar o cliente mais satisfeito.
A partir deste momento, vamos aplicar transformações nos dados com o intuito de melhorar a sua qualidade para que possamos entregar dados mais limpos e expressivos para o algoritmo de machine learning. A ideia é avaliar como os dados se relacionam entre si e com a variável alvo, para que possamos selecionar atributos mais significativos.
Como o conjunto de dados apresenta clientes que permaneceram por períodos distintos, a primeira transformação a ser feita será a "equalização" dos dados, transformando as variáveis que indicam valores totais (ao longo de todo período) para variáveis que indicam valores unitários (por dia). Assim teremos um ponto de partida comum:
# Criano uma função para equalizar os dados do dataset:
def equaliza_data(df):
'''Equaliza as variáveis numéricas e retorna o dataset transformado.
Args:
df: dataset a ser transformado.
'''
# Criando uma lista das colunas que não serão transformadas:
no_equaliz = ['state', 'account_length', 'area_code', 'international_plan', 'voice_mail_plan', 'churn']
# Criano um loop for para transformar as colunas de interesse:
for i in range(0, len(df.columns)):
if df.columns[i] not in no_equaliz:
df[df.columns[i]] = df[df.columns[i]] / df['account_length']
return df
# Aplicando a função ao dataset:
cust_churn_trnsf = equaliza_data(cust_churn)
Como a informação da variável account_length foi incorporada indiretamente às demais variáveis no item anterior, vamos excluir a variável do conjunto de dados. Da mesma forma, por termos um grande números de estados e códigos de área (classes) em relação ao número de observações, resolvemos excluir também as variáveis state e area_code:
cust_churn_trnsf = cust_churn_trnsf.drop(['account_length', 'state', 'area_code'], axis = 1)
Visto que as variáveis international_plan e voice_mail_plan indicam se o cliente contratou ou não um determinado plano oferecido pela empresa, vamos transformá-las em variáveis numéricas, utilizando 0 para indicar que o cliente não possui o plano e 1 para indicar que o cliente adquiriu o plano oferecido.
Vamos utilizar esta mesma lógica para tratar a variável churn, ou seja, 0 indicará que o cliente não cancelou, enquanto 1 indicará que o cancelamento foi efetivado.
# Selecionando as colunas que serão transformadas:
classes = ['international_plan', 'voice_mail_plan', 'churn']
# Aplicando a transformação às colunas:
for column in classes:
cust_churn_trnsf[column] = cust_churn_trnsf[column].apply(lambda x: 0 if x == "no" else 1)
Ainda que nem todos os algoritmos de aprendizado de máquina necessitem receber os dados normalizados, vamos aplicar o processo de normalização para deixar os dados padronizados em uma mesma escala e poder apresentar os mesmos dados a diferentes algoritmos:
# Criando o objeto scaler:
scaler = MinMaxScaler(feature_range = (0,1))
# Aplicando a padronização aos dados:
cust_churn_padrao = scaler.fit_transform(cust_churn_trnsf)
# Transformando o array criado para dataframe:
cust_churn_padrao = pd.DataFrame(cust_churn_padrao, columns = cust_churn_trnsf.columns)
Nesta etapa, vamos avaliar a correlação entre as variáveis para que possamos fazer uma pré-seleção daquelas que possam ser mais relevantes para o processo de construção do modelo de aprendizado:
# Calculando a correlação entre as variáveis:
cust_churn_corr = cust_churn_padrao.corr()
# Criando um gráfico da matriz de correlação:
fig, ax = plt.subplots(nrows = 1, ncols = 1, figsize = (17, 15))
sns.heatmap(cust_churn_corr, annot = True, fmt = '.3g', vmin = -1, vmax = 1, center = 0,
cmap = 'RdYlBu', square = True)
ax.set_title('Matriz de Correlação entre Variáveis')
fig.show()
Como já esperado, pode-se observar pela matriz acima que muitas variáveis apresentam uma correlação muito elevada entre si. Em outras palavras, podemos dizer que elas representam a mesma informação, como no caso, por exemplo, das variáveis total_day_minutes, total_day_calls e total_day_charge. Ou seja, as variáveis não são independentes entre si, pois se aumentássemos o total de minutos, automaticamente estaríamos aumentando o gasto e, provavelmente, teríamos um número maior de ligações.
Sendo assim, entre estas variáveis que são dependentes entre si, vamos selecionar apenas as que indicam o gasto:
# Variáveis a serem descartadas:
var_dependentes = ['total_day_minutes', 'total_day_calls', 'total_night_minutes', 'total_night_calls',
'total_intl_minutes', 'total_intl_calls']
# Descartando as variáveis:
cust_churn_padrao = cust_churn_padrao.drop(var_dependentes, axis = 1)
# Salvando a lista final de colunas selecionadas:
cols_final = cust_churn_padrao.columns
Como queremos que o modelo a ser criado aprenda igualmente sobre duas classes distintas (clientes que cancelam o plano e clientes que não o cancelam), precisamos garantir o balanceamento dos dados, ou seja, precisamos ter um número de observações similares para ambas as classes.
Primeiramente, vamos verificar se o conjunto de dados está desbalanceado:
# Agrupando por classe e contabilizando o número de observações:
cust_churn_padrao.groupby(['churn']).size()
Claramente, temos muito mais informações sobre clientes que não cancelaram do que sobre os clientes que efetivamente cancelaram o plano. Sendo assim, vamos aplicar o SMOTE para que a proporção de dados de cada classe seja mais proporcional:
# Dividindo os dados em variáveis preditoras e variável target:
X = cust_churn_padrao.iloc[:, :-1]
Y = cust_churn_padrao.iloc[:, -1]
# Aplicando SMOTE aos dados de treino:
sm = SMOTE(random_state = 101)
X_treino, Y_treino = sm.fit_sample(X, Y)
Nesta etapa, vamos fazer uso do algoritmo de aprendizado chamado Logistic Regression. Primeiramente, o modelo será criado para, então, alimentá-lo com os dados de treino. Criaremos outro modelo, utilizando o algoritmo XGBoost Classifier, para então compararmos o desempenho dos dois. Ainda uma terceira alternativa será criada, no caso, fazendo uso do algoritmo SVC.
Com os modelos treinados, será possível fazer as previsões a partir do conjunto de dados de teste para que, finalmente, possamos avaliar o desempenho de cada um dos modelos na predição das classes desejadas. Sendo assim, vamos carregar os dados de teste que foram fornecidos:
# Carregado o arquivo com o conjunto de dados para teste:
cust_churn_teste = pd.read_csv("projeto4_telecom_teste.csv")
Com o arquivo carregado, faremos as mesmas transformações realizadas no conjunto de dados de treino:
# Equalizando os dados:
cust_churn_teste_trnsf = equaliza_data(cust_churn_teste)
# Exclusão de variáveis:
cust_churn_teste_trnsf = cust_churn_teste_trnsf.drop(['account_length', 'state', 'area_code'], axis = 1)
# Tratamento de variáveis do tipo classe:
for column in classes:
cust_churn_teste_trnsf[column] = cust_churn_teste_trnsf[column].apply(lambda x: 0 if x == "no" else 1)
# Normalização dos dados de teste:
cust_churn_teste_padrao = scaler.fit_transform(cust_churn_teste_trnsf)
# Transformando o array criado para dataframe:
cust_churn_teste_padrao = pd.DataFrame(cust_churn_teste_padrao, columns = cust_churn_teste_trnsf.columns)
# Feature Selection:
cust_churn_teste_padrao = cust_churn_teste_padrao[cols_final]
# Dividindo os dados de teste em variáveis preditoras e variável target:
X_teste = cust_churn_teste_padrao.iloc[:, :-1]
Y_teste = cust_churn_teste_padrao.iloc[:, -1]
Criando o modelo de Regressão Logística:
# Criando o modelo de Regressão Logística:
model = LogisticRegression(random_state = 101)
# Treinando o modelo com os dados de treino:
model.fit(X_treino, Y_treino)
Com o modelo treinado, vamos fazer as previsões para os dados de teste:
# Aplicando o modelo aos dados de teste:
Y_pred = model.predict(X_teste)
Vamos criar um segundo modelo preditivo, desta vez utilizando o XGBoost Classifier:
# Criando o modelo XGBoost Classifier:
model_xgb = XGBClassifier(random_state = 101)
# Treinando o modelo com os dados de treino:
model_xgb.fit(X_treino, Y_treino)
Da mesma forma que anteriormente, faremos as previsões dos dados de teste, utilizando o novo modelo criado:
# Aplicando o novo modelo aos dados de teste:
Y_pred_xgb = model_xgb.predict(X_teste)
Criando nosso terceiro modelo preditivo, desta vez o Support Vector Classifier:
# Criando o modelo SVC:
model_svc = SVC(random_state = 101)
# Treinando o modelo com os dados de treino:
model_svc.fit(X_treino, Y_treino)
Fazendo as novas previsões com o modelo SVC:
# Aplicando o terceiro modelo aos dados de teste:
Y_pred_svc = model_svc.predict(X_teste)
Para fazermos a avaliação dos modelos treinados, precisamos comparar as previsões feitas por ele nos dados de teste com os resultados reais do conjunto de dados. Neste estudo, a métrica de avaliação utilizada será a acurácia:
# Calculando a acurácia do modelo de Regressão Logística:
accuracy = accuracy_score(Y_teste, Y_pred)
print('A acurácia do modelo de Regressão Logística para os dados de teste é de', round(accuracy*100), '%.')
# Calculando a acurácia do modelo XGBoost Classifier:
accuracy_xgb = accuracy_score(Y_teste, Y_pred_xgb)
print('A acurácia do modelo de classificação XGBoost para os dados de teste é de', round(accuracy_xgb*100), '%.')
# Calculando a acurácia do SVM:
accuracy_svc = accuracy_score(Y_teste, Y_pred_svc)
print('A acurácia do modelo Support Vector Classifier para os dados de teste é de', round(accuracy_svc*100), '%.')
Vamos criar uma matriz de confusão para cada um dos modelos criados para entendermos o desempenho do modelo na previsão de cada classe:
# Plotando a matriz de confusão 'normalizada' com o Seaborn:
fig, ax = plt.subplots(nrows = 1, ncols = 3, figsize = (15, 5))
class_names = ['Não', 'Sim']
plot_confusion_matrix(model, X_teste, Y_teste, display_labels = class_names, cmap=plt.cm.PuBu,
normalize = 'true', ax = ax[0])
ax[0].set_title('Matriz de Confusão Logistic Reg.')
plot_confusion_matrix(model_xgb, X_teste, Y_teste, display_labels = class_names, cmap=plt.cm.PuBu,
normalize = 'true', ax = ax[1])
ax[1].set_title('Matriz de Confusão XGBoost')
plot_confusion_matrix(model_svc, X_teste, Y_teste, display_labels = class_names, cmap=plt.cm.PuBu,
normalize = 'true', ax = ax[2])
ax[2].set_title('Matriz de Confusão SVC')
fig.show()
Através da matriz de confusão, é possível perceber que o modelo XGBoost, apesar de ter apresentado uma acurácia menor em relação aos demais modelos criados, é o modelo que aprendeu de maneira mais equilibrada sobre ambas as classes, ainda que sua acurácia para a classe que representa os clientes que cancelaram o plano seja muito baixa - perto dos 50%.
Os modelos de Regressão Logística e SVC apresentaram um desempenho geral muito superior, no entanto, quando fazemos a análise por classe, percebemos que o modelo aprendeu muito sobre clientes que não cancelam - acurácia superior a 90% - e, no entanto, não conseguiu identificar a classe que cancela o plano - abaixo de 30% de acurácia.
Vamos, portanto, tentar melhorar o modelo XGBoost Classifier, fazendo uso do Grid Search CV:
# Criando o modelo inicial:
estimator = XGBClassifier(objective = 'binary:logistic',
nthread = 4,
seed = 101)
# Selecionando parâmetros a serem analisados pelo Grid Search:
params = {"learning_rate": [0.05, 0.10, 0.15, 0.20, 0.25, 0.30],
"max_depth": [3, 4, 5, 6, 8, 10, 12, 15],
"min_child_weight": [1, 3, 5, 7],
"gamma": [0.0, 0.1, 0.2 , 0.3, 0.4],
"colsample_bytree": [0.3, 0.4, 0.5 , 0.7]}
# Criando o Grid Search:
grid_search = GridSearchCV(estimator = estimator,
param_grid = params,
scoring = 'roc_auc',
n_jobs = 4,
cv = 4,
verbose = True)
# Aplicando o Grid Search aos dados de treino:
grid_search.fit(X_treino, Y_treino)
# Retornando os melhores hiperparâmetros:
grid_search.best_estimator_
Fazendo uso dos hiperparâmetros indicados pelo Grid Search, vamos treinar um novo modelo XGBoost Classifier, treiná-lo e testá-lo com os mesmos dados de treino para, então, verificarmos a sua acurácia:
# Criando um novo modelo XGB Classifier com os parâmetros indicados pelo Grid Search:
model_xgb_tuned = XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,
colsample_bynode=1, colsample_bytree=0.7, gamma=0.1, gpu_id=-1,
importance_type='gain', interaction_constraints='',
learning_rate=0.1, max_delta_step=0, max_depth=15,
min_child_weight=1,
n_estimators=100, n_jobs=4, nthread=4, num_parallel_tree=1,
random_state=101, reg_alpha=0, reg_lambda=1, scale_pos_weight=1,
seed=101, subsample=1, tree_method='exact', validate_parameters=1)
# Treinando o novo modelo:
model_xgb_tuned.fit(X_treino, Y_treino)
# Aplicando o novo modelo aos dados de teste:
Y_pred_xgb_tuned = model_xgb_tuned.predict(X_teste)
# Calculando a acurácia do modelo XGBoost Classifier com novos hiperparâmetros:
accuracy_xgb_tuned = accuracy_score(Y_teste, Y_pred_xgb_tuned)
print('A acurácia do modelo de classificação XGBoost ajustado para os dados de teste é de', round(accuracy_xgb_tuned*100), '%.')
A acurácia não teve alteração em relação ao primeiro modelo criado, permanecendo em 75%. Vamos avaliar mais minuciosamente a acurácia através da matriz de confusão:
# Criando uma matriz de confusão:
disp = plot_confusion_matrix(model_xgb_tuned, X_teste, Y_teste,
display_labels = class_names,
cmap=plt.cm.PuBu,
normalize = 'true')
disp.ax_.set_title('Matriz de Confusão XGBoost Tuned')
plt.show()
Podemos perceber uma leve melhora na previsão da classe que indica os clientes que cancelaram o plano, embora esta acurácia ainda esteja muito baixa.
Vamos ainda retornar as previsões no formato de probabilidade, ou seja, qual é a probabilidade da observação pertencer a cada uma das classes:
# Retornando os resultados do modelo em forma de probabilidade:
Y_pred_xgb_prob = model_xgb_tuned.predict_proba(X_teste)
Para melhor observação, vamos transformar estas previsões e um dataframe e incluir também as previsões finais do modelo:
# Criando um dataframe com as probabilidades e incluindo as predições:
Y_pred_xgb_prob = pd.DataFrame(Y_pred_xgb_prob, columns = ['Prob_Não', 'Prob_Sim'])
Y_pred_xgb_prob['Pred'] = Y_pred_xgb_tuned
Visualizando o dataframe criado com as probabilidades e previsões finais:
Y_pred_xgb_prob
Criamos diferentes modelos que apresentaram problemas distintos: alguns apresentaram uma acurácia geral mais elevada, mas, ao observarmos mais atentamente, foi possível perceber que houve uma espécie de overfitting para uma das classes, enquanto outro apresentou uma acurácia geral mais baixa, porém conseguiu equilibrar melhor o entendimento das duas classes.
Ao escolhermos o modelo XGBoost Classifier para aplicar o Grid Search CV com o intuito de procurar pelos melhores hiperparâmetros, não obtivemos uma melhora significativa na acurácia do modelo. Outras possibilidades que ainda poderiam ser desenvolvidas seriam a transformação de algumas variáveis (por exemplo, criar uma única variável que representasse o gasto total) e a aplicação de alguma técnica de redução de dimensionalidade - como o PCA - transformando as variáveis em um número menor de componentes.