KNN
Introdução
O objetivo desta etapa foi aplicar o algoritmo K-Nearest Neighbors (KNN) sobre a base de dados de partidas de futebol, utilizando as bibliotecas pandas, numpy, matplotlib e scikit-learn.
Diferente da árvore de decisão, o KNN classifica novas observações com base na proximidade de exemplos já conhecidos.
A proposta é avaliar como variáveis como estádio, público, posse de bola, passes e chances criadas podem auxiliar na previsão do resultado da partida (class).
Foram desenvolvidas duas abordagens: uma implementação manual, para consolidar a compreensão do funcionamento do método, e outra com a biblioteca scikit-learn, permitindo comparação de resultados e visualização da fronteira de decisão por meio do PCA.
Base de dados
A base de dados utilizada neste projeto contém informações de partidas de futebol, totalizando 1140 linhas e 40 colunas. Entre as variáveis estão posse de bola, número de passes e chances criadas.
A variável alvo escolhida é a coluna class, que indica o resultado da partida (vitória do mandante, empate ou vitória do visitante).
Exploração dos Dados
A seguir foi feita uma análise do significado e composição de cada coluna presente na base, com a finalidade de identificar possíveis problemas a serem tratados posteriormente. As visualizações e estatísticas descritivas ajudam a compreender a natureza dos dados e orientar decisões de pré-processamento e modelagem.
A coluna class é a variável alvo do projeto e representa o resultado da partida: vitória do mandante ("h"), empate ("d") ou vitória do visitante ("a"). Trata-se de uma variável categórica com três possíveis valores, sendo o objeto de classificação do modelo de árvore de decisão. A análise exploratória dessa coluna é essencial para observar o balanceamento do conjunto de dados, isto é, se há proporções semelhantes ou discrepantes entre os resultados possíveis.
A coluna attendance representa o público presente em cada partida, um indicador de contexto do jogo que pode refletir fatores como mando de campo, relevância do confronto e engajamento da torcida. Em bases de futebol, o público tende a variar bastante entre estádios e rodadas, podendo apresentar assimetria (jogos muito cheios em arenas grandes) e valores atípicos (clássicos, finais).
Do ponto de vista analítico, é uma variável contínua útil para observar a distribuição de torcedores ao longo das partidas e investigar relações com o resultado (class).
A coluna stadium identifica o estádio onde a partida foi disputada. Trata-se de uma variável categórica, associada ao contexto e à capacidade do local, podendo refletir fatores como mando de campo, perfil da torcida, relevo do gramado e até particularidades logísticas. Na análise exploratória, é útil observar a frequência de jogos por estádio, verificando balanceamento da amostra (quais arenas têm mais/menos partidas) e possíveis vieses (ex.: concentração em poucos estádios). Relações mais profundas com o resultado (class) podem ser investigadas em etapas seguintes, mas aqui focamos em entender a composição dessa variável no conjunto de dados.
A coluna home_possessions representa a porcentagem de posse de bola do time mandante em cada partida. Trata-se de uma variável numérica contínua, normalmente variando entre 30% e 70% na maioria dos jogos, podendo indicar estilos de jogo (times que mantêm a bola ou que jogam mais reativamente). A análise exploratória dessa variável permite observar a distribuição da posse de bola entre os mandantes, identificar valores atípicos e comparar a média do controle de jogo ao longo das rodadas. Posteriormente, poderá ser interessante relacionar essa posse com o resultado final (class) para verificar padrões.
A coluna away_possessions representa a porcentagem de posse de bola do time visitante em cada partida. Por ser uma variável numérica contínua, sua distribuição ajuda a observar o comportamento dos visitantes em termos de controle de jogo, identificar valores atípicos e comparar a tendência média de posse fora de casa. Em etapas posteriores, pode ser relacionada ao resultado (class) para investigar padrões de desempenho como visitante.
A coluna Home Team identifica o time mandante na partida. Embora esteja codificada numericamente no dataset, sua natureza é categórica (IDs de times). Na análise exploratória, é útil observar a frequência de jogos por mandante, verificando o balanceamento da amostra entre os times que atuam em casa e possíveis concentrações. Relações com o resultado (class) podem ser exploradas depois; aqui focamos em entender a composição dessa variável.
A coluna Away Team identifica o time visitante na partida. Apesar de codificada como número, sua natureza é categórica (IDs de times). Na análise exploratória, observar a frequência de jogos por visitante ajuda a avaliar o balanceamento da amostra e possíveis concentrações de partidas em determinados clubes.
A coluna home_pass indica o número de passes realizados pelo time mandante em cada partida. É uma variável numérica contínua que ajuda a caracterizar o estilo de jogo do mandante, podendo variar bastante entre equipes mais ou menos dependentes da posse de bola. Na análise exploratória, observar a distribuição dos passes permite identificar médias, dispersão e valores atípicos.
A coluna away_pass indica o número de passes realizados pelo time visitante em cada partida. Sendo uma variável numérica contínua, sua distribuição mostra como os visitantes se comportam em termos de construção de jogadas e controle de posse fora de casa. A análise exploratória ajuda a entender a média de passes, variações entre os jogos e eventuais valores extremos.
A coluna home_chances representa a quantidade de chances de gol criadas pelo time mandante durante a partida. É uma variável numérica discreta que indica o nível de ofensividade da equipe jogando em casa. A análise exploratória permite identificar a frequência de jogos com poucas ou muitas oportunidades e verificar a dispersão desse tipo de estatística.
A coluna away_chances indica a quantidade de chances de gol criadas pelo time visitante durante a partida. É uma variável numérica discreta que ajuda a compreender a ofensividade dos times jogando fora de casa. A análise exploratória mostra como os visitantes se comportam em termos de criação de oportunidades, permitindo identificar padrões de equilíbrio ou diferenças marcantes em relação aos mandantes.
Pré-processamento
Após a exploração inicial da base, foram aplicados procedimentos de pré-processamento para preparar os dados para o treinamento do modelo.
Entre as etapas realizadas estão:
- Conversão de tipos: variáveis originalmente em texto com valores numéricos (como
attendance) foram transformadas em formato numérico. - Tratamento de valores categóricos: colunas como
stadium,Home TeameAway Teamforam mantidas como categóricas, sendo posteriormente convertidas em variáveis numéricas por meio de técnicas de codificação. - Remoção de colunas irrelevantes ou redundantes: colunas de identificação e de tempo (
date,clock,links), bem como estatísticas que representam vazamento de informação do resultado (ex.:Goals Home,Away Goals), foram descartadas do conjunto de treino. - Separação entre features e target: as variáveis explicativas (
X) foram definidas a partir de aproximadamente dez colunas relevantes da base, enquanto a variável alvo (y) é a colunaclass. - Divisão em treino e teste: o conjunto de dados foi dividido em duas partes, garantindo estratificação do alvo para manter o equilíbrio das classes.
| stadium | class | attendance | Home Team | Away Team | home_possessions | away_possessions | home_pass | away_pass | home_chances | away_chances |
|---|---|---|---|---|---|---|---|---|---|---|
| 8 | 0 | 52395 | 1 | 10 | 70.6 | 29.4 | 91.1 | 79.3 | 3 | 0 |
| 7 | 1 | 59475 | 2 | 11 | 53.6 | 46.4 | 87.1 | 84.6 | 1 | 0 |
| 12 | 0 | 0 | 13 | 6 | 60.7 | 39.3 | 88.9 | 82.3 | 1 | 0 |
| 12 | 2 | 31395 | 13 | 11 | 64 | 36 | 81.3 | 73.7 | 1 | 0 |
| 2 | 0 | 53018 | 5 | 19 | 63.7 | 36.3 | 86.8 | 74.1 | 4 | 0 |
| 7 | 0 | 0 | 2 | 25 | 64.5 | 35.5 | 89.1 | 79.1 | 1 | 0 |
| 14 | 1 | 25043 | 11 | 9 | 48.9 | 51.1 | 81.1 | 80.1 | 0 | 5 |
| 4 | 2 | 27010 | 22 | 1 | 28.5 | 71.5 | 75.4 | 90.5 | 0 | 4 |
| 10 | 0 | 17080 | 9 | 16 | 68.9 | 31.1 | 84.7 | 68.2 | 0 | 1 |
| 9 | 1 | 0 | 17 | 5 | 41.9 | 58.1 | 74.7 | 83.4 | 3 | 4 |
| 17 | 0 | 32231 | 12 | 14 | 66.6 | 33.4 | 88.5 | 74.6 | 1 | 0 |
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn import tree
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
df = pd.read_csv("./src/mydata.csv")
# Excluir as colunas não desejadas
df = df.drop(columns= ["date", "clock", "links", "Goals Home", "Away Goals", "home_shots", "away_shots", "home_on", "away_on",
"home_off", "away_off", "home_blocked", "away_blocked", "home_corners", "away_corners",
"home_offside", "away_offside", "home_tackles", "away_tackles", "home_duels", "away_duels",
"home_saves", "away_saves", "home_fouls", "away_fouls", "home_yellow", "away_yellow",
"home_red", "away_red"])
# Label encoding dos estadios em texto
df["stadium"] = label_encoder.fit_transform(df["stadium"])
# Transformar resultado do jogo times em números
df["class"] = df["class"].replace({'h':0, 'd': 1, 'a':2})
# Transforma o públido de string para número
df["attendance"] = df["attendance"].str.replace(',', '').astype(int)
print(df.sample(frac=.01).to_markdown(index=False))
| date | clock | stadium | class | attendance | Home Team | Goals Home | Away Team | Away Goals | home_possessions | away_possessions | home_shots | away_shots | home_on | away_on | home_off | away_off | home_blocked | away_blocked | home_pass | away_pass | home_chances | away_chances | home_corners | away_corners | home_offside | away_offside | home_tackles | away_tackles | home_duels | away_duels | home_saves | away_saves | home_fouls | away_fouls | home_yellow | away_yellow | home_red | away_red | links |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 17/01/2021 | 7:15pm | Etihad Stadium | h | 0 | 1 | 4 | 11 | 0 | 72.2 | 27.8 | 13 | 2 | 6 | 0 | 3 | 1 | 4 | 1 | 90.4 | 72.2 | 1 | 0 | 11 | 1 | 0 | 0 | 55.6 | 60 | 53.8 | 46.2 | 0 | 2 | 9 | 8 | 0 | 0 | 0 | 0 | https://www.skysports.com/football/manchester-city-vs-crystal-palace/stats/429023 |
| 20th November 2021 | 5:30pm | Anfield | h | 53,092 | 5 | 4 | 2 | 0 | 62.5 | 37.5 | 19 | 5 | 9 | 3 | 3 | 0 | 7 | 2 | 88.4 | 81 | 4 | 0 | 6 | 1 | 2 | 8 | 57.1 | 33.3 | 50 | 50 | 3 | 5 | 15 | 12 | 2 | 0 | 0 | 0 | https://www.skysports.com/football/liverpool-vs-arsenal/stats/446401 |
| 1/11/2020 | 4:30pm | Old Trafford | a | 0 | 3 | 0 | 2 | 1 | 53.3 | 46.7 | 8 | 7 | 2 | 2 | 3 | 5 | 3 | 0 | 85.1 | 85.6 | 0 | 0 | 6 | 3 | 4 | 0 | 70 | 75 | 66.7 | 33.3 | 1 | 3 | 12 | 12 | 3 | 3 | 0 | 0 | https://www.skysports.com/football/manchester-united-vs-arsenal/stats/428902 |
| 16/01/2021 | 3:00pm | London Stadium | h | 0 | 14 | 1 | 23 | 0 | 44.8 | 55.2 | 15 | 10 | 4 | 2 | 6 | 3 | 5 | 5 | 72.8 | 73.8 | 0 | 0 | 7 | 4 | 5 | 1 | 62.5 | 46.2 | 54.4 | 45.6 | 2 | 2 | 12 | 9 | 2 | 1 | 0 | 0 | https://www.skysports.com/football/west-ham-united-vs-burnley/stats/429025 |
| 21st May 2023 | 2:00pm | Amex Stadium | h | 31,507 | 6 | 3 | 20 | 1 | 63 | 37 | 26 | 5 | 8 | 1 | 11 | 3 | 7 | 1 | 91.7 | 79.6 | 1 | 1 | 4 | 3 | 1 | 1 | 69.2 | 62.5 | 73.3 | 26.7 | 0 | 5 | 8 | 9 | 2 | 5 | 0 | 0 | https://www.skysports.com/football/brighton-and-hove-albion-vs-southampton/464996 |
| 25th April 2022 | 8:00pm | Selhurst Park | d | 25,357 | 11 | 0 | 19 | 0 | 52.5 | 47.5 | 17 | 9 | 7 | 2 | 6 | 4 | 4 | 3 | 75.2 | 69.1 | 0 | 0 | 6 | 3 | 1 | 1 | 53.8 | 58.6 | 57.8 | 42.2 | 2 | 7 | 12 | 13 | 2 | 2 | 0 | 0 | https://www.skysports.com/football/crystal-palace-vs-leeds-united/stats/446623 |
| 3rd October 2021 | 2:00pm | Tottenham Hotspur Stadium | h | 53,076 | 8 | 2 | 7 | 1 | 56.3 | 43.7 | 17 | 14 | 8 | 3 | 6 | 5 | 3 | 6 | 79.7 | 71.1 | 3 | 1 | 5 | 8 | 0 | 1 | 57.1 | 57.1 | 48.8 | 51.2 | 2 | 6 | 11 | 14 | 2 | 1 | 0 | 0 | https://www.skysports.com/football/tottenham-hotspur-vs-aston-villa/stats/446355 |
| 12th December 2021 | 2:00pm | The King Power Stadium | h | 31,959 | 18 | 4 | 4 | 0 | 47.3 | 52.7 | 8 | 12 | 5 | 3 | 2 | 3 | 1 | 6 | 79.3 | 79.8 | 2 | 0 | 4 | 6 | 0 | 1 | 65 | 61.5 | 46.7 | 53.3 | 3 | 1 | 9 | 16 | 2 | 3 | 0 | 0 | https://www.skysports.com/football/leicester-city-vs-newcastle-united/stats/446444 |
| 21st November 2021 | 2:00pm | Etihad Stadium | h | 52,571 | 1 | 3 | 17 | 0 | 73.3 | 22.7 | 17 | 4 | 7 | 1 | 6 | 0 | 4 | 3 | 93.7 | 69.6 | 4 | 0 | 7 | 1 | 0 | 2 | 46.7 | 38.9 | 75 | 25 | 1 | 4 | 5 | 7 | 1 | 1 | 0 | 0 | https://www.skysports.com/football/manchester-city-vs-everton/stats/446402 |
| 26/12/2020 | 8:00pm | Bramall Lane | a | 0 | 25 | 0 | 17 | 1 | 43.8 | 56.2 | 10 | 7 | 2 | 3 | 5 | 3 | 3 | 1 | 72.9 | 79.1 | 1 | 1 | 6 | 3 | 0 | 2 | 50 | 76.9 | 38.8 | 61.2 | 2 | 1 | 11 | 10 | 2 | 4 | 0 | 0 | https://www.skysports.com/football/sheffield-united-vs-everton/stats/428984 |
| 12/3/2021 | 8:00pm | St James' Park, Newcastle | d | 0 | 4 | 1 | 7 | 1 | 49 | 51 | 12 | 15 | 3 | 6 | 4 | 4 | 5 | 5 | 71.9 | 76.2 | 1 | 1 | 2 | 2 | 0 | 4 | 65 | 50 | 44 | 56 | 6 | 3 | 10 | 10 | 2 | 3 | 0 | 0 | https://www.skysports.com/football/newcastle-united-vs-aston-villa/stats/429114 |
Divisão dos Dados
Com a base pré-processada, realizou-se a divisão entre conjuntos de treinamento e teste.
O objetivo dessa etapa é garantir que o modelo seja avaliado em dados que ele nunca viu durante o treinamento, permitindo uma medida mais confiável de sua capacidade de generalização.
Foi utilizada a função train_test_split da biblioteca scikit-learn, com os seguintes critérios:
- 70% dos dados destinados ao treinamento, para que o modelo aprenda os padrões da base;
- 30% dos dados destinados ao teste, para avaliar o desempenho em novos exemplos;
- Estratificação pelo alvo (class), garantindo que a proporção entre vitórias do mandante, empates e vitórias do visitante fosse mantida em ambos os conjuntos;
- Random State fixado, assegurando reprodutibilidade na divisão.
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn import tree
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
df = pd.read_csv("./src/mydata.csv")
# Excluir as colunas não desejadas
df = df.drop(columns= ["date", "clock", "links", "Goals Home", "Away Goals", "home_shots", "away_shots", "home_on", "away_on",
"home_off", "away_off", "home_blocked", "away_blocked", "home_corners", "away_corners",
"home_offside", "away_offside", "home_tackles", "away_tackles", "home_duels", "away_duels",
"home_saves", "away_saves", "home_fouls", "away_fouls", "home_yellow", "away_yellow",
"home_red", "away_red"])
# Label encoding dos estadios em texto
df["stadium"] = label_encoder.fit_transform(df["stadium"])
# Transformar resultado do jogo times em números
df["class"] = df["class"].replace({'h':0, 'd': 1, 'a':2})
# Transforma o públido de string para número
df["attendance"] = df["attendance"].str.replace(',', '').astype(int)
# Variáveis independentes (features)
x = df[[
"stadium", "attendance",
"Home Team", "Away Team",
"home_possessions", "away_possessions",
"home_pass", "away_pass",
"home_chances", "away_chances"
]]
# Variável dependente (alvo)
y = df["class"]
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=27, stratify=y)
Treinamento do Modelo
Nesta seção foi implementado o algoritmo KNN de forma manual, a partir do zero, para consolidar o entendimento do funcionamento do método.
A implementação considera a distância euclidiana entre os pontos, identifica os vizinhos mais próximos e atribui a classe com maior frequência.
Esse exercício é importante para compreender a lógica por trás do KNN antes de utilizar bibliotecas prontas.
Acurácia (KNN k=5): 0.51
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler
DATA_PATH = "./src/mydata.csv"
df = pd.read_csv(DATA_PATH)
y_map = {"h": 0, "d": 1, "a": 2}
y = df["class"].astype(str).str.strip().map(y_map).astype(int)
num_cols = [
"attendance",
"home_possessions", "away_possessions",
"home_pass", "away_pass",
"home_chances", "away_chances",
]
cat_cols = ["stadium", "Home Team", "Away Team"]
df["attendance"] = (
df["attendance"]
.astype(str)
.str.replace(r"[^0-9]", "", regex=True)
.replace("", "0")
.astype(float)
)
for c in num_cols:
df[c] = pd.to_numeric(df[c], errors="coerce")
df[c] = df[c].fillna(df[c].median())
X_cat = pd.get_dummies(
df[cat_cols].astype(str).apply(lambda s: s.str.strip()),
drop_first=False, dtype=int
)
X_num = df[num_cols].copy()
scaler = StandardScaler()
X_num[num_cols] = scaler.fit_transform(X_num[num_cols])
X = pd.concat([X_num, X_cat], axis=1).values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.20, random_state=42, stratify=y
)
class KNNClassifier:
def __init__(self, k=5):
self.k = k
def fit(self, X, y):
self.X_train = X
self.y_train = np.array(y)
def predict(self, X):
return np.array([self._predict(x) for x in X])
def _predict(self, x):
distances = np.sqrt(((self.X_train - x) ** 2).sum(axis=1))
k_idx = np.argsort(distances)[:self.k]
k_labels = self.y_train[k_idx]
vals, counts = np.unique(k_labels, return_counts=True)
return vals[np.argmax(counts)]
knn = KNNClassifier(k=5)
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)
acc = accuracy_score(y_test, y_pred)
print(f"Acurácia (KNN k={knn.k}): {acc:.2f}")
Usando Scikit-Learn
Nesta seção foi implementado o algoritmo KNN de forma manual, a partir do zero, para consolidar o entendimento do funcionamento do método.
A implementação considera a distância euclidiana entre os pontos, identifica os vizinhos mais próximos e atribui a classe com maior frequência.
Esse exercício é importante para compreender a lógica por trás do KNN antes de utilizar bibliotecas prontas.
Accuracy: 0.42
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from io import StringIO
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
# 1) Carregar sua base
df = pd.read_csv("./src/mydata.csv")
# 2) Alvo (class): h=mandante, d=empate, a=visitante
y_map = {"h": 0, "d": 1, "a": 2}
y = df["class"].astype(str).str.strip().map(y_map).astype(int)
# 3) Seleção de features (≈10, sem vazamento)
num_cols = [
"attendance",
"home_possessions", "away_possessions",
"home_pass", "away_pass",
"home_chances", "away_chances",
]
cat_cols = ["stadium", "Home Team", "Away Team"]
# 4) Limpezas mínimas para numéricas (ex.: attendance vem como texto com pontuação)
df["attendance"] = (
df["attendance"]
.astype(str)
.str.replace(r"[^0-9]", "", regex=True)
.replace("", "0")
.astype(float)
)
for c in num_cols:
df[c] = pd.to_numeric(df[c], errors="coerce")
df[c] = df[c].fillna(df[c].median())
# 5) One-hot para categóricas; escala para numéricas
X_cat = pd.get_dummies(
df[cat_cols].astype(str).apply(lambda s: s.str.strip()),
drop_first=False, dtype=int
)
X_num = df[num_cols].copy()
scaler = StandardScaler()
X_num[num_cols] = scaler.fit_transform(X_num[num_cols])
X = pd.concat([X_num, X_cat], axis=1).values
# 6) Split (mesma estrutura)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 7) PCA só para visualização (2D)
pca = PCA(n_components=2)
X_train_2d = pca.fit_transform(X_train)
X_test_2d = pca.transform(X_test)
# 8) KNN (baseline igual)
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train_2d, y_train)
predictions = knn.predict(X_test_2d)
print(f"Accuracy: {accuracy_score(y_test, predictions):.2f}")
# 9) Fronteira de decisão (no espaço 2D do PCA)
plt.figure(figsize=(12, 10))
h = 0.05
x_min, x_max = X_train_2d[:, 0].min() - 1, X_train_2d[:, 0].max() + 1
y_min, y_max = X_train_2d[:, 1].min() - 1, X_train_2d[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, cmap=plt.cm.RdYlBu, alpha=0.3)
sns.scatterplot(x=X_train_2d[:, 0], y=X_train_2d[:, 1], hue=y_train,
palette="deep", s=100, edgecolor="k", alpha=0.8, legend="full")
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.title("KNN Decision Boundary (Football Matches)")
buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())
Avaliação do Modelo
Após o treinamento do algoritmo KNN, o modelo foi avaliado no conjunto de teste, obtendo resultados de acurácia em torno de 0.42 (usando scikit-learn) e 0.51 (na implementação manual).
Esses valores mostram que o modelo consegue acertar parte das previsões, mas ainda apresenta uma performance limitada.
Esse resultado deve ser interpretado considerando a natureza do problema: prever o resultado de uma partida de futebol é uma tarefa complexa, com alto grau de incerteza.
Mesmo dispondo de estatísticas como posse de bola, passes, chances criadas, estádio e público, o desfecho de um jogo depende também de fatores externos como arbitragem, lesões, clima, motivação e até elementos de acaso.
Portanto, ainda que houvesse mais dados disponíveis, a previsão exata continuaria sendo incerta.
A figura abaixo mostra a fronteira de decisão do KNN no espaço bidimensional reduzido pelo PCA.
As regiões coloridas representam as áreas de influência de cada classe, enquanto os pontos correspondem às partidas reais.
É possível observar uma forte sobreposição entre classes, com pontos de diferentes resultados distribuídos de forma bastante misturada. Isso reforça as limitações do modelo:
- Baixa capacidade preditiva: as variáveis utilizadas ajudam a descrever o contexto, mas não criam fronteiras claras para separar vitórias, empates e derrotas.
- Balanceamento do alvo: os empates, menos frequentes, tendem a ser mal classificados, prejudicando a acurácia global.
- Sensibilidade do algoritmo: o KNN depende fortemente da escala, da escolha de
ke da representação dos dados, o que gera variações nos resultados. - Complexidade do domínio: a imprevisibilidade do futebol limita naturalmente a precisão que qualquer modelo pode alcançar.
Portanto, as acurácias obtidas refletem tanto as limitações do KNN quanto a complexidade do fenômeno esportivo. Mais do que “acertar resultados”, o exercício evidencia os desafios de aplicar Machine Learning em cenários reais de alta incerteza.
Relatório Final
O projeto teve como objetivo aplicar o algoritmo KNN sobre uma base de partidas de futebol, explorando seu potencial no contexto esportivo.
A análise foi conduzida em etapas bem definidas:
- Exploração dos Dados (EDA): foram selecionadas e analisadas cerca de dez variáveis relevantes, incluindo
stadium,attendance,Home Team,Away Team,home_possessions,away_possessions,home_pass,away_pass,home_chanceseaway_chances. - Pré-processamento: colunas irrelevantes (como IDs e dados de tempo) e estatísticas ligadas diretamente ao resultado (como gols) foram removidas. Variáveis categóricas foram transformadas em numéricas via One-Hot Encoding e variáveis contínuas foram padronizadas.
- Divisão dos Dados: o conjunto foi separado em treino (70%) e teste (30%), preservando a proporção entre vitórias, empates e derrotas através da estratificação.
- Treinamento e Avaliação: o KNN foi aplicado em duas versões — manual e com scikit-learn — alcançando acurácias de 0.42–0.51, valores que evidenciam desempenho limitado, mas condizente com a natureza do problema.
A avaliação trouxe alguns aprendizados importantes: - O KNN é sensível à preparação dos dados e à escolha de hiperparâmetros, o que explica diferenças entre implementações.
- As variáveis utilizadas caracterizam parcialmente os jogos, mas não são suficientes para prever com exatidão seus resultados.
- A visualização da fronteira de decisão mostrou que as classes apresentam alta sobreposição, o que dificulta a separação clara.
- O futebol, por ser um evento com forte componente de imprevisibilidade, impõe limites naturais ao desempenho de qualquer modelo.
Conclusão
Assim como na árvore de decisão, a experiência com o KNN reforça a importância de interpretar os resultados com cautela.
A acurácia próxima de 0.5 não deve ser vista apenas como limitação do algoritmo, mas como reflexo da complexidade do domínio esportivo.
O projeto destacou a relevância da análise exploratória, do cuidado com o pré-processamento e da avaliação crítica dos modelos. Além disso, abre espaço para trabalhos futuros que explorem variáveis adicionais (desempenho histórico dos times, estatísticas de jogadores, forma recente, fatores externos) e ajustes de hiperparâmetros do KNN para buscar melhorias.
Mais do que prever com exatidão, este trabalho evidencia o potencial e os limites do uso de algoritmos de aprendizado de máquina em contextos reais, nos quais a incerteza é parte inerente do fenômeno analisado.