Identificar, desde o início, se um cliente está ou não satisfeito com a relação com uma empresa pode permitir que esta empresa promova medidas proativas que melhorem a satisfação do cliente antes que ele a abandone. O Santander, ao promover uma competição Kaggle com o objetivo de possibilitar esta identificação antecipada, acredita que clientes insatisfeitos costumam trocar de banco e raramente verbalizam esta insatisfação antes de fazer a troca.
Este projeto tem como objetivo criar um modelo preditivo capaz de prever a satisfação dos clientes do Banco Santander, fazendo uso de dados disponibilizados na plataforma do Kaggle, dados estes que podem ser acessados aqui.
O arquivo de referência para este estudo apresenta 370 variáveis preditoras (features) e uma variável-alvo que indica 0 para cliente satisfeito e 1 para cliente insatisfeito. Infelizmente, o dicionário de dados não foi fornecido, tornando as variáveis preditoras anônimas e, portanto, impedindo que possamos entender exatamente qual é a informação que cada variável representa. Por isso, não será feita uma análise de dados mais aprofundada que possibilitaria extrair informações e insights dos dados fornecidos.
# 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 sklearn.decomposition import PCA
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 xgboost import XGBClassifier
# Leitura do arquivo:
cust_sat = pd.read_csv('train.csv')
cust_sat.head()
Faremos apenas uma análise descritiva dos dados, observando valores mínimos, máximos, média, possíveis outliers e as suas relações com a variável target. Como mencionado anteriormente, uma análise exploratória dos dados só seria possível caso tivéssemos mais informações sobre o que os dados numéricos disponibilizados representam.
Primeiramente, vamos verificar se existe equilíbrio entre as observações de classes de clientes satisfeitos e insatisfeitos (0 e 1), pois, em não havendo equilíbrio, torna-se necessário fazer o balanceamento das classes para evitar que o modelo aprenda muito sobre uma das categorias e pouco sobre a outra.
# Contabilizando os dados por classe:
cust_sat[['ID', 'TARGET']].groupby(['TARGET']).count()
É possível observar que as classes estão desbalanceadas, tornando necessária a aplicação de alguma técnica estatística que equilibre as duas classes, evitando problemas no modelo que será criado. Neste caso, faremos uso da técnica SMOTE que será executada antes do treinamento do modelo preditivo.
A partir de agora, vamos fazer observar as estatísticas por variável (número de observações, média, desvio padrão, mínimo, máximo e quartis). Como este problema envolve um grande número de variáveis, vamos destacar alguns subconjuntos que podem representar a variabilidade observada nos dados.
cust_sat.iloc[:, 0:7].describe()
cust_sat.iloc[:, 90:100].describe()
cust_sat.iloc[:, 171:181].describe()
cust_sat.iloc[:, 241:247].describe()
cust_sat.iloc[:, 330:336].describe()
cust_sat.iloc[:, 365:].describe()
Com exceção das colunas "ID" e "TARGET", cujos números representam, respectivamente, a chave de identificação do cliente e a categoria cliente satisfeito ou insatisfeito, as demais variáveis representam valores numéricos contínuos que apresentam uma grande variedade de magnitude entre elas (dezenas, centenas, milhares, etc.). Sendo assim, posteriormente faremos a normalização dos dados para que os mesmos sejam representados em uma mesma escala.
Como passo seguinte, vamos utilizar faixas similares aos subsets acima para criarmos uma matriz de correlação entre as variáveis seleicionadas e também entre elas e a variável target. Desta forma, podemos observar o nível de importância de cada um dos atributos na classificação de satisfação dos clientes para, então, decidirmos de que maneira será feita a seleção das variáveis mais importantes. Para este passo, será criada uma função que gera a plotagem da matriz de correlação para o subset indicado.
# Criando uma função para geração da matriz de correlação:
def subset_corr(start_ind, final_ind, df = cust_sat):
""" Cria um plot indicando a correlação entre as variáveis do subset e a variável target
Args:
start_ind: número inteiro, iniciando em 1 - zero é a variável 'ID';
final_ind: número inteiro, no máximo 370.
"""
targ = df['TARGET']
sub_df = df.iloc[:, start_ind:final_ind]
sub_df = pd.concat([sub_df, targ], axis = 1)
subset_corr = sub_df.corr()
fig, ax = plt.subplots(nrows = 1, ncols = 1, figsize = (15, 12))
corr_plot = sns.heatmap(subset_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()
subset_corr(1, 11)
subset_corr(90, 100)
subset_corr(170, 180)
subset_corr(240, 250)
subset_corr(330, 340)
De modo geral, apesar de os dados apresentados nas matrizes acima mostrarem valores muito distintos de correlação com a variável target, pode-se dizer que a grande maioria pertence a uma mesma escala, sendo difícil selecionar os que se destacam por uma correlação maior, seja ela positiva ou negativa.
Também é possível encontrar muitas variáveis que apresentam forte correlação entre si, o que pode representar algum problema para o modelo, caso elas não sejam independentes entre si. Também podemos perceber variáveis, como o caso da imp_trasp_var17_out_hace3, que são representadas por valores zero em todos os registros.
A partir destas observações, vamos excluir as variáveis cujos valores mínimo e máximo são iguais, sendo que todas as demais variáveis serão utilizadas no treinamento do modelo, ainda que, devido ao grande número de atributos, aplicaremos redução de dimensionalidade com PCA.
A partir deste momento, vamos fazer o tratamento dos dados, contemplando as questões observadas anteriormente na etapa de análise descritiva do conjunto de dados. Faremos a exclusão das variáveis que não influenciam nos resultados, colocaremos os dados em uma escala padrão para, posteriormente, aplicarmos a redução de dimensionalidade com maior efetividade e, então, balancearmos as classes para treinamento do modelo.
Pela análise descritiva dos dados, bem como pela avaliação das correlações, conseguimos identificar que o conjunto de dados possui variáveis que apresentam um único valor para todas as observações registradas. Com isso, podemos concluir que esta variável não é capaz de interferir na diferenciação das classes que queremos prever e, portanto, estas variáveis serão eliminadas do conjunto de dados, permitindo, assim, uma primeira redução no número de variáveis.
# Criando um loop for para armazenar em lista os índices das colunas cujos valores mínimo e máximo são iguais:
ind_list = []
for i in range(len(cust_sat.columns)):
if cust_sat.iloc[:, i].min() == cust_sat.iloc[:, i].max():
ind_list.append(i)
# Excluindo as colunas correspondentes aos índices salvos em ind_list:
cust_sat = cust_sat.drop(cust_sat.iloc[:, ind_list], axis = 1)
cust_sat.head()
Com este procedimento, o dataset original, que apresentava originalmente 371 colunas, agora apresenta 337 colunas, ou seja, 34 colunas foram excluídas nesta etapa.
O grande número de variáveis disponibilizado apresenta também uma grande variedade de escalas numéricas, o que, para alguns algoritmos de aprendizado, pode representar um problema. Ainda que para alguns algoritmos é verdade que a escala dos dados não afeta o resultado, queremos aplicar, posteriormente, a redução de dimensionalidade (neste caso, através do algoritmo PCA), ou seja, queremos entregar ao modelo um número menor de variáveis. Com os dados padronizados, a definição dos componentes principais feita pelo PCA pode ser mais eficiente.
Antes de aplicarmos a normalização aos dados, vamos dividí-los em 'X' e 'Y' - variáveis independentes e variável dependente:
# Criando as variáveis 'X' e 'Y' a partir do conjunto de dados atual:
X = cust_sat.iloc[:, 1:-1]
Y = cust_sat.iloc[:, -1]
# Padronizando os dados com o MinMaxScaler():
scaler = MinMaxScaler(feature_range = (0,1))
X_norm = scaler.fit_transform(X)
Com os dados já padronizados, vamos reduzir as 335 variáveis preditoras restantes em 30 componentes principais, através da aplicação do algoritmo Principal Component Analysis:
pca = PCA(n_components = 30)
X_comp = pca.fit_transform(X_norm)
Como visto anteriormente, as classes 0 e 1 estão desbalanceadas no conjunto de dados fornecido, o que significa dizer que temos muitas informações sobre os clientes satisfeitos e relativamente poucas informações sobre clientes insatisfeitos. Para amenizar este problema e evitar que o modelo aprenda muito mais sobre uma das classes, vamos utilizar a função SMOTE.
Apenas aplicaremos esta técnica aos dados que servirão para treinar o modelo e, por isso, antes de fazê-lo devemos fazer a divisão dos dados em dados de treino e dados de teste:
# Dividindo os dados em treino e teste:
X_treino, X_teste, Y_treino, Y_teste = train_test_split(X_comp, Y, test_size = 0.3, random_state = 101)
# Aplicando SMOTE aos dados de treino:
sm = SMOTE(random_state = 102)
X_treino_bal, Y_treino_bal = sm.fit_sample(X_treino, Y_treino)
Finalizamos, assim, a etapa de pré-processamento de dados e estamos prontos para a criação do modelo preditivo.
Nesta etapa, vamos fazer uso do algoritmo de aprendizado chamado XGBoost. Primeiramente, o modelo será criado para, então, alimentá-lo com os dados de treino.
Com o modelo treinado, será possível fazer as previsões a partir do conjunto de dados de teste para que, finalmente, possamos avaliar o desempenho do modelo na predição das classes desejadas. Para este estudo, desejamos uma acurácia mínima de 70%.
Para efeito de estudo, não nos aprofundaremos nos diversos parâmetros do XGBoost que possibilitariam o ajuste fino no intuito de aprimorar o desempenho do modelo. Sendo assim, faremos a sua aplicação com os parâmetros default do próprio algoritmo:
# Criando e treinando o modelo:
modelo = XGBClassifier()
modelo.fit(X_treino_bal, Y_treino_bal)
Com o modelo treinado, podemos aplicar os dados teste para que o modelo faça as suas previsões:
# Fazendo as previsões:
Y_pred = modelo.predict(X_teste)
Para fazermos a avaliação do modelo treinado, 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:
accuracy = accuracy_score(Y_teste, Y_pred)
print('A acurácia do modelo de classificação XGBoost para os dados de teste é de', round(accuracy*100), '%.')
A acurácia de 80% é relativamente boa, maior do que o mínimo de 70% estabelecido. No entanto, para entendermos melhor o comportamento do modelo, vamos criar algumas matrizes de confusão que nos permitirão visualizar a eficácia do modelo para ambas as classes estudadas.
# Criando a primeira matriz de confusão:
confusion_matrix(Y_teste, Y_pred)
Para que possamos visualizar a matriz de confusão de maneira mais amigável, utilizaremos a função crosstab do Pandas e, na seqüência, faremos uma plotagem da mesma através do seaborn que permite uma visualização ainda mais clara quanto à eficácia do modelo:
# Criando a matriz de confusão com Pandas:
pd.crosstab(Y_teste, Y_pred)
# Plotando a matriz de confusão 'normalizada' com o Seaborn:
class_names = [0, 1]
disp = plot_confusion_matrix(modelo, X_teste, Y_teste,
display_labels = class_names,
cmap=plt.cm.PuBu,
normalize = 'true')
disp.ax_.set_title('Matriz de Confusão')
plt.show()
Embora a acurácia do modelo criado tenha ficado em aproximadamente 80%, é possível observar na matriz de confusão que o modelo acertou 81% dos clientes que estão, de fato, satisfeitos. No entanto, quando o alvo são os clientes insatisfeitos, o modelo teve uma acurácia de apenas 59%.
Fica claro que, embora tenhamos feito o balanceamento dos dados, o modelo ainda assim aprendeu mais sobre os clientes satisfeitos do que sobre os insatisfeitos. Além dos parâmetros do modelo que poderiam ser modificados na tentativa de melhorar o seu desempenho, outro possível fator de melhora seria uma avaliação mais aprofundada das variáveis, o que demandaria também uma explicação sobre o significado de cada uma delas.