Módulo 2 — Construcción de la red cronológica (chronnet)
En el módulo 1 limpiamos y exploramos los reportes SAR. Ahora los convertimos en una red cronológica — el objeto sobre el que trabajan todos los módulos siguientes.
Una red criminal no es una foto: es una película. El mismo par de bancos puede operar juntos en marzo y no volver a hacerlo nunca.
Si solo preguntamos “¿quién está conectado con quién?” perdemos el orden de los eventos — y en lavado de activas el orden es la evidencia: el dinero sale de A, llega a B, se fracciona en C.
Un chronnet (chronological network) transforma una lista de eventos con lugar y fecha en una red donde una arista significa “después de operar aquí, la red operó allá”.
Cada evento \(e_a\) tiene su ubicación \(l_a = (x_a, y_a)\) y su instante \(t_a\), ordenados \(t_1 \le t_2 \le \dots \le t_{N_E}\).
Eventos aquí: las transacciones reportadas (las filas del dataset). Ubicaciones: las sedes de los bancos.
Se construye con transacciones, no con formularios SAR: un SAR agrupa muchas transacciones con rango de fechas propio (begin_date–end_date) — 534 eventos con fechas imprecisas no alcanzan la resolución de un día.
Cada fila tiene fecha exacta: por eso el conteo de eventos es 9.002 (dos por fila: originador y beneficiario) y no 534.
2. Paso 1 — Los vértices: las entidades financieras
El vértice es cada banco, posicionado en las coordenadas de su sede (data/bancos_geo.csv).
Solo los bancos que aparecen en al menos un reporte se convierten en vértices.
# A tibble: 5 × 6
bank_id bank_name country iso lat lon
<chr> <chr> <chr> <chr> <dbl> <dbl>
1 banco-de-credito-del-peru Banco De Credito Del Pe… United… USA -12.0 -77.0
2 banco-de-comercio Banco De Comercio Peru PER -12.0 -77.0
3 ukranian-post-international Ukranian Post Internati… United… USA 40.7 -74.0
4 rietumu-banka Rietumu Banka Latvia LVA 56.9 24.1
5 city-national-bank City National Bank United… USA 40.7 -74.0
select() elige columnas; head() muestra las primeras filas.
El identificador estable es bank_id (slug en minúsculas), no el nombre — los nombres vienen con variantes.
3. Paso 2 — Los eventos: cada reporte genera dos
Cada fila del dataset es un flujo originador → beneficiario.
Lo convertimos en dos eventos: uno para el banco que envía y otro para el que recibe.
# A tibble: 5 × 3
t node rol
<date> <chr> <chr>
1 2000-02-08 banco-de-credito-del-peru origen
2 2000-02-08 banco-de-comercio-lima-peru-per destino
3 2000-02-29 banco-de-credito-del-peru origen
4 2000-02-29 banco-de-comercio-lima-peru-per destino
5 2000-03-07 banco-de-credito-del-peru origen
bind_rows() apila las dos tablas; transmute() crea columnas nuevas; arrange(t) ordena cronológicamente.
El orden temporal es el corazón del chronnet.
4. Paso 3 — Ventanas de tiempo: solo días activos
El período se divide en ventanas de \(\Delta t = 1\) día, y solo entran las ventanas activas (días con al menos un evento).
Si entre el 3 y el 10 de agosto no hubo reportes, el chronnet une directamente el 3 con el 10: un salto conecta dos días activos consecutivos, no dos días calendario.
Los huecos sin datos no rompen la secuencia.
Sea \(n_m^{(k)}\) el número de eventos de la entidad \(m\) en la ventana activa \(k\).
Code
conteo_ventanas <- eventos_dual %>%mutate(ventana =as.Date(t)) %>%count(ventana, node, name ="n") %>%arrange(ventana)cat("Ventanas activas (días con al menos un evento):", n_distinct(conteo_ventanas$ventana), "\n")
Ventanas activas (días con al menos un evento): 1762
# Función completa en R/03_chronnet.R (pipeline)chronnet_create <-function(dataset, self_loops =TRUE, mode ="directed", dt_days =1) { d <- dataset %>%mutate(ventana =floor(as.numeric(difftime(t, min(t, na.rm =TRUE),units ="days")) / dt_days)) %>%count(ventana, node, name ="n") %>%arrange(ventana) ventanas <-sort(unique(d$ventana)) pesos <-list()for (k inseq_len(length(ventanas) -1)) { a <- d %>%filter(ventana == ventanas[k]) %>%transmute(from = node, n_i = n) b <- d %>%filter(ventana == ventanas[k +1]) %>%transmute(to = node, n_j = n) par <-crossing(a, b) # parejas: entidades del día k × entidades del día k+1if (!self_loops) par <- par %>%filter(from != to) pesos[[k]] <- par %>%mutate(w = n_i * n_j) %>%select(from, to, w) } el <-bind_rows(pesos) %>%group_by(from, to) %>%summarise(weight =sum(w), .groups ="drop") g <-graph_from_data_frame(el, directed = (mode =="directed"))attr(g, "Nk") <- d %>%group_by(ventana) %>%summarise(N =sum(n), .groups ="drop") %>%pull(N) g}
crossing(a, b) produce el producto cartesiano “quienes actuaron hoy” × “quienes actuaron mañana”.
Multiplicar n_i * n_j y sumar implementa la fórmula de \(w_{ij}\).
6. ¿Dirigido o no dirigido?
El chronnet es dirigido: la arista \(i \to j\) significa que la actividad de \(j\)sigue a la de \(i\), y no al revés.
La dirección es la señal temporal — no indica flujo de dinero.
En los módulos 3–4 algunas métricas (comunidades, transitividad, caminos) usan la versión no dirigida (colapsando A→B y B→A en una sola conexión) porque los algoritmos lo requieren — lo diremos siempre que ocurra.
7. Chequeo de reproducibilidad
La identidad contable fija el peso total desde los conteos de eventos solos.
Un error de agregación se detecta numéricamente, no a ojo.
Code
# Construcción inline (la función chronnet_create se definió arriba en este módulo;# el pipeline equivalente vive en R/03_chronnet.R)eventos_dual <-bind_rows( tx %>%transmute(t = fecha, node = originator_bank_id, rol ="origen"), tx %>%transmute(t = fecha, node = beneficiary_bank_id, rol ="destino")) %>%arrange(t)chronnet <-chronnet_create(eventos_dual, self_loops =TRUE, mode ="directed", dt_days =1)Nk <-attr(chronnet, "Nk")identidad <-sum(Nk[-length(Nk)] * Nk[-1])cat("sum(w_ij) =", sum(E(chronnet)$weight), "\n")
El chronnet sale conectado de un solo bloque: su regla une toda entidad activa en un día con toda entidad activa el día siguiente, así que el componente gigante (LCC) absorbe toda la red.
Lo comprobamos: cuántos componentes hay y de qué tamaño:
Code
comp_c <-components(chronnet)tibble(componente =seq_len(comp_c$no),n_nodos =sort(comp_c$csize, decreasing =TRUE)) %>%mutate(acumulado =cumsum(n_nodos) /sum(n_nodos)) %>%slice_head(n =10) %>% knitr::kable(caption ="Los 10 componentes más grandes del chronnet",col.names =c("Ranking", "Nº de nodos", "% acumulado de nodos"))
Los 10 componentes más grandes del chronnet
Ranking
Nº de nodos
% acumulado de nodos
1
2275
1
Code
cat("Total:", comp_c$no, "componentes | LCC =", max(comp_c$csize), "nodos (",round(100*max(comp_c$csize) /vcount(chronnet), 1), "% de la red )\n")
Total: 1 componentes | LCC = 2275 nodos ( 100 % de la red )
Aclaración de vocabulario:
LCC (largest connected component, componente gigante): el subgrafo conectado más grande de una red dada.
Aquí el LCC es el chronnet completo: la red de trabajo de todo el taller.
Ojo a la interpretación: esa conectividad total es propia de la construcción — dos bancos quedan unidos si operaron en días consecutivos, aunque jamás hayan transado entre sí.
El dinero no vive en las aristas del chronnet: se calcula desde los reportes y se pega como atributo (monto_total por banco, monto circulado por par), como veremos en los módulos 3 a 6.
9. Por qué no podar aristas
Conservamos TODAS las aristas, incluso las de peso 1.
Los datos criminales son caros de obtener: cada registro puede ser significativo.
Podar aristas débiles eliminaría registros infrecuentes pero cruciales.
Conservar todo mantiene los componentes completos.
Cada punto es un banco; su tamaño = su actividad (en cuántas sucesiones temporales participa).
Cada línea es una arista \(i \to j\); el grosor es \(w_{ij}\) (intensidad de la sucesión).
La mayoría forma una periferia con pocas conexiones; unos pocos nodos centrales concentran las sucesiones.
Lectura criminal
Las aristas fuertes trazan cadenas operativas: qué entidad “activa” a cuál en la secuencia temporal de los reportes SAR.
Si después de cada operación sospechosa en un banco de Dubai sigue sistemáticamente una en un banco de las Islas Vírgenes, el chronnet lo dibuja como una arista gruesa.
El grosor de la línea es la frecuencia de esa sucesión — y la frecuencia es la costumbre, y la costumbre es el patrón que la investigación busca.