Skip to content

Modelo de Machine Learning - Árvore de Decisões

Para esse projeto, foi utilizado um dataset obtido no Kaggle. Os dados usados podem ser baixados aqui.

Objetivo

O dataset apresenta diversos dados relacionados à cada um dos personagens da série de livros A Song of Ice and Fire, escrita por George R. R. Martin, inspiração para a famosa série Game of Thrones. O objetivo dessa análise é o modelo fazer a predição da importância do personagem para a série no sentido de trama. Uma variável categórica será criada a partir das variáveis presentes no dataset, classificando a relevância do personagem. Essa variável será avaliada pelo modelo de Machine Learning.

Workflow

Os pontos "etapas" são o passo-a-passo da realização do projeto.

Etapa 1 - Exploração de Dados

O dataset escolhido é composto por 1946 linhas e 30 colunas, contendo um personagem distinto em cada linha e diversas informações sobre cada um.

Colunas do dataset

Coluna Tipo Descrição
S.No Inteiro Identificador único do personagem
plod Float Valor não especificado
name String Nome do personagem
title String Alcunha atribuída ao personagem dentro do mundo
gender Binário Sexo do personagem: 0 = feminino, 1 = masculino
culture String Grupo social ao qual o personagem pertence
dateOfBirth Inteiro Data de nascimento. Valores positivos = depois do ano 0, negativos = antes do ano 0
DateoFdeath Inteiro Data de morte. Valores positivos = depois do ano 0, negativos = antes do ano 0
mother String Nome da mãe do personagem
father String Nome do pai do personagem
heir String Nome do herdeiro do personagem
house String Nome da casa à qual o personagem pertence
spouse String Nome do cônjuge do personagem
book1 Binário Indica se o personagem apareceu no primeiro livro
book2 Binário Indica se o personagem apareceu no segundo livro
book3 Binário Indica se o personagem apareceu no terceiro livro
book4 Binário Indica se o personagem apareceu no quarto livro
book5 Binário Indica se o personagem apareceu no quinto livro
isAliveMother Binário Indica se a mãe do personagem está viva
isAliveFather Binário Indica se o pai do personagem está vivo
isAliveHeir Binário Indica se o herdeiro do personagem está vivo
isAliveSpouse Binário Indica se o cônjuge do personagem está vivo
isMarried Binário Indica se o personagem é casado
isNoble Binário Indica se o personagem é nobre
age Inteiro Idade do personagem (referência: ano 305 D.C.)
numDeadRelations Inteiro Número de personagens mortos com os quais o personagem se relaciona
boolDeadRelations Binário Indica se há personagens mortos relacionados ao personagem
isPopular Binário Indica se o personagem é considerado popular
popularity Float Índice entre 0 e 1 que indica o quão popular é o personagem
isAlive Binário Indica se o personagem está vivo

Estudo da coluna plod

No dataset, temos uma coluna que possui um índice que aponta algo não identificado: o plod. Para investigar seu significado, são necessárias algumas análises:

  • Inspeção dos valores: Primeiro, foram realizadas algumas linhas de código para verificar os valores da coluna;

Tipo de dado: float64

Valor mínimo: 0.0

Valor máximo: 1.0

Valor médio: 0.366

Exemplo de valor: 0.946

import pandas as pd

df = pd.read_csv("docs\decision-tree\dados.csv", sep=",", encoding="UTF-8")

print(f"Tipo de dado: {df["plod"].dtype}\n")
print(f"Valor mínimo: {df["plod"].min()}\n")
print(f"Valor máximo: {df["plod"].max()}\n")
print(f"Valor médio: {format(df["plod"].mean(), ".3f")}\n")
print(f"Exemplo de valor: {df.loc[0, "plod"]}")

A análise da saída obtida nos permite observar que os valores estão sempre no intervalo [0,1], sugerindo que representam uma probabilidade ou índice normalizado.

  • Correlações entre plod e as outras colunas: Levando isso em consideração, é necessário realizar um cálculo de correlações para descobrir a principal variável no cálculo do plod:

popularity: 0.35458415491153905

isAliveFather: -0.3525990385007833

book4: -0.4041512952984149

isAlive: -0.41731839569897605

import pandas as pd

df = pd.read_csv("docs\decision-tree\dados.csv", sep=",", encoding="UTF-8")

df_numerico = df.select_dtypes(include=["number"])

correl = df_numerico.corr()["plod"].sort_values(ascending=False)

for col, corr in correl.items():
    if (corr > 0.3 or corr < -0.3) and corr != 1:
        print(f"{col}: {corr}\n")

É possível observar que a correlação mais forte entre plod e qualquer outra coluna no dataset é com a coluna isAlive. Esse dado nos permite criar uma hipótese de que plod é a estimativa da probabilidade de morte do personagem.

  • Comparação com a coluna isAlive: Em seguida, para verificar a hipótese estabelecida, será feito um gráfico de boxplot para analisar a relação de plod e isAlive;
2025-08-29T08:50:45.454937 image/svg+xml Matplotlib v3.10.5, https://matplotlib.org/
import matplotlib.pyplot as plt
import pandas as pd
from io import StringIO

df = pd.read_csv("docs\decision-tree\dados.csv", sep=",", encoding="UTF-8")

plod_alive = df[df["isAlive"] == 1]["plod"]
plod_dead = df[df["isAlive"] == 0]["plod"]

plt.rcParams["figure.figsize"] = (10, 5)
fig, ax = plt.subplots(facecolor="white")
ax.set_facecolor("white")

ax.boxplot([plod_alive, plod_dead], labels=["Vivo", "Morto"],
           patch_artist=True,
           boxprops=dict(facecolor="lightblue", color="black"),
           medianprops=dict(color="red"))

ax.set_title("Distribuição de plod por estado de vida", color="black")
ax.set_ylabel("plod (probabilidade de morte)", color="black")
ax.set_xlabel("Estado de vida (isAlive)", color="black")
ax.grid(axis="y", linestyle="--", alpha=0.7, color="gray")

ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_color("black")
ax.spines["bottom"].set_color("black")

buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=False)
print(buffer.getvalue())

No gráfico, observa-se que personagens vivos (isAlive = 1) tendem a possuir baixos valores de plod, enquanto personagens mortos (isAlive = 0) geralmente têm valores altos. Além disso, é possível observar diversos outliers dentre os personagens vivos, que são provavelmente personagens que aparecem pouco e/ou possuem informações incompletas. Essa ideia é fortalecida pelo fato de que a variável popularity (popularidade) também possui correlação moderada com plod.

Portanto, os padrões do gráfico indicam, novamente, que plod funciona como uma estimativa da probabilidade de morte do personagem, reforçando a hipótese inicial. Essa coluna provavelmente foi calculada com algum modelo preditivo anterior.

É necessário ressaltar que isso é uma observação exploratória, baseada nos dados disponíveis, e será considerada no pré-processamento e na escolha das features do modelo.

Exploração aprofundada da coluna popularity

  • Estatísticas descritivas: Primeiramente, vamos calcular alguns valores essenciais dessa coluna;
popularity
count 1946
mean 0.0895843
std 0.160568
min 0
25% 0.0133779
50% 0.0334448
75% 0.0869565
max 1
import pandas as pd

df = pd.read_csv("docs\decision-tree\dados.csv", sep=",", encoding="UTF-8")

print(df["popularity"].describe().to_markdown())

Na saída, observa-se que popularity é um índice que indica a popularidade do personagem, variando entre 0 e 1, com o valor 0 para irrelevante e 1 para popular.

  • Gráfico de dispersão de popularity: O gráfico relaciona o índice popularity com a soma das 5 variáveis book, que indicam a presença de um personagem em cada livro em binário. Os livros considerados nessas variáveis são apenas a narrativa principal da história, sem spin-offs e personagens que são apenas citados e referenciados.
2025-08-29T08:50:45.708002 image/svg+xml Matplotlib v3.10.5, https://matplotlib.org/
import matplotlib.pyplot as plt
import pandas as pd
from io import StringIO

df = pd.read_csv("docs/decision-tree/dados.csv", sep=",", encoding="UTF-8")

df["book_freq"] = df[["book1", "book2", "book3", "book4", "book5"]].sum(axis=1)

plt.rcParams["figure.figsize"] = (10, 5)
fig, ax = plt.subplots(facecolor="white")
ax.set_facecolor("white")

ax.scatter(df["book_freq"], df["popularity"], alpha=0.7, color="red", edgecolor="black")

ax.set_title("Popularidade X Frequência nos livros", color="black")
ax.set_xlabel("Frequência em livros (soma)", color="black")
ax.set_ylabel("Popularidade", color="black")
ax.grid(axis="y", linestyle="--", alpha=0.7, color="gray")

ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["left"].set_color("black")
ax.spines["bottom"].set_color("black")

buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=False)
print(buffer.getvalue())

A análise do gráfico indica que, de 1 até 5 aparições, o número de personagens populares aumenta de forma diretamente proporcional, com alguns out-liers.

Contudo, podemos observar que diversos personagens que não apareceram na série principal de livros, possuindo soma de aparições igual a 0, são extremamente populares. Isso acontece pois há personagens de spin-offs muito amados pela comunidade, além de outros personagens que são apenas citados ao longo da história, sem aparecer diretamente, e também adquirem alta popularidade.

Etapa 2 - Pré-processamento

O objetivo do projeto é realizar uma predição da relevância dos personagens na trama principal, a variável categórica relevance que possuirá as seguintes categorias: Low, Medium, High e Very High.

1° Passo: Criação de book_freq

Primeiramente, é importante criar uma variável representante para a frequência em livros para cada personagem. Ao invés de utilizar 5 variáveis book diferentes, criaremos a variável boof_freq. Para isso, será feita a soma dos 5 valores das variáveis book, o que resultará em um intervalo de [0,5]. Contudo, para que esse valores sejam normalizados, e possuam um número entre 0 e 1, é feita a divisão desse resultado por 5.

df["book_freq"] = df[["book1", "book2", "book3", "book4","book5"]].sum(axis=1) / 5

2° Passo: Seleção de colunas

Em seguida, é necessário definir quais são as variáveis serão utilizadas para prever a relevância. Elas são as seguintes:

  • plod: Se esse valor for alto, há maior chance do personagem ser irrelevante

  • title: Se o personagem tiver um título, qualquer que seja, já possui alguma relevância

  • culture: Se o personagem tiver alguma cultura, qualquer que seja, já possui alguma relevância

  • mother: Se o personagem tiver paretentesco revelado, provavelmente tem alguma importância

  • father: Se o personagem tiver paretentesco revelado, provavelmente tem alguma importância

  • heir: Se o personagem tiver paretentesco revelado, provavelmente tem alguma importância

  • house: Se o personagem tiver uma casa, deve ser mais relevante

  • book_freq: Se o personagem aparece frequentemente, deve ser relevante

  • isNoble: Se o personagem for um nobre, tem mais chances de ser importante

  • popularity: Se o personagem for popular, também aumenta sua chance de relevância

A seleção das colunas foi feita, em código, da seguinte forma:

cols = ["plod", "title", "culture", "mother", "father", "heir", "house",
"book_freq", "isNoble", "popularity"]

df = df[cols]

3° Passo: Tratamento de valores faltantes

Precisamos garantir que não existam valores faltantes no dataframe. Por isso, será feita uma alteração em todas as linhas restantes que possuem valor NA. Contudo, temos que tratar diferentemente cada tipo de variável para o preenchimento dos vazios. As regras utilizadas serão as seguintes:

  • Faltantes númericos serão preenchidos com a mediana da coluna - plod, popularity

  • Faltantes categóricos nominais serão preenchidos com "Unknown" (Desconhecido) - title, culture, mother, father, heir, house

  • Faltantes binários serão preenchidos com a moda da coluna (valor mais frequente) - isNoble

cols = ["plod", "popularity"]
for col in cols:
    df.fillna({col: df[col].median()}, inplace=True)

cols = ["title", "culture", "mother", "father", "heir", "house"]
for col in cols:
    df.fillna({col: "Unknown"}, inplace=True)

df.fillna({"isNoble": df["isNoble"].mode()[0]}, inplace=True)

4° Passo: Binarização dos categóricos nominais

As variáveis categóricas no dataframe tem, simplesmente, muitas categorias para a realização de Label ou One-hot Encoding. Além disso, o único dado importante provindo dessas no modelo sendo criado é se existem ou não essas informações sobre o personagem. Portanto, as colunas title, culture, mother, father, heir e house serão binarizadas. Ou seja, se possuírem um valor, assumirão o valor 1. Caso contrário, 0.

Além disso, os nomes das colunas serão alterados, adicionando um "has_" antes do nome original da variável.

cols = ["title", "culture", "mother", "father", "heir", "house"]

for col in cols:
    df[f"has_{col}"] = (df[col] != "Unknown").astype(int)
    df.drop(columns=[col], inplace=True)

5° Passo: Inversão e renomeação de plod

A variável plod, que indica a probabilidade de morte, possui uma relação inversamente proporcional à relevância do personagem. Por isso, é necessária a inversão dessa variável. Além disso, renomear a variável para survival_prob deixará mais claro o seu propósito.

df["survival_prob"] = 1 - df["plod"]
df.drop(columns="plod", inplace=True)

6° Passo: Criação da variável target relevance_category a partir do score relevance_score

Agora, precisamos criar a variável categórica que será avaliada pelo modelo. Utilizaremos a seguinte distribuição de pesos:

  • popularity: Popularidade - 25%

  • book_freq: Frequência de aparições - 25%

  • survival_prob: Probabilidade de sobrevivência - 15%

  • isNoble: É nobre - 10%

  • has_title: Tem um título - 10%

  • has_house: Possui uma casa - 5%

  • has_culture: Tem uma cultura - 5%

  • has_mother + has_father + has_heir: Possui parentesco - 5%

Com o relevance_score definido, criaremos a nova coluna, relevance_category, a partir dos seguintes valores de score:

  • x < 0.25: Low (Baixa relevância)

  • 0.25 <= x < 0.5: Medium (Relevância média)

  • 0.5 <= x < 0.75: High (Alta relevância)

  • 0.75 <= x <= 1: Very High (Relevância muito alta)

Resultado final do pré-processamento

Colunas após tratamento: ['book_freq', 'isNoble', 'popularity', 'has_title', 'has_culture', 'has_mother', 'has_father', 'has_heir', 'has_house', 'survival_prob', 'relevance_score', 'relevance_category']

Valores ausentes após pré-processamento: 0

Formato do dataset final: (1946, 12)

Features: 10

Target: relevance_category

Distribuição da variável target:

relevance_category count
Medium 1078
Low 458
High 379
Very High 31
import pandas as pd

df = pd.read_csv("docs\decision-tree\dados.csv", sep=",", encoding="UTF-8")

# 1° Passo: Criando a coluna "book_freq", já normalizando-a

df["book_freq"] = df[["book1", "book2", "book3", "book4", "book5"]].sum(axis=1) / 5

# 2° Passo: Dropando colunas que não serão utilizadas

cols = [
    "plod", "title", "culture", "mother", "father", "heir", "house", "book_freq", "isNoble", "popularity"
]

df = df[cols]

# 3° Passo: Tratamento de valores faltantes

cols = ["plod", "popularity"]
for col in cols:
    df.fillna({col: df[col].median()}, inplace=True)

cols = ["title", "culture", "mother", "father", "heir", "house"]
for col in cols:
    df.fillna({col: "Unknown"}, inplace=True)

df.fillna({"isNoble": df["isNoble"].mode()[0]}, inplace=True)

# 4° Passo: Binarização das variáveis categóricas nominais

cols = ["title", "culture", "mother", "father", "heir", "house"]

for col in cols:
    df[f"has_{col}"] = (df[col] != "Unknown").astype(int)
    df.drop(columns=[col], inplace=True)

# 5° Passo: Inversão da variável "plod" e renomeação para "survival_prob"

df["survival_prob"] = 1 - df["plod"]
df.drop(columns="plod", inplace=True)

# 6° Passo: Criar variável target "relevance_category" a partir do score "relevance_score"

def calculate_relevance_score(row):

    score = (
        row["popularity"] * 0.25 +
        row["book_freq"] * 0.25 +
        row["survival_prob"] * 0.15 +
        row["isNoble"] * 0.10 +
        row["has_title"] * 0.10 +
        row["has_house"] * 0.05 +
        row["has_culture"] * 0.05 +
        (row["has_mother"] + row["has_father"] + row["has_heir"]) * 0.05 / 3
    )

    return min(max(score, 0), 1)

def categorize_relevance(score):
    if score < 0.25:
        return "Low"
    elif score < 0.5:
        return "Medium"
    elif score < 0.75:
        return "High"
    else:
        return "Very High"

df["relevance_score"] = df.apply(calculate_relevance_score, axis=1)
df["relevance_category"] = df["relevance_score"].apply(categorize_relevance)

features = [
    "book_freq", "popularity", "survival_prob", "isNoble",    
    "has_title", "has_culture", "has_mother", "has_father", 
    "has_heir", "has_house",
]

target = "relevance_category"

# df.to_csv("dados_processado.csv", index=False)

print(f"Colunas após tratamento: {df.columns.tolist()}\n")
print(f"Valores ausentes após pré-processamento: {df.isnull().sum().sum()}\n") 
print(f"Formato do dataset final: {df.shape}\n")
print(f"Features: {len(features)}\n")
print(f"Target: {target}\n")
print(f"Distribuição da variável target:\n")
print(df[target].value_counts().to_markdown())

Etapa 3 - Divisão de dados

Na etapa de divisão de dados, separaremos o conjunto de dados processado em dois grupos distintos:

  • Conjunto de Treino: É utilizado para ensinar o modelo a reconhecer padrões

  • Conjunto de Teste: É utilizado para avaliar o desempenho do modelo com dados ainda não vistos

Para realizar a divisão, utilizaremos a função train_test_split() do scikit-learn. Os parâmetros utilizados serão:

  • test_size=0.2: Define que 20% dos dados serão utilizados para teste, enquanto o restante será usado para treino.

  • random_state=42: Parâmetro que controla o gerador de número aleatórios utilizado para sortear os dados antes de separá-los. Garante reprodutibilidade.

  • stratify=y: Esse atributo definido como y é essencial devido à natureza da coluna relevance_category. Com essa definição, será mantida a mesma proporção das categorias em ambos os conjuntos, reduzindo o viés.

Treino: 1556 amostras

Teste: 390 amostras

Proporção: 80.0% treino, 20.0% teste

Distribuição das classes -

Treino:

relevance_category count
Medium 862
Low 366
High 303
Very High 25

Teste:

relevance_category count
Medium 216
Low 92
High 76
Very High 6
import pandas as pd
from sklearn.model_selection import train_test_split

df = pd.read_csv("dados_processado.csv")

features = [
    "book_freq", "popularity", "survival_prob", "isNoble",
    "has_title", "has_culture", "has_mother", "has_father", 
    "has_heir", "has_house"
]

target = "relevance_category"

x = df[features]
y = df[target]

x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42, stratify=y)

print(f"Treino: {x_train.shape[0]} amostras\n")
print(f"Teste: {x_test.shape[0]} amostras\n")
print(f"Proporção: {x_train.shape[0]/x.shape[0]*100:.1f}% treino, {x_test.shape[0]/x.shape[0]*100:.1f}% teste\n")

print("Distribuição das classes - \n")
print("Treino:\n")
print(y_train.value_counts().to_markdown(), "\n")
print("Teste:\n")
print(y_test.value_counts().to_markdown(), "\n")

Os dados, agora, estão devidamente divididos. Esta divisão adequada é de extrema importância, pois ajuda a evitar overfitting e garante que o modelo possa generalizar bem para novos personagens não vistos durante o treinamento.

Etapa 4 - Treinamento do Modelo

Agora, será realizado o treinamento do modelo. O objetivo dessa etapa é ensinar o algoritmo a reconhecer padrões nos dados que são fornecidos, e determinar a importância narrativa de cada personagem na série principal de livros de A Song of Ice and Fire.

Precisão do Modelo: 0.9513
Importância das Features:

Feature Importância
0 book_freq 0.439754
2 survival_prob 0.200915
3 isNoble 0.146227
1 popularity 0.102306
5 has_culture 0.054061
9 has_house 0.034825
4 has_title 0.021911
6 has_mother 0.000000
7 has_father 0.000000
8 has_heir 0.000000

2025-08-29T08:50:56.371129 image/svg+xml Matplotlib v3.10.5, https://matplotlib.org/

import matplotlib.pyplot as plt
import pandas as pd
from sklearn import tree
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
from io import StringIO

df = pd.read_csv("dados_processado.csv")

features = [
    "book_freq", "popularity", "survival_prob", "isNoble",
    "has_title", "has_culture", "has_mother", "has_father", 
    "has_heir", "has_house"
]

target = "relevance_category"

x = df[features]
y = df[target]

x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42, stratify=y)

classifier = tree.DecisionTreeClassifier(random_state=42)
classifier.fit(x_train, y_train)

y_pred = classifier.predict(x_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Precisão do Modelo: {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() + "<br>")

plt.figure(figsize=(20, 10))
tree.plot_tree(
    classifier, 
    feature_names=features,
    class_names=classifier.classes_,
    filled=True,
    rounded=True,
    max_depth=3, 
    fontsize=10
)

buffer = StringIO()
plt.savefig(buffer, format="svg")
print(buffer.getvalue())

Etapa 5 - Avaliação do modelo

Acurácia do modelo

O modelo alcançou uma acurácia impressionante de 95,13% no conjunto teste, demonstrando uma ótima capacidade de previsão com personagens ainda não vistos com base nas features escolhidas.

Importância das features

A análise da importância das features revela quais foram as variáveis mais importantes para a previsão e decisões do modelo:

Feature Importância Descrição
book_freq 43,98% Frequência de aparição nos livros da série principal
survival_prob 20,09% Probabilidade de sobrevivência
isNoble 14,62% Status nobre
popularity 10,23% Índice de popularidade
has_culture 5,41% Possui cultura conhecida
has_house 3,48% Pertence a uma casa
has_title 2,19% Tem algum título
has_mother 0,00% Há informação sobre a mãe
has_father 0,00% Há informação sobre o pai
has_heir 0,00% Possui herdeiro

Insights importantes sobre o modelo

  • Frequência em livros é determinante: A feature book_freq responde à aproximadamente 44% da importância, confirmando que personagens com mais aparições em diferentes livros da série principal possuem maior importância.

  • Features desnecessárias: O modelo também demonstrou que algumas features (has_mother, has_father, has_heir) não têm nenhuma importância na predição.

Etapa 6 - Relatório Final

O projeto geral foi um sucesso, com a obtenção de um modelo com uma acurácia de 95,13%. O modelo, além de alta performance, possui features relevantes identificadas e bem estabelecidas: book_freq, survival_prob e isNoble.

Limitações do modelo

Contudo, há limitações no modelo:

  • Features Redudantes: has_mother, has_father e has_heir possuem importância nula para a predição do sistema

  • Possível viés: É possível que, pela variável relevance_score ter sido manualmente estabelecida, pode haver viés

Possíveis melhorias

  • Validação de plod: Durante a primeira etapa, na exploração da base de dados, poderia ter sido feita uma Regressão Linear Múltipla completa para validar completamente a hipótese de que plod é a probabilidade de morte do personagem.

  • Remoção de features desnecessárias: As features relacionadas à parentesco podem ser removidas do modelo sem nenhum impacto na predição.

Considerações finais

A árvore de decisão se mostrou muito capaz de fazer a predição de narrativas literárias complexas como A Song of Ice and Fire. Além do excelente resultado de acurácia, foram providos insights importantes sobre a obra pelo modelo.

Além disso, foi possível observar que tanto a Etapa 1 quanto a Etapa 2 foram muito mais longas do que as posteriores, demonstrando a importância de entender e limpar o dataset antes do uso.