Prática: Árvores de Decisão

Visualização de Árvores de Decisão
Visualização de Árvores de Decisão
Código em Python de como construir Árvores de Decisão, a base para algoritmos avançados que se equiparam (e muitas vezes superam) as Redes Neurais.

Nós já sabemos da importância de entendermos e conhecermos as Árvores de Decisão. Se você ainda não entende muito bem a teoria, recomendamos fortemente a leitura do post Árvores de Decisão: Algoritmos Baseados em Árvores. Um outro post importante para o entendimento completo desta prática é o Medidas de Performance: Modelos de Classificação.

Se você já conhece a teoria, vamos à prática!

Você pode executar e editar todo o código apresentado aqui num Notebook no Google Colab, se preferir.

Objetivos

Para minimizar as perdas de um banco fictício, precisamos desenvolver um processo de tomada de decisão sobre para quem o banco deve aprovar empréstimos e para quem não. Os perfis demográfico e socioeconômico do cliente são considerados pelos fictícios gerentes de empréstimos antes da tomada de decisão sobre o pedido de empréstimo.

Com base na base de dados de clientes que pegaram empréstimos no banco fictício, temos classificados os clientes inadimplentes e os clientes e quitaram as suas dívidas.

Nosso objetivo é construir um Modelo de Machine Learning que irá prever se um cliente que aplica para um empréstimo pode ser ou não um cliente inadimplente.

Descrição dos Dados

Nossa base de dados é um dataset público, disponibilizado pelo Center for Machine Learning and Intelligent Systems da Universidade da Califórnia, UCI.

Link do dataset: https://archive.ics.uci.edu/ml/datasets/statlog+(german+credit+data)

Nota: Trata-se de um dataset de um banco da Alemanha, doado para uso público em 1994. Toda a base de dados original está em Inglês. Foi feita uma tradução livre e pequenas manipulações de dados para fins didáticos.

A base de dados é composta pelas seguintes colunas.

  • saldo_corrente: saldo na conta corrente (categórica).
  • duracao_emp_meses: duração do empréstimo, em meses (numérica).
  • historico_credito: histórico de crédito (categórica).
  • motivo: motivo para pedido de empréstimo (categórica).
  • quantia: valor do empréstimo pedido (numérica).
  • saldo_poupanca: saldo na conta poupança (categórica).
  • tempo_empregado: tempo no emprego atual (categórica).
  • porcentagem_renda: porcentagem da renda comprometida pela parcela do empréstimo (numérica).
  • anos_residencia: tempo de moradia na residência atual, em anos (numérica).
  • idade: idade do cliente, em anos (numérica).
  • outro_credito: se o cliente possui empréstimos em outros estabelecimentos (categórica).
  • residencia: se mora em residência própria ou alugada (categórica).
  • qtd_emprestimos_existentes: quantidade de empréstimos existentes neste banco (numérica).
  • emprego: categoria de emprego (categórica).
  • dependentes: quantidade de dependentes (numérica).
  • telefone: se o cliente possui telefone, informação relevante na época (categórica).
  • inadimplente: classificação se o cliente foi inadimplente ou não, nossa variável alvo.

Importando as Bibliotecas

# Manipulação de dados
import numpy as np
import pandas as pd

# Visualização de dados
import matplotlib.pyplot as plt
import seaborn as sns

# Divisão dos dados
from sklearn.model_selection import train_test_split

# Algoritmos de Machine Learning
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier

# Métricas de performance
from sklearn import metrics
from sklearn.metrics import (f1_score,
                            accuracy_score,
                            recall_score,
                            precision_score,
                            confusion_matrix,
                            plot_confusion_matrix,
                            roc_auc_score)

# Ajustes de Hiperparametros
from sklearn.model_selection import GridSearchCV

# Optional para Annotations das funções
from typing import Optional

# Ignorar alertas
import warnings
warnings.filterwarnings('ignore')

Nós iremos construir nosso modelo de Árvores de Decisão usando a biblioteca scikit-learn.

Carregando e Explorando os Dados

import ssl
ssl._create_default_https_context = ssl._create_unverified_context

# Local do dataset online
url_dataset = 'https://raw.githubusercontent.com/lopes-andre/datasets/main/credito.csv'

# Carrega os dados em um DataFrame
data = pd.read_csv(url_dataset)
data.head()
DataFrame Pandas
# Verifica o shape dos dados
print(f'Shape dos dados: {data.shape}\n')

print(f'Esta base de dados tem {data.shape[0]} linhas e {data.shape[1]} colunas.')

Temos como saída:

Shape dos dados: (1000, 17)

Esta base de dados tem 1000 linhas e 17 colunas.

E com apenas uma linha de código conseguimos extrair todo o resumo estatístico dos dados.

# Resumo Estatístico dos dados
data.describe()
Resumo estatístico dos dados.

Observações

  • Nós podemos com apenas uma linha de código ver todo o resumo estatístico dos dados.
  • Este método nos retorna as seguintes informações:
    • Contagem de entradas de cada coluna.
    • Média.
    • Desvio Padrão.
    • Valores mínimo e máximo de cada coluna.
    • Primeiro quartil, Mediana e terceiro quartil.
  • Todas as entradas numéricas são retornadas.

Para analisar os dados das colunas Categóricas, podemos usar um outro trecho de código. A célula abaixo irá isolar as colunas do tipo object e analisar as entradas de cada uma destas colunas.

# Lista de variáveis categóricas
colunas_cat = data.select_dtypes(include=['object']).columns.tolist()

# Loop para imprimir a contagem de valores únicos em cada coluna categórica
for coluna in colunas_cat:
    print(f'### Coluna <{coluna}> ###')
    print(data[coluna].value_counts())
    print('-' * 40)

E a saída é:

### Coluna <saldo_corrente> ###
desconhecido    394
< 0 DM          274
1 - 200 DM      269
> 200 DM         63
Name: saldo_corrente, dtype: int64
----------------------------------------
### Coluna <historico_credito> ###
bom          530
critico      293
ruim          88
muito bom     49
perfeito      40
Name: historico_credito, dtype: int64
----------------------------------------
### Coluna <motivo> ###
moveis/eletrodomesticos    473
carro                      337
negocios                    97
educacao                    59
renovacao                   22
carr0                       12
Name: motivo, dtype: int64
----------------------------------------
### Coluna <saldo_poupanca> ###
< 100 DM         603
desconhecido     183
100 - 500 DM     103
500 - 1000 DM     63
> 1000 DM         48
Name: saldo_poupanca, dtype: int64
----------------------------------------
### Coluna <tempo_empregado> ###
1 - 4 anos      339
> 7 anos        253
4 - 7 anos      174
< 1 ano         172
desempregado     62
Name: tempo_empregado, dtype: int64
----------------------------------------
### Coluna <outro_credito> ###
nenhum    814
banco     139
loja       47
Name: outro_credito, dtype: int64
----------------------------------------
### Coluna <residencia> ###
propria    713
alugada    179
outros     108
Name: residencia, dtype: int64
----------------------------------------
### Coluna <emprego> ###
qualificado        630
nao-qualificado    200
gerencial          148
desempregado        22
Name: emprego, dtype: int64
----------------------------------------
### Coluna <telefone> ###
nao    596
sim    404
Name: telefone, dtype: int64
----------------------------------------
### Coluna <inadimplente> ###
nao    700
sim    300
Name: inadimplente, dtype: int64
----------------------------------------

Com mais apenas uma linha podemos verificar os tipos de dados de cada coluna.

# Verifica os tipos das colunas e quantidade de entradas
data.info()
Informações sobre todas as colunas.

Repare que temos dados nulos/faltantes, pois nem todas as colunas possuem 1000 entradas não-nulas.

Vamos executar um código para verificar exatamente quantos dados faltantes cada coluna tem.

# Verificando dados nulos
print('Colunas com dados nulos:')
display(data.isnull().sum()[data.isnull().sum() > 0])
Colunas com dados nulos/faltantes.

Observações sobre o Resumo dos Dados

  • Os valores monetários estão em Deutsche Mark (DM), moeda da Alemanha na época, anterior ao Euro.
  • As colunas duracao_emp_meses , porcentagem_renda e anos_residencia têm valores nulos/faltantes. Valores nulos podem causar resultados inesperados em modelos preditivos, portanto iremos tratar esses valores com Engenharia de Atributos.
  • A média de idade é aproximadamente 35 anos e a mediana é 33 anos.
  • A média de valor dos empréstimos está em torno de 3271 DM (Deutsche Mark), mas há um grande range de 250 DM a 18434 DM. Poderíamos analisar melhor estes dados na Análise Exploratória de Dados.
  • A média de parcelas dos empréstimos está em torno de 21 meses e a mediana em 18 meses.
  • Temos poucos clientes desempregados na base de dados.
  • Há uma classe na coluna motivo que parece ter sofrido erro de digitação. Iremos tratar isso com a Engenharia de Atributos.
  • A nossa variável alvo, inadimplente, está desbalanceada. Apenas 30% das observações estão na Classe 1 (inadimplente) e 70% na Classe 0 (não inadimplente).

A Análise Exploratória dos Dados para este Dataset pode ficar bem extensa, portanto deixaremos para abordar ela completa em outro post, ok?

Vamos direto para a Engenharia de Atributos (ou Feature Engineering).

Engenharia de Atributos

Durante a fase de Engenharia de Atributos iremos preparar o dataset para a modelagem preditiva. Poderíamos ter realizado algumas dessas transformações antes da Análise Exploratória de Dados, mas para fins didáticos centralizamos aqui nesta sessão todos os passos.

Corrigindo Erros nos Atributos

Como mencionado acima, há um erro de digitação em uma das categorias do atributo motivo . Vamos analisar este ponto e corrigir conforme necessário.

# Exibe as categorias da variável motivo
data['motivo'].value_counts()
Entrada com erro de digitação.

Vamos corrigir as entradas com "carr0" e substituir essas entradas por "carro", como deveria ser.

# Corrige o erro de digitação
corrige_carro = {'carr0': 'carro'}
data.replace(corrige_carro, inplace=True)

# Verifica as categorias novamente
data['motivo'].value_counts()
Correção na entrada “carro”.

Note que a entrada "carr0", que era aparentemente um erro de digitação, já não existe mais.

O problema foi corrigido. Nós substituímos as entradas "carr0" por "carro".

Transformando Variáveis Categóricas em Numéricas para Modelagem

A maioria dos algoritmos de Machine Learning não lidam bem com variáveis categóricas em forma de texto. Para isto, precisamos converter as variáveis categóricas em numéricas, para facilitar os cálculos matemáticos dos algoritmos.

As variáveis ordinais, que apresentam uma ordem lógica, podem ser convertidas usando a mesma função acima, porém com uma lógica diferente: atribuindo valores numéricos sequenciais.

# Convertendo variáveis Categóricas Ordinais
conversao_variaveis = {
    'saldo_corrente': {
        'desconhecido': -1,
        '< 0 DM': 1,
        '1 - 200 DM': 2,
        '> 200 DM': 3,
    },
    'historico_credito': {
        'critico': 1,
        'ruim': 2,
        'bom': 3,
        'muito bom': 4,
        'perfeito': 5
    },
    'saldo_poupanca': {
        'desconhecido': -1,
        '< 100 DM': 1,
        '100 - 500 DM': 2,
        '500 - 1000 DM': 3,
        '> 1000 DM': 4,
    },
    'tempo_empregado': {
        'desempregado': 1,
        '< 1 ano': 2,
        '1 - 4 anos': 3,
        '4 - 7 anos': 4,
        '> 7 anos': 5,
    },
    'telefone': {
        'nao': 1,
        'sim': 2,
    }
}

data.replace(conversao_variaveis, inplace=True)
data.sample(5)
Amostra aleatória dos dados após a substituição.

OneHotEncoding para Variáveis Não Ordinais

Para variáveis categóricas podemos aplicar a técnica de OneHotEncoding. Nesta técnica, cada categoria se transforma em uma coluna de valores binários (0 ou 1). Por exemplo, o atributo motivo que possui 5 categorias, vai se transformar em 4 colunas distintas.

Exemplo

O atributo motivo possui 5 categorias:

  1. moveis/eletrodomesticos
  2. carro
  3. negocios
  4. educacao
  5. renovacao

Ao aplicar a técnica de OneHotEncoding, o DataFrame ficaria da seguinte forma.

motivomotivo_carromotivo_negociosmotivo_educacaomotivo_renovacao
carro1000
negocios0100
educacao0010
renovacao0001
moveis/eletrodomesticos0000
Aplicação de OneHotEncoding

Para evitar a Multicolinearidade, nós configuramos a função para dropar a primeira coluna, pois ela não é necessária. Caso a observação não se encaixe em nenhuma das 4 categorias acima, ela obviamente vai se encaixar na quinta, que no nosso caso é a moveis/eletrodomesticos . 0 em todas as colunas significa que está nesta categoria.

Como essa técnica de OneHotEncoding deve ser aplicada apenas sobre as colunas categóricas, vamos isolar esse tipo de colunas.

# Gera a lista de variáveis categóricas
cols_cat = data.select_dtypes(include='object').columns.tolist()

# Removendo 'inadimplente' pois é nossa variável Alvo
cols_cat.remove('inadimplente')

cols_cat

E ao verificarmos quais colunas estão na lista, temos:

['motivo', 'outro_credito', 'residencia', 'emprego']

E, finalmente, implementamos o OneHotEncoding.

# Implementa o OneHotEncoding
data = pd.get_dummies(data, columns=cols_cat, drop_first=True)

data.head()
Últimas colunas do DataFrame após OneHotEncoding.

Convertendo a Variável Alvo

A nossa variável alvo, inadimplente é a única variável que ainda precisa ser convertida. Para classificação binária (duas classes) vamos dividir as classes em Classe 0 (não) e Classe 1 (sim).

Ficando desta forma as entradas 0 para não inadimplentes e 1 para clientes inadimplentes.

# Convertendo a variável alvo
conversao_alvo = {
    'inadimplente': {'nao': 0, 'sim': 1}
}

data.replace(conversao_alvo, inplace=True)
data['inadimplente']
Variável alvo categórica convertida em numérica.

Lidando com Valores Faltantes

Existem diversas formas de tratar valores faltantes. Nós podemos remover as entradas, substituir os valores faltantes com a Média ou Mediana das colunas, ou muitas outras abordagens.

Ao invés de dropar/remover essas linhas com valores faltantes, iremos substituir os valores faltantes com a sua Média.

# Imputando os valores nulos com a média
data = data.fillna(data.mean())

E, novamente, verificamos se ainda há dados faltantes.

# Verifica valores nulos novamente
data.isnull().sum()
Verificação de dados nulos/faltantes.

Não temos mais dados nulos/faltantes.

Neste ponto, finalizamos a preparação dos dados.

Divisão dos Dados

Iremos agora separar as características de cada paciente, as variáveis independentes, da nossa variável alvo, ou variável dependente.

Lembre-se que chamamos de X o conjunto de características (features) e chamamos de y a nossa resposta de interesse, nossa variável alvo a ser descoberta (target).

# Variáveis independentes (características)
X = data.drop(['inadimplente'], axis=1)

# Variável dependente (alvo)
y = data['inadimplente']

Precisamos agora dividir a nossa base de dados entre Treino e Teste. Já discutimos a importância desta divisão, onde separamos uma parte dos dados (70% neste caso) para realizarmos o treino do modelo e uma outra parte (30%) para testarmos e vermos se o modelo de fato aprendeu, ou se apenas “decorou” respostas e se “ajustou demais” ao problema (Overfitting).

Como temos um certo desbalanceio na nossa variável alvo, é interessante mantermos as mesmas proporções de classes positivas e negativas tanto na base de treino quanto na de teste. A divisão é aleatória, e não devemos perder esta proporção.

Para isso, iremos fazer uso do argumento stratify=y da função train_test_split() disponível na biblioteca Scikit-learn. Este argumento irá manter as devidas proporções das classes de y para treino e teste.

# Divisão dos dados em Treino e Teste
X_train, X_test, y_train, y_test = train_test_split(X, y, 
                                                   test_size=0.30,
                                                   random_state=1,
                                                   stratify=y) # mantém as proporções das classes

Lembram que as classes estavam desbalanceadas? Isso é de se esperar, pois muito provavelmente apenas uma parcela pequena de clientes de um banco devem ser inadimplentes.

Nesse nosso caso, temos 70% de clientes não inadimplentes (Classe 0) e 30% de clientes inadimplentes (Classe 1). Ao usarmos o argumento stratify=y na função train_test_split(), nós dizemos para a biblioteca manter essa proporção quando fizer a divisão entre bases de treino e base de teste.

Vamos verificar estas proporções.

# Verifica as proporções de classes nos dados
print('### Proporção de Classes em Treino ###')
print(f'Porcentagem de entradas Classe 0: {y_train.value_counts(normalize=True).values[0] * 100}%')
print(f'Porcentagem de entradas Classe 1: {y_train.value_counts(normalize=True).values[1] * 100}%')
print()

print('### Proporção de Classes em Teste ###')
print(f'Porcentagem de entradas Classe 0: {y_test.value_counts(normalize=True).values[0] * 100}%')
print(f'Porcentagem de entradas Classe 1: {y_test.value_counts(normalize=True).values[1] * 100}%')
Proporções de classes nas bases de treino e teste.

Funções para Performance dos Modelos

Iremos agora declarar algumas funções úteis para monitorarmos a performance dos nossos modelos.

Se você precisa entender melhor como avaliamos modelos de classificação, recomendo fortemente a leitura do post Medidas de Performance: Modelos de Classificação.

def performance_modelo_classificacao(
    model: object,
    flag: Optional[bool] = True):
    
    '''
    Função para computar as diferentes métricas de performance para modelos de classificação.

    model: modelo para prever os valores de X
    flag: se imprimimos ou não os resultados
    '''
    
    # Lista para armazenar os resultados de Treino e Validação
    score_list = []
    
    # Predição em Treino e Validação
    pred_train = model.predict(X_train)
    pred_val = model.predict(X_test)
    
    # Acurácia do modelo
    train_acc = model.score(X_train, y_train)
    val_acc = model.score(X_test, y_test)
    
    # Recall do modelo 
    train_recall = recall_score(y_train, pred_train)
    val_recall = recall_score(y_test, pred_val)
    
    # Precisão do modelo
    train_prec = precision_score(y_train, pred_train)
    val_prec = precision_score(y_test, pred_val)
    
    # F1-Score do modelo
    train_f1 = f1_score(y_train, pred_train)
    val_f1 = f1_score(y_test, pred_val)
    
    # Popula a lista
    score_list.extend((train_acc, val_acc, train_recall, val_recall, train_prec, val_prec, train_f1, val_f1))
    
    # Imprime a lista se flag=True (default)
    if flag:
        print(f'Acurácia na base de Treino: {train_acc}')
        print(f'Acurácia na base de Teste: {val_acc}')
        print(f'\nRecall na base de Treino: {train_recall}')
        print(f'Recall na base de Teste: {val_recall}')
        print(f'\nPrecisão na base de Treino: {train_prec}')
        print(f'Precisão na base de Teste: {val_prec}')
        print(f'\nF1-Score na base de Treino: {train_f1}')
        print(f'F1-Score na base de Teste: {val_f1}')
        
    # Retorna a lista de valores em Treino e Validação
    return score_list
def matriz_confusao(
    model: object,
    X: pd.DataFrame,
    y_actual: pd.Series,
    labels: Optional[tuple] = (1, 0)):
    
    '''
    Plota a Matriz de Confusão com porcentagens.

    model: modelo para prever os valores de X
    X: atributos usados para a classficação
    y_actual: classificação real, variável alvo
    '''
    
    # Predição em Validação
    y_predict = model.predict(X)
    
    # Pega os dados da Matriz de Confusão
    cm = confusion_matrix(y_actual, y_predict, labels=[0, 1])
    df_cm = pd.DataFrame(cm, index=['Real - Não (0)', 'Real - Sim (1)'],
                        columns=['Previsto - Não (0)', 'Previsto - Sim (1)'])
    
    # List of labels for the Confusion Matrix
    group_counts = [f'{value:.0f}' for value in cm.flatten()]
    group_percentages = [f'{value:.2f}%' for value in (cm.flatten()/np.sum(cm))*100]
    
    labels = [f'{v1}\n{v2}' for v1, v2 in zip(group_counts, group_percentages)]
    labels = np.asarray(labels).reshape(2, 2)
    
    # Plot the Confusion Matrix
    plt.figure(figsize=(10, 7))
    sns.heatmap(df_cm, annot=labels, fmt='')
    plt.xlabel('Classe Prevista', fontweight='bold')
    plt.ylabel('Classe Verdadeira', fontweight='bold')
    plt.show()

Treino dos Modelos de Árvores de Decisão

O treino do nosso primeiro modelo vai ser extremamente simples. Depois iremos adicionar um pouco de complexidade.

Nós iremos usar a classe sklearn.tree.DecisionTreeClassifier para construir de forma automatizada a nossa melhor Árvore de Decisão.

Para isso iremos instanciar um objeto DecisionTreeClassifier() e fazermos com que ele se ajuste aos nossos dados de treino, que é o nosso processo de treino, com o método .fit().

Criando e Treinando o Modelo de Árvores de Decisão

# Instanciando o Modelo
arvore_d = DecisionTreeClassifier(random_state=1)

# Treinando o modelo
arvore_d.fit(X_train, y_train)
Representação em HTML do objeto do modelo treinado.

Métricas do modelo de Árvores de Decisão

arvore_d_scores = performance_modelo_classificacao(arvore_d)
Métricas de performance do modelo de Árvores de Decisão.

Perceberam um forte Overfitting? A Árvore de Decisão cresceu sem controle e acertou 100% de todas as observações de treino, mas falhou na base de teste. Aparentemente o modelo está decorando as respostas da base de treino e sua performance real está similar a jogar cara ou coroa.

Vamos tentar visualizar isso na Matriz de Confusão.

Matriz de Confusão para a Árvore de Decisão

# Matriz de Confusão de treino
matriz_confusao(arvore_d, X_train, y_train)
Matriz de Confusão do modelo de Árvores de Decisão - Base de Treino
Matriz de Confusão do modelo de Árvores de Decisão – Base de Treino.
# Matriz de Confusão de teste
matriz_confusao(arvore_d, X_test, y_test)
Matriz de Confusão do modelo de Árvores de Decisão - Base de Teste
Matriz de Confusão do modelo de Árvores de Decisão – Base de Teste.

Perceberam que na primeira matriz tivemos 0 erros e na segunda muitos erros?

Vamos agora visualizar quais decisões essa árvore está tomando, e em que ordem.

Visualizando a Árvore de Decisão

feature_names = list(X_train.columns)

plt.figure(figsize=(20, 30))
tree.plot_tree(arvore_d, feature_names=feature_names, filled=True,
            fontsize=9, node_ids=True, class_names=True);
Modelo de Árvores de Decisão extremamente complexo e com Overfitting.
Modelo de Árvores de Decisão extremamente complexo e com Overfitting.

É uma árvore extremamente complexa e profunda! Um modelo complexo demais tende ao Overfitting. Para evitar que nossas Árvores de Decisão crescam sem controle, nós vamos fazer uso de uma técnica de Poda. Vamos fazer a Pré-Poda, para sermos mais exatos.

Árvores de Decisão com Pré-Poda

Vamos, primeiramente, controlar a profundidade desta Árvore de Decisão a deixando mais simples. Para isso, vamos usar o parâmetro max_depth quando instanciarmos o objeto do modelo.

Criando e Treinando Árvores de Decisão Podadas

# Instanciando o Modelo
arvore_d1 = DecisionTreeClassifier(random_state=1, max_depth=3)

# Treinando o modelo
arvore_d1.fit(X_train, y_train)
Representação em HTML do objeto do modelo treinado.

Métricas do modelo de Árvores de Decisão Podada

arvore_d1_scores = performance_modelo_classificacao(arvore_d1)
Métricas de performance do modelo de Árvores de Decisão podada.

Agora parece que nós temos um Underfitting, concordam? Talvez o modelo esteja simples demais para aprender algo suficiente da base de treino.

Vamos analisar novamente a Matriz de Confusão.

# Matriz de Confusão de treino
matriz_confusao(arvore_d1, X_train, y_train)
Matriz de Confusão do modelo de Árvores de Decisão podada – Base de Treino.

Visualizando o modelo de Árvores de Decisão Podada

feature_names = list(X_train.columns)

plt.figure(figsize=(15, 10))
tree.plot_tree(arvore_d1, feature_names=feature_names, filled=True,
            fontsize=9, node_ids=True, class_names=True);
Modelo de Árvores de Decisão podada muito simples e com Underfitting.

De fato a nossa Árvore de Decisão está bem simples. Aparentemente simples demais para nossos dados, causando assim um Underfitting.

Ajustar a profundidade máxima da árvore para três níveis não foi uma boa estratégia. Vocês devem se lembrar que temos outros parâmetros que podemos trabalhar para controlar o crescimento da árvore, certo? Se você não lembra, leia o post Árvores de Decisão: Algoritmos Baseados em Árvores.

Mas como encontrar os valores ideais de parâmetros?

Ajuste de Hiperparâmetros

Ajuste de Hiperparâmatros (do Inglês, Hyperparameter Tuning) é o processo de realizar alterações nos parâmetros de um modelo com o intuíto de melhorar a sua performance.

Para isso podemos usar a classe GridSearchCV(), que fará uma série de tentativas combinando diferentes parâmetros definidos dentro de uma grade e implementando a Validação Cruzada (Cross Validation) para chegar até a melhor combinação.

Criando e Treinando Árvores de Decisão Tunadas

# Escolhe o Algoritmo
algo = DecisionTreeClassifier(random_state=1)

# Grade de parâmetros para combinar
parameters = {'max_depth': np.arange(1, 10),
             'min_samples_leaf': [1, 2, 5, 7, 10, 15, 20],
             'max_leaf_nodes': [2, 3, 5, 10],
             'min_impurity_decrease': [0.001, 0.01, 0.1]
             }

# Métrica usada para comparar as combinações de parâmetros
acc_scorer = metrics.make_scorer(metrics.recall_score)

# Roda a Grid Search
grid_obj = GridSearchCV(algo, parameters, scoring=acc_scorer, cv=5)
grid_obj = grid_obj.fit(X_train, y_train)

# Cria o modelo com a melhor combinação
arvore_d2 = grid_obj.best_estimator_

# Treina o modelo
arvore_d2.fit(X_train, y_train)
Representação em HTML do objeto do modelo treinado.

Métricas da Árvore de Decisão Tunada

arvore_d2_scores = performance_modelo_classificacao(arvore_d2)
Métricas de performance da Árvore de Decisão tunada.

Você deve ter notado que todas essas tentativas de diferentes combinações de parâmetros demora um pouco para executar. Mas vejam só! Nossa profundidade ideal é de 7 níveis, com 10 nós folhas. O algoritmo escolheu essa melhor combinação dentro do espaço amostral que oferecemos pra ele.

Genial, né? E nosso modelo teve uma certa melhora. Vamos ver a Matriz de Confusão?

Matriz de Confusão para a Árvore de Decisão Tunada

# Matriz de Confusão de treino
matriz_confusao(arvore_d2, X_train, y_train)
Matriz de Confusão do modelo de Árvores de Decisão podada – Base de Treino.
# Matriz de Confusão de teste
matriz_confusao(arvore_d2, X_test, y_test)
Matriz de Confusão do modelo de Árvores de Decisão podada – Base de Teste.

Comparando os Modelos de Árvores de Decisão

Agora vamos listar todos os modelos para compararmos as métricas de performance.

# Lista com todos os modelos
modelos = ['Árvore de Decisão',
          'Árvore de Decisão Podada', 
          'Árvore de Decisão Tunada']

# Nomes das colunas
colunas = ['Treino_Acurarcia', 'Val_Acurarcia', 'Treino_Recall', 'Val_Recall',
          'Treino_Precisao', 'Val_Precisao', 'Treino_F1', 'Val_F1']

# DataFrame com todos os modelos e seus respectivos scores
modelos_scores = pd.DataFrame([arvore_d_scores, arvore_d1_scores, arvore_d2_scores], 
                             columns=colunas, index=modelos).apply(lambda x: round(x, 2))

modelos_scores.T
Comparação final das métricas dos diferentes modelos de Árvores de Decisão treinados.

Conclusões

  • Todos os nossos modelos estão ou apresentando Overfitting ou apresentando Underfitting até o momento.
  • Não conseguimos encontrar ainda um algoritmo que apresente uma performance aceitável para o nosso objetivo com este projeto.
  • Provavelmente precisaremos usar um algoritmo mais avançado para este problema. Possivelmente as Random Forests sejam uma boa escolha!

Caso tenha ficado com alguma dúvida, entre em contato conosco.

Colabore com a nossa comunidade trazendo conteúdo de qualidade em Português, seja conteúdo próprio ou traduzido. Iremos ficar muito felizes de receber material de vocês.

Para conhecer mais sobre nós e saber como colaborar, visite o post abaixo.

É sempre um prazer estar com vocês por aqui!

#NoBrains #NoGains 🧠

0 Shares:
4 comentários
  1. Cara…. muito conteúdo bom nesse canal… Já inclui na minha lista de favoritos… Além de apresentar um passo a passo esclarecedor com uma didática fantástica… Me ajudou muito para um trabalho de TCC que estou fazendo para uma Especialização em Ciência de Dados… Muito obrigado!

    1. Poxa Márcio, muito obrigado! Ficamos extremamente felizes de saber que conseguimos colaborar de alguma forma. Qualquer coisa, só entrar em contato com a gente. E compartilha um pouco mais sobre o seu TCC com a gente! 🙂

  2. Excelente conteúdo. Na teoria nós poderíamos implementar uma árvore que manipule dados categóricos sem precisar de encoders, mas o sklearn nos obriga a aplicar um encoder. Você acha que valeria a pena implementar um algoritmo baseado em árvore que não precise que as variáveis categóricas sejam codificadas ou o custo computacional é muito grande?

    1. Ótima pergunta, Matheus! A Scikit-Learn nos obriga de fato a ter todas as variáveis como numéricas. Eu acredito que o custo computacional seja de fato um dos fatores mais importantes para isso, mas existem algumas outras coisas que podem ser levadas em consideração.

      Um ponto importante, na minha opinião, no encoding das variáveis categóricas em numéricas é a noção de ordem. Algumas variáveis categóricas são ordinais, possuem uma ordem implícita (ex: faixa etária, como criança, jovem, adulto, idoso) e outras não deveriam ter ordem alguma, as variáveis não ordinais (ex: estado, cidade, estado civil, etc). O modelo não conseguiria capturar essa noção de ordem sem um encoding bem feito. Para as ordinais podemos transformar em números (1, 2, 3, 4, etc) e as não ordinais fazemos o One Hot Encoding.

      Acredito também que manter tudo como numérico ajude o algoritmo a calcular a Impureza de Gini e o ganho em cada split. E facilite também na hora de nos retornar a importância das variáveis, pra nos dar um pouco de explicabilidade.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Você também pode gostar