import pandas as pdimport numpy as np# para gráficosimport seaborn as snsimport matplotlib.pyplot as plt## para modelos preditivos: sklearn# para separar os dados em treino e testefrom sklearn.model_selection import train_test_split, cross_validate, GridSearchCV# regressão logísticafrom sklearn.linear_model import LogisticRegression# árvore de decisãofrom sklearn.tree import DecisionTreeClassifier, plot_tree# random forestfrom sklearn.ensemble import RandomForestClassifier# matriz de confusão e métricasfrom sklearn.metrics import precision_score, recall_score, accuracy_score, f1_score, roc_curve, roc_auc_score, ConfusionMatrixDisplay
c:\Users\julio\miniconda3\lib\site-packages\numpy\_distributor_init.py:30: UserWarning: loaded more than 1 DLL from .libs:
c:\Users\julio\miniconda3\lib\site-packages\numpy\.libs\libopenblas64__v0.3.23-246-g3d31191b-gcc_10_3_0.dll
c:\Users\julio\miniconda3\lib\site-packages\numpy\.libs\libopenblas64__v0.3.23-gcc_10_3_0.dll
warnings.warn("loaded more than 1 DLL from .libs:"
9.1 Introdução
Imagine que você é advogado em um grande escritório, responsável por gerenciar uma carteira de centenas de processos. Cada caso traz consigo uma série de informações: o juiz responsável, o histórico das partes envolvidas, a natureza da causa, entre outros. Diante desse conjunto de dados, surge a pergunta: como prever quais casos têm maior chance de sucesso, e quais vale mais a pena fazer um acordo?
Pense em poder antecipar se um juiz concederá ou não uma liminar com base em decisões anteriores e características do caso. Ou em prever se um acordo é a melhor estratégia, considerando fatores como o histórico das partes e o contexto jurídico.
Até agora, utilizamos modelos de regressão dentro do paradigma inferencial, onde buscamos entender a relação entre variáveis e fazer afirmações utilizando testes de hipóteses e intervalos de confiança.
Agora, vamos explorar modelos dentro paradigma preditivo, onde buscamos aumentar o desempenho das nossas previsões. No contexto jurídico, isso é especialmente útil em aplicações corporativas, já que muitas vezes nosso interesse é conseguir avaliar se um caso terá resultado positivo ou negativo, sem necessariamente precisar entender a relação entre as variáveis.
Além dos modelos de regressão, também vamos trabalhar com modelos de classificação, que são os casos em que a variável dependente é categórica. É importante destacar que podemos utilizar abordagens inferenciais em modelos de classificação, assim como podemos utilizar abordagens preditivas em modelos de regressão. Então, estamos mudando esses dois aspectos ao mesmo tempo: i) o tipo de variável dependente (antes era numérica e agora é categórica) e ii) a abordagem de inferência ou predição (antes era inferencial e agora é preditiva).
No direito, as variáveis dependentes numéricas geralmente se resumem a valor e tempo. Já as variáveis dependentes categóricas são variadas. Por exemplo:
Resultado de um processo;
Unanimidade
Recorreu ou não recorreu
Concedeu ou não uma liminar
Provisionar ou não o caso
Acordo ou não
…
Note que, em todos esses casos, a variável dependente é categórica, não numérica. A quantidade de categorias pode variar: o resultado de um processo pode ser uma variável binária (procedente ou improcedente), mas também pode ter várias categorias (por exemplo, procedente, parcialmente procedente, improcedente). Existem modelos mais apropriados para cada caso.
Vale enfatizar que, como estamos no paradigma preditivista, nosso interesse não está mais em entender a relação das variáveis, mas em construir um modelo que possa fazer boas previsões para a variável dependente com base nas variáveis independentes. Esse modelo pode ser simples ou complexo, o importante é que ele funcione bem para esse objetivo.
Um modelo preditivo, essencialmente, tenta criar uma função
\[
f(x) = y
\]
Onde \(x\) é a variável independente e \(y\) é a variável dependente. O objetivo é que essa função seja capaz de prever o valor de \(y\) a partir de um novo valor de \(x\). Por exemplo, se quisermos prever o resultado de um processo, \(x\) é a variável independente, como assunto do processo e \(y\) é a variável dependente, como a procedência.
Na prática, o que temos é
\[
f(x) \approx y
\]
ou seja, a função não é perfeita, mas é capaz de prever o valor de \(y\) com uma boa margem.
9.1.1 Overfitting
Aqui, precisaremos tomar cuidado com overfitting. Vamos criar modelos com estrutura cada vez mais complexa, podendo levar os valores de \(x\) até \(y\) de várias formas diferentes. O problema em fazer isso é que nossa função pode ficar tão complexa que ela deixa de funcionar bem para os casos que não foram usados para treinar o modelo. Para lidar com isso, vamos separar nossa base de dados em treino e teste. O modelo será treinado com a base de treino e testado com a base de teste. Se o modelo funcionar bem para a base de treino, mas mal para a base de teste, é sinal de que ele está com problemas de overfitting.
9.1.2 No free lunch theorem
Uma dúvida que podemos ter é: será que não existe um modelo que funcione melhor para todas as situações? Ou seja, se o objetivo é prever a variável dependente, será que não existe um modelo preditivo que funcione melhor sempre?
A resposta é não, e isso pode ser demonstrado pelo teorema No Free Lunch. O teorema diz que, se o objetivo é prever uma variável dependente, não existe um modelo preditivo que funcione melhor para todas as situações.
9.1.3 A ferramenta: scikit-learn
Vamos utilizar o scikit-learn, que é uma biblioteca de Python muito usada para machine learning.
image.png
9.1.4 Base de dados
Para nossos exemplos, vamos utilizar uma base de dados de processos na área cível, da empresa Vivo. Nosso objetivo será predizer a decisão da sentença de primeiro grau (nossa variável dependente) a partir de uma série de variáveis independentes (comarca, juiz, valor da causa, etc).
vivo = pd.read_csv('https://github.com/jtrecenti/main-cdad2/releases/download/data/vivo.csv')vivo.head(3)
Nossa variável dependente, então, será a desfecho_vivo. Começamos fazendo uma conta delas:
vivo['desfecho_vivo'].value_counts()
desfecho_vivo
Derrota (Total ou Parcial) 6644
Vitória 3495
Ativo 1302
Acordo 504
Extinto sem análise do mérito 134
Desistência 58
Name: count, dtype: int64
Nessa primeira etapa, vamos trabalhar apenas com os resultados de vitória (da vivo) ou derrota, total ou parcial. Assim, temos uma variável binária, ou seja, com apenas duas categorias. Para facilitar o uso dentro do scikit-learn, vamos dar o valor 1 para o desfecho de vitória e 0 para o desfecho de derrota, colocando isso na variável y. Além disso, vamos filtrar a base de dados para ter apenas os casos que tiveram desfecho de vitória ou derrota.
# criando uma variável binária para a variável dependentevivo['y'] = np.where(vivo['desfecho_vivo'] =='Vitória', 1, 0)vivo_f = vivo.query('desfecho_vivo == ["Vitória", "Derrota (Total ou Parcial)"]')# criamos essa cópia para facilitar as contas que faremos em seguidavivo_f = vivo_f.copy()# estamos tirando os vazios dessa variável por enquanto# depois vamos ver o que fazer com elesvivo_f = vivo_f.dropna(subset=['juiz_tempo_vara'])vivo_f.head(3)
id
assunto
valor
virtual
polo_ativo
desfecho_vivo
data_decisao
teve_revelia
data_distribuicao
adv_polo_ativo
...
pags_contestacao
juiz_tempo_vara
juiz_tempo_posse
juiz_sexo
juiz_rendimento
comarca
circunscricao
regiao
entrancia
y
0
10353896220148260576
Indenizacao Por Dano Moral
15000.0
Sim
Jailson Fonseca Dos Santos
Derrota (Total ou Parcial)
2015-11-26
Não
2014-12-05
Jorge Antonio Pantano Pansani
...
6
12.607803
30.324435
Masculino
37547.51
Sao Jose Do Rio Preto
Sao Jose Do Rio Preto
Sj Rio Preto
Entrância Final
0
1
10023887720148260482
Medida Cautelar
724.0
Sim
Antonio Antenor Da Silva
Derrota (Total ou Parcial)
2014-08-19
Não
2014-03-05
Maycon Liduenha Cardoso
...
0
10.480493
21.297741
Masculino
34769.26
Presidente Prudente
Presidente Prudente
Presidente Prudente
Entrância Final
0
2
00012546820158260297
Obrigacao De Fazer / Nao Fazer
20000.0
Não
Aparecido Barbato
Derrota (Total ou Parcial)
2015-04-08
Não
2015-02-11
Fabio Cesar Tondato
...
0
8.563997
12.509240
Masculino
34903.59
Jales
Jales
Aracatuba
Entrância Final
0
3 rows × 24 columns
9.1.5 Treino e teste
No paradigma preditivo, a base de dados é dividida em duas partes: i) treino e ii) teste. A base de treino é utilizada para ajustar o modelo, e a base de teste é utilizada para avaliar a qualidade do modelo ao final da análise. No momento, vamos esquecer completamente a base de teste, e nos concentrar apenas na base de treino.
No scikit learn, geralmente separamos a variável dependente e as variáveis independentes em objetos distintos. Por convenção, vamos chamar a variável dependente de y e as variáveis independentes de X. A letra maiúscula serve para denotar que X, por corresponder a várias colunas da base, é uma matriz, enquanto y é minúsculo por corresponder a apenas uma variável.
# Separando as variáveis preditoras/independentes (X) e a variável alvo/dependente (y)# Vamos começar com um modelo simples, que considera apenas o valor e o tempo de vara do juiz como variáveis independentes.X = vivo_f[['valor', 'juiz_tempo_vara']]y = vivo_f['y']# Dividindo os dados em conjuntos de treino e teste# Utilizamos esse random_state para garantir que a divisão seja sempre a mesma# O valor de 0.25 indica que 25% dos dados serão usados para teste# Esse valor de 0.25 é arbitrário, poderia ser outro valorX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)X_train.head(10)
valor
juiz_tempo_vara
1843
13308.0
8.563997
946
8000.0
2.852841
10292
5000.0
8.563997
5858
1000.0
12.416153
2692
7288.0
4.194387
11964
5040.0
4.194387
3208
5000.0
2.852841
2974
38160.0
13.796030
1878
5204.0
6.666667
10661
1000.0
22.036961
# Vamos ver a distribuição da variável dependente nos conjuntos de treino e testey_train.value_counts('y')
y
0 0.650388
1 0.349612
Name: proportion, dtype: float64
Alguns comentários sobre o processo de separação:
test_size=0.25: 25% dos dados serão usados para teste, e 80% dos dados serão usados para treino. Esse valor é arbitrário, e pode ser alterado de acordo com a quantidade de dados disponíveis. Por exemplo, se tivermos uma base muito grande, podemos usar um valor menor para teste, como 0.1 (10% dos dados para teste e 90% para treino).
random_state=42: é uma semente para a função train_test_split. Isso garante que, se você rodar o código novamente, a base de treino e teste será a mesma, o que é importante para comparar modelos.
9.2 Regressão logística
Para os exemplos abaixo, vamos utilizar um modelo de classificação binária, onde a variável dependente é binária (apenas duas categorias). Nosso objetivo é prever a decisão da sentença de primeiro grau a partir de uma série de variáveis independentes.
Como vimos na apostila de correlação e regressão: a regressão logística é uma generalização da regressão linear para variáveis dependentes binárias. A regressão logística é dada pela fórmula:
Onde \(p\) é a probabilidade de a variável dependente ser 1, \(e\) é o número de Euler (aproximadamente 2.71828), e \(\beta_0, \beta_1, ..., \beta_p\) são os coeficientes da regressão. A interpretação dos coeficientes é a mesma da regressão linear: o coeficiente \(\beta_1\) é a mudança na probabilidade de a variável dependente ser 1 para uma unidade a mais de \(x_1\), mantendo todas as outras variáveis constantes.
Para entender a fórmula, vamos olhar um caso com apenas uma variável independente. Nesse caso, a fórmula da regressão logística é:
Veja que, ainda que a relação entre a variável de interesse (dependente) e a variável explicativa (independente) tenha uma forma estranha, a equação que associa os parâmetros da variável explicativa ainda é linear. Por isso que a regressão logística faz parte da classe de “modelos lineares generalizados”.
Por exemplo, digamos que a variável independente seja o valor pedido na ação. Os valores ajustados da regressão logística ficariam com o seguinte desenho:
# Obs: o código abaixo foi usado apenas para gerar o gráfico. Vamos olhar com mais calma o que ele faz mais para frentedef grafico_modelo(model):# Gerar dados de exemplo np.random.seed(42) X = np.random.rand(100, 1) *10000# Valores da ação entre 0 e 10000 y = (X >5000+ np.random.normal(0, 1000, size=(100, 1))).astype(int).ravel() # Decisão favorável com incerteza# Ajustar o modelo de regressão logística model.fit(X, y)# Criar pontos para a curva logística X_plot = np.linspace(0, 10000, 1000).reshape(-1, 1) y_plot = model.predict_proba(X_plot)[:, 1] df_line = pd.DataFrame({'X': X_plot.ravel(),'y': y_plot, }) df_scatter = pd.DataFrame({'X': X.ravel(),'y': y, }) sns.scatterplot(data=df_scatter, x='X', y='y', color ='royalblue') sns.lineplot(data=df_line, x='X', y='y', color ='red') plt.xlabel('Valor da Ação') plt.ylabel('Decisão Favorável')model = LogisticRegression()grafico_modelo(model)
Note que os dados (os pontos em azul) são sempre 0 ou 1, por serem categóricos. É possível observar, no exemplo, que na faixa de valores entre 4000 e 6000, é possível encontrar tanto casos procedentes quanto improcedentes. Já abaixo de 4000, praticamente tudo é improcedente e, acima de 6000, praticamente tudo é procedente. O que a curva logística faz é ajustar uma curva que mostra a incerteza do modelo nos casos em que há uma sobreposição dos valores da variável independente.
Vale notar, novamente, que apesar de termos uma curva que é fácil de visualizar e interpretar, nosso interesse não é, nesse momento, interpretar a curva. Nosso interesse é utilizar o modelo para prever a decisão da sentença de primeiro grau. Para isso, precisaremos extrair métricas de qualidade do modelo que nos permitam comparar diferentes modelos e escolher o melhor deles. Ou seja, a visualização é boa para dar a intuição, mas não é nosso objetivo final.
9.2.1 Ajustando um modelo de regressão logística
Para ajustar um modelo de regressão logística, vamos utilizar a função LogisticRegression da biblioteca scikit-learn.
Lembre-se que, para ajustar um modelo de classificação, precisamos de uma base de treino. Testamos a qualidade do modelo na base de teste.
# cria um modelo vaziomodelo = LogisticRegression()# ajusta o modelomodelo_ajustado = modelo.fit(X_train, y_train)# imprime o modelomodelo_ajustado
c:\Users\julio\miniconda3\lib\site-packages\sklearn\linear_model\_logistic.py:469: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.
Increase the number of iterations (max_iter) or scale the data as shown in:
https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
n_iter_i = _check_optimize_result(
LogisticRegression()
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
LogisticRegression()
Agora, não temos mais um .summary(), como existia no statsmodels, para estudar os resultados do modelo. Apenas por completude, vamos ajustar o mesmo modelo no statsmodels:
# não vamos mais usar statsmodels, é apenas para mostrar como seria a mesma coisa com eleimport statsmodels.formula.api as smfmodelo_stats = smf.logit('y ~ valor + juiz_tempo_vara', data=X_train.join(y_train)).fit()modelo_stats.summary()
Optimization terminated successfully.
Current function value: 0.588215
Iterations 6
Logit Regression Results
Dep. Variable:
y
No. Observations:
7597
Model:
Logit
Df Residuals:
7594
Method:
MLE
Df Model:
2
Date:
Mon, 16 Sep 2024
Pseudo R-squ.:
0.09115
Time:
22:17:24
Log-Likelihood:
-4468.7
converged:
True
LL-Null:
-4916.8
Covariance Type:
nonrobust
LLR p-value:
2.333e-195
coef
std err
z
P>|z|
[0.025
0.975]
Intercept
1.1606
0.071
16.267
0.000
1.021
1.300
valor
-3.039e-05
5.38e-06
-5.650
0.000
-4.09e-05
-1.98e-05
juiz_tempo_vara
-0.2400
0.009
-26.696
0.000
-0.258
-0.222
Vamos comparar os valores ajustados pelo modelo com os valores obtidos no scikit-learn:
# extraindo pelo statsmodelsmodelo_stats.params
Intercept 1.160603
valor -0.000030
juiz_tempo_vara -0.239957
dtype: float64
# extraindo pelo sklearnmodelo_ajustado.intercept_, modelo_ajustado.coef_
Veja que os valores batem, mas a forma de extrair os valores ajustados é diferente. No statsmodels, usamos o .params para obter os valores ajustados, enquanto na regressão logística do scikit-learn usamos o .intercept_ e o coef_.
9.3 Métricas de qualidade do modelo
Agora, como podemos calcular a acurácia do modelo? A acurácia é a proporção de casos que o modelo acertou. Para calcular a acurácia, vamos usar a o método .score() da biblioteca scikit-learn. Esse método calcula a acurácia do modelo, ou seja, a proporção de casos que o modelo acertou. Vamos calcular a acurácia do modelo para a base de treino e para a base de teste.
# na base de treinomodelo.score(X_train, y_train)
0.6935632486507832
# na base de testemodelo_ajustado.score(X_test, y_test)
0.7039084090011843
Veja que as acurácias na base de treino e de teste são diferentes. No caso, a acurácia na base de teste está maior. Quando a acurácia na base de treino é bem maior que na base de teste, isso é sinal de que o modelo está com problemas de overfitting. Ou seja, o modelo está muito ajustado à base de treino, e não consegue generalizar para a base de teste, que é o que usamos para avaliar como o modelo se comportaria em novos casos.
A saída do modelo é um número entre zero e um, representando uma probabilidade. No entanto, nossa variável dependente ou é zero, ou é um. Para calcular a acurácia, portanto, precisamos transformar a saída do modelo em uma variável binária. Uma forma de fazer isso é considerar que, se a saída do modelo for maior que 0.5, a variável dependente é 1, e se for menor que 0.5, a variável dependente é 0. Isso é feito automaticamente pelo método .score() por trás dos panos.
Vamos ver isso na prática. Vamos calcular a acurácia manualmente, a partir da saída do modelo. Para isso, vamos usar o método .decision_function() da regressão logística, que nos dá a saída do modelo para cada observação. Vamos considerar que, se a saída do modelo for maior que 0.5, a variável dependente é 1, e se for menor que 0.5, a variável dependente é 0. Vamos comparar essa saída com a variável dependente real, e calcular a acurácia manualmente.
# lembrando do score calculado:modelo.score(X_test, y_test)
0.7039084090011843
9.3.2 Só a acurácia interessa?
Dependendo do que estamos querendo prever, a acurácia pode não ser a melhor métrica. Por exemplo, se estamos interessados em prever casos raros, a acurácia pode ser uma métrica enganosa. Imagine que temos uma base de dados com 99% de casos de vitória e 1% de casos de derrota. Se um modelo chutar que todos os casos são de vitória, ele terá uma acurácia de 99%. No entanto, ele não está fazendo um bom trabalho em prever os casos de derrota, certo?
Para lidar com isso, precisamos dar atenção aos erros e acertos do modelo, dependendo de qual é o valor da variável dependente. A ferramenta utilizada para visualizar todos esses erros e acertos é a matriz de confusão. A matriz de confusão é uma tabela que mostra os acertos e erros do modelo, separados por categoria da variável dependente.
Para calcular a matriz de confusão, vamos usar a função confusion_matrix da biblioteca scikit-learn. Vamos calcular a matriz de confusão para a base de teste.
# utilizando as previsoesConfusionMatrixDisplay.from_predictions(y_test, valor_predito)
A mesma matriz pode ser obtida utilizando a função from_estimator(), evitando que tenhamos de fazer a previsão manualmente.
# utilizando o modeloConfusionMatrixDisplay.from_estimator(modelo_ajustado, X_test, y_test)
Vamos explicar os quadrantes da matriz de confusão:
No eixo x, temos os valores preditos (0 para derrota, 1 para vitória). No eixo y, temos os valores reais (0 para derrota, 1 para vitória). Os quadrantes da matriz de confusão são:
Canto superior esquerdo: verdadeiros negativos (VN). São os casos em que o modelo previu 0 e o valor real era 0.
Canto superior direito: falsos positivos (FP). São os casos em que o modelo previu 1, mas o valor real era 0.
Canto inferior esquerdo: falsos negativos (FN). São os casos em que o modelo previu 0, mas o valor real era 1.
Canto inferior direito: verdadeiros positivos (VP). São os casos em que o modelo previu 1 e o valor real era 1.
A tabela abaixo resume a matriz de confusão:
Predito: Negativo (0)
Predito: Positivo (1)
Verdade: Negativo (0)
VN (Verdadeiro Negativo)
FP (Falso Positivo)
Verdade: Positivo (1)
FN (Falso Negativo)
VP (Verdadeiro Positivo)
Idealmente, gostaríamos que a matriz de confusão tivesse apenas valores na diagonal principal, ou seja, apenas verdadeiros positivos e verdadeiros negativos. No entanto, isso é raro de acontecer. O que queremos, então, é que o modelo tenha o menor número possível de falsos positivos e falsos negativos.
No nosso caso, o valor de VN é bem alto, então o modelo parece estar acertando bem os casos de derrota. No entanto, o valor de FN também é bem alto, então o modelo está errando bastante nos casos de vitória.
Já os valores de FN e VP são bem baixos. Isso é um sinal de que o modelo está tendendo a prever tudo como derrota. Veja que ele decidiu marcar apenas 2 casos como vitória! Isso é um problema, e vamos corrigir em breve.
Como vimos, a acurácia sozinha não consegue capturar tudo o que desejaríamos para entender a qualidade do modelo. Então vamos ver outras métricas que podem ser úteis.
9.3.3 Precision, recall e f1-score
Além da acurácia, temos outras métricas que podem ser úteis para avaliar a qualidade do modelo. Duas métricas muito utilizadas são a precision (precisão) e o recall (revocação).
A precisão é a proporção de verdadeiros positivos em relação ao total de casos previstos como positivos. Pela terminologia da matriz de confusão:
\[
\text{Precisão} = \frac{VP}{VP + FP}
\]
Na tabela, podemos visualizar pela coluna de preditos positivos:
Finalmente, temos a f1-score, que é a média harmônica entre precisão e recall. Por combinar precisão e recall, é útil quando temos classes desbalanceadas.
Os valores que calculamos acima valem para o valor de corte de 0.5, ou seja, se a saída do modelo for maior que 0.5, a predição para variável dependente é 1, e se for menor que 0.5, a variável dependente é 0. No entanto, podemos variar o valor de corte para ver como as métricas se comportam. E isso afeta as métricas de precisão e recall.
Por exemplo, se reduzirmos o valor de corte para 0.01, o modelo vai prever muito mais casos como vitória. Isso aumenta o número de verdadeiros positivos, mas também aumenta o número de falsos positivos. Ou seja, isso pode aumentar a revocação, mas diminui a precisão. Se, por outro lado, aumentarmos o valor de corte para 0.99, o modelo vai prever muito menos casos como vitória. Isso diminui o número de falsos positivos, mas também diminui o número de verdadeiros positivos. Ou seja, isso pode (ou não) aumentar a precisão, mas diminui a revocação.
Vamos ver como ficam as métricas para diferentes valores de corte. Para isso, vamos usar o método predict_proba() da regressão logística, que nos dá a probabilidade de cada observação ser 0 ou 1. Vamos variar o valor de corte de 0.01 a 0.99, e calcular as métricas para cada valor de corte.
def grafico_calibracao(modelo): prob_predito = modelo.predict_proba(X_test)[:,1]# Definindo intervalos de valores de corte cortes = np.linspace(0, 1, 100)# Listas para armazenar as métricas precision_scores = [] recall_scores = [] accuracy_scores = [] f1_scores = []# Calculando as métricas para diferentes valores de cortefor corte in cortes: y_pred = (prob_predito >= corte).astype(int) precision_scores.append(precision_score(y_test, y_pred, zero_division=0)) recall_scores.append(recall_score(y_test, y_pred)) accuracy_scores.append(accuracy_score(y_test, y_pred)) f1_scores.append(f1_score(y_test, y_pred))# Criando um DataFrame com os resultados df = pd.DataFrame({'Corte': cortes,'Precision': precision_scores,'Recall': recall_scores,'Accuracy': accuracy_scores,'F1-Score': f1_scores }) df_melt = df.melt(id_vars='Corte', value_vars=['Precision', 'Recall', 'Accuracy', 'F1-Score']) sns.lineplot( data=df_melt, x='Corte', y='value', hue='variable' )return(df)grafico_calibracao(modelo_ajustado)
Corte
Precision
Recall
Accuracy
F1-Score
0
0.000000
0.331228
1.000000
0.331228
0.497628
1
0.010101
0.331225
0.998808
0.331623
0.497477
2
0.020202
0.331616
0.997616
0.333202
0.497770
3
0.030303
0.331352
0.996424
0.332807
0.497323
4
0.040404
0.332803
0.996424
0.337150
0.498956
...
...
...
...
...
...
95
0.959596
0.000000
0.000000
0.668772
0.000000
96
0.969697
0.000000
0.000000
0.668772
0.000000
97
0.979798
0.000000
0.000000
0.668772
0.000000
98
0.989899
0.000000
0.000000
0.668772
0.000000
99
1.000000
0.000000
0.000000
0.668772
0.000000
100 rows × 5 columns
No gráfico, veja que existe uma faixa de valores entre 0.2 e 0.4 em que a precisão e a revocação são altas (e, portanto, o F1 também). A acurácia também acompanha essas curvas (apesar de isso não ser uma regra geral). Isso é um sinal de que, nessa faixa de valores, o modelo está conseguindo prever bem.
O melhor valor de corte depende muito da aplicação. Se nosso interesse está em acertar os casos em que a Vivo perde, talvez seja melhor usar um valor de corte mais alto, para diminuir os falsos positivos. Se nosso interesse está em acertar os casos em que a Vivo ganha, talvez seja melhor usar um valor de corte mais baixo, para diminuir os falsos negativos. Se o interesse é ter um equilíbrio entre os dois, talvez seja melhor usar um valor de corte que maximiza o F1-score.
9.3.5 Curva ROC
O último critério que vamos ver é a curva ROC. A curva ROC é uma curva que mostra a relação entre a revocação e a taxa de falsos positivos. Vamos ver como ela é construída.
# valores importantes da curva ROCfalse_positive, recall, cortes = roc_curve(y_test, prob_predito)df_valores = pd.DataFrame({'Falsos Positivos': false_positive,'Recall': recall,'Corte': cortes,'Diferença': recall - false_positive})sns.lineplot(data = df_valores, x ='Falsos Positivos', y ='Recall')plt.xlabel('Taxa de Falsos Positivos')plt.ylabel('Taxa de Verdadeiros Positivos (Recall)')
Text(0, 0.5, 'Taxa de Verdadeiros Positivos (Recall)')
Idealmente, gostaríamos que a curva ROC estivesse o mais próximo possível do canto superior esquerdo, ou seja, com revocação 1 e taxa de falsos positivos 0. Isso significaria que o modelo está acertando todos os casos positivos e errando nada dos casos negativos. No entanto, isso é quase impossível de acontecer. O que queremos, então, é que o valor de corte que faz com que a curva ROC esteja o mais próximo possível do canto superior esquerdo.
O melhor corte é algo em torno de 0.34. Esse valor maximiza a revocação e minimiza a taxa de falsos positivos. Esse valor também está próximo do valor que maximiza o F1-score. Esses valores, no entanto, não precisam ser exatamente iguais. Dependendo da aplicação, podemos escolher um valor de corte que maximiza a revocação, a precisão, o F1-score, a acurácia ou qualquer outra métrica que seja importante.
O mais interessante da curva ROC é que ela nos dá uma métrica que não depende do valor de corte. Esse valor é a área abaixo da curva, ou AUC. A AUC é uma métrica que varia de 0 a 1, e que nos dá uma ideia de quão bem o modelo está conseguindo separar as classes. Se o modelo tivesse taxa de falsos positivos zero e revocação 1 para algum corte, a AUC seria 1, porque seria a área abaixo de um quadrado unitário.
Nesse caso, a área abaixo da curva é 0.717. Isso significa que o modelo está conseguindo separar as classes razoavelmente bem. No entanto, ainda há espaço para melhorias. Para isso, vamos adicionar algumas variáveis categóricas no modelo.
9.3.6 Adicionando variáveis categóricas
Vamos adicionar algumas variáveis categóricas no modelo. Para isso, precisamos transformar essas variáveis em variáveis dummy. Uma variável dummy é uma variável que assume apenas dois valores, 0 ou 1. Fazemos isso para que o modelo consiga interpretar a variável categórica, porque o modelo só sabe lidar com variáveis numéricas.
Para transformar uma variável categórica em variáveis dummy, vamos usar a função pd.get_dummies() do pandas.
Note que a variável comarca foi transformada em várias variáveis dummy, uma para cada categoria. Existe um detalhe, no entanto, que é importante levar em conta. Se temos \(n\) categorias, precisamos de \(n-1\) variáveis dummy. Isso porque a última categoria é redundante: se todas as outras variáveis dummy forem zero, a última variável dummy será 1. Isso é o que chamamos de dummy trap. Para evitar o dummy trap, vamos usar o argumento drop_first=True na função pd.get_dummies().
Agora, ajustamos o modelo com as novas variáveis. Vamos ver como ficaram as métricas do modelo na base de teste.
# ajusta o modelo logísticomodelo = LogisticRegression()modelo_ajustado = modelo.fit(X_train, y_train)
c:\Users\julio\miniconda3\lib\site-packages\sklearn\linear_model\_logistic.py:469: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.
Increase the number of iterations (max_iter) or scale the data as shown in:
https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
n_iter_i = _check_optimize_result(
Note que o scikit reclamou que o número de iterações atingiu o limite. Isso ocorreu por um problema numérico, que pode ser resolvido aumentando o número de iterações. Vamos fazer isso e ajustar o modelo novamente.
modelo = LogisticRegression(max_iter=1000)modelo_ajustado = modelo.fit(X_train, y_train)# valores ajustados para o modelomodelo_ajustado.coef_
A acurácia na base de teste ficou em 0.787, que é 8% maior que o modelo anterior. Vale a pena, no entanto, olhar todas as outras métricas, usando a função que criamos anteriormente.
grafico_calibracao(modelo_ajustado)
O desenho das curvas de precisão, revocação, acurácia e F1-score parece ter ficado mais suave. Agora, podemos ver que o valor de corte que maximiza a acurácia é claramente diferente o valor de corte que maximiza o F1-score.
# valores importantes da curva ROCfalse_positive, recall, cortes = roc_curve(y_test, prob_predito)sns.lineplot(x = false_positive, y = recall)plt.xlabel('Taxa de Falsos Positivos')plt.ylabel('Taxa de Verdadeiros Positivos (Recall)')
Text(0, 0.5, 'Taxa de Verdadeiros Positivos (Recall)')
auc = roc_auc_score(y_test, prob_predito)auc
0.8358540906487598
Agora chegamos em um AUC de 83.6%, o que é um valor bem melhor do que tínhamos antes.
Em seguida, veremos ver se conseguimos melhorar ainda mais nossos resultados mudando o modelo para uma árvore de decisão.
9.4 Modelos baseados em árvores
O modelo logístico é uma excelente ferramenta para construir modelos que ajudam a predizer o resultado de um processo. No entanto, ele tem algumas limitações. Uma delas é que ele assume que a relação entre as variáveis independentes e a variável dependente é linear. Isso pode ser um problema em casos em que a relação é não-linear. Outra limitação é que ele assume que as variáveis independentes não atuam de forma conjunta, ou seja, cada uma tem um efeito separado. Isso pode ser um problema quando as variáveis independentes interagem entre si.
A árvore de decisão é um modelo que consegue lidar com essas situações. Trata-se de um modelo que divide a base de dados em subgrupos, de forma a maximizar a homogeneidade dentro de cada subgrupo. Ela cria regras de corte baseadas nos dados, de forma que a conclusão sobre a variável dependente seja o resultado de várias perguntas de sim/não.
Vamos ajustar um modelo de árvore de decisão para a base de dados da Vivo. Para isso, vamos usar a função DecisionTreeClassifier da biblioteca scikit-learn. Vamos considerar o modelo com as variáveis categóricas.
Obs: outra vantagem dos modelos com árvores é que não precisamos transformar as variáveis categóricas em variáveis dummy, porque a árvore pode criar regras de corte baseadas nas categorias. No entanto, infelizmente, o modelo de árvore do scikit-learn não sabe trabalhar com variáveis categóricas. Por isso, ainda precisamos transformar as variáveis categóricas em variáveis dummy.
Veja como ficaria o desenho da regressão logística com uma variável, mas utilizando árvores de decisão:
model = DecisionTreeClassifier(max_depth=2)grafico_modelo(model)
9.4.1 Exemplo da Vivo
Aqui, consideramos um parâmetro chamado max_depth, que controla a profundidade da árvore. Quanto maior o valor de max_depth, mais complexa a árvore. Com max_depth=3, a árvore ficará assim:
A primeira regra é juiz_tempo_vara <= 6.935. Se essa regra for verdadeira, a árvore vai para a esquerda. Se for falsa, a árvore vai para a direita. Se a árvore for para a esquerda, a próxima regra é valor <= 8229.7. Se essa regra for verdadeira, a árvore vai para a esquerda. Se for falsa, a árvore vai para a direita. E assim por diante. No caso das variáveis categóricas, a regra é criada com o valor 0.5, para separar os valores 0 e 1 das dummies.
Por exemplo:
Valor da causa: 10000
Tempo de vara do juiz: 5
Comarca: Presidente Prudente
Nessas regras, vamos andar primeiro para a esquerda, porque juiz_tempo_vara <= 6.935 é verdadeiro. Em seguida, vamos para a direita, porque valor <= 8229.7 é falso. E, por fim, vamos para a direita, porque Presidente Prudente <= 0.5 é falso. Nesse caso, a previsão do modelo é “Vitória”.
Veja que agora os ramos da árvore estão mais complexos, e as regras de classificação podem ser mais complicadas.
O que você acha que acontece quando a árvore fica muito complexa?
9.4.2 Overvitting em árvores
O maior problema das árvores de classificação é que elas são muito suscetíveis ao overfitting. Isso acontece porque a árvore pode criar regras muito específicas para a base de treino, que não generalizam bem para a base de teste. Isso é um problema, porque o objetivo do modelo é prever bem a base de teste, e não a base de treino.
Vamos ver isso acontecendo na prática. Vamos ajustar modelos com diferentes valores de max_depth, e ver como eles se comportam na base de treino e na base de teste.
Note que a acurácia no treino ou fica igual ou aumenta à medida que aumentamos a complexidade da árvore. Mas a acurácia no teste começa a cair. Esse é o momento em que o modelo apresenta problemas de overfitting. Ou seja, ele está muito ajustado à base de treino, e não consegue generalizar para a base de teste. Nesse caso, parece que o melhor valor de max_depth é 10, que é o valor que maximiza a acurácia na base de teste.
Obs: Veja que a acurácia está bem melhor que o modelo linear logístico. O modelo de árvore nem sempre tem desempenho melhor do que o modelo logístico. Isso depende muito da base de dados. No entanto, o modelo de árvore é mais flexível, e pode ser uma boa alternativa quando a relação entre as variáveis é não-linear.
Agora, vale notar que estamos, novamente, olhando apenas para a acurácia, e temos de olhar outras métricas para entender melhor o desempenho do modelo. Vamos ver a matriz de confusão para o modelo com max_depth=10.
A árvore de decisão, no seu último nível, concluirá que a decisão é favorável ou desfavorável dependendo da frequência das categorias dentro do filtro aplicado. Se a maioria dos casos for favorável, a decisão será favorável, e vice-versa. A proporção de casos favoráveis, então, é a probabilidade de a decisão ser favorável estimada pelo modelo.
Veja que, nesse caso, o melhor valor de corte está entre 0.4 e 0.6, então o corte de 0.5 já está bem próximo do ideal.
9.4.3 Validação cruzada
No gráfico que compara a acurácia no treino e no teste para diferentes valores de max_depth, verificamos a acurácia do modelo várias vezes na base de teste. No entanto, o uso excessivo da base de teste pode levar a problemas de overfitting. Isso acontece porque o modelo pode acabar, de forma não intencional, se ajustando à base de teste. É como se esses dados estivessem sendo considerados no nosso treinamento, certo?
Idealmente, no entanto, devemos utilizar apenas a base de treino para tudo, sendo a base de testes escondida e apenas acessada para avaliar a qualidade do modelo final. Para isso, podemos usar a validação cruzada. A validação cruzada é uma técnica que divide a base de treino em várias partes, e ajusta o modelo em cada uma dessas partes, criando mini-bases de teste que depois são descartadas. Isso permite que o modelo seja ajustado usando somente a base de treino, sem sujá-lo com a base de teste, mas ainda permitindo avaliar a qualidade do modelo e lidar com overvitting.
A validação cruzada é uma técnica essencial para modelagem preditiva. É ela que nos permite utilizar modelos extremamente complexos, mas ainda controlando o overfitting.
Para fazer a validação cruzada, portanto, partimos da base de treino.
A técnica mais tradicional de validação cruzada, que é a que vamos utilizar aqui, é a validação cruzada \(k\)-fold. Nessa técnica, dividimos a base de treino em \(k\) partes (geralmente de forma aleatória), ajustamos o modelo em \(k-1\) partes, e avaliamos na parte restante. Isso é feito \(k\) vezes, de forma que cada parte é usada uma vez como base de teste. No final, tiramos a média das métricas de qualidade do modelo.
9.4.4 Validação cruzada para árvores
Para fazer a validação cruzada, precisamos de algumas ferramentas novas do scikit-learn. A primeira é a função cross_validate, que faz a validação cruzada para um modelo e uma base de dados. A segunda é a função GridSearchCV, que faz a validação cruzada para vários modelos e várias bases de dados, e nos ajuda a escolher o melhor modelo.
Isso tudo foi feito para um valor específico de max_depth. O que queremos na prática, no entanto, é escolher o melhor valor de max_depth para o modelo. Para isso, vamos usar a função GridSearchCV, que faz a validação cruzada para vários valores de max_depth, e nos ajuda a escolher o melhor valor. Isso é chamado de grid search, ou busca em grade, porque criamos uma grade dos chamados hiperparâmetros do modelo (neste caso, o max_depth), e testamos todos os valores possíveis. A função GridSearchCV nos dá o melhor valor de max_depth e o melhor modelo.
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Veja que, pelo grid search realizado e pela métrica escolhida (F1-Score), o melhor valor de max_depth é 7. Isso é um sinal de que o modelo de árvore de decisão com essa profundidade é o melhor modelo para a base de dados da Vivo.
Uma coisa que eu não contei é que existem vários outros parâmetros em uma árvore de decisão para controlar. Por exemplo, podemos controlar a quantidade mínima de observações em cada folha (o parâmetro min_samples_leaf), a quantidade mínima de observações em cada nó (o parâmetro min_samples_split), a quantidade máxima de variáveis a serem consideradas em cada nó (o parâmetro max_features), e muitos outros. O GridSearchCV nos permite testar todos esses parâmetros de uma vez, e escolher o melhor modelo. O único problema em fazer isso é que o número de modelos a serem testados cresce exponencialmente com o número de parâmetros. Por isso, é importante ter uma ideia do que cada parâmetro faz, e testar apenas os parâmetros mais importantes, ou então utilizar uma técnica de otimização mais sofisticada, como a otimização bayesiana (fora do escopo da nossa disciplina).
parametros = {'max_depth': range(5, 10),'min_samples_split': range(2, 10),'min_samples_leaf': range(5, 20)}modelo_arvore = DecisionTreeClassifier()# obs: esse código pode demorar um pouco para rodargrid = GridSearchCV( modelo_arvore, # modelo que queremos usar parametros, # parâmetros que queremos testar scoring=criterios, # métricas que queremos avaliar refit='f1', # métrica que queremos otimizar. Nesse caso, a AUC cv=10, # número de folds na validação cruzada n_jobs=-1, # número de processadores a serem usados, -1 significa todos verbose=1# exibir mensagens, quanto maior o número, mais mensagens)grid.fit(X_train, y_train)
---------------------------------------------------------------------------NameError Traceback (most recent call last)
Cell In[1], line 7 1 parametros = {
2'max_depth': range(5, 10),
3'min_samples_split': range(2, 10),
4'min_samples_leaf': range(5, 20)
5 }
----> 7 modelo_arvore =DecisionTreeClassifier()
9# obs: esse código pode demorar um pouco para rodar 11 grid = GridSearchCV(
12 modelo_arvore, # modelo que queremos usar 13 parametros, # parâmetros que queremos testar (...) 18 verbose=1# exibir mensagens, quanto maior o número, mais mensagens 19 )
NameError: name 'DecisionTreeClassifier' is not defined
Com o grid search realizado, podemos ver que os valores que maximizam a AUC são max_depth=9, min_samples_split=2 e min_samples_leaf=11. Isso significa que o modelo de árvore de decisão com esses parâmetros é o melhor modelo para a base de dados da Vivo a partir do critério da área abaixo da curva ROC.
Depois de realizar essa validação cruzada, podemos ajustar o modelo com os melhores parâmetros e ver como ele se comporta na base de teste.
# ajustando modelo com os melhores parâmetrosmodelo_arvore_final = grid.best_estimator_modelo_arvore_final.fit(X_train, y_train)modelo_arvore_final.score(X_test, y_test)
0.837741808132649
Veja que a melhor acurácia ficou em torno de 84%, que é bem melhor que o modelo logístico, e também é melhor que o modelo de árvore com max_depth=4, que foi o melhor na verificação sem validação cruzada. Isso é um sinal de que o grid search foi bem-sucedido em encontrar o melhor modelo para a base de dados da Vivo.
Mas ainda pode ficar melhor! Agora, podemos otimizar a regra de corte para maximizar a acurácia.
Finalizamos o conteúdo sobre árvores de decisão. Agora, vamos ver um outro tipo de modelo mais poderoso que a árvore de decisão, que é o modelo de floresta aleatória.
9.4.5 Florestas aleatórias
A floresta aleatória é um dos algoritmos de machine learning mais famosos, sendo utilizado como “modelo base” para diversos problemas e competições como as do Kaggle.
Como funciona: A floresta aleatória é uma coleção de árvores de decisão, que são ajustadas em subconjuntos aleatórios da base de dados. A previsão da floresta aleatória é a média das previsões de cada árvore.
Por que funciona: A floresta aleatória é um modelo muito poderoso porque consegue capturar a complexidade dos dados sem sofrer tanto de overfitting. Isso acontece porque, ao fazer uma média dos resultados de cada árvore, a floresta aleatória consegue capturar a tendência dos dados sem se ajustar demais à base de treino.
Vejamos no exemplo visual como seria essa floresta.
model = RandomForestClassifier()grafico_modelo(model)
Veja que a floresta aleatória é capaz de criar regras complexas, mas também não são regras tão agressivas quanto as da árvore de decisão. Ou seja, ela conseguiria se adaptar tanto à estrutura de um modelo logístico quanto à estrutura de uma árvore de decisão.
Vamos ver agora uma grid search para a floresta aleatória. Vamos testar os seguintes valores:
n_estimators: o número de árvores na floresta
max_depth: a profundidade de cada árvore
Tecnicamente, seria possível testar muitos outros parâmetros, como min_samples_split, min_samples_leaf, max_features, etc. No entanto, isso pode ser muito custoso computacionalmente (considere que cada floresta aleatória pode estar rodando 100 árvores por baixo dos panos!)… Por isso, vamos testar apenas esses dois parâmetros.
parametros = {'n_estimators': [100, 200, 300],'max_depth': range(6, 15),'min_samples_split': range(2, 10),}modelo_floresta = RandomForestClassifier()# obs: esse código pode demorar um pouco para rodargrid = GridSearchCV( modelo_floresta, # modelo que queremos usar parametros, # parâmetros que queremos testar scoring=criterios, # métricas que queremos avaliar refit='roc_auc', # métrica que queremos otimizar. Nesse caso, a AUC cv=10, # número de folds na validação cruzada n_jobs=-1, # número de processadores a serem usados, -1 significa todos verbose=1# exibir mensagens, quanto maior o número, mais mensagens)grid.fit(X_train, y_train)
Fitting 10 folds for each of 216 candidates, totalling 2160 fits
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.