Introdução

O Expected Goals (xG) é uma métrica estatística que estima a probabilidade de um chute resultar em gol com base em variáveis como posição do chute, ângulo, distância ao gol e tipo de assistência. Um xG de 0,3 significa que, em média, 3 em cada 10 chutes naquela situação resultam em gol.

Ao comparar o xG acumulado de um jogador ao longo da temporada com os seus gols reais, é possível identificar dois perfis extremos:

  • Overperformers: marcam mais gols do que o xG prevê — excelentes finalizadores ou simplesmente muito eficientes.
  • Underperformers: marcam menos gols do que o xG prevê — desperdiçam oportunidades claras ou passam por uma fase ruim de finalização.

Nesta análise, utilizamos dados de chutes do Brasileirão Série A 2024, coletados via pacote worldfootballR, que agrega estatísticas do FBref.

#devtools::install_github("JaseZiv/worldfootballR")
library(worldfootballR)
library(tidyverse)
theme_set(theme_minimal())

Coleta dos dados

Os dados são obtidos diretamente do FBref por meio da função load_fb_match_shooting(), que retorna o log de chutes de cada partida para o campeonato selecionado. Cada linha representa um chute individual, com informações sobre o jogador, o clube, o resultado (gol ou não) e o xG daquele chute específico. Para isso, bastan informar a liga de interesse, qual o gênero dos jogadores (ou jogadoras) do torneio e a divisão desejada.

dados <- load_fb_match_shooting(country = "BRA", gender = "M", tier = "1st")

Processamento e agregação

A partir do log de chutes, são realizados os seguintes passos:

  1. Filtro de temporada: mantemos apenas a temporada 2024, que é a mais recente disponível no momento da publicação deste post.
  2. Remoção de cobranças de pênalti: chutes originados de pênaltis são excluídos ((pen) no nome), pois o xG de pênalti distorce a comparação.
  3. Agregação por jogador: somamos o xG de todos os chutes e contamos o total de gols marcados na temporada.
df_jogadores <- 
  dados |>
  filter(Season_End_Year == 2024) |>
  mutate(xG = as.numeric(xG)) |>
  filter(!grepl(pattern = " \\(pen\\)", Player)) |> 
  group_by(Player, Squad) |>
  mutate(
    xG_season = sum(xG, na.rm = TRUE),
    goals     = sum(Outcome == "Goal", na.rm = TRUE)
  ) |>
  filter(Outcome == "Goal") |> 
  select(Player, Squad, xG_season, goals) |> 
  distinct()

Visualização: Gols reais vs xG

O gráfico abaixo plota os gols marcados (eixo Y) contra o xG acumulado (eixo X). A linha tracejada representa a reta de igualdade perfeita (gols = xG): jogadores acima dela marcaram mais do que o esperado; abaixo, menos. Quanto mais distante da diagonal, mais extremo é o desempenho relativo ao modelo estatístico.

ggplot(df_jogadores, aes(x = xG_season, y = goals)) +
  geom_point() + 
  labs(title    = "Gols Reais vs Expected Goals (xG)", 
       subtitle = "Brasileirão Série A 2024",
       x        = "xG acumulado na temporada",
       y        = "Gols marcados",
  ) + 
  geom_abline(slope = 1, intercept = 0, linetype = "dashed") + 
  scale_x_continuous(breaks = seq(0, 15, 5), minor_breaks = seq(0, 15, 1), limits = c(0, 15)) +
  scale_y_continuous(breaks = seq(0, 15, 5), minor_breaks = seq(0, 15, 1), limits = c(0, 15)) +
  coord_equal()

Análise de regressão: identificando overperformers e underperformers

Para além da inspeção visual, podemos quantificar o desvio de cada jogador de forma mais rigorosa usando regressão linear simples (gols ~ xG). O modelo estima a relação média entre xG e gols reais para todos os jogadores. O resíduo de cada jogador (a diferença entre o valor observado e o valor predito pelo modelo) captura o quanto ele se afasta do padrão geral do campeonato.

Essa abordagem é mais justa do que simplesmente subtrair gols - xG, pois leva em conta a tendência geral do campeonato, onde o xG pode sistematicamente subestimar ou superestimar.

# regressao linear

modelo_lm <- lm(goals ~ xG_season, data = df_jogadores)
summary(modelo_lm)
## 
## Call:
## lm(formula = goals ~ xG_season, data = df_jogadores)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -4.3539 -0.5884  0.0013  0.4496  4.3542 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  0.44336    0.09944   4.458 1.14e-05 ***
## xG_season    0.95423    0.03285  29.050  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.193 on 323 degrees of freedom
## Multiple R-squared:  0.7232,	Adjusted R-squared:  0.7223 
## F-statistic: 843.9 on 1 and 323 DF,  p-value: < 2.2e-16
# residuos e adicionar ao dataframe

df_jogadores <- 
  df_jogadores |>
  ungroup() |>
  mutate(pred_goals = predict(modelo_lm),
         residuo    = goals - pred_goals
  )

# overperformer 
df_jogadores |> 
  slice_max(residuo,  n = 1)
## # A tibble: 1 × 6
##   Player Squad         xG_season goals pred_goals residuo
##   <chr>  <chr>             <dbl> <int>      <dbl>   <dbl>
## 1 Wesley Internacional       6.5    11       6.65    4.35
# underperformer
df_jogadores |> 
  slice_min(residuo,  n = 1)
## # A tibble: 1 × 6
##   Player Squad     xG_season goals pred_goals residuo
##   <chr>  <chr>         <dbl> <int>      <dbl>   <dbl>
## 1 Rony   Palmeiras      8.29     4       8.35   -4.35

Conclusão

A análise mostra que o xG, embora seja um bom preditor geral, deixa espaço para variação individual considerável. O melhor overperformer da temporada demonstra uma eficiência de finalização muito acima da média do campeonato, um indicador de que sua qualidade técnica supera o que as situações de jogo por si só justificariam. Por outro lado, o maior underperformer desperdiçou oportunidades que, em média, qualquer atacante do campeonato teria convertido.

Vale ressaltar que resíduos elevados em uma única temporada podem refletir tanto habilidade genuína quanto variância amostral. Assim, jogadores com poucas finalizações têm resíduos mais voláteis. Uma análise longitudinal utilizando múltiplas temporadas seria necessária para separar talento real de sorte.