AAYMA
RU
Войти

Граф денег

Локальный инструмент AML-анализа: по трём parquet строит направленный взвешенный граф переводов, назначает роли узлам, считает приоритет проверки…

Рабочий MVP
Команда «ZeroClick» 4💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: ZeroClick
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Рабочий MVP

Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)

Суть

Локальный инструмент AML-анализа: по трём parquet строит направленный взвешенный граф переводов, назначает роли узлам, считает приоритет проверки, кластеризует и показывает интерактивную карту.

Как устроено

starter.py считает для каждого узла in/out-степени, обороты, взвешенный PageRank и pass_through, затем назначает роли упорядоченными правилами (coordinator → distributor → consolidator → transit → terminal → peripheral) с учётом обрыва на depth=4. Priority_score складывается из нормированных PageRank, степеней и оборота плюс бонус за роль. Louvain (NetworkX, seed=42) работает на неориентированной проекции. validate_outputs.py проверяет 3 CSV по требованиям ТЗ, а веб-интерфейс на Canvas без CDN показывает граф с фильтрами, TOP-20 и карточками узлов.

Ключевые решения

  • NetworkX
  • PageRank
  • Louvain community detection
  • rule-based role assignment
  • Canvas визуализация

Особенности

Отдельный валидатор соответствия выходных CSV требованиям ТЗ. Узлы на обрыве depth=4 не превращаются в terminal автоматически. В README есть раздел о масштабировании до ~1 млн узлов (Polars/DuckDB, igraph, level-of-detail).

Стек

Python · NetworkX · pandas · HTML/CSS/JS


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-b91ed6e6-zeroclick.

README

README.md

Граф денег — HackAlem AI

Локальный аналитический инструмент для восстановления структуры транзакционной сети: роли узлов, приоритет проверки, кластеры и визуализация направлений движения денег.

Все роли и приоритеты — гипотезы для дальнейшей проверки, а не утверждения о виновности клиента.

Быстрый запуск

Установка зависимостей выполняется один раз:

py -m pip install -r requirements.txt

После этого проект запускается одной командой:

py run.py

Откроется локальный сайт http://127.0.0.1:8000.

Есть два сценария:

  1. Через интерфейс: выбрать edges.parquet, nodes.parquet, transactions.parquet и нажать «Загрузить и пересчитать».
  2. Через папку: положить три файла в data/ и открыть сайт; интерфейс увидит их и позволит запустить пересчёт.

После нажатия кнопки сайт:

  • сохраняет 3 parquet в data/;
  • запускает starter.py;
  • создаёт ровно 3 обязательных CSV в out/;
  • запускает validate_outputs.py;
  • создаёт данные визуализации;
  • показывает время полного пересчёта и укладывается ли оно в лимит 5 минут.

Структура

project/
├── starter.py              аналитический пайплайн
├── clustering.py           Louvain-кластеризация
├── validate_outputs.py     автоматические проверки требований ТЗ
├── build_web_data.py       CSV/parquet → JSON для браузера
├── run.py                  единая точка запуска
├── requirements.txt
├── solution_schema.svg     схема решения
├── DEMO.md                 план 5-минутного демо
├── data/
│   ├── edges.parquet
│   ├── nodes.parquet
│   └── transactions.parquet
├── out/                    только 3 конкурсные CSV
│   ├── nodes_roles.csv
│   ├── clusters.csv
│   └── top_nodes.csv
├── runtime/
│   └── nodes_metrics.parquet # внутренние метрики интерфейса
└── web/
    ├── index.html
    ├── graph_data.json      создаётся автоматически
    └── assets/
        ├── styles.css
        └── app.js

Входные данные

  • nodes.parquet — узлы: gid, depth, is_seed;
  • edges.parquet — агрегированные связи: src, dst, sum_kzt, n_tx, depth;
  • transactions.parquet — отдельные переводы с датами.

Граф направленный и взвешенный: направление — src → dst, вес — sum_kzt.

Базовые метрики

Для каждого узла считаются:

  • in_deg — число разных плательщиков;
  • out_deg — число разных получателей;
  • in_kzt / out_kzt — входящий / исходящий оборот внутри наблюдаемого графа;
  • in_tx / out_tx — число переводов;
  • pagerank — PageRank с весом sum_kzt;
  • pass_through = out_kzt / in_kzt;
  • truncated_by_depth — depth=4 и out_deg=0.

Формальные правила ролей

Правила применяются по порядку сверху вниз: первая подошедшая роль становится основной.

1. coordinator

pagerank >= 95-й перцентиль PageRank
AND in_deg > 0
AND out_deg > 0

role_score = 0.95.

2. distributor

Если узел не стал coordinator:

out_deg >= max(10, 90-й перцентиль out_deg)

role_score = min(1, 0.7 + out_deg / threshold * 0.1).

3. consolidator

Если предыдущие правила не сработали:

in_deg >= max(3, 90-й перцентиль in_deg)

role_score = min(1, 0.7 + in_deg / threshold * 0.1).

4. transit

is_seed = False
AND in_deg > 0
AND out_deg > 0
AND 0.8 <= pass_through <= 1.2

role_score = max(0.6, 1 - |pass_through - 1|).

Seed исключены из этого правила, потому что их входящие суммы в выгрузке неполны.

5. terminal

out_deg = 0
AND in_deg > 0
AND truncated_by_depth = False

role_score = 0.80.

Узлы на depth=4 с нулевыми исходящими не считаются terminal автоматически: на них мог просто закончиться обход графа.

6. peripheral

Все остальные узлы.

role_score = 0.50.

Priority score

Сначала показатели нормализуются делением на максимум по графу:

PageRank_norm
in_degree_norm
out_degree_norm
money_norm = (in_kzt + out_kzt) / max(in_kzt + out_kzt)

Базовый приоритет:

0.35 * PageRank_norm
+ 0.25 * in_degree_norm
+ 0.20 * out_degree_norm
+ 0.20 * money_norm

Затем добавляется объяснимый бонус за структурную роль:

coordinator  +0.15
consolidator +0.10
distributor  +0.08
transit      +0.05
terminal     +0.02
peripheral   +0.00

Итог ограничивается диапазоном 0..1.

В top_nodes.csv поле why содержит сам priority_score, роль, входящую и исходящую степень, PageRank и текстовое объяснение роли.

Кластеризация

Используется Louvain community detection:

networkx.community.louvain_communities

Поскольку Louvain применяется к неориентированной сети, для кластеризации строится G.to_undirected(). Вес sum_kzt сохраняется.

seed=42 фиксирован для воспроизводимости.

Узлы без рёбер не теряются: каждый такой узел получает собственный cluster_id.

clusters.csv

  • cluster_id — номер кластера;
  • n_nodes — число узлов;
  • n_seed — число seed;
  • sum_kzt_internal — внутренний оборот;
  • top_gids — до 5 узлов с максимальным PageRank;
  • hypothesis — структурная гипотеза по размеру, seed и доле внутренних связей.

Выходные файлы

nodes_roles.csv

Сохраняется ровно фиксированная схема ТЗ:

gid, role, role_score, cluster_id, priority_score, evidence

Расширенные метрики не добавляются в конкурсную CSV. Для интерфейса они хранятся отдельно в runtime/nodes_metrics.parquet.

clusters.csv

Одна строка на кластер.

top_nodes.csv

Не менее 20 строк:

rank, gid, role, priority_score, why

Интерфейс

Интерфейс — локальный HTML/CSS/JavaScript без внешнего CDN.

В нём есть:

  • поиск по gid;
  • выбранный узел и его связи;
  • стрелки направления src → dst;
  • цвет по роли или кластеру;
  • размер узла по priority_score;
  • карточка узла с evidence;
  • фильтры по роли и кластеру;
  • TOP-20;
  • таблица кластеров и просмотр узлов кластера;
  • zoom и перемещение графа.

Canvas используется для производительной отрисовки без тысяч DOM-элементов.

Ограничения данных и подхода

  1. Граф обрывается на depth=4, поэтому отсутствие исходящих на границе не доказывает, что деньги действительно «осели».
  2. Видны только исходящие переводы в пределах выборки; полный баланс клиента неизвестен.
  3. У seed-клиентов входящие суммы занижены, поэтому pass_through для них не используется для роли transit.
  4. Переводы ниже порога выгрузки не видны.
  5. Нет ФИО, ИИН, возраста, остатков и других клиентских атрибутов.
  6. Нет ground truth по ролям: пороги и веса — объяснимые эвристики, которые должны рассматриваться как гипотезы.
  7. Max-нормализация priority_score чувствительна к экстремальным значениям.
  8. Временные признаки из transactions.parquet пока не входят в основной score.

Масштабирование до ~1 млн узлов

Текущая реализация на NetworkX рассчитана на хакатонный граф и не предназначена для миллиона узлов без изменений.

При росте до ~1 млн узлов:

  • хранение и агрегации перевести на Polars/DuckDB или распределённую обработку;
  • NetworkX заменить на igraph, graph-tool, Spark GraphFrames или другой более компактный графовый движок;
  • PageRank и community detection считать пакетно / параллельно;
  • не передавать миллион узлов одним JSON в браузер;
  • интерфейс должен запрашивать через API только выбранный узел, соседей, текущий кластер и агрегированные уровни;
  • для визуализации использовать progressive loading / level-of-detail;
  • вычисленные метрики хранить в колонночном формате и обновлять инкрементально.

Логика ролей и схема выходных CSV при этом могут сохраниться.

Автоматическая проверка

После аналитики run.py вызывает:

py validate_outputs.py

Проверяется:

  • все gid присутствуют;
  • обязательные колонки заполнены;
  • role_score и priority_score находятся в 0..1;
  • evidence непустой и не длиннее 200 символов;
  • у каждого узла есть cluster_id;
  • сумма n_nodes по кластерам равна числу узлов;
  • TOP содержит минимум 20 строк и отсортирован;
  • узлы на обрыве depth=4 не превращены автоматически в terminal.

Что не реализовано из optional

Это не блокирует сдачу:

  • временные паттерны;
  • циклы / возвратные потоки;
  • anomaly detection;
  • анализ устойчивости сети при удалении top-N;
  • LLM/AI-ассистент аналитика.

Их можно добавить как развитие проекта.

Звёзды
⭐ 0
Форки
0
Язык
Python
Последний push
в прошлом месяце
Коммиты
15
Авторы
3
Файлы
20
Ветки
1

Авторы коммитов

Yerema12 10nurisPHP 3Асik 2

История коммитов

  • 2026-09-23 17:53Update 5Yerema12
  • 2026-09-23 17:41Update4Yerema12
  • 2026-09-23 17:31Update3Yerema12
  • 2026-09-23 17:17Update2Yerema12
  • 2026-09-23 17:14Demo.md updateYerema12
  • 2026-09-23 17:06NewUpdateYerema12
  • 2026-09-23 16:44inurisPHP
  • 2026-09-23 16:42Obnova anasYerema12
Все коммиты (15)
  • 2026-09-23 16:37Merge branch 'main' of https://github.com/BAITC-Hacks/hack-b91ed6e6-zeroclicknurisPHP
  • 2026-09-23 16:37interfacenurisPHP
  • 2026-09-23 16:14update README: setup, roles, clusteringАсik
  • 2026-09-23 15:31add clustering module (Louvain), decouple from role/priority_scoreАсik
  • 2026-09-23 15:06Third pushYerema12
  • 2026-09-23 14:49SecondPushYerema12
  • 2026-09-23 13:53FirstPushYerema12
Структура репозитория (20 файлов)
DEMO.md
README.md
build_web_data.py
clustering.py
data/edges.parquet
data/nodes.parquet
data/transactions.parquet
out/clusters.csv
out/nodes_roles.csv
out/top_nodes.csv
requirements.txt
run.py
runtime/nodes_metrics.parquet
solution_schema.svg
starter.py
validate_outputs.py
web/assets/app.js
web/assets/styles.css
web/graph_data.json
web/index.html

Обсуждение 0 комментариев

Войдите, чтобы комментировать

Пока нет комментариев — будьте первым.