Crédito Pessoal: Algoritmo SVM

Este miniprojeto tem como objetivo apresentar o algoritmo de classificação SVM (Support Vector Machine) aplicado a empréstimo pessoal . As métricas de machine learning vão verificar a eficiência do modelo na previsão de tomadores de empréstimos que têm o perfil de não honrar as dívidas. A base de dados foi extraída da página do Kaggle, o link da página encontra-se a seguir.

https://www.kaggle.com/laotse/credit-risk-dataset

As features (variáveis) utilizadas no modelo são:

person_age = Idade de cada tomador de empréstimo

person_income = Renda anual, em dólares, do tomador de empréstimo

person_home_ownership = Status do imóvel do tomar de empréstimo (rent = aluguel, mortage = financiado, own = casa própria, other = outros)

person_emp_length = Tempo de trabalho do tomador de empréstimo (em anos)

loan_intent = Intenção do empréstimo ( education = educação, medical = recursos médicos, venture = financiar outro débito, personal = pessoal, debtconsolidation = refinanciamento de dívidas, homeimprovement = crédito para renovação de algum bem)

loan_grade = Classificação de risco do tomador ( as classes vão de A-G, sendo que a classificação A é a classificação de menor risco)

loan_amnt = Montante de empréstimo solicitado por cada cliente (em dólares)

loan_int_rate = Taxa de juros de cada empréstimo

loan_status = Status de pagamento de cada tomador de empréstimo ( No-Default – Pagou: 0, Default – Não Pagou: 1)

cb_person_default_on_file = Histórico de default de cada tomador de empréstimo ( Y – Yes: Tem histórico de default, N – No: Não tem histórico de default)

cb_person_cred_hist_length = Histórico de cada tomador no mercado de crédito. (em anos). Quantos anos os tomadores de crédito estão ativos no mercado de crédito.

 Vamos aos códigos!!! Bibliotecas python utilizadas neste miniprojeto
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import math
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from google.colab import drive
from sklearn.model_selection import cross_val_predict
from sklearn import metrics
from sklearn.svm import SVC
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

Carregando a base de dados via csv para variável credit_risk. 
Além disso, verificamos se há valores missings values na base de dados com o método isnull
credit_risk = pd.read_csv("/content/drive/MyDrive/Cientista de Dados/Base/credit_risk_dataset.csv")
credit_risk.isnull().sum()
A base de dados base_1 representa uma amostra de valores não nulos das colunas person_emp_length e loan_int_rate, entretanto a base_2 apresenta uma amostra de valores diferentes de zero simultaneamente para aquelas colunas. 

Portanto, as features person_emp_length e loan_int_rate permanecem na base base _2, enquanto na base_1 essas features são excluídas.
base1 = credit_risk.drop(["person_emp_length","loan_int_rate"], axis=1)
base2 = credit_risk.loc[(credit_risk["person_emp_length"].notnull()) & (credit_risk["loan_int_rate"].notnull()),:]
Excluir a feature loan_percent_income das duas bases. Esta coluna representa a fração do empréstimo sobre a renda. Além disso, a variável correlaciona com outras variáveis e pode ocasionar problemas de multicolineariedade para o modelo.
base1.drop(["loan_percent_income"], axis=1, inplace=True)
base2.drop(["loan_percent_income"], axis=1, inplace=True)
Alterar a métrica da coluna person_income (que atualmente é anual) para renda média mensal. Além disso, vamos alterar o tipo das variáveis person_age e person_incomeque que são object para o tipo int (inteiro).
base1["person_income"] = base1["person_income"].apply( lambda x: x/12)
base1 = base1.astype({"person_age": int, "person_income":int})

base2["person_income"] = base2["person_income"].apply( lambda x: x/12)
base2 = base2.astype({"person_age": int, "person_income":int})
1º Hipótese do Nosso Modelo: Vamos estabelecer que só é possível realizar empréstimo pessoais tomadores que tiverem até 80 anos de idade.

2º Hipótese do Nosso Modelo: Estabelecer que a renda mensal média dos tomadores de empréstimo são inferiores a $75.000

Os códigos abaixo definem essas regras do jogo.
base1 = base1.loc[(base1["person_age"] < 80) & (base1["person_income"] <= 75000),:]
base2 = base2.loc[(base2["person_age"] < 80) & (base2["person_income"] <= 75000),:]
Relação quantitativa entre person_age, person_income e person_home_ownership. Os gráficos apresentam o montante de tomadores de empréstimo classificados por categoria. Visualmente, percebemos que há uma concentração de tomadores default que vivem de aluguel e possuem uma renda média mensal inferior a 10.000 dólares. Este resultado salarial é bastante expressivo para os tomadores default que possuem casas própria e têm imóveis financiados. A distribuição de idade entre as categorias não revelam a priori um padrão de tomadores, uma vez que há uma distribuição não concentrada ao longo das idades entre tomadores default e no-default.

As informações das figuras são construídas com base na amostra base1 que não contém amostras com valores das colunas person_emp_length e loan_int_rate. 
f,axes=plt.subplots(1,2,figsize=(15,6))
sns.stripplot(y= "person_income", x="person_home_ownership", hue="loan_status", data=base1, ax=axes[0])
sns.stripplot(y= "person_age", x="person_home_ownership",hue="loan_status", data=base1, ax=axes[1])
plt.show()
A figura abaixo apresenta dois histogramas. Ambos representam o número de tomadores de crédito divididos por categoria de empréstimo e por perfil de pagador (default ou no-default). Visualmente, não há muitas diferenças entre os histogramas. A base_2 tem mais informações, pois é uma amostra com mais observações em comparação a amostra 1. Os empréstimos concentram-se entre as categorias A, B e C. Há um ponto de inflexão a partir da categoria D, em que há uma inversão da proporção entre usuários default e no-default. 
# BASE_1

fig = make_subplots(rows=1, cols=2, subplot_titles=("Base 1", "Base 2"))
loan_nodefault = base2[base2["loan_status"] == 0].groupby("loan_grade").count().reset_index()
loan_default = base2[base2["loan_status"] == 1].groupby("loan_grade").count().reset_index()
fig.add_trace(go.Bar(x=loan_nodefault["loan_grade"],  y=loan_nodefault["person_age"], text=loan_nodefault["person_age"],  textposition="outside", name="Clientes Não Default"),1,1)
fig.add_trace(go.Bar(x=loan_default["loan_grade"],  y=loan_default["person_age"], text=loan_default["person_age"], textposition="outside",name="Clientes Default"),1,1)
fig.update_layout(xaxis_title='Loan_Grade', yaxis_title='Quantidade de Clientes' , template="simple_white")
fig.update_traces( textfont_size=10)

# BASE_2
loan_nodefault = base1[base1["loan_status"] == 0].groupby("loan_grade").count().reset_index()
loan_default = base1[base1["loan_status"] == 1].groupby("loan_grade").count().reset_index()
fig.add_trace(go.Bar(x=loan_nodefault["loan_grade"], y=loan_nodefault["person_age"],  text=loan_nodefault["person_age"], textposition="outside", name="Clientes Não Default"),1,2)
fig.add_trace(go.Bar(x=loan_default["loan_grade"],  y=loan_default["person_age"], text=loan_default["person_age"], textposition="outside",name="Clientes Default"),1,2)
fig.update_layout(xaxis_title='Loan_Grade', legend=dict(orientation="h", y=-0.2,  x=0.1), yaxis_title='Quantidade de Clientes' , template="simple_white")
fig.update_traces(textposition='outside', textfont_size=9)
fig.update_xaxes(title_text="Loan_Grade", row=1, col=1)
fig.update_xaxes(title_text="Loan_Grade", row=1, col=2)
O primeiro gráfico apresenta a relação entre o tamanho do empréstimo e a renda média do tomador de empréstimo. Percebemos que existe uma relação positivo entre essas duas variáveis, isto é, quanto maior o tamanho do empréstimo, maior é a renda média do tomador. Na fronteira superior da nuvem de pontos, podemos perceber que há uma concentração de usuários default. Esse resultado pode sugerir que para uma dada renda mensal constante, o tomador tende a realizar o default da dívida, se o tamanho do empréstimo é superior a renda média do tomador.

O segundo gráfico representa a relação entre a idade e as taxas de juros dos tomadores de empréstimos. Existe uma ilustração de escada neste gráfico, isto é, a proporção de tomadores de empréstimo cai significativamente, quando os usuários ficam mais velhos. Outra informação muito interessante é a distribuição em camadas entre as categorias de empréstimos e os valores das taxas de juros. A categoria A, B e C possuem taxas de juros mais atrativas e baixas em relação as categorias D,E,F e G.
plot1 = {"person_income": np.log(base2["person_income"]),"loan_amnt":np.log(base2["loan_amnt"]), "loan_status":base2.loan_status}
df = pd.DataFrame(plot1)

f,axes=plt.subplots(1,2,figsize=(15,6))

sns.scatterplot(x="person_income", y="loan_amnt", hue="loan_status",  data=df, ax=axes[0])
sns.scatterplot(x="person_age", y="loan_int_rate", hue="loan_grade", data=base2, ax=axes[1], legend=True)

Os gráficos de densidade apresentam a proporção de tomadores default e no-default por diferentes características. Percebemos que não existe discrepâncias na amostra entre as idades de tomadores default e no-default, uma vez que existe uma sobreposição entre as curvas de densidades. Esse fenômeno ocorre também para o histórico de crédito entre os tomadores de crédito. Os gráficos de densidade person_income e loan_amnt nos informam que existem perfis diferentes entre tomadores de crédito default e no-default. Usuários default tendem a tomar empréstimos de até 10.000 dólares, enquanto os usuários no-default que têm credibilidade assumem, em média, dívidas acima de 10.000 dólares em comparação aos tomadores default.
col_names = ["person_age","person_income","loan_amnt", "cb_person_cred_hist_length"]
default = base2[base2["loan_status"] == 1]
normal = base2[base2["loan_status"] == 0]

fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(10,10))
fig.subplots_adjust(hspace=1, wspace=1)

idx = 0

for col in col_names:
    idx += 1
    plt.subplot(2, 2, idx)
    sns.kdeplot(default[col], label="No-Default", color='blue', shade=True)
    sns.kdeplot(normal[col], label="Default", color='orange', shade=True)
    plt.title(col, fontsize=11)
    plt.legend()
    plt.tight_layout()

Suport Vector Machine (SVM)

O Suport Vector Machine (SVM) é um algoritmo de machine learning supervisonado que classifica um conjunto de features em diferentes classes. O SVM utiliza hiperplanos como fronteiras de decisões para classificar regiões. A escolha ótima do algoritmo é encontrar um hiperplanos tal qual maximiza a maior distância entre a margem do plano e as amostras para diferentes classes. A figura abaixo retirada do blog Kdnuggets ilustra dois hiperplanos que apresentam as escolhas de decisões do algoritmo SVM.

Support Vector Machine Algorithm
Fonte: Kdnuggets

Os vetores de suporte são amostras que influenciam a orientação e a posição dos hiperplanos. Esses vetores estão próximos da margem que maximizam os hiperplanos. O algoritmo SVM é eficaz em datasets com múltiplas features e para casos onde o número de features é superior ao número de observações. Além disso, o SVM possibilita especificar diferentes funções de densidade kernel. Essas funções podem ser lineares ou não lineares. As funções lineares são os hiperplanos de dimensão formados por retas, enquanto as funções não-lineares são formadas de acordo com a função de densidade de kernel.

Support Vector Machine — Simply Explained | by Lujing Chen | Towards Data  Science
Fonte: Towards Data Science
A biblioteca LabelEnconder permite a transformação dos dados categóricos. Os dados da coluna loan_grade e cb_person_default_on_file deixam de ser não-numéricos e passam ser numéricos.
enc = LabelEncoder()
loan_grade_status_1 = enc.fit_transform(base1["loan_grade"])
loan_grade_status_2 = enc.fit_transform(base2["loan_grade"])

cb_person_default_on_file_1 = enc.fit_transform(base1["cb_person_default_on_file"])
cb_person_default_on_file_2 = enc.fit_transform(base2["cb_person_default_on_file"])

base1["loan_grade_status"] = loan_grade_status_1
base2["loan_grade_status"] = loan_grade_status_2

base1["cb_person_default_on_file"] = cb_person_default_on_file_1
base2["cb_person_default_on_file"] = cb_person_default_on_file_2

Exclusão de algumas colunas que não mais necessárias para a base de dados.
base_1 = base1.drop(["person_home_ownership","loan_intent","loan_grade"], axis=1)
base_2 = base2.drop(["person_home_ownership","loan_intent","loan_grade"], axis=1)
Separar a base de dados em amostras treino e teste.

X_train = Amostra Treino de todas as features, exceto a coluna loan_status
X_test = Amostra Teste de todas as features, exceto a coluna loan_status
Y_test = Amostra teste com dados Categóricos da coluna loan_status (Default = 1 e No-Default = 0)
X_test = Amostra Teste de todas as features, exceto a coluna loan_status
Y_train = Amostra Treino com dados Categóricos da coluna loan_status (Default = 1 e No-Default = 0)
x_1,y_1 = base_1.loc[:,(base_1.columns != 'loan_status')], base_1.loc[:,'loan_status']
x_2,y_2 = base_2.loc[:,(base_2.columns != 'loan_status')], base_2.loc[:,'loan_status']

x_train_1, x_test_1, y_train_1, y_test_1 = train_test_split(x_1, y_1, test_size = 0.3, random_state=42)
x_train_2, x_test_2, y_train_2, y_test_2 = train_test_split(x_2, y_2, test_size = 0.3, random_state=42)
Aplicando o algoritmo SVC para os dados de treino e em seguida rodamos o modelo de previsão para os dados teste. Lembre-se: Estamos testando sempre duas amostras distintas, por isso são dois modelos diferentes.
clf_1 = SVC().fit(x_train_1,y_train_1)
clf_1.predict(x_test_1)

clf_2 = SVC().fit(x_train_2,y_train_2)
clf_2.predict(x_test_2)
Matriz Confusão da Amostra 1: O modelo apresentou uma boa identificação de usuários No-Default (Predito = 0 e Real = 0) e confundiu bastante os usuários que eram Default por usuários que são No-Default ( Real = 1 e Predito = 0). No entanto, o resultado contrário foi proporcionalmente menor, ou seja, o modelo consegue distinguir e errar razoavelmente menos os usuários que são No-Default (Real = 0 e Predito = 1).
pd.crosstab(y_test_1, clf_1.predict(x_test_1), rownames=["Real"], colnames=["Predito"], margins=True)
Matriz Confusão da Amostra 2: O resultado com a amostra 2 foi menos eficaz em comparação a amostra 1. A predição correta caiu para os dois cenários, isto é, para categorização exata de tomadores Default (Real e Predito = 0) e No-Default (Real e Predito = 1). Há uma pequena melhora na confusão de identificação de usuários Default e No-Default, mas o modelo não faz boas escolhas, diante a redução da métrica de Precisão que apresentaremos a seguir.
pd.crosstab(y_test_2, clf_2.predict(x_test_2), rownames=["Real"], colnames=["Predito"], margins=True)
Métricas de Avaliação: A amostra 1 apresentou métricas de avaliação mais eficazes do que a amostra 2. Apesar de ter resultados muitos próximos, a adesão de outras features no modelo não contribuiu para melhorar a performance do modelo. Esse resultado é factível, quando comparamos os indicadores Re-call e F1-Score entre as amostras. Os cálculos desses indicadores podem ser compreendidos no link abaixo: https://iamarthurabreu.wordpress.com/2020/10/14/vinho-algoritmo-knn/
resultados_1 = cross_val_predict(clf_1, x_test_1, y_test_1, cv=5)
resultados_2 = cross_val_predict(clf_2, x_test_2, y_test_2, cv=5)

print("Amostra 1 \n", metrics.classification_report(y_test_1, resultados_1, target_names=["No-Default_1","Default_1"]))
print("Amostra 2 \n", metrics.classification_report(y_test_2, resultados_2, target_names=["No-Default_2","Default_2"]))
O código abaixo apresenta à acurácia para alguns modelos de curvas Kernel diferentes. Lembra-se que os hiperplanos podem ser modelados por diferentes curvaturas. De acordo com o valor da acurácia, o modelo Kernel Poly apresentou um valor superior aos outros testes. Além disso, o modelo Kernel Poly apresentou uma acurácia maior até mesmo para o modelo com a Kernel Default (RBF) informada nos códigos anteriores. Todos os modelos do Pipeline tiveram o tratamento de dados com o algoritmo StandarScaler. Este será o próximo assunto deste portfólio. 
Fonte: Kdnuggets
pipe_1 = Pipeline([('scaler1', StandardScaler()), ('svc', SVC(kernel="poly"))])
pipe_2 = Pipeline([('scaler1', StandardScaler()), ('svc', SVC(kernel="linear"))])
pipe_3 = Pipeline([('scaler1', StandardScaler()), ('svc', SVC(kernel="sigmoid"))])

pipelines = [pipe_1, pipe_2, pipe_3]
pipe_dict = {0: 'Kernel Poly', 1: 'Kernel Linear', 2: 'Kernel Sigmoid'}
for pipe in pipelines:
  pipe.fit(x_test_1, y_test_1)
print("Base_1")
for i,model in enumerate(pipelines):
  print("{} Test Accuracy:{}".format(pipe_dict[i],model.score(x_test_1, y_test_1)))
for pipe in pipelines:
  pipe.fit(x_test_2, y_test_2)
print("Base_2")
for i,model in enumerate(pipelines):
  print( "{} Test Accuracy:{}".format(pipe_dict[i],model.score(x_test_2, y_test_2)))

Referências Bibliográficas

https://www.kdnuggets.com/2020/03/machine-learning-algorithm-svm-explained.html

https://monkeylearn.com/blog/introduction-to-support-vector-machines-svm/

https://towardsdatascience.com/support-vector-machine-simply-explained-fee28eba5496

https://www.freecodecamp.org/news/svm-machine-learning-tutorial-what-is-the-support-vector-machine-algorithm-explained-with-code-examples/

https://www.kaggle.com/laotse/credit-risk-dataset

View at Medium.com

Leave a comment