О проекте
Команда: 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.
Есть два сценария:
- Через интерфейс: выбрать
edges.parquet,nodes.parquet,transactions.parquetи нажать «Загрузить и пересчитать». - Через папку: положить три файла в
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-элементов.
Ограничения данных и подхода
- Граф обрывается на
depth=4, поэтому отсутствие исходящих на границе не доказывает, что деньги действительно «осели». - Видны только исходящие переводы в пределах выборки; полный баланс клиента неизвестен.
- У seed-клиентов входящие суммы занижены, поэтому
pass_throughдля них не используется для ролиtransit. - Переводы ниже порога выгрузки не видны.
- Нет ФИО, ИИН, возраста, остатков и других клиентских атрибутов.
- Нет ground truth по ролям: пороги и веса — объяснимые эвристики, которые должны рассматриваться как гипотезы.
- Max-нормализация priority_score чувствительна к экстремальным значениям.
- Временные признаки из
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-ассистент аналитика.
Их можно добавить как развитие проекта.
Авторы коммитов
История коммитов
- 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 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.