О проекте
Команда: BenzTeam
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Рабочий MVP
Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)
Суть
Анализ транзакционного графа для AML: C++17-ядро считает метрики узлов, Python назначает роли, находит кластеры, ранжирует узлы и выдаёт три CSV и локальный UI с карточкой узла.
Как устроено
Python читает parquet и передаёт граф в C++17-ядро через JSON на stdin/stdout. Ядро считает degree, контрагентов, суммы в тиынах (int64), число переводов, pass-through и retention. Затем Python считает PageRank, betweenness (k=300) и Louvain на ненаправленной проекции. Роли (consolidator, distributor, transit, coordinator, terminal, peripheral) задаются прозрачными процентильными правилами. priority_score = PageRank 35% + betweenness 18% + оборот 14% + прочие компоненты; к каждому узлу пишется числовое evidence.
Ключевые решения
- PageRank
- betweenness centrality
- Louvain
- rule-based роли по процентилям
- взвешенный priority_score
- обработка обрыва depth=4
Особенности
Метрики считаются в C++17 с суммами в int64-тиынах, есть Python-fallback. Узлы на 4-м колене без исходящих не считаются terminal. README во многом повторяет стартовый кит, весь проект залит одним коммитом в 17:48.
Стек
Python · C++17 · CMake · nlohmann/json · NetworkX · pandas · Parquet
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-1d18544b-benzteam.
README
README.mdСтартовый код — кейс «Граф денег»
Чтобы вы не тратили час из пяти на загрузку parquet и сборку графа.
Быстрый запуск MVP
python -m pip install -r repos/starter/requirements.txt
python repos/starter/starter.py
В консоли обязательно должна появиться строка Metrics engine: C++17 (...graph_core.exe).
Если бинарник отсутствует, приложение завершится с понятной ошибкой и командой сборки.
Сборка C++17-ядра
Из корня проекта в Developer PowerShell for Visual Studio:
cmake -S cpp_core -B cpp_core/build -A x64
cmake --build cpp_core/build --config Release
Готовый бинарник: cpp_core/build/bin/graph_core.exe. В обычном PowerShell на этой
машине cmake может отсутствовать в PATH; тогда откройте папку cpp_core через
File → Open → Folder в Visual Studio и соберите конфигурацию x64-Release.
nlohmann/json v3.11.3 уже сохранён в cpp_core/third_party, сеть для сборки не нужна.
Ядро получает единственный JSON из stdin и пишет единственный JSON в stdout; ошибки
пишутся в stderr. Суммы внутри ядра хранятся как int64_t в тиынах, что сохраняет
дробные KZT без ошибок округления. Плотный индекс узлов хранится в vector, а
gid → index — в unordered_map.
Отдельная smoke-проверка ядра из корня проекта:
'{"nodes":[1,2],"edges":[{"src":1,"dst":2,"sum_tiyn":125050,"n_tx":2}]}' | .\cpp_core\build\bin\graph_core.exe
Ответ должен содержать "engine":"cpp17", для узла 1 out_kzt:1250.5, а для
узла 2 in_kzt:1250.5. Аварийный запуск сохранённой Python-реализации:
python repos/starter/starter.py --python-metrics
Он явно печатает Metrics engine: Python fallback. Контрольная точка исходного MVP
находится в backup_python_mvp_20260923.
Одна команда после установки зависимостей пересчитывает все результаты и открывает UI на
http://127.0.0.1:8000. Введите полный gid: карточка показывает роль, приоритет,
числовое evidence, кластер и входящие/исходящие связи. Только генерация CSV без UI:
python repos/starter/starter.py --no-ui
Результаты находятся в repos/starter/out/: nodes_roles.csv, clusters.csv,
top_nodes.csv. Пути можно переопределить через --data и --out.
Критерии ролей MVP
Роли задаются прозрачными правилами по направленному графу. Пороговые процентили считаются на текущей выгрузке, поэтому не зависят от абсолютного масштаба сумм:
consolidator: высокий входной degree (от 75-го процентиля), минимум два источника, входных связей заметно больше исходящих;distributor: высокий выходной degree (от 75-го процентиля), минимум три получателя, исходных связей заметно больше входящих;transit: есть вход и выход, отношениеout_kzt/in_kztлежит в диапазоне 0.65–1.35;coordinator: есть вход и выход, высокое направленное betweenness либо одновременно высокие входной и выходной degree;terminal: есть вход, нет выхода, но только если узел не обрезан глубиной 4;peripheral: изолированный, слабоструктурный или обрезанный на depth=4 узел.
role_score — уверенность правила. Итоговый priority_score ограничивается диапазоном
[0, 1] и состоит из: PageRank 35%, betweenness 18%, оборот 14%, структурная
значимость 8%, значимость роли 10%, близость к seed 8% и значимость кластера 7%.
Все величины нормализованы процентилями или непосредственно лежат в [0, 1].
evidence — человекочитаемая гипотеза с числами, а не утверждение о нарушении.
Кластеры — Louvain на ненаправленной проекции графа (только для community detection;
роли и денежные метрики сохраняют направление). Seed без рёбер также попадают в CSV
отдельными кластерами. Расчёт детерминирован (seed=42).
Пайплайн обрабатывает все узлы и формирует заполненные роли, кластеры и приоритеты.
Что уже посчитано
| Колонка | Смысл |
|---|---|
in_deg / out_deg | от скольких разных клиентов получил / скольким разным отправил |
in_kzt / out_kzt | сколько получил / отправил в тенге внутри графа |
in_tx / out_tx | количество переводов |
pagerank | взвешенный по сумме PageRank |
pass_through | out_kzt / in_kzt — доля полученного, ушедшая дальше |
truncated_by_depth | узел на 4-м колене без исходящих |
depth, is_seed | из nodes.parquet |
Четыре ловушки, на которых ломаются наивные решения
1. out_deg == 0 не значит «деньги осели».
444 узла лежат на 4-м колене — обход просто закончился на них. Настоящий ли это конечный получатель или у него есть переводы дальше, которых нет в выгрузке, — по этим данным напрямую не видно. Придумайте, как их различить, и объясните логику.
2. У seed-клиентов in_kzt занижен.
Граф собран от seed по исходящим. Деньги, которые seed получил извне выборки, в данных отсутствуют. Поэтому pass_through у них бывает 20 и больше — это не аномалия, это устройство выгрузки. Не стройте роль на «сколько получил» для seed вслепую.
3. Суммы и количество переводов — разные сигналы.
Один перевод на 4 млн и сорок переводов по 100 тыс. дают одинаковый in_kzt, но означают разное поведение. Смотрите обе величины.
4. Граф направленный и взвешенный.
nx.degree_centrality на неориентированной проекции сотрёт всю суть кейса — направление движения денег. Если используете ненаправленные методы (например, кластеризацию), оговорите это явно.
Требования к выгрузкам
Схема зафиксирована в ТЗ, жюри проверяет её механически:
nodes_roles.csv— ровно 2 248 строк, все колонки заполнены,evidenceсодержит числа, а не «высокий скор»;clusters.csv— строка на кластер, включая гипотезу о его назначении;top_nodes.csv— не менее 20 строк, отсортированных по приоритету.
Лишние колонки добавлять можно, обязательные удалять — нет.
Подсказки по инструментам
import networkx as nx
nx.pagerank(G, weight="sum_kzt") # влияние узла
nx.hits(G) # хабы и авторитеты — разделяет источники и сборщиков
nx.betweenness_centrality(G, weight=...) # посредничество: кто «между»
nx.community.louvain_communities(UG) # кластеры (на неориентированной проекции)
nx.simple_cycles(G, length_bound=6) # возвратные потоки
nx.weakly_connected_components(G) # изолированные фрагменты сети
transactions.parquet используется для проверки агрегации рёбер и расчёта числа
активных дней узла. Временные лаги и сложные последовательности переводов в MVP не
моделируются.
Схема решения
edges.parquet + nodes.parquet + transactions.parquet
│
▼
Python: чтение и проверки
│ JSON через stdin
▼
C++17: degree, контрагенты, суммы, tx,
pass-through и retention
│ JSON через stdout
▼
Python analytics: PageRank, betweenness,
Louvain, роли, evidence и priority_score
│
┌─────────┴──────────┐
▼ ▼
три CSV-файла локальный web UI
Ограничения данных и интерпретации
- Выгрузка построена от seed только по исходящим переводам до depth=4, поэтому это наблюдаемая часть сети, а не полный банковский граф.
- Переводы менее 5 000 KZT отсутствуют; период ограничен июлем 2026 года.
- Входящие потоки seed неполны, поэтому
in_kztнельзя трактовать как полный оборот. - Depth=4 без исходящих не считается terminal автоматически: такие узлы получают
peripheralи пометку о границе наблюдения. - Роли являются аналитическими гипотезами для проверки, а не выводами о виновности.
- Betweenness приближённый (
k=300), Louvain использует ненаправленную проекцию только для поиска сообществ.
Масштабирование примерно до 1 млн узлов
Текущий JSON и NetworkX рассчитаны на хакатонный набор. Для порядка 1 млн узлов сохраняется C++-модель с плотными индексами, но обмен следует заменить потоковым или бинарным форматом (Arrow/Parquet), граф и метрики полностью перенести в C++, а UI оставить на серверных локальных подграфах. PageRank считать итеративно на CSR-графе, betweenness — выборочно, сообщества — пакетным Louvain/Leiden. Полный граф в браузер не передаётся: поиск загружает только выбранный узел и ограниченное окружение.
Пятиминутная демонстрация
- Запустить
python repos/starter/starter.pyи показать строкуMetrics engine: C++17. - Показать три заполненных CSV и 2 248 строк в
nodes_roles.csv. - В UI найти distributor, consolidator и coordinator, разобрать числовое evidence.
- Показать стрелки переводов, цвета ролей, cluster_id, фильтры и top-20.
- Объяснить ловушки seed/depth=4 и завершить архитектурной схемой выше.
Что оценивается выше всего
Не количество метрик, а то, сможете ли вы за минуту объяснить жюри, почему конкретный gid получил свою роль. Считайте столько, сколько сможете защитить.
Авторы коммитов
История коммитов
- 2026-09-23 17:48Разработал систему анализа транзакционного графа на C++17 и Python, которая определяет роли участников, выявляет финансовые кластеры и ранжирует узлы для AML-проверки.StartBenz
Структура репозитория (15 файлов)
.gitignore AGENTS.md README.md cpp_core/CMakeLists.txt cpp_core/src/main.cpp cpp_core/third_party/nlohmann/json.hpp repos/data/edges.parquet repos/data/nodes.parquet repos/data/transactions.parquet repos/starter/out/clusters.csv repos/starter/out/nodes_roles.csv repos/starter/out/top_nodes.csv repos/starter/requirements.txt repos/starter/starter.py run.ps1
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.