Tabela, Trave e Tendências: O Brasileirão sob a Lupa Estatística

Author

Lucas Santos, Mariana Medeiros, Tales Antonio

Published

April 24, 2025

.

1. Introdução

O Campeonato Brasileiro de Futebol, popularmente conhecido como Brasileirão, é a principal competição nacional de clubes no Brasil. Disputado desde 1970 em diferentes formatos, ele se consolidou em 2003 no modelo de pontos corridos, onde os 20 clubes participantes se enfrentam em turno e returno, totalizando 38 rodadas. A cada temporada, os dados gerados por centenas de partidas oferecem uma rica oportunidade para a análise estatística do desempenho dos clubes, jogadores e aspectos táticos do jogo. Este trabalho tem como objetivo explorar estatisticamente as informações do campeonato. As bases de dados utilizadas contêm informações detalhadas sobre partidas, gols, cartões e estatísticas táticas.

As análises incluem:

  • Média de público e lotação dos estádios por time.
  • Confrontos com maior público e número de gols.
  • Desempenho dos times em casa e fora.
  • Distribuição de gols por período do jogo e principais artilheiros.
  • Analise de cartões
  • relação de posse de bola com a taxa de vitórias

Pacotes Utilizados

library(tidyverse)
library(lubridate)
library(gt)
library(DT)

2. Carregamento das Bases

dados_cartao <- read_csv("campeonato-brasileiro-cartoes.csv")
dados_corner <- read_csv("campeonato-brasileiro-estatisticas-full.csv")
dados_full <- read_csv("campeonato-brasileiro-full.csv")
dados_gol <- read_csv("campeonato-brasileiro-gols.csv")
dados_tratados <- read_csv("dados_tratados.csv")

3. Tratamento de Times

Padronização dos Times

dados_tratados$data <- ymd(dados_tratados$data)

dados_tratados <- dados_tratados |>
  arrange(data) |>
  filter(ano_campeonato >= 2015) |>
  mutate(time_mandante = case_when(
    time_mandante == "Goiás EC" ~ "Goiás",
    time_mandante == "Santos FC" ~ "Santos",
    time_mandante == "Atlético-PR" ~ "Athletico-PR",
    T ~ time_mandante
  )) |>
  mutate(time_visitante = case_when(
    time_visitante == "Goiás EC" ~ "Goiás",
    time_visitante == "Santos FC" ~ "Santos",
    time_visitante == "Atlético-PR" ~ "Athletico-PR",
    T ~ time_visitante
  ))

Média/Média Relativa de Público dos Times Mandantes por Estádio

publico <- dados_tratados |>
  mutate(media_relativa = publico / publico_max) |>
  group_by(time_mandante) |>
  summarise(media = mean(publico, na.rm = T),
            media_relativa = mean(media_relativa, na.rm = T)) |>
  arrange(desc(media))

tabela_publico <- datatable(publico,
                            rownames = F,
                            colnames = c("Time", "Média", "Média Relativa")
                            ) |>
  formatPercentage("media_relativa", 2) |>
  formatRound("media", 0)

tabela_publico

4. Análise de Público

publico_ano <- dados_tratados |>
  mutate(media_relativa = publico / publico_max) |>
  group_by(time_mandante, ano_campeonato) |>
  summarise(media = mean(publico, na.rm = T),
            media_relativa = mean(media_relativa, na.rm = T))

clubes_grandes <- c("Flamengo", "Corinthians", "Palmeiras", "Vasco da Gama", "Fluminense",
                    "São Paulo", "Botafogo", "Atlético-MG",
                    "Grêmio", "Internacional", "EC Bahia", "Fortaleza")

Evolução da Média de Publico por Time

plot_1 <- publico_ano |>
  filter(time_mandante %in% clubes_grandes) |>
  ggplot(aes(x = ano_campeonato, y = media, color = time_mandante)) +
  geom_line(size = 1.2, alpha = 0.8) +
  geom_point(size = 2) +
  labs(
    title = "Evolução da Média de Público por Time (2015–2024)",
    x = "Ano", 
    y = "Média de Público",
    color = "Clubes"
  ) +
  scale_color_manual(values = c(
    "Flamengo" = "red3",
    "Corinthians" = "black",
    "Palmeiras" = "green4",
    "Vasco da Gama" = "gray20",
    "Fluminense" = "deeppink",
    "São Paulo" = "firebrick",
    "Botafogo" = "gold",
    "Atlético-MG" = "dimgray",
    "Grêmio" = "dodgerblue2",
    "Internacional" = "orange",
    "EC Bahia" = "blue4",
    "Fortaleza" = "steelblue"
  ),
  labels = c("Flamengo" = "Mengão")) +
  theme_minimal()

plot_1

Evolução da lotação do estádio por Time

plot_2 <- publico_ano |>
  filter(time_mandante %in% clubes_grandes) |>
  ggplot(aes(x = ano_campeonato, y = media_relativa, color = time_mandante)) +
  geom_line(size = 1.5) +
  geom_point(size = 3) +
  labs(title = "Evolução da lotação do estádio por Time", 
       x = "Ano", 
       y = "Lotação (%)",
       color = "Clubes") +
  scale_color_manual(values = c(
    "Flamengo" = "red3",
    "Corinthians" = "black",
    "Palmeiras" = "green4",
    "Vasco da Gama" = "gray20",
    "Fluminense" = "deeppink",
    "São Paulo" = "firebrick",
    "Botafogo" = "gold",
    "Atlético-MG" = "dimgray",
    "Grêmio" = "dodgerblue2",
    "Internacional" = "orange",
    "EC Bahia" = "blue4",
    "Fortaleza" = "steelblue"
  ),
  labels = c("Flamengo" = "Mengão")) +
  theme_minimal()

plot_2

5. Análise de Confrontos

dados_tratados <- dados_tratados |>
  mutate(
    confronto = map2_chr(
      time_mandante,
      time_visitante,
      ~ paste(sort(c(.x, .y)), collapse = " x ")
    )
  )

confronto_stats <- dados_tratados |>
  group_by(confronto) |>
  summarise(n_partidas = n(),
            media_publico = mean(publico, na.rm = T),
            max_publico = max(publico, na.rm = T)) |>
  filter(n_partidas >= 12) |>
  arrange(desc(media_publico))

Top 10 Confrontos com Maior Média de Público

plot_3 <- confronto_stats |>
  slice_max(order_by = media_publico, n = 10) |>
  ggplot(aes(x = reorder(confronto, media_publico), y = media_publico)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = "Top 10 Confrontos com Maior Média de Público",
       x = "Confronto", y = "Média de Público")

plot_3

6. Análise de Gols e Índices

dados_tratados <- dados_tratados |>
  mutate(vencedor = case_when(
    gols_mandante > gols_visitante ~ time_mandante,
    gols_mandante < gols_visitante ~ time_visitante,
    T ~ "Empate"),
    gols_total = gols_mandante + gols_visitante
  )

Times Com Mais Gols

gols_times <- bind_rows(
  dados_tratados |>
    mutate(
      time = time_mandante,
      gols_marcados = gols_mandante,
      gols_sofridos = gols_visitante
    ) |>
    select(time, gols_marcados, gols_sofridos),
  
  dados_tratados |>
    mutate(
      time = time_visitante,
      gols_marcados = gols_visitante,
      gols_sofridos = gols_mandante
    ) |>
    select(time, gols_marcados, gols_sofridos)
) |>
  group_by(time) |>
  summarise(
    gols_marcados = sum(gols_marcados, na.rm = T),
    gols_sofridos = sum(gols_sofridos, na.rm = T)
  ) |>
  mutate(
    saldo = gols_marcados - gols_sofridos,
    indice = gols_marcados / gols_sofridos
  ) |>
  arrange(desc(gols_marcados))

datatable(gols_times,
          rownames = FALSE,
          options = list(pageLength = 15),
          colnames = c("Clube", "Gols Feitos", "Gols Sofridos", "Saldo de Gols", "Índice Ofensivo")) |>
  formatRound("indice", 2)
gols_times |>
  slice_head(n = 20) |>
  ggplot(aes(x = reorder(time, indice), y = indice)) +
  geom_col(fill = "royalblue") +
  coord_flip() +
  labs(title = "Índice Ofensivo (Gols Feitos / Gols Sofridos)",
       x = "Clube", y = "Índice") +
  theme_minimal()

7. Análise de Gols por Tempo

dados_gol <- dados_gol |> 
  mutate(atleta = recode(atleta, "Gabriel Barbosa Almeida" = "Gabriel Barbosa")) |> 
  separate(minuto, into = c("minuto", "min_extra"), sep = "\\+", fill = "right") |>
  mutate(minuto = as.numeric(minuto),
    tempo_gol = case_when(
      minuto <= 15 ~ 1,
      minuto %in% 16:30 ~ 2,
      minuto %in% 31:45 ~ 3,  #criando estratos para o tempo que saiu o gol
      minuto %in% 46:60 ~ 4,
      minuto %in% 61:75 ~ 5,
      minuto %in% 76:90 ~ 6)
  )
gols_por_periodo <- dados_gol |>
  count(tempo_gol, name = "quantidade_gols") |>
  mutate(proporcao = quantidade_gols / sum(quantidade_gols) * 100)

ggplot(gols_por_periodo, aes(x = tempo_gol, y = proporcao)) +
  geom_col(fill = "steelblue") +
  labs(title = "Distribuição de gols por intervalo de 15 minutos",
       x = "Intervalo de tempo", y = "Porcentagem de gols") +
  theme_minimal()

dados_gol |>
  count(atleta, name = "quantidade_gols") |>
  slice_max(order_by = quantidade_gols, n = 10) |>
  ggplot(aes(x = reorder(atleta, quantidade_gols), y = quantidade_gols)) +
  geom_col(fill = "steelblue") +
  geom_text(aes(label = quantidade_gols), hjust = -0.1, size = 3) +
  coord_flip() +
  labs(title = "Top 10 Artilheiros do Campeonato Brasileiro (2015–2024)",
       x = "Atleta", 
       y = "Total de Gols") +
  theme_minimal()

8. Análise de Cartões

Total de cartões por time

cartoes_por_time <- dados_filtrados |>
  group_by(clube) |>
  summarise(
    total_amarelos = sum(amarelos, na.rm = TRUE),
    total_vermelhos = sum(vermelhos, na.rm = TRUE)
  )

Média de cartões amarelos e vermelhos

medias_por_time <- total_jogos |>
  left_join(cartoes_por_time, by = "clube") |>
  mutate(
    media_amarelos = total_amarelos / total_jogos,
    media_vermelhos = total_vermelhos / total_jogos
  ) |>
  arrange(desc(media_amarelos))

Gráfico: Média de cartões amarelos e vermelhos por time

medias_long <- medias_por_time |>
  pivot_longer(
    cols = c(media_amarelos, media_vermelhos),
    names_to = "tipo_cartao",
    values_to = "media"
  )

ggplot(medias_long, 
       aes(x = reorder(clube, -media), 
           y = media, 
           fill = tipo_cartao)) +
  geom_bar(stat = "identity", 
           position = "dodge") +
  geom_text(aes(label = round(media, 2)),
            position = position_dodge(width = 0.9),
            vjust = -0.5,
            size = 3) +
  scale_fill_manual(
    values = c("media_amarelos" = "gold2", "media_vermelhos" = "firebrick3"),
    labels = c("Amarelos", "Vermelhos")
  ) +
  labs(
    title = "Média de Cartões por Jogo (2015–2023)",
    x = "Clube",
    y = "Cartões/jogo",
    fill = " "
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    legend.position = "top"
  )

Gráfico: Média de cartões por mês

dados_filtrados |> 
  mutate(mes = month(data, label = TRUE, abbr = TRUE),
         mes_num = month(data)) |> 
  filter(mes_num >= 4, mes_num <= 12) |> 
  group_by(mes) |> 
  summarise(media_cartoes = mean(total_cartoes)) |> 
  ggplot(aes(x = mes, y = media_cartoes, group = 1)) +
  geom_line(color = "steelblue", linewidth = 1) +
  geom_point(size = 3, color = "firebrick") +
  labs(title = "Média de Cartões por Mês (2015-2023)",
       x = "Mês",
       y = "Média de Cartões")

Gráfico: Cartões por dia da semana e hora

dados_filtrados |>
  mutate(
    dia_semana = wday(data, label = TRUE, abbr = FALSE),
    hora = as.factor(hour(hora))
  ) |>
  group_by(dia_semana, hora) |>
  summarise(
    total_cartoes = sum(total_cartoes),
    .groups = "drop"
  ) |>
  ggplot(aes(x = hora, y = dia_semana, fill = total_cartoes)) +
  geom_tile() +
  scale_fill_gradient(low = "white", high = "red") +
  labs(title = "Cartões por Dia da Semana e Hora (2015–2023)") +
  theme_minimal()

9. Análise Tática

Correlação entre posse de bola e vitórias

vitorias_casa <- dados_tratados |>
  filter(gols_mandante > gols_visitante) |>
  count(time_mandante, name = "vitorias_casa") |> 
  rename(time = time_mandante)

vitorias_fora <- dados_tratados |>
  filter(gols_visitante > gols_mandante) |>
  count(time_visitante, name = "vitorias_fora") |> 
  rename(time = time_visitante)

jogos_casa <- dados_tratados |>
  count(time_mandante, name = "jogos_casa") |>
  rename(time = time_mandante)

jogos_fora <- dados_tratados |>
  count(time_visitante, name = "jogos_fora") |>
  rename(time = time_visitante)

desempenho_vitorias <- jogos_casa |>
  full_join(jogos_fora, by = "time") |>
  full_join(vitorias_casa, by = "time") |>
  full_join(vitorias_fora, by = "time") |>
  mutate(
    taxa_de_vitoria_casa = vitorias_casa / jogos_casa,
    taxa_de_vitoria_fora = vitorias_fora / jogos_fora
  ) |>
  arrange(desc(taxa_de_vitoria_casa))

resultado_mandante <- desempenho_vitorias |> 
 mutate(derrotas_casa = jogos_casa - vitorias_casa,
        derrotas_fora = jogos_fora - vitorias_fora) |>
 select(-taxa_de_vitoria_casa, -taxa_de_vitoria_fora)

tabela_resultado_mandante <- datatable(resultado_mandante,
                                       rownames = F)

tabela_resultado_mandante
dados_corner <- dados_corner |> 
  filter(partida_id >= 4987) |> 
  select(-rodata)



base <- dados_full |> 
  filter(ID >= 4987) |> 
  select(ID, mandante, visitante) |> 
  rename("partida_id" = ID)


dados_partidas <- dados_corner |>
  left_join(base, by = "partida_id") |>
  # Definir um identificador para mandante/visitante
  mutate(tipo_time = case_when(
    clube == mandante ~ "mandante",
    clube == visitante ~ "visitante",
    TRUE ~ NA_character_
  )) |>
  # Pivotar os dados para ter dados de mandante e visitante na mesma linha
  pivot_wider(
    id_cols = c(partida_id, mandante, visitante),
    names_from = tipo_time,
    values_from = c(clube, chutes, chutes_no_alvo, posse_de_bola, passes, faltas, 
                    cartao_amarelo, cartao_vermelho, impedimentos, escanteios),
    names_sep = "_"
  ) |>
  select(-mandante, -visitante)

# Verificando o resultado
print(dados_partidas)
# A tibble: 3,419 × 21
   partida_id clube_visitante clube_mandante chutes_visitante chutes_mandante
        <dbl> <chr>           <chr>                     <dbl>           <dbl>
 1       4987 Coritiba        Chapecoense                  16              13
 2       4988 Atletico-MG     Palmeiras                    10              17
 3       4989 Joinville       Fluminense                    3              26
 4       4990 Ponte Preta     Gremio                       17              11
 5       4993 Corinthians     Cruzeiro                     11              13
 6       4994 Figueirense     Sport                         7              17
 7       4991 Internacional   Athletico-PR                 17              10
 8       4992 Flamengo        Sao Paulo                    12              19
 9       4996 Goias           Vasco                         5              11
10       4995 Santos          Avai                          8              16
# ℹ 3,409 more rows
# ℹ 16 more variables: chutes_no_alvo_visitante <dbl>,
#   chutes_no_alvo_mandante <dbl>, posse_de_bola_visitante <chr>,
#   posse_de_bola_mandante <chr>, passes_visitante <dbl>,
#   passes_mandante <dbl>, faltas_visitante <dbl>, faltas_mandante <dbl>,
#   cartao_amarelo_visitante <dbl>, cartao_amarelo_mandante <dbl>,
#   cartao_vermelho_visitante <dbl>, cartao_vermelho_mandante <dbl>, …
dados_juntos <- dados_partidas |>
  left_join(dados_full, by =c("partida_id" = "ID")) 

dados_juntos <- dados_juntos |>
  mutate(vencedor = case_when(
      mandante_Placar > visitante_Placar ~ mandante,
      mandante_Placar < visitante_Placar ~ visitante,
      TRUE ~ "Empate")
  ) |>
  mutate(resultado_mandante = case_when(
      vencedor == mandante ~ "Vitória",
      vencedor == visitante ~ "Derrota",
      vencedor == "Empate" ~ "Empate")
  )

dados_limpos <- dados_juntos |>
  mutate(
    posse_de_bola_mandante = as.numeric(str_remove(posse_de_bola_mandante, "%")),
    posse_de_bola_visitante = as.numeric(str_remove(posse_de_bola_visitante, "%"))
  ) |>
  filter(!is.na(posse_de_bola_mandante) & !is.na(posse_de_bola_visitante))


# Agora você pode usar estas colunas convertidas para analisar a relação com vitórias
posse_por_resultado <- dados_limpos |>
  group_by(resultado_mandante) |>
  summarise(
    media_posse_mandante = mean(posse_de_bola_mandante, na.rm = TRUE),
    media_posse_visitante = mean(posse_de_bola_visitante, na.rm = TRUE),
    contagem = n()
  )

# 1. Visualização da relação entre posse de bola do mandante e resultado
ggplot(dados_limpos, aes(x = resultado_mandante, 
                         y = posse_de_bola_mandante)) +
  geom_boxplot(aes(fill = resultado_mandante)) +
  labs(title = "Relação entre Posse de Bola do Mandante e Resultado",
       x = "Resultado",
       y = "Posse de Bola do Mandante (%)") +
  theme_minimal()

# 2. Visualização da posse de bola média do mandante por resultado
posse_por_resultado <- dados_limpos |>
  group_by(resultado_mandante) |>
  summarise(
    media_posse_mandante = mean(posse_de_bola_mandante, na.rm = TRUE)
  )

ggplot(posse_por_resultado, aes(x = resultado_mandante, 
                                y = media_posse_mandante, 
                                fill = resultado_mandante)) +
  geom_col() +
  labs(title = "Posse de Bola Média do Mandante por Resultado",
       x = "Resultado",
       y = "Posse de Bola (%)") +
  scale_fill_manual(values = c("Vitória" = "forestgreen", 
                               "Empate" = "goldenrod", 
                               "Derrota" = "firebrick")) +
  theme_minimal()

# 3. Analisando as chances de vitória por diferentes faixas de posse de bola
dados_limpos <- dados_limpos |>
  mutate(
    # Criando faixas de posse de bola
    faixa_posse = cut(posse_de_bola_mandante, 
                       breaks = c(0, 40, 50, 60, 100),
                       labels = c("Baixa (<40%)", "Média-Baixa (40-50%)", 
                                  "Média-Alta (50-60%)", "Alta (>60%)"))
  )

# Taxa de vitória por faixa de posse de bola
taxa_vitoria_por_posse <- dados_limpos |>
  group_by(faixa_posse) |>
  summarise(
    total_jogos = n(),
    vitorias = sum(resultado_mandante == "Vitória", na.rm = TRUE),
    empates = sum(resultado_mandante == "Empate", na.rm = TRUE),
    derrotas = sum(resultado_mandante == "Derrota", na.rm = TRUE),
    taxa_vitoria = vitorias / total_jogos * 100
  )

ggplot(taxa_vitoria_por_posse, aes(x = faixa_posse, 
                                   y = taxa_vitoria, 
                                   fill = faixa_posse)) +
  geom_bar(stat = "identity") +
  labs(title = "Taxa de Vitória por Faixa de Posse de Bola",
       x = "Faixa de Posse de Bola",
       y = "Taxa de Vitória (%)") +
  theme_minimal()