Módulo 1 — Datos y análisis exploratorio

Code
library(readr); library(dplyr); library(ggplot2); library(lubridate); library(scales); library(DT); library(tidyr); library(igraph)
tx <- read_csv("../outputs/tx_limpio.csv", show_col_types = FALSE)
resumen <- read_csv("../outputs/resumen_datos.csv", show_col_types = FALSE)
chronnet <- readRDS("../outputs/chronnet.rds")
knitr::opts_chunk$set(message = FALSE, warning = FALSE)

1. ¿Qué son estos datos?

  • SAR (Suspicious Activity Report): reporte de operación sospechosa presentado por los bancos ante el FinCEN (unidad de inteligencia financiera de EE. UU.) y filtrado por la ICIJ en los FinCEN Files (2020).
  • Cada fila = un flujo que un banco juzgó sospechoso entre un banco originador y un banco beneficiario, con fechas y montos.

⚠️ Sospecha no es delito: la presencia de un banco en estos datos significa que reportó (o apareció en) movimientos que activaron sus controles antilavado, no que se le haya comprobado participación criminal. La red muestra dónde se detectó actividad sospechosa, con el sesgo de reporte que eso implica.

2. Vocabulario

  • SAR: reporte de operación sospechosa. Un mismo SAR (un mismo formulario) puede contener varias filas de transacciones.
  • Originador: banco que envía el dinero. Beneficiario: banco que lo recibe.
  • Chronnet: la red de trabajo de todo el taller (módulo 2). Nodo = banco; arista dirigida = co-actividad en días consecutivos (peso = producto de eventos). Los montos no viven en las aristas: se calculan desde los reportes y se pegan como atributos (monto_total por banco, monto circulado por par).

3. AED — Conteos y tipos de variable

Conteos básicos. Ojo a la diferencia entre filas y SARs:

Code
tibble(
  Concepto = c("Filas (transacciones reportadas)", "SARs únicos (formularios)",
               "Bancos distintos", "Países distintos",
               "Monto total reportado (USD)"),
  Valor = c(nrow(tx), n_distinct(tx$icij_sar_id),
            n_distinct(c(tx$originator_bank_id, tx$beneficiary_bank_id)),
            n_distinct(c(tx$originator_iso, tx$beneficiary_iso), na.rm = TRUE),
            dollar(sum(tx$amount_transactions, na.rm = TRUE)))
) %>% knitr::kable()
Concepto Valor
Filas (transacciones reportadas) 4501
SARs únicos (formularios) 534
Bancos distintos 2275
Países distintos 129
Monto total reportado (USD) $35,620,551,620

Lectura:

  • n_filas ≠ n_sars: un formulario SAR lista muchas transacciones — 4.501 filas pero solo 534 formularios (~8 transacciones por SAR).
  • Al contar “casos” hay que decir si hablamos de transacciones o de formularios — son cosas distintas.
  • Tipos de variable: fechas (begin_date, end_date), categóricas (bancos, países, ISO) y numéricas (number_transactions, amount_transactions).
  • id es solo un correlativo de fila; el identificador real de la entidad es el *_bank_id.

4. AED — Top bancos por monto reportado (originadores)

Tabla interactiva con filtros por columna (cuadros bajo los encabezados). Top 50:

Code
tx %>%
  group_by(originator_bank, originator_bank_country) %>%
  summarise(
    monto = sum(amount_transactions, na.rm = TRUE),
    n_transacciones = sum(number_transactions, na.rm = TRUE),
    n_reportes = n(),
    .groups = "drop"
  ) %>%
  arrange(desc(monto)) %>%
  slice_head(n = 50) %>%
  mutate(monto = dollar(monto)) %>%
  datatable(filter = "top", options = list(pageLength = 10,
            language = list(url = "//cdn.datatables.net/plug-ins/1.13.4/i18n/es-ES.json")),
            colnames = c("Banco originador", "País", "Monto reportado", "Nº transacciones", "Nº reportes SAR"))

Lectura criminal

  • Los originadores top son por donde el dinero reportado como sospechoso sale.
  • Patrón fraccionamiento: muchos reportes de montos pequeños (movimientos bajo umbrales de reporte).
  • Patrón consolidación: pocos reportes, montos muy grandes (el dinero se junta antes de seguir).
  • Ninguno de los dos patrones implica por sí solo ilicitud — ambos ocurren en comercio legítimo.
  • Lo que una investigación prioriza: la combinación de ambos extremos en la misma red.

5. AED — Top países en los flujos

  • Cada país suma el monto donde aparece como originador o beneficiario.
  • Las etiquetas muestran monto | nº de transacciones:
Code
paises <- bind_rows(
  tx %>% transmute(pais = originator_bank_country, monto = amount_transactions, n = number_transactions),
  tx %>% transmute(pais = beneficiary_bank_country, monto = amount_transactions, n = number_transactions)
) %>%
  group_by(pais) %>%
  summarise(monto = sum(monto, na.rm = TRUE),
            n_transacciones = sum(n, na.rm = TRUE),
            .groups = "drop") %>%
  mutate(pais = ifelse(is.na(pais), "(Sin país registrado)", pais)) %>%
  slice_max(monto, n = 15)

ggplot(paises, aes(x = reorder(pais, monto), y = monto / 1e9)) +
  geom_col(fill = "#0D9488") +
  geom_text(aes(label = paste0(round(monto / 1e9, 1), " B | ", scales::comma(n_transacciones), " tx")),
            hjust = -0.05, size = 3, color = "#334155") +
  coord_flip(clip = "off") +
  scale_y_continuous(expand = expansion(mult = c(0, 0.25))) +
  labs(x = "", y = "Monto reportado (miles de millones USD)") +
  theme_minimal()

5.1 Los NA de país: qué son y cómo los tratamos

Code
n_sin <- sum(is.na(tx$originator_bank_country) | is.na(tx$beneficiary_bank_country))
n_bancos_sin <- tx %>%
  filter(is.na(originator_bank_country) | is.na(beneficiary_bank_country)) %>%
  summarise(n = n_distinct(c(originator_bank_id, beneficiary_bank_id), na.rm = TRUE)) %>% pull(n)
cat("Registros con al menos un extremo sin país:", n_sin, "(", round(100*n_sin/nrow(tx),1), "%)\n")
Registros con al menos un extremo sin país: 0 ( 0 %)
Code
cat("Bancos distintos afectados:", n_bancos_sin, "\n")
Bancos distintos afectados: 0 

Lectura:

  • Origen del NA: el campo país venía vacío en el archivo original de la ICIJ.
  • Descartarlos no es opción: se caerían esos bancos con todas sus conexiones y la red se fragmentaría artificialmente.
  • Estrategia: relacionarlos igual — el nodo entra con su bank_id y nombre; solo el atributo país queda NA.
  • Consecuencia: en los mapas no se ven (sin coordenadas), pero en la red computan grado, centralidad y comunidades normalmente.
  • Mantenimiento: cuando un analista identifique el país real, se actualiza data/bancos_geo.csv y el nodo aparece en el mapa en la siguiente corrida.

Lectura criminal

  • Un banco sin país registrado puede ser una entidad cuya jurisdicción el reportante no supo (o no quiso) identificar.
  • Los nodos “apátridas” suelen ser los más interesantes: operan en la red pero fuera del mapa.
  • Se conservan en el grafo y se investigan por separado.

6. AED — Distribución de montos por registro

Escala logarítmica en X para poder ver toda la cola; Y completo hasta la frecuencia máxima:

Code
ggplot(tx, aes(x = amount_transactions)) +
  geom_histogram(bins = 60, fill = "#D4883A") +
  scale_x_log10(labels = dollar) +
  labs(x = "Monto por registro (USD, escala log)", y = "Frecuencia") +
  theme_minimal()

Cómo interpretarla:

  • La mayoría de los reportes se acumula a la izquierda (miles o millones de dólares); una cola larga a la derecha llega a decenas de miles de millones.
  • Cola pesada a la derecha = patrón típico del lavado: muchísimos movimientos pequeños (fraccionamiento, smurfing) y pocos gigantes (consolidación).
  • Si fuera simétrica (campana), sospecharíamos datos truncados o censados.

Lectura criminal

  • La cola derecha son los flujos que un analista quiere ver primero: montos que solo unos pocos bancos del mundo pueden mover rutinariamente.
  • La montaña izquierda son los movimientos pequeños que, en conjunto, mueven el mismo volumen.
  • La curva es la foto del fenómeno: muchos movimientos pequeños, pocos movimientos grandes.

7. AED — Línea de tiempo por año-trimestre

  • Barras: nº de reportes SAR.
  • Línea: monto total del trimestre (eje derecho):
Code
tl <- tx %>%
  mutate(trimestre = floor_date(fecha, "quarter")) %>%
  group_by(trimestre) %>%
  summarise(n = n(), monto_bn = sum(amount_transactions, na.rm = TRUE) / 1e9, .groups = "drop")
factor_escala <- max(tl$n) / max(tl$monto_bn)

ggplot(tl, aes(trimestre)) +
  geom_col(aes(y = n), fill = "#0D9488", alpha = 0.85) +
  geom_line(aes(y = monto_bn * factor_escala), color = "#D4883A", linewidth = 1) +
  scale_y_continuous(name = "Nº reportes SAR",
    sec.axis = sec_axis(~ . / factor_escala, name = "Monto (miles de millones USD)")) +
  labs(x = "", caption = "Barras: nº de reportes | Línea: monto total del trimestre") +
  theme_minimal()