Na primeira parte deste artigo, cobrimos a arquitetura geral, a paleta cromática, a extração do grafo e o motor de plotagem base_plot_tactics(). Nesta segunda parte, percorremos o restante do código linha a linha: os utilitários auxiliares, o tema completo, o processamento por zona, o painel comparativo com patchwork, a leitura da base, e o bloco de saídas que gera os arquivos finais. É aqui que o pipeline se revela completo — do read_csv() ao ggsave().
12. Utilitários Auxiliares
Antes de qualquer plot, o script define pequenos utilitários que evitam repetição e centralizam regras de formatação.
12.1 Abreviação de nomes
abreviar_nome <- function(nome) {
if (is.na(nome)) return("")
partes <- str_split(nome, " ")[[1]]
if (length(partes) > 1) {
return(paste0(substr(partes[1], 1, 1), ". ", partes[length(partes)]))
}
nome
}
A regra transforma "Rodrigo Garro" em "R. Garro" e "Yuri Alberto" em "Y. Alberto". O detalhe importante: usa a primeira letra do primeiro nome + o último sobrenome. Isso evita ambiguidades em bases brasileiras onde muitos jogadores têm nomes compostos (ex.: “Matheus Santos” → “M. Santos” diferencia de “Matheus Pereira” → “M. Pereira”). É uma decisão de design que prioriza o sobrenome como identificador, o que é culturalmente correto no futebol.
12.2 Limiares de posição — detalhamento
Na Parte I mencionamos determinar_posicao(), mas vale detalhar os limiares exatos:
linha <- case_when(x <= 12 ~ "gk", x <= 38 ~ "def", x <= 62 ~ "med", TRUE ~ "ata")
lado <- case_when(y <= 30 ~ "dir", y <= 45 ~ "dir_c", y <= 55 ~ "c",
y <= 70 ~ "esq_c", TRUE ~ "esq")
São quatro faixas longitudinais (x) e cinco faixas laterais (y), produzindo uma matriz de até 20 combinações, mas apenas 12 siglas reais são retornadas (GK, LD, ZD, ZC, ZE, LE, MD, ME, MC, AD, AE, ATA). A simplificação faz sentido: ninguém lê “ZD_C” ou “ME_C” no meio de um nó de 3,6pt. O case_when final colapsa os casos residuais em categorias genéricas.
12.3 Quebras inteiras nas legendas
quebras_int <- function(lim) {
b <- scales::breaks_extended(n = 4)(lim)
b <- unique(round(b[b == round(b)]))
if (length(b) == 0) b <- unique(round(lim))
b
}
Essa função é mais sutil do que parece. breaks_extended() pode retornar valores decimais (ex.: 2,5 passes), o que é absurdo numa contagem. O filtro b[b == round(b)] mantém apenas inteiros, e o fallback cobre o caso extremo em que nenhum inteiro sobrevive (ex.: rede com apenas 3 e 4 passes). É um exemplo de robustez silenciosa: o código não falha em redes pequenas, apenas se adapta.
12.4 Reatribuição de funções mascaradas
select <- dplyr::select
filter <- dplyr::filter
lag <- dplyr::lag
intersect <- dplyr::intersect
union <- dplyr::union
setdiff <- dplyr::setdiff
Parece redundante depois de library(tidyverse), mas é uma rede de segurança. Se o usuário carregar library(MASS) em outra sessão (que mascara select), ou library(stats) (que tem lag, filter, intersect), o script continua funcionando. É um padrão defensivo comum em scripts que serão compartilhados com colegas em ambientes diferentes.
13. O Tema Completo
A configuração do theme() em base_plot_tactics() merece uma análise detalhada, porque é onde a identidade visual se materializa. Além de theme_pitch() (do ggsoccer, que remove eixos e grades padrão), o código define:
13.1 Hierarquia tipográfica
plot.title = element_text(size = 24 * esc, face = "bold", family = ST$f_title,
hjust = 0.5, margin = margin(b = 6))
plot.subtitle = element_text(size = 14 * esc, hjust = 0.5,
margin = margin(b = 12))
plot.caption = element_text(size = 8.5 * esc, lineheight = 1.3,
hjust = 0.5, margin = margin(t = 12))
Três níveis bem marcados: 24pt bold (título), 14pt regular (subtítulo), 8,5pt (legenda de siglas). Todos com hjust = 0.5 para centralização — decisão que combina com o formato vertical 1080×1440 e reforça a leitura de “poster” do gráfico.
O family = ST$f_title no título usa a "Roboto Slab" (serifada), enquanto o restante usa a mesma família configurada em ST$f_base. Isso cria contraste tipográfico serifada vs. sem serifada, um recurso editorial clássico que diferencia o título do corpo.
13.2 Legenda horizontal em duas fileiras
legend.position = "bottom",
legend.direction = "horizontal",
legend.box = "horizontal",
legend.box.just = "center",
legend.box.spacing = unit(0.5, "cm"),
legend.spacing.x = unit(1.2, "cm"),
A legenda tem duas entradas (tamanho dos nós e espessura das conexões), e ambas devem caber abaixo do campo sem quebrar linha. O legend.spacing.x = unit(1.2, "cm") é o detalhe que separa uma legenda amadora de uma profissional: sem ele, as duas legendas ficam grudadas e ilegíveis.
13.3 O caption como mini-glossário
caption = paste0("GK = Goleiro | Z = Zagueiro | L = Lateral | ...\nCampo Analítico")
Em vez de um rodapé genérico (“Fonte: Opta”), o script usa o caption como glossário de siglas — resolvendo o problema de leitores que não sabem o que significa “ZD” ou “AE”. A quebra \n separa o glossário da assinatura “Campo Analítico”.
13.4 Margens generosas
plot.margin = margin(22, 22, 22, 22)
22 pontos em todos os lados. Isso pode parecer exagero, mas em formato vertical 1080×1440 é o que garante respiro visual ao redor do campo, evitando que o gráfico pareça “apertado” no feed do Instagram.
14. O Parâmetro esc — Escala Global
Um dos parâmetros mais úteis do base_plot_tactics() é esc (escala), aplicado a quase todos os elementos tipográficos e de tamanho:
size = 3.2 * esc, # números das zonas
size = 3.6 * esc, # sigla da posição
size = 4.6 * esc, # nome do jogador
range = c(8, 26) * esc, # tamanho dos nós
size = 24 * esc, # título
size = 14 * esc, # subtítulo
size = 11 * esc, # legenda
Com esc = 1 temos o gráfico individual padrão. Com esc = 0.75 (usado no comparativo) todos os elementos encolhem 25% simultaneamente — sem precisar reescrever o tema. É um mecanismo elegante de responsividade que mantém a proporção interna do gráfico intacta.
Sem esse parâmetro, seria necessário duplicar o código do tema para cada formato de saída. Com ele, o mesmo tema serve para 1080×1440 (individual) e 1080×1080 (comparativo lado a lado).
15. Espelhamento e Coordenadas
if (isTRUE(ST$espelhar_y)) p <- p + scale_y_reverse()
p + coord_flip(clip = "off")
Essa linha é contraintuitiva à primeira vista, mas essencial. Vamos desmontar:
coord_flip()inverte os eixos, colocando o campo na horizontal (ataque da esquerda para a direita). Mas com isso, o eixo y original (que no OPTA vai de 0 = direita a 100 = esquerda do time) é mapeado para o eixo vertical invertido da imagem.- Sem
scale_y_reverse(), o lado direito do time (y próximo de 0) apareceria em cima na imagem, e o lado esquerdo embaixo — o inverso do que se espera numa leitura de campo. - Aplicando
scale_y_reverse()antes docoord_flip(), o resultado final é: lado direito do time à direita, lado esquerdo à esquerda. Exatamente como o técnico vê o campo.
O parâmetro ST$espelhar_y permite desligar essa correção caso a base já venha com coordenadas invertidas — outra decisão de design que prevê variabilidade entre fontes de dados (StatsBomb, Opta, Wyscout têm convenções diferentes).
O clip = "off" é o que permite que rótulos com geom_text_repel() “saiam” do painel sem serem cortados — fundamental quando o nome do jogador é longo e precisa se afastar do nó.
16. Processamento por Zona
processar_rede_por_zona <- function(df, zona = c("baixo", "medio", "alto"),
jogador_foco = NULL, titulares = NULL) {
zona <- match.arg(zona)
limites <- switch(zona,
"baixo" = c(0, 33.33),
"medio" = c(33.33, 66.66),
"alto" = c(66.66, 100))
df_zona <- df %>% filter(x >= limites[1], x <= limites[2])
processar_rede_logica(df_zona, "Geral", jogador_foco, titulares)
}
A função atua como wrapper de processar_rede_logica(): filtra os eventos por faixa de x e chama o mesmo motor de extração. Isso é possível porque a posição dos nós é calculada dentro do subconjunto filtrado — ou seja, no bloco baixo, o “ponto médio” de um zagueiro é a média dos passes que ele deu no bloco baixo, não a média da partida inteira.
Essa é uma decisão analítica importante: cada bloco tem sua própria geometria, o que faz sentido tático. Um lateral no bloco alto está posicionado onde ele efetivamente atuou naquele terço, não onde ele fica em média na temporada.
O match.arg() no início é uma proteção contra erros de digitação: se alguém chamar processar_rede_por_zona(df, "alto ") (com espaço), a função falha com mensagem clara em vez de retornar silenciosamente NULL.
17. O Painel Comparativo com patchwork
O comparativo 1º × 2º tempo é o trecho mais elaborado do script. Vamos percorrer suas camadas.
17.1 Extração das duas metades
col_tempo <- if ("period" %in% names(df_time)) "period" else "tempo"
d1 <- processar_rede_logica(df_time[df_time[[col_tempo]] == "FirstHalf", ], ...)
d2 <- processar_rede_logica(df_time[df_time[[col_tempo]] == "SecondHalf", ], ...)
ds <- purrr::compact(list(d1, d2))
O col_tempo é uma detecção automática de schema: algumas bases usam "period", outras "tempo". Essa linha é uma adaptação à realidade fragmentada do ecossistema de dados de futebol — evita que o script quebre se a base for trocada.
O purrr::compact() remove elementos NULL (quando uma das metades tem <4 passes ou não existe), permitindo que o resto do código trate apenas o caso com dados.
17.2 Escalas compartilhadas
lim_s <- range(unlist(map(ds, ~ .x$nos$valor_no)))
lim_l <- range(unlist(map(ds, ~ .x$conexoes$freq)))
Esses dois limites são calculados antes de plotar qualquer painel, varrendo as duas metades. São passados como size_lim e lw_lim para base_plot_tactics(), garantindo que um nó de 15 passes no 1º tempo tenha exatamente o mesmo diâmetro no 2º tempo. Sem isso, cada painel criaria sua própria escala e o comparativo seria enganoso — um erro clássico em visualização que o script evita explicitamente.
17.3 Painel condicional com fallback
painel <- function(d, titulo) {
if (is.null(d)) {
return(ggplot() + theme_void() +
annotate("text", x = 0, y = 0, label = "Dados insuficientes", ...) +
labs(title = titulo) + ...)
}
base_plot_tactics(d$conexoes, d$nos, titulo, "Passes de ruptura",
aspect = 1.5, esc = 0.75,
size_lim = lim_s, lw_lim = lim_l)
}
Se uma das metades não tem dados suficientes, o painel não desaparece — ele vira uma mensagem “Dados insuficientes”. Isso preserva a estrutura de dois painéis e mantém a simetria do comparativo, mesmo em casos-limite (jogo cancelado no intervalo, base incompleta, etc.).
O aspect = 1.5 e esc = 0.75 são específicos do comparativo: painéis mais largos e textos 25% menores para caber lado a lado em 1080×1080.
17.4 Composição com patchwork
p_comp <- painel(d1, "1º TEMPO") + painel(d2, "2º TEMPO") +
plot_layout(guides = "collect") +
plot_annotation(
title = paste("EVOLUÇÃO TÁTICA -", nome_exibicao),
subtitle = "Campo Analítico",
theme = theme(...)
) &
theme(legend.position = "bottom")
Três detalhes importantes:
plot_layout(guides = "collect"): as duas legendas (uma de cada painel) são unificadas em uma só ao final. Sem isso, o comparativo teria duas barras de legenda idênticas, ocupando o dobro de espaço — visualmente poluído e redundante.plot_annotation()adiciona título e subtítulo globais ao conjunto dos dois painéis — não a cada painel individualmente.& theme(legend.position = "bottom")aplica o tema a todos os painéis via operador&do patchwork (diferente de+, que aplicaria só ao último).
Essa combinação guides = "collect" + plot_annotation + & theme é o padrão canônico do patchwork para composições comparativas. O script a executa corretamente, o que não é trivial.
18. Leitura dos Dados
arquivo <- "C:/Users/aldre/Downloads/Corinthians_vs_Vitoria_base2024.csv"
partida <- read_csv(arquivo, show_col_types = FALSE)
Duas observações:
- Caminho absoluto hardcoded — um ponto frágil. Em produção, isso deveria vir de
here::here(), de uma variável de ambiente ou decommandArgs(). Mas para um script pessoal de análise, é aceitável e reduz o atrito. show_col_types = FALSEsuprime o aviso de parsing doreadr, que em bases grandes pode poluir o console com centenas de linhas. É uma escolha pragmática: o analista confia no schema da base.
18.1 Checagem exploratória
table(partida$teamId)
table(partida$teamName)
# names(partida); glimpse(partida)
# table(partida$type); ...
Três table() executados e várias linhas comentadas de diagnóstico. O padrão é claro: o autor verifica manualmente teamId, teamName e a estrutura geral antes de rodar as análises. As linhas comentadas (names, glimpse, table($type)) são um “kit de emergência” pronto para ser descomentado quando algo quebra.
Esse é um contraste interessante com a parte automatizada do pipeline: mesmo com toda a robustez, o analista mantém pontos de inspeção manual. É o reconhecimento de que dados de eventos sempre têm particularidades.
19. O Bloco de Saídas
dir_saida <- "saidas_santos"
dir.create(dir_saida, showWarnings = FALSE)
O diretório de saída é nomeado "saidas_santos" — resquício de um script anterior (o time analisado é Corinthians). É um detalhe revelador: o pipeline foi reaproveitado de outro contexto, o que reforça sua modularidade.
O showWarnings = FALSE evita o aviso “directory already exists” quando o script roda várias vezes seguidas. É o comportamento esperado em pipelines idempotentes.
19.1 Iteração com purrr::iwalk()
relatorio[names(relatorio) != "comparativo"] %>%
purrr::compact() %>%
purrr::iwalk(~ salvar_plot(.x, file.path(dir_saida, paste0("santos_", .y, ".png"))))
Esse é um dos trechos mais elegantes do script. Vamos destrinchá-lo:
- Filtro
names(relatorio) != "comparativo"— remove o comparativo, que será salvo com dimensões diferentes. purrr::compact()— remove elementosNULL(ex.: sep_rupturaforNULLpor falta de dados). Impede quesalvar_plot()tente salvar um plot inexistente.purrr::iwalk()— itera sobre a lista nomeada passando o valor e o nome (~ .x= valor,~ .y= nome). É a função certa para quando o nome do elemento precisa ser usado no corpo da iteração.paste0("santos_", .y, ".png")— cria nomes de arquivo descritivos automaticamente:santos_geral.png,santos_ruptura.png,santos_bloco_baixo.png, etc.
Em cinco linhas de código, o pipeline gera cinco arquivos PNG diferentes com nomes corretos e apenas os que existem. É o tipo de código que separa um script de um pipeline.
19.2 Saída do comparativo
if (!is.null(relatorio$comparativo)) {
salvar_plot(relatorio$comparativo,
file.path(dir_saida, "santos_comparativo.png"),
largura_px = 1080, altura_px = 1440)
}
Curiosidade: o comentário do topo do script diz que o comparativo é 1080×1080, mas a chamada real usa 1080×1440. É uma inconsistência de documentação — mas provavelmente intencional: como o comparativo tem dois painéis lado a lado e uma legenda embaixo, 1440px de altura dá mais espaço para o campo não ficar “esmagado”.
O if (!is.null(...)) é a proteção contra o caso em que nenhuma das metades tem dados e o comparativo não pôde ser gerado.
20. Pré-visualização
print(relatorio$geral)
print(relatorio$ruptura)
print(relatorio$bloco_baixo)
print(relatorio$bloco_medio)
print(relatorio$bloco_alto)
print(relatorio$comparativo)
Seis print() explícitos ao final do script. Isso é uma escolha deliberada: dentro de um script Rscript não há exibição automática, e dentro do RStudio os print() garantem que cada gráfico seja renderizado em sequência na aba Plots.
O padrão é o oposto de walk(relatorio, print) — que seria mais conciso, mas perderia a intenção clara de cada linha. Aqui, o autor prioriza legibilidade do fluxo sobre elegância funcional. São escolhas defensáveis em ambos os lados.
21. O Que Este Script Ensina
Depois dessa leitura completa, algumas lições arquiteturais se destacam:
21.1 Separação de responsabilidades
A estrutura em camadas — configuração → utilitários → plotagem → processamento → orquestração → saída — faz com que qualquer mudança fique contida em uma única camada. Trocar de time? Altera o gerar_relatorio_tatico(). Trocar a paleta? Altera o ST. Trocar a fonte? Altera o font_add_google().
21.2 Robustez silenciosa
O script raramente falha: ele degrada graciosamente. Sem dados suficientes, retorna NULL. Sem internet, cai para "sans". Sem inteiros nos breaks, usa fallback. Um painel do comparativo ausente vira “Dados insuficientes”. Essa resiliência é o que diferencia um script de análise descartável de um pipeline utilizável em produção.
21.3 Comparabilidade explícita
A ideia de calcular size_lim e lw_lim antes de plotar os dois painéis é um princípio de visualização séria: escalas compartilhadas são obrigatórias em comparações. Sem isso, o gráfico mente. O código implementa essa regra como feature, não como afterthought.
21.4 Composição declarativa com patchwork
O uso de plot_layout(guides = "collect") + plot_annotation() + & theme() demonstra domínio de uma das bibliotecas mais poderosas do ecossistema R moderno. O comparativo é, tecnicamente, dois gráficos independentes tratados como um só.
21.5 Hierarquia tipográfica e respiro
O contraste Roboto Slab (serifada) no título × sans-serif no resto, somado às margens de 22pt, à legenda com spacing.x = 1.2cm e ao glossário no caption, produzem um resultado que comunica antes de ser lido. É design editorial aplicado à análise de dados.
22. Um Pipeline, Não Um Script
O que separa este código de um script exploratório é que ele foi desenhado para rodar várias vezes sobre bases diferentes, sempre produzindo os mesmos seis artefatos com a mesma qualidade. A assinatura de um pipeline maduro está lá:
- Uma linha para gerar tudo:
gerar_relatorio_tatico(partida, 1237, "Corinthians"). - Nomenclatura automática: os arquivos recebem nome a partir do time e do tipo de análise.
- Idempotência: rodar duas vezes não gera erro, sobrescreve os arquivos.
- Degradação controlada: casos-limite (sem dados, sem internet) não quebram o fluxo.
- Identidade visual blindada: qualquer alteração de cor, fonte ou dimensão está centralizada em
STesalvar_plot().
Para quem quer adaptar esse código ao seu próprio fluxo, os pontos de entrada são claros:
- Substituir o caminho do
arquivono topo. - Ajustar
STse a identidade visual for de outro time/analista. - Eventualmente parametrizar o
id_do_timee onome_exibicao(hoje passados na chamada degerar_relatorio_tatico()).
O resto — extração, classificação, plotagem, composição, salvamento — já está resolvido.
23. Conclusão da Série
Este artigo percorreu, em duas partes, os 250+ linhas de código que compõem o pipeline de redes de passes. Vimos como a arquitetura em camadas permite manter clareza mesmo com alta densidade funcional, como o ggplot2 é estendido com ggsoccer, ggrepel, patchwork e showtext, e como pequenas decisões — de luminância de texto a quebras inteiras nas escalas — separam uma visualização amadora de um relatório tático profissional.
O maior mérito do código não está em nenhuma linha isolada, mas na coerência do conjunto: cada escolha reforça as demais, e o resultado é um pipeline que se comporta como um produto analítico, não como um script de exploração. É um excelente modelo para quem deseja construir ferramentas similares — seja para um clube, para uma consultoria ou para análise pública em redes sociais.
Se você for replicar ou estender este código, a recomendação final é: mantenha as camadas. Resista à tentação de “só colocar mais um caso no meio”. Cada nova funcionalidade tem um lugar claro — nos utilitários, no processamento ou na orquestração — e respeitar essa fronteira é o que vai manter o pipeline utilizável daqui a seis meses, quando você já não se lembrar de nenhuma das decisões originais.
🚀 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