K-Means
Introdução
O objetivo desta etapa foi aplicar o algoritmo K-Means Clustering sobre a base de partidas de futebol da Premier League.
Diferente dos modelos anteriores (Árvore de Decisão e KNN), o K-Means é um algoritmo não supervisionado, ou seja, não utiliza a variável-alvo durante o treinamento.
A proposta aqui é identificar padrões ocultos nos dados a partir de estatísticas das partidas — como posse de bola, passes, chances criadas, público e estádio — e verificar como esses agrupamentos se relacionam com os resultados reais (class).
Para fins de visualização, os dados foram reduzidos para duas dimensões via PCA, permitindo a representação gráfica dos clusters e seus centróides.
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 |
|---|---|---|---|---|---|---|---|---|---|---|
| 5 | 2 | 0 | 10 | 11 | 64.2 | 35.8 | 89.3 | 80.9 | 0 | 4 |
| 6 | 2 | 0 | 19 | 18 | 67.5 | 62.5 | 84.7 | 69.9 | 3 | 3 |
| 1 | 2 | 31475 | 6 | 7 | 66.5 | 33.5 | 87.7 | 78.9 | 1 | 1 |
| 22 | 2 | 0 | 23 | 8 | 38.3 | 61.7 | 74.2 | 84.2 | 1 | 1 |
| 23 | 1 | 20650 | 21 | 4 | 52.2 | 47.8 | 76.9 | 76.4 | 1 | 3 |
| 12 | 2 | 31658 | 13 | 3 | 45.7 | 54.3 | 76.4 | 81.2 | 0 | 1 |
| 10 | 0 | 17051 | 9 | 20 | 48.3 | 51.7 | 69.2 | 75.6 | 3 | 1 |
| 17 | 2 | 39061 | 12 | 9 | 70.6 | 29.4 | 90.8 | 71.9 | 2 | 3 |
| 13 | 0 | 0 | 3 | 19 | 41.5 | 58.5 | 77.1 | 83.3 | 5 | 2 |
| 8 | 0 | 53319 | 1 | 12 | 55.7 | 44.3 | 86 | 84.5 | 1 | 1 |
| 12 | 2 | 0 | 13 | 24 | 67.8 | 32.2 | 83.2 | 67 | 1 | 1 |
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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13th November 2022 | 2:00pm | Amex Stadium | a | 31,581 | 6 | 1 | 7 | 2 | 65.1 | 34.9 | 7 | 8 | 2 | 2 | 1 | 3 | 4 | 3 | 86.3 | 72 | 1 | 0 | 11 | 4 | 3 | 1 | 42.9 | 52.2 | 55.6 | 44.4 | 0 | 1 | 14 | 11 | 2 | 7 | 0 | 0 | https://www.skysports.com/football/brighton-and-hove-albion-vs-aston-villa/464786 |
| 9/5/2021 | 12:00pm | Molineux | h | 0 | 13 | 2 | 6 | 1 | 60.7 | 39.3 | 16 | 5 | 6 | 3 | 3 | 1 | 7 | 1 | 88.9 | 82.3 | 1 | 0 | 10 | 5 | 0 | 2 | 57.1 | 66.7 | 21.7 | 78.3 | 2 | 4 | 12 | 7 | 2 | 2 | 0 | 2 | https://www.skysports.com/football/wolverhampton-wanderers-vs-brighton-and-hove-albion/stats/429186 |
| 8/11/2020 | 12:00pm | The Hawthorns | a | 0 | 24 | 0 | 8 | 1 | 41.2 | 58.8 | 12 | 19 | 2 | 5 | 7 | 6 | 3 | 8 | 66.5 | 78.1 | 1 | 2 | 9 | 6 | 1 | 1 | 73.7 | 62.5 | 64.6 | 35.4 | 4 | 2 | 17 | 12 | 1 | 0 | 0 | 0 | https://www.skysports.com/football/west-bromwich-albion-vs-tottenham-hotspur/stats/428915 |
| 20th May 2023 | 12:30p | Tottenham Hotspur Stadium | a | 61,514 | 8 | 1 | 9 | 3 | 61.8 | 38.2 | 22 | 11 | 8 | 4 | 6 | 6 | 8 | 1 | 86.2 | 77.7 | 2 | 2 | 8 | 2 | 1 | 1 | 50 | 54.5 | 59.1 | 40.9 | 1 | 6 | 11 | 9 | 1 | 1 | 0 | 0 | https://www.skysports.com/football/tottenham-hotspur-vs-brentford/465002 |
| 18/01/2021 | 8:00pm | Emirates Stadium | h | 0 | 2 | 3 | 4 | 0 | 66.4 | 33.6 | 20 | 4 | 6 | 1 | 11 | 2 | 3 | 1 | 89.9 | 74.9 | 2 | 0 | 7 | 2 | 4 | 1 | 71.4 | 36.4 | 36 | 64 | 1 | 3 | 9 | 8 | 0 | 0 | 0 | 0 | https://www.skysports.com/football/arsenal-vs-newcastle-united/stats/429017 |
| 27/11/2020 | 8:00pm | Selhurst Park | a | 0 | 11 | 0 | 4 | 2 | 57.4 | 42.6 | 12 | 14 | 3 | 7 | 8 | 5 | 1 | 2 | 82.1 | 73.6 | 1 | 2 | 7 | 3 | 0 | 0 | 53.3 | 45.8 | 54.9 | 45.1 | 5 | 3 | 9 | 10 | 2 | 2 | 0 | 0 | https://www.skysports.com/football/crystal-palace-vs-newcastle-united/stats/428930 |
| 15th April 2023 | 3:00pm | St. Mary's Stadium | a | 30,309 | 20 | 0 | 11 | 2 | 58.2 | 41.8 | 11 | 10 | 4 | 2 | 4 | 4 | 3 | 4 | 84.2 | 75.7 | 1 | 0 | 6 | 2 | 3 | 1 | 46.7 | 77.3 | 58.8 | 41.2 | 0 | 4 | 6 | 13 | 3 | 1 | 0 | 0 | https://www.skysports.com/football/southampton-vs-crystal-palace/464941 |
| 19th December 2021 | 2:00pm | St James' Park, Newcastle | a | 52,127 | 4 | 0 | 1 | 4 | 28.1 | 71.9 | 5 | 18 | 1 | 7 | 4 | 8 | 0 | 3 | 81.1 | 91.1 | 0 | 5 | 3 | 6 | 4 | 2 | 53.8 | 47.6 | 60 | 40 | 3 | 1 | 9 | 6 | 1 | 2 | 0 | 0 | https://www.skysports.com/football/newcastle-united-vs-manchester-city/stats/446462 |
| 26/01/2021 | 8:15pm | St. Mary's Stadium | a | 0 | 20 | 1 | 2 | 3 | 58 | 42 | 13 | 9 | 5 | 5 | 5 | 4 | 3 | 0 | 85.4 | 77.3 | 0 | 4 | 4 | 5 | 2 | 2 | 64.7 | 53.3 | 47.6 | 52.4 | 1 | 4 | 11 | 6 | 1 | 2 | 0 | 0 | https://www.skysports.com/football/southampton-vs-arsenal/stats/429035 |
| 18th February 2023 | 3:00pm | Goodison Park | h | 39,232 | 17 | 1 | 19 | 0 | 50 | 50 | 15 | 8 | 6 | 0 | 7 | 4 | 2 | 4 | 73.4 | 69 | 2 | 1 | 6 | 4 | 1 | 0 | 56.5 | 58.3 | 47.4 | 52.6 | 0 | 3 | 8 | 6 | 2 | 3 | 0 | 0 | https://www.skysports.com/football/everton-vs-leeds-united/464869 |
| 18th September 2022 | 12:00p | Gtech Community Stadium | a | 17,122 | 9 | 0 | 2 | 3 | 36.1 | 63.9 | 5 | 13 | 2 | 7 | 3 | 4 | 0 | 2 | 74.1 | 86 | 1 | 2 | 3 | 3 | 3 | 1 | 61.9 | 75 | 72.4 | 27.6 | 4 | 2 | 10 | 10 | 0 | 2 | 0 | 0 | https://www.skysports.com/football/brentford-vs-arsenal/464706 |
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
O modelo foi treinado com 3 clusters, em referência às três classes possíveis do resultado da partida: vitória do mandante, empate e vitória do visitante.
Após o pré-processamento (padronização das variáveis numéricas e transformação das categóricas em dummies), o algoritmo K-Means foi aplicado sobre os dados transformados.
Em seguida, os dados foram projetados em duas dimensões com PCA para permitir a visualização gráfica.
Na figura abaixo é possível observar a formação dos três clusters identificados pelo algoritmo e seus respectivos centróides (em vermelho):
import base64
from io import BytesIO
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
df = pd.read_csv("./src/mydata.csv")
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").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
)
scaler = StandardScaler()
X_num = df[num_cols].copy()
X_num[num_cols] = scaler.fit_transform(X_num[num_cols])
X = pd.concat([X_num, X_cat], axis=1).values
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X)
kmeans = KMeans(n_clusters=3, init="k-means++", max_iter=300, n_init=10, random_state=42)
labels = kmeans.fit_predict(X_pca)
plt.figure(figsize=(12, 10))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=labels, cmap="viridis", s=50)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
c="red", marker="*", s=220, label="Centroids")
plt.title("K-Means Clustering (Football Matches) - PCA 2D")
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.legend()
buf = BytesIO()
plt.savefig(buf, format="png", transparent=True, bbox_inches="tight")
buf.seek(0)
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
html_img = f'<img src="data:image/png;base64,{img_base64}" alt="KMeans clustering" />'
print(html_img)
Esse resultado mostra que o algoritmo conseguiu dividir os dados em agrupamentos distintos, mas com fronteiras de separação difusas, já que as estatísticas utilizadas não garantem divisões claras entre os diferentes resultados das partidas.
Avaliação do Modelo
Para avaliar a qualidade do clustering, os rótulos de clusters foram mapeados para as classes reais (class) por meio de um voto majoritário dentro de cada grupo.
Dessa forma, foi possível calcular métricas de classificação mesmo em um cenário originalmente não supervisionado.
O modelo obteve uma acurácia aproximada de 47.92%, com a seguinte matriz de confusão:
Acurácia: 47.92%
Matriz de Confusão:
| Classe Pred 0 | Classe Pred 1 | Classe Pred 2 | |
|---|---|---|---|
| Classe Real 0 | 254 | 0 | 141 |
| Classe Real 1 | 107 | 0 | 98 |
| Classe Real 2 | 129 | 0 | 183 |
import pandas as pd
from sklearn.metrics import accuracy_score, confusion_matrix
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
df = pd.read_csv("./src/mydata.csv")
num_cols = [
"attendance",
"home_possessions", "away_possessions",
"home_pass", "away_pass",
"home_chances", "away_chances",
]
cat_cols = ["stadium", "Home Team", "Away Team"]
target = "class"
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
)
scaler = StandardScaler()
X_num = df[num_cols].copy()
X_num[num_cols] = scaler.fit_transform(X_num[num_cols])
X = pd.concat([X_num, X_cat], axis=1).values
y_map = {"h": 0, "d": 1, "a": 2}
y = df[target].astype(str).str.strip().map(y_map).astype(int).values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
pca = PCA(n_components=2)
X_train_pca = pca.fit_transform(X_train)
kmeans = KMeans(n_clusters=3, init="k-means++", max_iter=300, n_init=10, random_state=42)
labels = kmeans.fit_predict(X_train_pca)
cluster_map = {}
classes = np.unique(y_train)
for c in np.unique(labels):
mask = labels == c
if mask.sum() == 0:
cluster_map[c] = classes[0]
continue
counts = np.bincount(y_train[mask], minlength=classes.max()+1)
majority = counts.argmax()
cluster_map[c] = majority
y_pred_train = np.array([cluster_map[c] for c in labels])
acc = accuracy_score(y_train, y_pred_train)
cm = confusion_matrix(y_train, y_pred_train, labels=np.sort(classes))
cm_df = pd.DataFrame(
cm,
index=[f"Classe Real {cls}" for cls in np.sort(classes)],
columns=[f"Classe Pred {cls}" for cls in np.sort(classes)]
)
print(f"Acurácia: {acc*100:.2f}%")
print("<br>Matriz de Confusão:")
print(cm_df.to_html(index=True))
As observações principais são:
- O algoritmo conseguiu capturar alguns padrões nas vitórias do mandante (Classe 0) e visitante (Classe 2), mas ainda houve bastante confusão entre elas.
- A classe de empate (Classe 1) foi a mais difícil de identificar, refletindo sua baixa frequência no dataset e a sobreposição natural entre variáveis.
- A visualização em 2D reforça essa dificuldade, mostrando que as fronteiras entre grupos são pouco definidas.
Relatório Final
O experimento com K-Means evidenciou os desafios de aplicar técnicas não supervisionadas no domínio esportivo:
- Exploração: foram utilizadas variáveis contextuais (estádio, público) e estatísticas gerais (posse, passes, chances), previamente normalizadas e transformadas em numéricas.
- Treinamento: o K-Means gerou 3 clusters, interpretados como padrões de desempenho relacionados ao resultado da partida.
- Avaliação: ao comparar clusters com classes reais, a acurácia foi de ~47.9%, valor semelhante aos modelos supervisionados, mas com maiores limitações na interpretação.
Alguns pontos importantes emergem da análise: - O K-Means não tem acesso direto às classes, e portanto os agrupamentos não correspondem necessariamente a vitórias, empates ou derrotas.
- A sobreposição natural dos dados mostra que estatísticas gerais de jogo não são suficientes para definir fronteiras claras entre resultados.
- O futebol continua sendo altamente imprevisível, com variáveis externas (clima, arbitragem, forma dos jogadores, estratégia) que não estão refletidas na base.
Conclusão
O uso do K-Means neste projeto demonstrou o potencial dos algoritmos não supervisionados para explorar padrões em conjuntos de dados complexos, mas também reforçou suas limitações frente a tarefas de previsão.
A acurácia de ~47.9% mostra que, mesmo quando os clusters são posteriormente relacionados às classes reais, o modelo não consegue separar bem as situações de vitória, empate e derrota.
Assim como nos experimentos anteriores, o aprendizado principal não está apenas no valor numérico da métrica, mas no entendimento de que nem todo fenômeno pode ser descrito por padrões lineares ou consistentes.
Esse resultado abre espaço para trabalhos futuros, como o uso de modelos híbridos (combinando supervisionados e não supervisionados) ou a incorporação de variáveis mais ricas, como desempenho de jogadores, estatísticas avançadas e fatores contextuais adicionais.