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:
| Camada | Responsabilidade |
|---|---|
| Configuração | Fontes, cores, tema, escalas |
| Utilitários | Abreviação de nomes, posicionamento, zonas, cores de texto |
| Motor de plotagem | base_plot_tactics() — função central do ggplot |
| Processamento | Filtragem de passes, criação de nós e arestas |
| Orquestração | gerar_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:
- Localizar cada jogador em uma zona canônica — facilita relatórios textuais.
- Segmentar a rede por blocos (defensivo, médio, ofensivo) com um simples
filter()sobrex.
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âmetroST$espelhar_y, permitindo ajustes rápidos.geom_text_repel(): usa o pacoteggrepelpara evitar sobreposição de nomes, comnudge_ydependente 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_limelw_limpermitem 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.ratioconfigurá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ção | Pergunta que responde |
|---|---|
| Rede Geral | Qual a estrutura de circulação da equipe? |
| Ruptura | Quem são os jogadores de progressão? |
| Bloco Baixo | Como a equipe constrói desde a defesa? |
| Bloco Médio | Como se organiza na zona de criação? |
| Bloco Alto | Como finaliza e ocupa o último terço? |
| Comparativo 1º×2º tempo | A 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:
- 1080×1440 é proporção 3:4 — formato ideal para Instagram, carrosséis e relatórios verticais.
- 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. Oon.exitrestaura a configuração global após o salvamento. - 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:
- Fallback de fontes: se o Google Fonts falhar (offline), o código cai automaticamente para
"sans"— o pipeline não quebra. - Overriding de funções mascaradas:
select <- dplyr::selectevita conflitos comMASSestats. purrr::compact()para remover elementosNULLantes de iterar — evita erros em análises com dados insuficientes.if (nrow(df_filt) < 4) return(NULL): proteção contra redes degeneradas (menos de 4 passes não formam grafo útil).map_chr,map2_chr,map2_int: funções purrr tipadas, mais seguras quesapply.
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
positionoustartingXIda 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 viaigraph— 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 emggplot2a 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