Module 5 — Key players: logistical pivots

In modules 3 and 4 we characterized and validated the network. Now we identify the key players: those who move the money and those who connect circuits.

Code
library(igraph); library(ggraph); library(dplyr); library(ggplot2); library(scales); library(readr); library(leaflet); library(plotly); library(DT)
chronnet <- readRDS("../../outputs/chronnet.rds")
cen <- read_csv("../../outputs/centralidad.csv", show_col_types = FALSE)
sub <- readRDS("../../outputs/subgrafo_claves.rds")
knitr::opts_chunk$set(message = FALSE, warning = FALSE)

1. Three measures, three questions

Measure Question What it captures
Degree \(k_i\) With how many banks does it share time windows? Reach in the sequence
Strength \(s_i\) How much money does it move? Volume (attribute from the reports)
Betweenness \(C_i^{betw} = \sum_{j,k} \frac{n_i^{jk}}{g^{jk}}\) How many shortest chronnet paths go through it? Bridge between circuits

Betweenness, word by word:

  • For each pair of banks \((j, k)\), count how many of the shortest paths between them pass through \(i\) (\(n_i^{jk}\)), divided by the total (\(g^{jk}\)).
  • Sum over all pairs.

Interpretation:

  • A vertex with high betweenness is not the one moving the most money or holding the most connections.
  • It is the one connecting circuits that would otherwise be isolated in the temporal sequence.
  • Removing a bridge is not the fastest way to fragment edges, but it leaves whole communities cut off from the flow of activity.

Criminal reading

  • A bank with high betweenness is the “correspondent of correspondents”: it may move little money of its own, but it sits in the middle of everyone else’s temporal path.
  • In practice, these are the nodes a network places between its circuits so they do not operate at the same times.
  • It is exactly the role an investigator wants to break.

2. Top banks by strength (money moved)

  • Interactive table with filters — search by country, amount range or name.
  • Degree is topological (chronnet); strength is reported money (attribute).
Code
cen %>%
  slice_max(fuerza, n = 50) %>%
  select(bank_name, country, grado, actividad, fuerza, intermediacion) %>%
  mutate(fuerza = round(fuerza, 0), actividad = round(actividad, 0),
         intermediacion = round(intermediacion, 1)) %>%
  datatable(filter = "top", options = list(pageLength = 10),
            colnames = c("Bank", "Country", "Degree", "Activity", "Strength (USD)", "Betweenness"))

3. Degree vs. strength: accumulators and distributors

  • Each point is a bank; point size is its betweenness.
  • Hover to see all its data:
Code
nodos <- tibble(
  nombre = V(chronnet)$bank_name, pais = V(chronnet)$country,
  grado = degree(chronnet), fuerza = V(chronnet)$monto_total,
  intermediacion = betweenness(chronnet, directed = TRUE),
  monto_total = V(chronnet)$monto_total, tx_total = V(chronnet)$tx_total,
  first = V(chronnet)$first, last = V(chronnet)$last
) %>% filter(grado > 0)

p <- ggplot(nodos, aes(grado, fuerza)) +
  geom_point(aes(size = intermediacion), alpha = 0.4, color = "#0D9488") +
  scale_x_log10(labels = comma) + scale_y_log10(labels = dollar) +
  geom_smooth(method = "lm", se = FALSE, color = "#D4883A") +
  labs(x = "Degree (log)", y = "Strength / money (log)", size = "Betweenness") +
  theme_minimal()
ggplotly(p, tooltip = c("nombre", "pais", "grado", "fuerza", "intermediacion", "monto_total", "tx_total", "first", "last"))

Reading the quadrants:

  • Above the line (strength ≫ degree) = accumulators: few temporal contacts, lots of money — pass-through accounts and concentration centers.
  • To the right (high degree, moderate strength) = distributors: they share windows with many banks and fractionate funds.
  • Cutting an accumulator stops money; cutting a distributor fragments the sequence.

4. Key-player subnetwork

Which measure defines the subnetwork:

  • Nodes above the 90th percentile of strength \(s_i\) (total money moved, in + out).
  • Of the 2,275 banks in the chronnet, the ~230 with the highest strength enter — the 10% concentrating most of the reported money.
  • We do not use degree or betweenness here: strength answers “who handles the volume?”, and for a first read of the elite it is the right question.
  • (The other two measures are used in module 6 to decide whom to remove.)

Why this section exists:

  • The full network (2,275 nodes) cannot be read.
  • The subnetwork keeps the dominant flows and shows who operates with whom inside the elite.

The labels:

  • In the previous plot they all overlapped and were unreadable; the solution is not to label everything, it is to filter.
  • Below, two readings: first without labels (the table filters identify nodes), then only the 30 largest — where they fit.
Code
# The saved subgraph does not carry community: we bring it from the main chronnet
V(sub)$comunidad <- V(chronnet)$comunidad[match(V(sub)$name, V(chronnet)$name)]
ggraph(sub, layout = "stress") +
  geom_edge_link(aes(width = monto), alpha = 0.3, color = "grey55") +
  geom_node_point(aes(size = monto_total,
                      color = as.factor(comunidad)), alpha = 0.9) +
  scale_size(range = c(1, 10)) + scale_edge_width(range = c(0.2, 3), guide = "none") +
  theme_graph() + theme(legend.position = "none")

Code
# Only label the 30 nodes with highest strength: where labels fit
top30 <- tibble(name = V(sub)$name, fuerza = V(sub)$monto_total) %>%
  slice_max(fuerza, n = 30) %>% pull(name)

ggraph(sub, layout = "stress") +
  geom_edge_link(aes(width = monto), alpha = 0.25, color = "grey70") +
  geom_node_point(aes(size = monto_total,
                      color = as.factor(comunidad)), alpha = 0.9) +
  geom_node_text(data = NULL,
                 aes(label = ifelse(name %in% top30, bank_name, "")),
                 repel = TRUE, size = 2.8, max.overlaps = Inf) +
  scale_size(range = (c(1, 10))) + scale_edge_width(range = c(0.2, 3), guide = "none") +
  theme_graph() + theme(legend.position = "none")

The filters that give it meaning:

  • The table in section 2 filters the elite by country or amount range.
  • This plot answers: “who operates with whom among those I filtered?”.
  • The analyst’s workflow: table → filter → plot.

Criminal reading

  • The subnetwork is a picture of the highest-volume nodes: the banks between which most of the money reported as suspicious circulates.
  • Edge width follows the documented money between the pair.
  • When two elite entities transact again and again, the width records it: that repetition distinguishes a structural relationship from an isolated transaction.

5. Map above/below the threshold

  • Orange = key players (90th percentile of strength).
  • Grey = rest of the network.
  • Hover over the nodes to see their data:
Code
sub_ids <- V(sub)$name
mapa_df <- cen %>%
  filter(!is.na(lat), !is.na(lon)) %>%
  left_join(tibble(bank_id = V(chronnet)$name, monto_total = V(chronnet)$monto_total,
                   tx_total = V(chronnet)$tx_total, first = V(chronnet)$first, last = V(chronnet)$last),
            by = "bank_id") %>%
  mutate(clave = bank_id %in% sub_ids,
         radio = 3 + 10 * scales::rescale(log1p(fuerza)))

tooltip <- function(df) lapply(paste0(
  "<b>", df$bank_name, "</b> (", df$country, ")<br>",
  "💰 Total money: ", dollar(df$monto_total), "<br>",
  "🔢 Total transactions: ", comma(df$tx_total), "<br>",
  "📍 ", round(df$lat, 3), ", ", round(df$lon, 3), "<br>",
  "🗓️ Active: ", df$first, " → ", df$last), htmltools::HTML)

leaflet(mapa_df) %>%
  addProviderTiles(providers$OpenStreetMap) %>%
  addCircleMarkers(data = mapa_df %>% filter(!clave),
                   lng = ~lon, lat = ~lat, radius = 2, group = "Rest of network",
                   color = "#64748B", stroke = FALSE, fillOpacity = 0.25) %>%
  addCircleMarkers(data = mapa_df %>% filter(clave),
                   lng = ~lon, lat = ~lat, radius = ~radio, group = "Key players",
                   color = "#D4883A", stroke = FALSE, fillOpacity = 0.9,
                   label = tooltip(mapa_df %>% filter(clave))) %>%
  addLayersControl(overlayGroups = c("Key players", "Rest of network"),
                   options = layersControlOptions(collapsed = FALSE)) %>%
  setView(lng = 0, lat = 20, zoom = 2)

Criminal reading

  • The map shows the geography of the elite: the orange hubs coincide with global financial centers and known pass-through jurisdictions.
  • A key player geographically isolated from the rest of its own community (same color in the plot above, distance on this map) is a transnational bridge.
  • It is the kind of node connecting circuits on different continents.