Educacional Artigos Livros Serviços Academy
Academy / Rede de Passes

Redes de Passes no Futebol: Um Pipeline Completo em R para Análise Tática Automatizada

Resumo

Este artigo apresenta uma análise técnica e conceitual de um pipeline desenvolvido em R para construção e visualização de redes de passes (pass networks) no futebol, a partir de dados de eventos (event data). O código combina processamento de dados com tidyverse, modelagem geométrica do campo com ggsoccer, e renderização tipográfica com showtext, produzindo imagens padronizadas (1080×1440 px) prontas para relatórios e redes sociais. Exploramos as decisões de design, a lógica de extração dos grafos, o sistema de zonas inspirado no modelo UEFA e a arquitetura modular que permite gerar diferentes recortes táticos — geral, ruptura, por bloco e comparativo entre tempos — a partir de uma única base de eventos.


1. Introdução

A rede de passes é uma das ferramentas mais populares na análise tática moderna. Ao representar jogadores como nós (vértices) e o volume de passes entre eles como arestas (conexões), ela revela estruturas coletivas que a simples estatística individual não captura: centralidade, formação de subgrupos, dependência de um articulador, assimetrias entre os lados do campo.

O código aqui analisado automatiza todo esse fluxo: lê uma base de eventos CSV, identifica os titulares, extrai passes bem-sucedidos, calcula nós e conexões, e devolve seis visualizações temáticas — todas dentro da mesma identidade visual. É um exemplo maduro de como unir rigor analítico e consistência gráfica.


2. Arquitetura Geral do Script

O pipeline segue um desenho modular com cinco camadas bem separadas:

CamadaResponsabilidade
ConfiguraçãoFontes, cores, tema, escalas
UtilitáriosAbreviação de nomes, posicionamento, zonas, cores de texto
Motor de plotagembase_plot_tactics() — função central do ggplot
ProcessamentoFiltragem de passes, criação de nós e arestas
Orquestraçãogerar_relatorio_tatico() — dispara todas as análises

Essa separação é uma boa prática: se amanhã o campo mudar de proporção ou a paleta for atualizada, apenas a camada de configuração precisa ser tocada.


3. Identidade Visual e a Paleta Analítica

Antes de qualquer plot, o script define uma paleta cromática coesa:

ST <- list(
  bg         = "#F8F8F4",   # off-white para respiro
  pitch      = "#F8F8F4",
  pitch_line = "#B3CED9",   # azul-cinza suave
  grid_line  = "#1A1A1A",   # grade tracejada escura
  conn_color = "#00c7b1",   # turquesa das conexões
  node_low   = "#00c7b1",   # nós com poucos passes
  node_high  = "#00e5c8"    # nós com muitos passes
)

A escolha do turquesa (#00c7b1) como cor dominante é estratégica: é vibrante o suficiente para destacar as redes sobre o fundo claro, mas não cansa a vista quando há muitas arestas sobrepostas. O gradiente de nós (node_low → node_high) é aplicado via scales::gradient_n_pal, garantindo que jogadores com mais passes recebam um tom ligeiramente mais claro — um recurso sutil de hierarquia visual.

Adicionalmente, o script define uma escala ordinal auxiliar (rosa → laranja → amarelo → verde claro → verde-azulado) que está comentada no código e serve para futuras métricas contínuas (xG, progressões, etc.). É um lembrete de que o sistema foi pensado para ser extensível.

3.1 Legenda customizada com key_glyph

Um detalhe avançado é a criação de glyphs personalizados para a legenda:

  • key_circulo() desenha círculos com raio proporcional ao número de passes — a legenda literalmente mostra o que o tamanho significa.
  • draw_key_diag_bar() desenha barras diagonais com espessura crescente, refletindo a espessura das conexões.

Isso resolve um problema crônico de ggplot2: legendas genéricas que não comunicam a semântica real da escala.


4. Geometria do Campo e o Modelo de 18 Zonas

O script usa o sistema de coordenadas do StatsBomb/OPTA via pitch_opta, onde o campo vai de (0,0) a (100,100) em coordenadas relativas. Sobre essa base, é desenhada uma grade 6×3 que divide o campo em 18 zonas numeradas de 1 a 18:

calcular_zona_uefa <- function(x, y) {
  col_idx <- pmax(1, pmin(6, ceiling(x / 16.6667)))
  row_idx <- pmax(1, pmin(3, ceiling(y / 33.3333)))
  (col_idx - 1) * 3 + row_idx
}

Essa grade é inspirada no modelo de análise de espaços da UEFA e permite:

  1. Localizar cada jogador em uma zona canônica — facilita relatórios textuais.
  2. Segmentar a rede por blocos (defensivo, médio, ofensivo) com um simples filter() sobre x.

Os números 1–18 aparecem no plot com alpha = 0.22, funcionando como referência visual sutil sem poluir a leitura das redes.


5. Extração do Grafo: da Base de Eventos à Rede

O coração analítico está em processar_rede_logica(). A função executa uma sequência de passos:

5.1 Definição de titulares

titulares <- df %>%
  filter(!is.na(jogador)) %>% arrange(eventid) %>%
  distinct(jogador) %>% slice(1:11) %>% pull(jogador)

O critério é simples, mas eficaz: os 11 primeiros jogadores a aparecerem na base (ordenada por eventid) são considerados titulares. Ponto crítico — essa definição é feita uma única vez, no início de gerar_relatorio_tatico(), para evitar que diferentes recortes (por tempo, por zona) gerem listas inconsistentes. Essa é uma decisão arquitetural importante que garante comparabilidade entre os painéis.

5.2 Identificação do receptor

Como a base de eventos registra apenas o passador em cada evento, o receptor é obtido por lookahead:

mutate(receptor_nome = lead(jogador)) %>%
filter(receptor_nome %in% titulares)

Esse padrão — usar lead() para conectar eventos consecutivos — é a forma canônica de reconstruir uma rede de passes a partir de dados de eventos. O filtro receptor_nome %in% titulares descarta passes para reservas ou eventos espúrios.

5.3 Definição do “ponto” do jogador

A posição de cada nó não é a posição nominal do jogador, mas a média aritmética dos pontos de origem dos seus passes:

nos <- df_filt %>%
  group_by(jogador = passador_abrev) %>%
  summarise(x = mean(x), y = mean(y), valor_no = n())

Isso é importante: um lateral que sobe muito terá sua posição puxada para frente, refletindo seu comportamento real, não sua posição de escalação. É a posição funcional do jogador.

5.4 Classificação posicional automática

A função determinar_posicao() classifica cada nó em uma sigla (GK, ZD, LE, MC, AD, etc.) com base em limiares de x e y. É uma heurística simples, mas suficiente para fornecer rótulos curtos e legíveis dentro dos nós.

5.5 Filtro de “ruptura”

O tipo "Ruptura" filtra apenas passes que:

mutate(ganho_x = endX - x,
       is_ruptura = (ganho_x > 20 | endX > 66))

Ou seja, passes com ganho de 20+ metros ou que terminam no terço final. Isso isola as ações de progressão — o que realmente quebra linhas defensivas — em contraste com a rede geral, que inclui toda a circulação.


6. O Motor de Plotagem: base_plot_tactics()

A função base_plot_tactics() é o centro de gravidade do script. Ela recebe apenas conexoes e nos — o resto é cosmético. Suas principais decisões:

  • coord_flip() + scale_y_reverse(): inverte os eixos para que a orientação do campo fique horizontal (ataque da esquerda para a direita), com o lado direito do time aparecendo à direita da imagem. Isso é controlado pelo parâmetro ST$espelhar_y, permitindo ajustes rápidos.
  • geom_text_repel(): usa o pacote ggrepel para evitar sobreposição de nomes, com nudge_y dependente da posição vertical do nó (ifelse(info_nos$y > 50, -8, 8)), empurrando rótulos para o lado oposto ao centro do campo.
  • Escalas com limites comuns: os parâmetros size_lim e lw_lim permitem sincronizar as escalas de dois gráficos (usado no comparativo 1º × 2º tempo). Sem isso, um painel com menos passes pareceria vazio em relação ao outro.
  • aspect.ratio configurável: 1.2 no plot individual, 1.5 no comparativo (painéis mais largos para caber lado a lado).

6.1 Contraste automático de texto

cor_texto <- function(hex) {
  lum <- 0.299 * rgb[1,] + 0.587 * rgb[2,] + 0.114 * rgb[3,]
  ifelse(lum > 140, "#202020", "#FFFFFF")
}

A sigla posicional dentro do nó muda de preto para branco dependendo da luminância da cor de preenchimento. É a fórmula clássica de luminância relativa (ITU-R BT.601), garantindo legibilidade em qualquer tom do gradiente.


7. Os Seis Recortes Táticos

A função gerar_relatorio_tatico() orquestra seis visualizações, cada uma respondendo a uma pergunta diferente:

VisualizaçãoPergunta que responde
Rede GeralQual a estrutura de circulação da equipe?
RupturaQuem são os jogadores de progressão?
Bloco BaixoComo a equipe constrói desde a defesa?
Bloco MédioComo se organiza na zona de criação?
Bloco AltoComo finaliza e ocupa o último terço?
Comparativo 1º×2º tempoA equipe mudou de comportamento no intervalo?

Essa segmentação é especialmente valiosa para análise de intervalo: comparar rupturas no primeiro e segundo tempo revela se o técnico ajustou a altura da linha ou a agressividade da progressão.

7.1 Escalas compartilhadas no comparativo

Um detalhe fino do comparativo:

lim_s <- range(unlist(map(ds, ~ .x$nos$valor_no)))
lim_l <- range(unlist(map(ds, ~ .x$conexoes$freq)))

Os limites são calculados antes de plotar os dois painéis e passados via size_lim e lw_lim. É o que permite que um círculo com 15 passes no 1º tempo tenha o mesmo tamanho de um círculo com 15 passes no 2º tempo — condição sine qua non para qualquer comparação visual honesta.


8. Padronização de Saída

O utilitário salvar_plot() padroniza dimensões e DPI:

salvar_plot <- function(plot, arquivo, largura_px = 1080, altura_px = 1440, dpi = 150) {
  showtext_opts(dpi = dpi)
  on.exit(showtext_opts(dpi = 96))
  ggsave(..., width = largura_px/dpi, height = altura_px/dpi, dpi = dpi, ...)
}

Três detalhes importantes:

  1. 1080×1440 é proporção 3:4 — formato ideal para Instagram, carrosséis e relatórios verticais.
  2. O showtext_opts(dpi = dpi) é essencial: sem isso, as fontes do Google são rasterizadas em 96 DPI e ficam borradas em arquivos de alta resolução. O on.exit restaura a configuração global após o salvamento.
  3. O fundo (bg) é passado explicitamente, evitando aquele clássico “fundo branco no PNG mesmo com tema escuro”.

9. Boas Práticas Observadas

O script demonstra várias práticas que separam código de análise profissional de código de protótipo:

  1. Fallback de fontes: se o Google Fonts falhar (offline), o código cai automaticamente para "sans" — o pipeline não quebra.
  2. Overriding de funções mascaradas: select <- dplyr::select evita conflitos com MASS e stats.
  3. purrr::compact() para remover elementos NULL antes de iterar — evita erros em análises com dados insuficientes.
  4. if (nrow(df_filt) < 4) return(NULL): proteção contra redes degeneradas (menos de 4 passes não formam grafo útil).
  5. map_chr, map2_chr, map2_int: funções purrr tipadas, mais seguras que sapply.

10. Limitações e Extensões Sugeridas

Nenhum código é definitivo. Alguns pontos que merecem atenção em versões futuras:

  • Definição de titulares por “primeiros 11 a aparecerem” falha se houver substituições precoces por lesão. Uma alternativa mais robusta seria usar a coluna position ou startingXI da base.
  • Posição por média de passes não captura bem jogadores com atuação muito assimétrica (extremos que trocam de lado). Uma alternativa seria usar o medóide em vez da média.
  • Ausência de métricas de centralidade: betweenness, degree, closeness — calculáveis via igraph — enriqueceriam o diagnóstico tático com números objetivos.
  • Comparativo limitado a 2 tempos: poderia ser generalizado para comparar dois jogos, dois times ou dois períodos arbitrários.
  • Detecção de “ruptura” fixa em 20m: poderia ser parametrizada ou substituída por modelos mais sofisticados (ex.: line-breaking passes, packing rate).

11. Conclusão

O script analisado é um exemplo sólido de engenharia de visualização tática: não apenas gera gráficos bonitos, mas o faz de forma reprodutível, com identidade visual coerente, escalas comparáveis entre painéis e proteções contra casos-limite. A separação entre configuração, processamento e plotagem torna o código sustentável e extensível.

Do ponto de vista analítico, a combinação de rede geral + ruptura + três blocos + comparativo de tempos cobre as perguntas mais frequentes em uma comissão técnica: estrutura, progressão, construção, criação e finalização, além da evolução temporal. É, em essência, um relatório tático automatizado em uma única chamada de função.

Para quem trabalha com análise de desempenho no futebol, o padrão aqui demonstrado — dados de eventos → grafo → visualização padronizada — é um modelo replicável e um excelente ponto de partida para pipelines mais ambiciosos (dashboards interativos, integração com bancos de dados, modelos preditivos sobre centralidade de jogadores).


Referências Conceituais

  • Pass networks: conceito popularizado por análises do futebol europeu desde os anos 2010, com destaque em trabalhos do StatsBomb, Opta e comunidades de analistas como Ted Knutson e Michael Caley.
  • Modelo de zonas UEFA: grade 6×3 adotada em relatórios técnicos da UEFA e em estudos de packing e spatial control.
  • ggsoccer (Ben Torvaney): pacote R para desenhar campos de futebol em ggplot2 a partir de coordenadas OPTA/StatsBomb.
  • showtext (Yixuan Qiu): suporte a fontes Google em dispositivos gráficos do R, essencial para exportação em alta resolução.

🚀 Domine a Análise de Dados com a Formação Completa

Aprenda do zero ao avançado com o método Campo Analítico. Garanta sua vaga!

Quero me inscrever agora