Fundamentos de la detección de anomalías por proximidad
En el análisis de datos financieros, identificar comportamientos atípicos es crucial para mitigar riesgos. El uso de algoritmos basados en la proximidad, específicamente la construcción de grafos de k-vecinos más cercanos (k-NN), ofrece un marco robusto para localizar transacciones que se desvían del patrón habitual. A diferencia de los sistemas de recomendación que buscan similitud, la detección de fraude se centra en la distancia: un punto de datos aislado o significativamente alejado de sus vecinos suele representar una anomalía.
La relación fundamental entre similitud ($S$) y distancia ($D$) se define como $D = 1 - S$. Cuando dos registros son idénticos, su distancia es 0; cuando son totalmente opuestos, la distancia es 1. El proceso técnico para implementar esta solución consiste en:
- Vectorización de los atributos de la transacción.
- Cálculo de la matriz de distancias entre nodos.
- Selección de los $k$ vecinos con menor distancia para cada nodo.
- Persistencia de estas relaciones en una base de datos de grafos.
Optimización mediante búsqueda de vecinos más cercanos aproximados (ANN)
Calcular el grafo k-NN exacto requiere una complejidad computacional de $O(N^2)$, lo cual es inviable para volúmenes masivos de datos. Para resolver esto, se utilizan algoritmos de Approximate Nearest Neighbors (ANN). Una de las implementaciones más eficientes es HNSW (Hierarchical Navigable Small World), que permite encontrar vecinos cercanos con una precisión superior al 80% en una fracción del tiempo original.
A continuación, se presenta una lógica de implementación en Python para procesar estas distancias y almacenarlas en Neo4j:
def generar_grafo_proximidad(self, num_vecinos, usar_exacto, metrica, nombre_relacion):
inicio_proceso = time.time()
vectores, ids_transaccion = self.obtener_vectores_transaccionales()
if usar_exacto:
vecinos_ids, distancias = self.calcular_knn_exacto(vectores, ids_transaccion, num_vecinos, metrica)
else:
vecinos_ids, distancias = self.calcular_ann_hnsw(vectores, ids_transaccion, num_vecinos, metrica)
print(f"Tiempo de cómputo: {time.time() - inicio_proceso}s")
self.persistir_vecinos_en_grafo(ids_transaccion, vecinos_ids, distancias, nombre_relacion)
def persistir_vecinos_en_grafo(self, ids_origen, matriz_vecinos, matriz_distancias, etiqueta_rel):
query_limpieza = f"""
MATCH (t:Transaccion)-[r:{etiqueta_rel}]->()
WHERE t.idTransaccion = $id_ref
DELETE r
"""
query_vinculo = f"""
MATCH (origen:Transaccion)
WHERE origen.idTransaccion = $id_ref
UNWIND keys($mapa_vecinos) as id_destino
MATCH (destino:Transaccion)
WHERE destino.idTransaccion = toInteger(id_destino) AND destino.idTransaccion <> $id_ref
MERGE (origen)-[:{etiqueta_rel} {{distancia: $mapa_vecinos[id_destino]}}]->(destino)
"""
with self._driver.session() as sesion:
for indice, id_ref in enumerate(ids_origen):
lista_vecinos = matriz_vecinos[indice]
lista_dist = matriz_distancias[indice]
mapa_knn = {str(v): float(d) for v, d in zip(lista_vecinos, lista_dist)}
tx = sesion.begin_transaction()
tx.run(query_limpieza, {"id_ref": id_ref})
tx.run(query_vinculo, {"id_ref": id_ref, "mapa_vecinos": mapa_knn})
tx.commit()
if (indice + 1) % 1000 == 0:
print(f"{indice + 1} transacciones procesadas")
Métricas de distancia y evaluación
La elección de la métrica de distancia impacta directamente en la capacidad de detección. Las más comunes son:
| Métrica | Descripción Técnica |
|---|---|
| L2 (Euclidiana) | Calcula la línea recta entre dos puntos en el espacio n-dimensional. Útil para datos normalizados. |
| Mahalanobis | Mide la distancia entre un punto y una distribución. Es excelente para detectar anomalías multivariantes al considerar la correlación entre variables. |
Para validar si el grafo ANN es representativo del exacto, se puede ejecutar la siguiente consulta en Cypher que compara la coincidencia de los vecinos:
MATCH (t:Transaccion)
MATCH (t)-[r:DISTANCIA_ANN]->(vecino_ann:Transaccion)
WITH t, collect(vecino_ann.idTransaccion) AS lista_ann
MATCH (t)-[r2:DISTANCIA_EXACTA]->(vecino_ex:Transaccion)
WITH t.idTransaccion AS id, lista_ann, collect(vecino_ex.idTransaccion) AS lista_exacta
WHERE lista_ann = lista_exacta
RETURN count(*) AS coincidencias_perfectas
Cálculo del Scoring de Fraude
Una vez construido el grafo, se asigna una puntuación de anomalía a cada transacción. Existen dos enfoques principales:
- Puntuación k-NN Exacta: Se toma la distancia al k-ésimo vecino más cercano como el score de anomalía.
- Puntuación k-NN Promedio: Se calcula la media de las distancias a los $k$ vecinos más cercanos.
En escenarios no supervisados, el promedio suele ser más estable frente al ruido. La siguiente consulta permite identificar los nodos con mayor probabilidad de ser fraude basándose en la distancia mínima a sus vecinos:
MATCH (t:Transaccion)-[r:DISTANCIA_PROXIMIDAD]->(:Transaccion)
WITH t, min(r.distancia) AS riesgo, t.esFraude AS etiqueta
ORDER BY riesgo DESC
LIMIT 1000
WHERE etiqueta = 1
RETURN count(DISTINCT t) AS fraudes_detectados
Ajuste de parámetros y balanceo
El rendimiento de los algoritmos de proximidad es sensible al desequilibrio de las clases (donde el fraude es una minoría extrema). Para evaluar la eficacia del modelo sin sesgos por volumen, es recomendable crear un conjunto de prueba balanceado:
// Etiquetar una muestra balanceada para pruebas
MATCH (t:Transaccion {esFraude: 0})
WITH t, rand() AS aleatorio
ORDER BY aleatorio
LIMIT 492
SET t:MuestraControl;
MATCH (t:Transaccion {esFraude: 1})
SET t:MuestraControl;
Las pruebas experimentales demuestran que incrementar el valor de $k$ y utilizar la distancia de Mahalanobis tiende a mejorar la precisión en la identificación de transacciones fraudulentas, permitiendo establecer umbrales de decisión (Thresholds) para convertir los scores continuos en clasificaciones binarias (Fraude / Legítimo).