Skip to content

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.

2025-10-28T14:01:15.648741 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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).

2025-10-28T14:01:15.719085 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:15.902109 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:16.082361 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:16.216122 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:16.375668 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:16.579372 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:16.742518 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:16.873036 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:17.007315 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-10-28T14:01:17.141155 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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 Team e Away Team foram 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 coluna class.
  • 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.2216
  • home_chances — 0.1160
  • Home Team — 0.1119
  • Away Team — 0.1092
  • away_pass — 0.1039
  • home_pass — 0.1002
  • attendance — 0.0867
  • away_possessions — 0.0556
  • home_possessions — 0.0517
  • stadium — 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.