Árvore de Decisão
Objetivo
O objetivo geral deste roteiro é utilizar as bibliotecas pandas, numpy, matplotlib e scikit-learn, além de uma base escolhida no Kagle, para treinar e avaliar um algoritmo de árvore de decisão.
Base de Dados
A base de dados escolhida para a realização deste roteiro foi a MBA Admission Dataset. Esta base possui 6194 linhas e 10 colunas, incluido uma coluna de ID da aplicação e uma coluna de status da admissão, esta é a váriavel dependente que será objeto da classificação.
Análise da Base
A seguir foi feita uma análise do significado e composição de cada coluna presente na base com a finalidade de indentificar possíveis problemas á serem tradados posteriormente.
Esta coluna é composta pelos ID's das aplicações realizadas, ou seja trata-se de um valor numérico lógico, único a cada aplicação, desta forma pode-se afirmar que esta coluna não terá relevância para o algoritmo e deverá ser retirada da base para treinamento.
Esta coluna é preenchida com o genêro do aplicante, contendo apenas valores textuais entre "male" e "female", não incluindo opções como "non-binary", "other" ou "prefer not to inform". Logo, estes dados, por serem textuais e apresentarem binariedade, deverão ser transformados em uma variável dummy para que se atinja um melhor desempenho do algoritmo.
Esta coluna é preenchida com valores booleanos que classificam o aplicantente como "estrangeiro" ou "não-estrangeiro". Logo, estes dados, por serem textuais e apresentarem binariedade, deveriam ser transformados em uma variável dummy para que se atinja um melhor desempenho do algoritmo.
Entretanto, a classificação desta coluna tambem poder ser notada na coluna "race", pois todos os valores nulos presentes na posterior são unicamente referentes a alunos estrangeiros.
Esta coluna representa a performance acadêmica prévia do aplicante, que é calculada a partir do histórico escolar. Neste as notas particulares de cada matéria podem variar de 0 á 4, 0 sendo a pior nota possível e 4 a maior. Neste caso os GPA's dos aplicantes variam entre 2.65 e 3.77, apresentando uma curva normal. Devido ao fato destes valores já serem numéricos estes já estão adequados para o modelo.
Esta coluna representa em que curso o aplicante deseja entrar, podendo assumir um de três valores textuais: "Humanities", "STEM" e "Business". Neste caso, como a variavel é textual e não apresenta binariedade, a técnica correta para o tratamento desta coluna será o Label Enconding, transformando estes valores textuais em valores númericos.
Esta coluna representa a indentificação racial do aplicante, porém tambem há diversas linhas com valor nulo nesta coluna. Ao comparar o preenchimento desta coluna com as demais, percebe-se que o valor desta coluna so se apresenta nulo para estudantes estrangeiros, tornando a coluna "international" redundante.
Desta forma, para otimizar o modelo, devemos remover a coluna "international", prezando pela menor quantidade de colunas possível. E como esta coluna não apresentar binariedade, deverá ser utilizada a técnica de Label Enconding, transformando estes valores textuais e nulos em valores númericos.
Esta coluna representa o desempenho do aplicante na prova de adimissão, variando de 570 á 780, porém estas notas não apresentam uma curva normal, pois há muitos registros de notas menores que a média a mais do que há registos de notas maiores que a média. Devido ao fato destes valores já serem numéricos estes já estão adequados para o modelo.
Esta coluna representa o tempo de experiência prévia do aplicante no mercado, exibida em anos. Os valores podem variar de 1 á 9, apresentando uma curva normal. Devido ao fato destes valores já serem numéricos estes já estão adequados para o modelo.
Esta coluna representa a área de experiência prévia do aplicante no mercado, podendo assumir, nesta base um de quatorze valores textuais. E como esta coluna não apresenta binariedade, deverá ser utilizada a técnica de Label Enconding, transformando estes valores textuais em valores númericos.
Esta coluna apresenta valores em texto para os aplicantes admitos e na lista de espera, além de valores nulos para aqueles que não foram aceitos. Esta coluna é o objeto da classificação e portanto será separada das outras colunas da base, e os valores nulos deveram ser preenchidos.
Pré-processamento
Esta secção visa preparar os dados para o treinamento da árvore de decisão, atendendo as observações e análises feitas no tópico anterior.
| gender | gpa | major | race | gmat | work_exp | work_industry | admission |
|---|---|---|---|---|---|---|---|
| 1 | 3.33 | 1 | 0 | 720 | 5 | 9 | Admit |
| 1 | 3.12 | 2 | 2 | 580 | 5 | 9 | Refused |
| 0 | 3.32 | 2 | 5 | 640 | 4 | 1 | Refused |
| 1 | 3 | 2 | 4 | 590 | 6 | 8 | Refused |
| 0 | 3.35 | 0 | 0 | 690 | 6 | 13 | Admit |
| 1 | 3.26 | 1 | 5 | 690 | 5 | 1 | Admit |
| 1 | 3.1 | 2 | 0 | 630 | 4 | 10 | Refused |
| 1 | 3.49 | 2 | 4 | 670 | 4 | 1 | Waitlist |
| 1 | 3.31 | 1 | 0 | 610 | 5 | 0 | Refused |
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
df = pd.read_csv("./docs/base/MBA.csv")
#Excluir as conlunas não desejadas
df = df.drop(columns= ["application_id", "international"])
#Preencher os valores nulos da coluna "race"
df["race"] = df["race"].fillna("international")
#Preencher os valores nulos da coluna "admission"
df["admission"] = df["admission"].fillna("Refused")
#Label encoding das colunas em texto
df["race"] = label_encoder.fit_transform(df["race"])
df["gender"] = label_encoder.fit_transform(df["gender"])
df["major"] = label_encoder.fit_transform(df["major"])
df["work_industry"] = label_encoder.fit_transform(df["work_industry"])
print(df.sample(frac=.0015).to_markdown(index=False))
| application_id | gender | international | gpa | major | race | gmat | work_exp | work_industry | admission |
|---|---|---|---|---|---|---|---|---|---|
| 5365 | Female | True | 3.41 | Business | nan | 660 | 6 | Consulting | Admit |
| 4112 | Male | False | 2.87 | Humanities | White | 580 | 6 | Technology | nan |
| 1050 | Female | False | 3.2 | Business | White | 580 | 6 | Investment Banking | nan |
| 2665 | Male | False | 3.07 | STEM | White | 600 | 5 | Consulting | nan |
| 3949 | Female | False | 3.5 | Humanities | Asian | 720 | 5 | Financial Services | Admit |
| 1944 | Female | False | 3.32 | STEM | White | 720 | 2 | Media/Entertainment | Admit |
| 4875 | Male | True | 3.03 | STEM | nan | 620 | 7 | Consulting | nan |
| 553 | Female | False | 3.35 | Humanities | White | 640 | 5 | Investment Banking | Waitlist |
| 1080 | Female | False | 3.06 | Business | Hispanic | 730 | 5 | Nonprofit/Gov | nan |
Divisão dos dados
Devido a composição da coluna de admission, a seperação dos dados deve ser feita com maior atenção. Caso esta separação fosse feita com aleatoriedade, haveria a possibilidade de que a base de treinamento tornar-se enviesada. Portanto, esta deve ser executada com proporcionalidade a composição da coluna alvo. Tendo em vista situações como esta o sickit-learn já implementou o sorteamento extratificado como a opção stratify no comando train_test_split().
Além disto para o treinamento foi utilizado uma separação arbitrária da base em 70% treinamento e 30% validação.
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
label_encoder = LabelEncoder()
df = pd.read_csv("./docs/base/MBA.csv")
#Excluir as conlunas não desejadas
df = df.drop(columns= ["application_id", "international"])
#Preencher os valores nulos da coluna "race"
df["race"] = df["race"].fillna("international")
#Preencher os valores nulos da coluna "admission"
df["admission"] = df["admission"].fillna("Refused")
#Label encoding das colunas em texto
df["race"] = label_encoder.fit_transform(df["race"])
df["gender"] = label_encoder.fit_transform(df["gender"])
df["major"] = label_encoder.fit_transform(df["major"])
df["work_industry"] = label_encoder.fit_transform(df["work_industry"])
#Separar em vairaveis indenpendetes e dependente
x = df[["gender", "gpa", "major", "race", "gmat", "work_exp", "work_industry"]]
y = df["admission"]
#Separar em teste e validação
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=27, stratify=y)
Treinamento da Árvore
Precisão da Validação: 0.7784
Importância das Features:
| Feature | Importância | |
|---|---|---|
| 1 | gpa | 0.284064 |
| 4 | gmat | 0.281837 |
| 6 | work_industry | 0.158193 |
| 5 | work_exp | 0.119838 |
| 2 | major | 0.069255 |
| 3 | race | 0.069199 |
| 0 | gender | 0.017615 |
import matplotlib.pyplot as plt
import pandas as pd
from io import StringIO
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn import tree
label_encoder = LabelEncoder()
df = pd.read_csv("./docs/base/MBA.csv")
#Excluir as conlunas não desejadas
df = df.drop(columns= ["application_id", "international"])
#Preencher os valores nulos da coluna "race"
df["race"] = df["race"].fillna("international")
#Preencher os valores nulos da coluna "admission"
df["admission"] = df["admission"].fillna("Refused")
#Label encoding das colunas em texto
df["race"] = label_encoder.fit_transform(df["race"])
df["gender"] = label_encoder.fit_transform(df["gender"])
df["major"] = label_encoder.fit_transform(df["major"])
df["work_industry"] = label_encoder.fit_transform(df["work_industry"])
#Separar em vairaveis indenpendetes e dependente
x = df[["gender", "gpa", "major", "race", "gmat", "work_exp", "work_industry"]]
y = df["admission"]
#Separar em teste e validação
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=42, stratify=y)
# Criar e treinar o modelo de árvore de decisão
classifier = tree.DecisionTreeClassifier()
classifier.fit(x_train, y_train)
# Avaliar o modelo
y_pred = classifier.predict(x_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Precisão da Validação: {accuracy:.4f}")
feature_importance = pd.DataFrame({
'Feature': classifier.feature_names_in_,
'Importância': classifier.feature_importances_
})
print("<br>Importância das Features:")
print(feature_importance.sort_values(by='Importância', ascending=False).to_html())
plt.figure(figsize=(20, 10))
tree.plot_tree(classifier, max_depth=5, fontsize=10)
# Para imprimir na página HTML
buffer = StringIO()
plt.savefig(buffer, format="svg")
print(buffer.getvalue())
Avaliação do Modelo
Com este treinamento o modelo apresenta 78.48% de precisão, número satisfatório para um modelo de classificação real, e as colunas mais importantes em sua tomada de deicisão são as ponutações gpa e gmat com 29.4% e 27.7% de importância, respectivamente, e a coluna com menor relevancia para o modelo é a gender, com 1.7% de importância.
Entretando utilizar mais dados no treinamento do modelo poderia melhorara sua precisão. Logo, para compravar esta hipótese o modelo será treinado novamente com 80% da base de dados original para treinamento.
Retreinamento
Precisão da Validação: 0.7764
Importância das Features:
| Feature | Importância | |
|---|---|---|
| 1 | gpa | 0.335151 |
| 4 | gmat | 0.294273 |
| 6 | work_industry | 0.128850 |
| 3 | race | 0.089507 |
| 5 | work_exp | 0.083052 |
| 2 | major | 0.052624 |
| 0 | gender | 0.016543 |
import matplotlib.pyplot as plt
import pandas as pd
from io import StringIO
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn import tree
label_encoder = LabelEncoder()
df = pd.read_csv("./docs/base/MBA.csv")
#Excluir as conlunas não desejadas
df = df.drop(columns= ["application_id", "international"])
#Preencher os valores nulos da coluna "race"
df["race"] = df["race"].fillna("international")
#Preencher os valores nulos da coluna "admission"
df["admission"] = df["admission"].fillna("Refused")
#Label encoding das colunas em texto
df["race"] = label_encoder.fit_transform(df["race"])
df["gender"] = label_encoder.fit_transform(df["gender"])
df["major"] = label_encoder.fit_transform(df["major"])
df["work_industry"] = label_encoder.fit_transform(df["work_industry"])
#Separar em vairaveis indenpendetes e dependente
x = df[["gender", "gpa", "major", "race", "gmat", "work_exp", "work_industry"]]
y = df["admission"]
#Separar em teste e validação
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42, stratify=y)
# Criar e treinar o modelo de árvore de decisão
classifier = tree.DecisionTreeClassifier()
classifier.fit(x_train, y_train)
# Avaliar o modelo
y_pred = classifier.predict(x_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Precisão da Validação: {accuracy:.4f}")
feature_importance = pd.DataFrame({
'Feature': classifier.feature_names_in_,
'Importância': classifier.feature_importances_
})
print("<br>Importância das Features:")
print(feature_importance.sort_values(by='Importância', ascending=False).to_html())
plt.figure(figsize=(20, 10))
tree.plot_tree(classifier, max_depth=5, fontsize=10)
# Para imprimir na página HTML
buffer = StringIO()
plt.savefig(buffer, format="svg")
print(buffer.getvalue())
Avaliação do novo modelo
Com este retreinamento a hipótese anterior é rejeitada, pois ao utilizar 80% da base para treinamento a precisão geral do modelo caiu para 77.89%. Entretanto, as métricas de gpa e gmat continuaram sendo as mais relevantes, comprovando sua importância para o modelo.
Conclusão
Ao fim deste roteiro nota-se que as colunas não precisam estar normalizadas para que se treine uma árvore de decisão, aumentar os dados de treinamento do modelo, em detrimento dos dados de teste, pode prejudicar a precisão geral do mesmo e que grande parte do tempo de trabalho do cientista de dados é a análise e limpeza da base de dados original.