Random Forest
Introdução
Aplicar Random Forest para prever o resultado de partidas da Premier League (class: 0=mandante, 1=empate, 2=visitante) usando as ~10 variáveis já utilizadas nos modelos anteriores (estádio, público, posse, passes, chances e IDs dos times). Mantivemos o mesmo padrão do projeto: remoção de colunas com vazamento, limpeza de tipos (ex.: attendance), estratificação na divisão 70/30 e sem usar estatísticas de gols.
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 |
|---|---|---|---|---|---|---|---|---|---|---|
| 12 | 0 | 0 | 13 | 12 | 45.1 | 54.9 | 80.4 | 85.8 | 1 | 1 |
| 12 | 2 | 34624 | 13 | 2 | 37.4 | 62.6 | 79.6 | 89.3 | 0 | 1 |
| 14 | 1 | 0 | 11 | 10 | 40.4 | 59.6 | 75.5 | 83 | 0 | 1 |
| 19 | 2 | 0 | 24 | 2 | 38.1 | 61.9 | 74.3 | 82.7 | 2 | 4 |
| 15 | 0 | 52247 | 4 | 10 | 62.3 | 37.7 | 82.2 | 70 | 2 | 0 |
| 24 | 0 | 42164 | 7 | 8 | 48.7 | 51.3 | 85 | 85.3 | 3 | 1 |
| 12 | 1 | 30328 | 13 | 23 | 65.2 | 34.8 | 79.8 | 66 | 0 | 0 |
| 10 | 0 | 16957 | 9 | 19 | 31.1 | 68.9 | 64.7 | 82.4 | 3 | 3 |
| 21 | 0 | 54202 | 8 | 9 | 47 | 53 | 76.1 | 79.8 | 3 | 0 |
| 7 | 2 | 0 | 2 | 5 | 35.8 | 64.2 | 75.9 | 86.5 | 0 | 5 |
| 24 | 0 | 0 | 7 | 25 | 71.9 | 28.1 | 87.9 | 59.3 | 0 | 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12th March 2023 | 2:00pm | Craven Cottage | a | 24,426 | 10 | 0 | 2 | 3 | 44.7 | 55.3 | 12 | 15 | 2 | 7 | 6 | 3 | 4 | 5 | 82.7 | 86.5 | 0 | 4 | 4 | 8 | 2 | 2 | 57.1 | 70 | 59.3 | 40.7 | 4 | 2 | 10 | 9 | 0 | 1 | 0 | 0 | https://www.skysports.com/football/fulham-vs-arsenal/464898 |
| 18th February 2023 | 3:00pm | Amex Stadium | a | 31,619 | 6 | 0 | 10 | 1 | 65.3 | 34.7 | 21 | 5 | 7 | 2 | 10 | 0 | 4 | 3 | 87.7 | 73.1 | 5 | 0 | 10 | 2 | 4 | 0 | 40 | 38.5 | 59.3 | 40.7 | 1 | 7 | 12 | 14 | 0 | 5 | 0 | 0 | https://www.skysports.com/football/brighton-and-hove-albion-vs-fulham/464867 |
| 24/04/2021 | 8:00pm | Bramall Lane | h | 0 | 25 | 1 | 6 | 0 | 31.5 | 68.5 | 7 | 17 | 3 | 4 | 1 | 8 | 3 | 5 | 67.8 | 86.9 | 0 | 3 | 2 | 12 | 0 | 1 | 66.7 | 40 | 38.5 | 61.5 | 4 | 2 | 13 | 7 | 3 | 0 | 0 | 0 | https://www.skysports.com/football/sheffield-united-vs-brighton-and-hove-albion/stats/429164 |
| 10th March 2022 | 7:30pm | Carrow Road | a | 26,722 | 22 | 1 | 12 | 3 | 33.7 | 66.3 | 8 | 15 | 3 | 7 | 4 | 5 | 1 | 3 | 82.7 | 91.7 | 0 | 3 | 3 | 8 | 2 | 1 | 56.3 | 66.7 | 26.7 | 73.3 | 4 | 2 | 8 | 15 | 0 | 2 | 0 | 0 | https://www.skysports.com/football/norwich-city-vs-chelsea/stats/446584 |
| 11th May 2022 | 7:30pm | Elland Road | a | 36,549 | 19 | 0 | 12 | 3 | 32.1 | 67.9 | 5 | 17 | 0 | 4 | 4 | 10 | 1 | 3 | 78.6 | 90.8 | 1 | 0 | 1 | 5 | 0 | 3 | 62.5 | 52.9 | 45.5 | 54.5 | 0 | 0 | 10 | 14 | 1 | 0 | 1 | 0 | https://www.skysports.com/football/leeds-united-vs-chelsea/stats/446610 |
| 4th February 2023 | 3:00pm | Molineux | h | 31,664 | 13 | 3 | 5 | 0 | 41.8 | 58.2 | 12 | 22 | 6 | 4 | 5 | 8 | 1 | 10 | 74.9 | 85.5 | 3 | 1 | 2 | 7 | 1 | 2 | 62.5 | 70 | 46.7 | 53.3 | 2 | 3 | 9 | 7 | 1 | 1 | 0 | 0 | https://www.skysports.com/football/wolverhampton-wanderers-vs-liverpool/464854 |
| 31st December 2022 | 3:00pm | Vitality Stadium | a | 9,972 | 15 | 0 | 11 | 2 | 58.9 | 41.1 | 6 | 15 | 2 | 6 | 3 | 4 | 1 | 5 | 75.2 | 68.8 | 0 | 2 | 2 | 7 | 1 | 1 | 58.8 | 56.7 | 52 | 48 | 4 | 2 | 17 | 11 | 4 | 1 | 0 | 0 | https://www.skysports.com/football/bournemouth-vs-crystal-palace/464805 |
| 27th November 2021 | 3:00pm | Selhurst Park | a | 25,203 | 11 | 1 | 7 | 2 | 63.3 | 36.7 | 8 | 10 | 3 | 3 | 4 | 5 | 1 | 2 | 86.8 | 79.4 | 1 | 0 | 7 | 3 | 1 | 0 | 75 | 75 | 52 | 48 | 1 | 2 | 12 | 10 | 3 | 3 | 0 | 0 | https://www.skysports.com/football/crystal-palace-vs-aston-villa/stats/446413 |
| 18/10/2020 | 12:00pm | Bramall Lane | d | 0 | 25 | 1 | 10 | 1 | 41.5 | 58.5 | 10 | 15 | 6 | 6 | 3 | 5 | 1 | 4 | 80.2 | 84.4 | 2 | 1 | 2 | 5 | 4 | 0 | 60 | 57.1 | 55.6 | 44.4 | 5 | 5 | 5 | 9 | 0 | 2 | 0 | 0 | https://www.skysports.com/football/sheffield-united-vs-fulham/stats/428884 |
| 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 |
| 21/02/2021 | 7:00pm | Old Trafford | h | 0 | 3 | 3 | 4 | 1 | 71.6 | 28.4 | 15 | 10 | 7 | 6 | 4 | 3 | 4 | 1 | 86.7 | 68.9 | 1 | 0 | 6 | 4 | 2 | 1 | 54.5 | 56.3 | 66.7 | 33.3 | 5 | 4 | 9 | 11 | 1 | 2 | 0 | 0 | https://www.skysports.com/football/manchester-united-vs-newcastle-united/stats/429083 |
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 conjunto de treino (70%) foi usado para ajustar a floresta; o teste (30%) permaneceu isolado para avaliação justa.
Accuracy: 0.5673
Importância das Features:
| Feature | Importância |
|---|---|
| away_chances | 0.221561 |
| home_chances | 0.116014 |
| Home Team | 0.111948 |
| Away Team | 0.109174 |
| away_pass | 0.103906 |
| home_pass | 0.100182 |
| attendance | 0.086747 |
| away_possessions | 0.055630 |
| home_possessions | 0.051652 |
| stadium | 0.043184 |
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import LabelEncoder
import numpy as np
# ===================== Ler base =====================
df = pd.read_csv("./src/mydata.csv")
# ===================== Excluir colunas de vazamento/irrelevantes =====================
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"
], errors="ignore")
# ===================== Limpezas =====================
# stadium em texto -> label encoding
le_stadium = LabelEncoder()
df["stadium"] = le_stadium.fit_transform(df["stadium"].astype(str))
# class: h/d/a -> 0/1/2
df["class"] = df["class"].replace({"h": 0, "d": 1, "a": 2}).astype(int)
# attendance: "12,345" -> 12345
df["attendance"] = (
df["attendance"]
.astype(str)
.str.replace(r"[^0-9]", "", regex=True)
.replace("", "0")
.astype(float)
)
# garantir numéricos + imputar medianas nas features
num_cols = [
"attendance",
"home_possessions", "away_possessions",
"home_pass", "away_pass",
"home_chances", "away_chances"
]
for c in num_cols:
df[c] = pd.to_numeric(df[c], errors="coerce")
med = df[c].median() if df[c].notna().any() else 0
df[c] = df[c].fillna(med)
# ===================== Features e alvo =====================
X = df[[
"stadium", "attendance",
"Home Team", "Away Team",
"home_possessions", "away_possessions",
"home_pass", "away_pass",
"home_chances", "away_chances"
]].copy()
y = df["class"].copy()
# Alguns times podem ter NA; garantir numérico
X["Home Team"] = pd.to_numeric(X["Home Team"], errors="coerce").fillna(-1).astype(int)
X["Away Team"] = pd.to_numeric(X["Away Team"], errors="coerce").fillna(-1).astype(int)
# ===================== Split (70/30) com estratificação =====================
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.30, random_state=27, stratify=y
)
# ===================== Random Forest =====================
rf = RandomForestClassifier(
n_estimators=100, # número de árvores
max_depth=5, # profundidade máxima (controle de overfitting)
max_features='sqrt', # nº de features por split
random_state=27,
n_jobs=-1
)
rf.fit(X_train, y_train)
# ===================== Avaliação =====================
pred = rf.predict(X_test)
acc = accuracy_score(y_test, pred)
print(f"Accuracy: {acc:.4f}")
# Importância das features
feat_imp = pd.DataFrame({
"Feature": rf.feature_names_in_,
"Importância": rf.feature_importances_
}).sort_values("Importância", ascending=False)
print("<br>Importância das Features:")
print(feat_imp.to_html(index=False))
Avaliação do Modelo
Acurácia (teste): 0.5673.
Esse desempenho supera os resultados anteriores (Árvore ≈ 0,48; KNN ≈ 0,42–0,51), indicando melhor generalização e robustez da Random Forest no problema.
- Importância das variáveis (top-10):
away_chances— 0.2216home_chances— 0.1160Home Team— 0.1119Away Team— 0.1092away_pass— 0.1039home_pass— 0.1002attendance— 0.0867away_possessions— 0.0556home_possessions— 0.0517stadium— 0.0432
Leituras rápidas: - Chances criadas (mandante/visitante) e passes são os sinais mais relevantes — coerente com dinâmica ofensiva/controle de jogo.
- Os IDs dos times entram forte (efeitos fixos de qualidade/estilo).
- Estádio pesa menos, sugerindo que, após controlar por equipe e métricas do jogo, o local adiciona pouca informação.
Relatório Final
A Random Forest apresentou melhor desempenho que a Árvore de Decisão e o KNN, indicando ganho com agregação de múltiplas árvores e menor variância. Ainda assim, prever resultados de futebol segue difícil (classe de empate é rara e ruidosa), o que limita a acurácia.
Resultado geral: a RF é a melhor baseline até aqui para este conjunto de variáveis, com boa interpretabilidade via importâncias e espaço claro para melhoria com feature engineering e ajuste fino.