AAYMA
RU
Войти

Граф денег

Пайплайн ролей, кластеров и приоритетов для AML-сети с автономным HTML-вьюером на cytoscape, React-фронтендом и AI-ассистентом с 9 графовыми…

Доведённый продукт
Команда «Moggers» 13💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: Moggers
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Доведённый продукт

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

Суть

Пайплайн ролей, кластеров и приоритетов для AML-сети с автономным HTML-вьюером на cytoscape, React-фронтендом и AI-ассистентом с 9 графовыми инструментами.

Как устроено

Parquet → проверка целостности → направленный взвешенный граф → признаки (структура, достижимость от seed, временные паттерны) → Louvain по неориентированной проекции → роли по порогам, привязанным к распределениям датасета (coordinator, consolidator, distributor, transit, terminal, terminal_unverified, peripheral) → взвешенный приоритет. Выгрузки CSV, features.parquet, resilience.csv и самодостаточный viewer.html. AI-ассистент (gpt-4.1-mini) выбирает инструмент (convergence, trace_money, removal_impact, return_flows, data_gaps и др.), числа считает код, валидатор удаляет предложения с несуществующими gid; без ключа работает по шаблону.

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

  • Louvain
  • rule-based roles
  • articulation points
  • cycle/return flow detection
  • removal simulation top-N
  • tool calling
  • GID reference validator
  • cytoscape

Особенности

Расширение словаря ролью terminal_unverified для узлов на обрыве 4-го колена; побайтово воспроизводимые артефакты; однофайловый офлайн-вьюер, к которому чат подключается при запущенном локальном сервере; guardrails против выдуманных gid.

Стек

Python · pandas · NetworkX · cytoscape.js · React · Vite · TypeScript · Playwright

Модели

gpt-4.1-mini


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

README

README.md

Граф денег

Восстановление финансовой структуры организованной группы по транзакционной сети.

Вход — выгрузка исходящих переводов 81 клиента на 4 колена (2 248 узлов, 3 119 рёбер, 4 840 транзакций, июль 2026). Выход — роль каждого узла с обоснованием, кластеры сети и ранжированный список: кого из 2 248 клиентов смотреть первым и почему.

Запуск

Сайт и ИИ-ассистент (Windows, macOS, Linux)

После установки зависимостей pip install -e ".[web]" в окружении проекта и npm ci --prefix frontend выполните python start.py из корня проекта. Скрипт автоматически выбирает Python из .venv под текущую ОС, создаёт недостающие результаты анализа и сборку интерфейса.

Сайт: http://127.0.0.1:8000. Чат доступен через пункт «ИИ-ассистент» или http://127.0.0.1:8000/assistant#tab=chat. Ключ задаётся в локальном .env. Сервер предназначен для одного пользователя и слушает только localhost. После изменений frontend пересоберите его: npm run build --prefix frontend.

Пайплайн без сайта

./run.sh

Скрипт создаёт локальный .venv, ставит зависимости и считает всё от сырых .parquet до выгрузок в out/. Системный Python не трогается: на многих дистрибутивах он защищён (PEP 668), и установка пакетов туда запрещена.

Сам пересчёт — около 3 секунд на обычном ноутбуке (лимит по ТЗ — 5 минут); первый запуск дополнительно тратит время на создание окружения. Ни GPU, ни облака, ни внешних сервисов. Нужен Python ≥ 3.10 с модулем venv (пакет python3-venv в Debian и Ubuntu).

Вручную, если окружение уже есть:

PYTHONPATH=src python3 -m moneygraph.pipeline --data data --out out

Что на выходе

ФайлСтрокСодержание
out/nodes_roles.csv2 248роль, уверенность, кластер, приоритет и обоснование по каждому узлу
out/clusters.csv91размер, число seed, внутренний оборот и гипотеза по каждому кластеру
out/top_nodes.csv25ранжированный список на проверку с обоснованием
out/resilience.csv6что станет с сетью при изъятии топ-N узлов
out/viewer.html—экран просмотра: схема сети, роли, поиск по gid (один файл, офлайн)
out/features.parquet2 248все посчитанные признаки — для интерфейса и перепроверки
out/summary.json—сводка прогона: время, распределение ролей, факты о датасете

nodes_roles.csv содержит обязательные колонки схемы ТЗ (gid, role, role_score, cluster_id, priority_score, evidence) и все признаки, на которых построена роль — любую строку можно перепроверить, не заглядывая в код.

Экран просмотра

xdg-open out/viewer.html      # или просто открыть файл двойным кликом

Один самодостаточный HTML: cytoscape вшит внутрь, данные встроены в страницу. Ни сервера, ни npm, ни интернета — файл открывается в любом браузере.

Что на экране:

  • схема сети со стрелками направления денег и толщиной ребра по сумме;
  • роль узла кодируется цветом и формой — цвет не единственный канал;
  • поиск по gid: ввод номера сразу показывает узел и его окружение на 2 колена, все его связи подсвечиваются, суммы подписываются;
  • фильтры по роли и кластеру, список «кого смотреть первым»;
  • карточка узла: обоснование роли, метрики, возвратные потоки, точка сочленения, крупнейшие входящие и исходящие переводы — по ним можно кликать и идти по цепочке;
  • панель устойчивости: что станет с сетью при изъятии топ-N узлов.

Для демо есть прямая ссылка на узел: out/viewer.html#gid=100000003684369100.

Схема рисует не все 2 248 узлов сразу — это нечитаемо. По умолчанию показывается топ приоритета с окружением (около 450 узлов), остальное открывается через поиск, фильтры или переключатель охвата.

AI-ассистент аналитика

Два способа: в терминале или прямо в интерфейсе.

moneygraph-ask "кто собирает деньги с 100000003016635100 и 100000000331309100?"
moneygraph-ask                # диалоговый режим
moneygraph-serve              # схема сети и чат в одном окне -> http://127.0.0.1:8765

Для режима с LLM: pip install -e ".[assistant]". Скопируйте .env.example в .env в корне проекта и задайте настоящий OPENAI_API_KEY. Промокод на кредиты не является API-ключом. .env исключён из Git и загружается только на сервере; переменные окружения имеют приоритет. Модель по умолчанию — gpt-4.1-mini, изменить её можно через MONEYGRAPH_LLM_MODEL.

Без ключа ассистент тоже работает: разбирает намерение вопроса, вызывает те же инструменты и отвечает по шаблону. Пайплайн и выгрузки от ассистента не зависят и считаются полностью локально — интернет нужен только ему, и ТЗ это допускает.

Ассистент в интерфейсе, без потери самодостаточности

moneygraph-serve поднимает локальный эндпоинт на 127.0.0.1 (только стандартная библиотека, новых зависимостей нет). Страница определяет его сама:

Как открылиЧто получаете
out/viewer.html двойным кликомсхема, роли, поиск по gid — как обычно, чат показывает подсказку
out/viewer.html при запущенном серверето же плюс работающий чат
http://127.0.0.1:8765то же, но одним origin — CORS не нужен вовсе

Файл остаётся самодостаточным. Must-have 5 не требует ничего запускать: жюри открывает файл двойным кликом, и всё работает. Чат — надстройка, которая включается, если её есть чем обслужить.

Инструменты при этом остаются в Python в единственном экземпляре: переписывать их на JavaScript означало бы два кода, считающих одни и те же числа, — и расхождение между ответом ассистента и выгрузкой было бы вопросом времени. Ключ живёт в переменной окружения сервера и в браузер не попадает; страница никогда не обращается к внешнему API сама.

Номера счетов в ответе кликабельны: клик открывает узел на схеме с его окружением. Для репетиции демо есть прямые ссылки: #tab=chat и #ask=<вопрос>.

Что он умеет

Ассистент отвечает на вопросы, на которые сама выгрузка ответить не может, — те, что требуют обхода графа:

ИнструментВопрос аналитика
convergence«кто собирает деньги с этих пятерых?» — узлы, куда сходятся потоки сразу от нескольких счетов
trace_money«как деньги дошли от A до B?» — маршруты с суммами на каждом шаге и узким местом
removal_impact«что будет, если заблокировать эти счета?» — сколько сети отрежется от seed
return_flows«возвращаются ли деньги обратно?» — встречные переводы и замкнутые цепочки
data_gaps«чего мне не хватает и что запросить дальше?»
node_profile«почему у этого gid такая роль?»
counterparties«кто ему платит и кому платит он?»
cluster_profile«что за кластер №N?»
find_nodes«покажи координаторов с приоритетом выше 0.5»

Вопрос про схождение потоков — тот самый, ради которого кейс и заведён: вручную аналитик восстанавливает его обходом на четыре колена, здесь это один запрос.

Guardrails

Числа считает код, не модель. Модель выбирает инструмент, читает его JSON и формулирует ответ. Ни одного вычисления она не делает сама.

Каждое утверждение — со ссылкой на gid. Валидатор удаляет предложения, где упомянут несуществующий узел, и помечает ответ. Если после проверки не осталось текста, ответ собирается детерминированно.

Ограничения данных в системном промпте. Ассистент знает про обрыв на 4-м колене, занижённые входящие у seed и порог 5 000 ₸ — и обязан учитывать их в ответах.

Формулировки — гипотезы. «Признаки консолидации», а не «отмывает деньги».

Сбой внешнего API не ломает демо: ответ собирается детерминированно, в предупреждениях остаётся причина.

Как устроен пайплайн

data/*.parquet
   -> loading.py    загрузка и проверки целостности
   -> graph.py      направленный взвешенный граф (все 2 248 узлов, включая 19 без рёбер)
   -> features.py   структура + достижимость от seed + временны́е паттерны
   -> clusters.py   Louvain по неориентированной проекции
   -> roles.py      правила с порогами -> роль, уверенность, evidence
   -> priority.py   взвешенный приоритет -> кого смотреть первым
   -> viewer.py     автономный HTML со схемой сети
   -> out/*.csv + out/viewer.html

agent/ читает готовые out/features.parquet и граф: отдельный слой поверх пайплайна

Критерии ролей

Правила проверяются в порядке приоритета, первое сработавшее определяет роль. Все пороги собраны в src/moneygraph/config.py и привязаны к распределениям в этом датасете, а не взяты произвольно.

РольПравилоПорог и почему именно такой
coordinatorсобирает и перераспределяет: in_deg ≥ 5 и out_deg ≥ 10 и in_kzt ≥ 0.3 × out_kztтребование баланса потоков отсекает узлы, которые не собирают, а вливают средства извне выгрузки
coordinatorлибо структурно: посредничество в верхнем 1%, деньги доходят от ≥ 3 seed, связывает ≥ 2 кластераузел, через который проходят пути от нескольких независимых seed — это структура, а не совпадение
consolidatorin_deg ≥ 5 и in_deg ≥ 2 × out_deg1 760 узлов имеют ровно одного плательщика; порог 5 отсекает верхние 2.3% (51 узел) — это уже не бытовые переводы
distributorout_deg ≥ 10 и out_deg ≥ 2 × in_deg1 554 узла не отдают вовсе; 10+ получателей — у 64 узлов (2.8%)
transitесть вход и выход, коэффициент пропуска out_kzt/in_kzt в 0.7–1.3, узел не seedокно вокруг единицы = «принял и отдал столько же»; seed исключены, у них входящие занижены устройством выгрузки
terminaldepth ≤ 3 и удержано ≥ 80% полученногоглубина ≤ 3 обязательна: на 4-м колене отсутствие исходящих — артефакт обхода
terminal_unverifieddepth = 4, исходящих нет, но получено заметно (≥ 2 плательщика или сумма выше медианы)расширение словаря, см. раздел ниже
peripheralни один порог не пройден—

role_score (0–1) — уверенность в роли. Она снижается, когда вывод опирается на оборванную выгрузку, и масштабируется объёмом: транзит на одной операции у порога 5 000 ₸ получает низкую уверенность, а не единицу.

Расширение словаря: terminal_unverified

ТЗ допускает расширение словаря при условии документирования. Мы ввели одну дополнительную роль, потому что без неё решение обязано врать.

444 узла лежат на 4-м колене и не имеют исходящих. Наивное правило «out_deg = 0 ⇒ конечный получатель» назвало бы их стоками — но обход просто закончился на них, а не деньги. Из этих 444 у 183 узлов входящий профиль действительно похож на сток; они получают роль terminal_unverified с прямой формулировкой в evidence:

«Принял 300 тыс ₸ от 1 плательщика, исходящих нет — но узел на 4-м колене, где обход остановлен. Отличить конечного получателя от необследованного нельзя без выгрузки на 5-е колено.»

Настоящий terminal присваивается только при depth ≤ 3, где отсутствие исходящих — это поведение узла, а не граница выгрузки. Таких узлов 1 185.

Приоритет: кого смотреть первым

priority = ( 0.28·роль + 0.20·seed_sources + 0.16·betweenness + 0.14·оборот
           + 0.13·структура + 0.09·pagerank + 0.05·охват ) × (0.6 + 0.4·role_score)

Компонента «структура» — возвратные потоки и точки сочленения, см. раздел ниже.

Компоненты нормируются на 0–1 по выборке. Множитель по role_score опускает в списке узлы, у которых роль присвоена на слабом основании.

seed_sources — от скольких разных seed-клиентов деньги доходят до узла. Это самый содержательный признак в задаче: совпадение нескольких независимых seed на одном узле означает структуру, а не случайность.

В top_nodes.csv колонка why называет два главных вклада в ранг — чтобы позицию в списке можно было оспорить по числам.

Возвратные потоки и устойчивость сети

Два дополнительных сигнала, которых нет в степенях и суммах.

Деньги возвращаются отправителю

Ищем простые циклы длиной до 6 (networkx.simple_cycles). В графе их 1 541, и в них участвуют 303 узла. Самый явный случай — цикл длины 2: счета переводят друг другу напрямую. Таких узлов 266.

Признак попадает в выгрузку колонками min_cycle_len, cycle_count, reciprocal_partners и в evidence: «Деньги возвращаются отправителю: 6 встречных счетов». В приоритете прямой возврат весит больше длинной петли — цепочка из шести узлов может оказаться совпадением, взаимные переводы вряд ли.

23 из 25 узлов топ-листа имеют возвратные потоки — независимое подтверждение того, что приоритет отобрал именно инфраструктуру, а не случайные крупные счета.

Что будет, если изъять топ-N

out/resilience.csv отвечает на вопрос, была ли изъята инфраструктура или только листья. Главная метрика — сколько узлов остаётся достижимо от seed-клиентов.

ИзъятоОтрезано от seedКомпонент сталоОборот затронут
топ-14%936.6%
топ-516%20715.0%
топ-1034%31521.8%
топ-2052%41425.0%
топ-5063%65036.6%

Десять узлов — это 0.4% сети. Их изъятие отрезает от известной базы треть всех клиентов и дробит сеть с 35 связных компонент до 315. Это и есть ответ на вопрос кейса «кого смотреть первым».

Отдельно считаются точки сочленения (is_articulation, 527 узлов) — те, чьё изъятие разрывает свою компоненту. Считается за один проход по неориентированной проекции, в отличие от перебора «удали и посмотри».

Как учтены ограничения датасета

ОграничениеЧто сделано
Обход оборван на 4-м колене (444 узла)terminal только при depth ≤ 3; отдельная роль terminal_unverified
Видны только исходящие переводыpass_through не применяется к seed; узлы, отдающие втрое больше видимых входящих, помечаются в evidence как «источник средств вне выгрузки»
У seed входящие заниженыроль transit для seed не назначается вовсе
Порог 5 000 ₸дробление ниже порога невидимо — заявлено в ограничениях, на этом выводы не строятся
19 seed без единого ребрапопадают в выгрузку как peripheral с явным указанием причины
16 слабосвязных компоненткластеризация Louvain, 91 сообщество; 8 из них содержат более одного seed
Louvain теряет направлениекластеры отвечают только на вопрос «кто с кем связан»; все роли считаются по направленному графу

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

Это гипотезы для проверки, а не выводы о виновности. Формулировки в evidence намеренно даны как «признаки консолидации», «признаки транзита».

Размеченных ролей в данных нет, поэтому точность в привычном смысле не измеряется — критерии защищаются обоснованностью правил, а не accuracy.

Роли назначаются по структуре и суммам за один месяц. Внутрибанковский периметр, порог 5 000 ₸ и глубина 4 колена означают, что часть сети принципиально невидима: у 354 узлов исходящие превышают видимые входящие, и источник этих средств лежит за пределами выгрузки.

Betweenness считается структурно, без весов: веса-суммы нельзя подавать как длину пути, а инвертировать их — значит вводить произвольную шкалу.

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

Текущий пайплайн — 3 секунды на 2 248 узлах, но узкие места известны и упрутся не в загрузку, а в две метрики.

Betweenness — O(V·E), на миллионе узлов неприменим. Замена: приближение Брандеса по выборке опорных вершин (k семплов вместо всех) либо переход на graph-tool/igraph с параллельной реализацией. Точное значение здесь не нужно, нужен ранг.

Louvain на питоне заменяется на Leiden из igraph — он и быстрее, и не даёт несвязных сообществ.

Достижимость от seed уже масштабируется: это по одному BFS на seed, а не на узел. При росте числа seed до тысяч — переход на разреженное матричное умножение булевых масок достижимости.

Хранение и признаки переезжают с pandas на DuckDB или Polars: агрегации по рёбрам и транзакциям — обычные group-by, они считаются вне памяти.

Что не изменится: правила ролей. Они опираются на локальные признаки узла (степени, суммы, коэффициент пропуска) и считаются за один проход независимо от размера графа. Пороги, привязанные к перцентилям, пересчитываются на новой выборке.

Тесты

make test

62 проверки: схема всех выгрузок, побайтовая воспроизводимость двух прогонов, запуск CLI как его запустит жюри, и отдельно — ловушки датасета: terminal никогда не присваивается на 4-м колене, seed никогда не получает роль transit, 19 узлов без рёбер всё равно попадают в выгрузку.

Пороги ролей проверяются юнит-тестами на синтетических узлах: и срабатывание, и несрабатывание на шаг ниже порога.

Ассистент проверяется отдельно: девять инструментов, разбор намерения без LLM, удаление выдуманных gid из ответа и поведение при сбое внешнего API — LLM-ветка покрыта моками, поэтому проверена и без ключа.

Структура репозитория

src/moneygraph/
  config.py      все пороги с обоснованием
  loading.py     загрузка parquet и проверки целостности
  graph.py       построение направленного графа
  features.py    признаки: структура, seed-достижимость, время
  clusters.py    Louvain и гипотезы по кластерам
  roles.py       правила ролей и генерация evidence
  priority.py    ранжирование
  structure.py   циклы, точки сочленения, устойчивость сети
  viewer.py      генерация автономного HTML-экрана
  text.py        склонение числительных для читаемых формулировок
  assets/        вшитый cytoscape.js (чтобы экран работал офлайн)
  cli.py         CLI ассистента (moneygraph-ask)
  serve.py       локальный эндпоинт для чата в интерфейсе (moneygraph-serve)
  agent/         AI-ассистент: инструменты, промпты, валидатор ссылок, сессия
  pipeline.py    CLI: один запуск от parquet до выгрузок
data/            предоставленный датасет
starter/         стартовый код организаторов (не используется в пайплайне)
tests/           проверки схемы, воспроизводимости и правил
out/             выгрузки
Звёзды
⭐ 0
Форки
0
Язык
HTML
Последний push
в прошлом месяце
Коммиты
16
Авторы
3
Файлы
83
Ветки
1

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

Andrey Rudov 11NUKI1223 3aikinovatomiris 2

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

  • 2026-09-23 17:53Correct floating-point precision in node role metricsaikinovatomiris
  • 2026-09-23 17:32feat: подключить ИИ-ассистента и защиту от оффтопаaikinovatomiris
  • 2026-09-23 16:37Ассистент в интерфейсе: локальный эндпоинт, автоопределение, кликабельные gid в ответахAndrey Rudov
  • 2026-09-23 16:28AI-ассистент аналитика: 9 инструментов над графом, валидация ссылок на узлы, работа без ключаAndrey Rudov
  • 2026-09-23 16:19feat: add analyst frontend, local API and progressive graph explorationNUKI1223
  • 2026-09-23 16:00feat: export a reproducible dashboard snapshot with lossless identifiersNUKI1223
  • 2026-09-23 15:34fix: use internal edge turnover in cluster hypothesesNUKI1223
  • 2026-09-23 15:31Передача работы: карта репозитория, порядок входа для нового человекаAndrey Rudov
Все коммиты (16)
  • 2026-09-23 14:41run.sh ставит зависимости в локальный venv: системный Python защищён PEP 668Andrey Rudov
  • 2026-09-23 14:40Контекст проекта в репозитории: CLAUDE.md, проектные скиллы, ТЗ, датасет, журнал решенийAndrey Rudov
  • 2026-09-23 14:32Все артефакты воспроизводятся побайтово, включая parquet и viewerAndrey Rudov
  • 2026-09-23 14:29Возвратные потоки и устойчивость сети: циклы, точки сочленения, симуляция изъятия топ-NAndrey Rudov
  • 2026-09-23 14:21Документ передачи работы коллегамAndrey Rudov
  • 2026-09-23 14:20Экран просмотра: автономный HTML со схемой сети, ролями и поиском по gidAndrey Rudov
  • 2026-09-23 14:08Детерминированные выгрузки, тесты пайплайна, READMEAndrey Rudov
  • 2026-09-23 14:02Граф денег: пайплайн ролей, кластеров и приоритетовAndrey Rudov
Структура репозитория (83 файлов)
.claude/skills/moneygraph-demo/SKILL.md
.claude/skills/moneygraph-pipeline/SKILL.md
.env.example
.gitignore
CLAUDE.md
HANDOFF.md
Makefile
README.md
data/edges.parquet
data/nodes.parquet
data/transactions.parquet
docs/DATASET.md
docs/DEBUG_AUDIT_2026-09-23.md
docs/DECISIONS.md
docs/TZ.md
docs/data-checksums.sha256
docs/requirements-verified.txt
frontend/.nvmrc
frontend/.prettierrc.json
frontend/index.html
frontend/package-lock.json
frontend/package.json
frontend/playwright.config.ts
frontend/playwright.unit.config.ts
frontend/src/App.module.css
frontend/src/App.tsx
frontend/src/GraphCanvas.tsx
frontend/src/Network.module.css
frontend/src/Network.tsx
frontend/src/data.ts
frontend/src/global.css
frontend/src/graph.ts
frontend/src/main.tsx
frontend/src/types.ts
frontend/src/vite-env.d.ts
frontend/tests/app.spec.ts
frontend/tests/graph.spec.ts
frontend/tsconfig.json
frontend/vite.config.ts
out/clusters.csv
out/features.parquet
out/nodes_roles.csv
out/resilience.csv
out/summary.json
out/top_nodes.csv
out/viewer.html
pyproject.toml
requirements.txt
run.sh
src/moneygraph/__init__.py
src/moneygraph/agent/__init__.py
src/moneygraph/agent/llm.py
src/moneygraph/agent/prompts.py
src/moneygraph/agent/scope.py
src/moneygraph/agent/session.py
src/moneygraph/agent/tools.py
src/moneygraph/agent/validator.py
src/moneygraph/api.py
src/moneygraph/assets/cytoscape.min.js
src/moneygraph/cli.py
src/moneygraph/clusters.py
src/moneygraph/config.py
src/moneygraph/features.py
src/moneygraph/graph.py
src/moneygraph/loading.py
src/moneygraph/pipeline.py
src/moneygraph/priority.py
src/moneygraph/roles.py
src/moneygraph/serve.py
src/moneygraph/structure.py
src/moneygraph/text.py
src/moneygraph/viewer.py
start.py
starter/README.md
starter/requirements.txt
starter/starter.py
tests/test_mg_agent.py
tests/test_mg_api.py
tests/test_mg_pipeline.py
tests/test_mg_roles.py
tests/test_mg_scope.py
tests/test_mg_serve.py
web.sh

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

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

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