AAYMA
RU
Войти

Граф денег

AML-пайплайн: по выгрузке переводов 81 известного участника на 4 колена присваивает роль, кластер и приоритет всем 2 248 клиентам, выдаёт CSV по…

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

О проекте

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

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

Суть

AML-пайплайн: по выгрузке переводов 81 известного участника на 4 колена присваивает роль, кластер и приоритет всем 2 248 клиентам, выдаёт CSV по схеме ТЗ, офлайн-экран сети, аналитическую справку и AI-ассистента.

Как устроено

Строится ориентированный взвешенный граф (2 248 узлов, 3 119 рёбер), считаются входы/выходы, доля пропуска, seed выше по потоку, betweenness, время, циклы и маршруты A→B→C. Роли назначаются правилами сверху вниз с числовым evidence; кластеры — Louvain (seed=42) на неориентированной проекции; priority_score — взвешенная сумма (роль 0,35, seeds 0,20, объём 0,20, мост 0,15, сигналы 0,10), seed ×0,85. Дополнительно считаются sensitivity к порогам, resilience при блокировке топ-N и запросы недостающих данных; AI-ассистент работает через инструменты графа с guardrail и PII-фильтром.

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

  • NetworkX
  • Louvain
  • betweenness centrality
  • cycle detection
  • threshold sensitivity analysis
  • network resilience what-if
  • PII guardrail

Метрики

Расчёт всей сети 10–15 с; синтетический граф 175 тыс. узлов — 71 с; топ-20 связаны с 27,7% оборота, их блокировка сокращает крупнейшую компоненту с 1 877 до 1 289; 8 сообществ с 2+ seed стабильны на 5 seed Louvain

Особенности

Анализ устойчивости: сдвиг каждого порога на шаг и доля сохранившегося топ-20; what-if блокировки узлов; stability у кластеров; next_requests.csv подсказывает, какие данные запросить. Честно учтены ловушки данных (узлы 4-го колена не становятся terminal). Экран и ассистент задеплоены на Vercel.

Стек

Python · NetworkX · pandas · vanilla JS · Docker · Vercel


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

README

README.md

Граф денег: кого проверять первым

HackAlem AI, трек «Финансы», кейс Freedom. Команда git push --force.

Инструмент для AML-аналитика банка. На вход: выгрузка исходящих переводов 81 известного участника на 4 колена (три parquet-файла). На выход: роль, кластер и приоритет для каждого из 2 248 клиентов, три CSV по схеме ТЗ и экран со схемой сети. Главный вопрос аналитика: «кого из 2 248 смотреть первым и почему».

Все выводы сформулированы как гипотезы для проверки («признаки консолидации»), а не как утверждения о виновности.

Посмотреть без установки: https://graf-deneg.vercel.app (схема сети, аналитическая справка, три CSV). Эти страницы собраны той же командой из этого репозитория. AI-ассистент онлайн: https://graf-deneg-ai.vercel.app (код из app/, публикация scripts/deploy_ai.py).

Ценность за 30 секунд

БылоСтало
Аналитику известны 81 клиент, нижний уровень цепочкиРоль, кластер и приоритет у всех 2 248 клиентов сети
Ручная трассировка: часы работы на один узел (оценка из ТЗ)Одна команда, 10-15 секунд на всю сеть
Приоритет проверки выбирается интуитивноТоп-30 с числовым обоснованием; первым идёт узел, который рассылает 23 млн ₸ 99 получателям
Непонятно, каких данных не хватаетГотовые запросы: 444 узла на границе выборки, источники средств seed, оседание денег
Итог надо оформлять вручнуюout/report.html: аналитическая справка для руководителя, печатается в PDF
Непонятно, выдержит ли подход объём банкаЗамер: синтетический граф той же схемы в 175 тыс. узлов проходит основной расчёт за 71 с

На переводы с участием топ-20 приходится 27,7% оборота сети. Если их заблокировать, крупнейшая связная часть сократится с 1 877 до 1 289 клиентов. 8 сообществ объединяют 2+ известных участника: это кандидаты в организованные группы.

Быстрый старт: одна команда

python -m pip install -r requirements.txt
python -m pipeline --data data --out out

Расчёт занимает 10-15 секунд на обычном ноутбуке (ТЗ допускает до 5 минут). После установки библиотек интернет и API-ключи не нужны (для pip install интернет нужен один раз). На Windows можно просто дважды щёлкнуть _run.bat: он поставит библиотеки, запустит пайплайн и тесты.

Своя выгрузка той же схемы: python -m pipeline --data <папка с parquet> --out <папка для результатов>.

Что появится в out/:

ФайлЧто внутри
nodes_roles.csv2 248 строк: gid, role, role_score, cluster_id, priority_score, evidence + метрики узла
clusters.csvстрока на кластер: cluster_id, n_nodes, n_seed, sum_kzt_internal, top_gids, hypothesis
top_nodes.csvтоп-30 для проверки: rank, gid, role, priority_score, why
viewer.htmlэкран: схема сети, роли, кластеры, поиск по gid. Открывается двойным щелчком, без сервера и интернета
report.htmlаналитическая справка: главные выводы, кого проверять первым, гипотезы по группам, какие данные запросить
graph.jsonвесь результат одним файлом для экрана и AI-ассистента, включая карточку каждого узла
next_requests.csvкаких данных не хватает и какой запрос сделать следующим
resilience.csvчто будет с сетью, если заблокировать топ-N узлов
routes.csv, cycles.csvповторяющиеся маршруты A→B→C и возвратные потоки
sensitivity.csvкак меняются роли и топ-20, если сдвинуть пороги на шаг
summary.jsonсводка прогона: число ролей, кластеров, время этапов

Сценарий аналитика

  1. Аналитик получает от правоохранительных органов список клиентов по делу (81 gid).
  2. Кладёт выгрузку в data/ и запускает одну команду.
  3. Инструмент строит направленный граф, считает метрики и присваивает каждому узлу роль с числовым обоснованием.
  4. Аналитик открывает out/viewer.html: видит сеть с направлением денег, ролями и кластерами, ищет узел по gid.
  5. Открывает топ-лист: у каждой позиции есть обоснование вроде «получает от 9 плательщиков (из них seed: 3) 919 104 ₸; дальше отдаёт 8%». По нему формирует перечень на углублённую проверку, а next_requests.csv подсказывает, какие данные запросить.

Схема решения

flowchart LR
  A["data/*.parquet<br/>edges, nodes, transactions"] --> B["Граф<br/>направленный, взвешенный<br/>2 248 узлов, 3 119 рёбер"]
  B --> C["Метрики узла<br/>вход и выход, суммы, доля пропуска,<br/>seed выше по потоку, посредничество,<br/>время, циклы, маршруты"]
  C --> D["Роли по правилам<br/>6 ролей, порог и обоснование"]
  B --> E["Кластеры<br/>Louvain, seed=42"]
  D --> F["Приоритет 0..1<br/>роль, seed, оборот, мост, сигналы"]
  E --> F
  F --> G["CSV по схеме ТЗ<br/>nodes_roles, clusters, top_nodes"]
  F --> H["graph.json"]
  H --> I["Экран viewer.html<br/>схема, роли, поиск по gid"]
  H --> J["AI-ассистент аналитика<br/>вопросы о графе, опционально"]

Роли: правила и пороги

Правила проверяются сверху вниз, первое совпадение даёт роль. Все пороги в одном файле: pipeline/config.py. Для seed не используем отношение «отдал / получил»: их входящие в выгрузке занижены (граф собран от них).

ПорядокРольПравилоОбоснование в evidence
0peripheralУзел без единого перевода в выборке (19 seed)«Seed без внутрибанковских переводов от 5 000 ₸ в июле»
1coordinatorСобирает от 5+ плательщиков и рассылает 5+ получателям. Второй проход: получает от 2+ точек консолидации«Собирает от 19 плательщиков 1,82 млн ₸ и рассылает 61 получателю 4,99 млн ₸»
2distributorРассылает 10+ получателям (веер)«Рассылает 116 получателям 3,11 млн ₸, в среднем 26 796 ₸ на получателя»
3consolidatorПолучает от 5+ плательщиков, или от 3+, среди которых 2+ seed«Получает от 9 плательщиков (из них seed: 3) 919 104 ₸; дальше отдаёт 8%»
4transitНе seed, отдаёт дальше 80-120% полученного. Флаг «сквозной», если 50%+ ушло в течение 2 дней после поступления«Получил 696 800 ₸, передал дальше 98%; 96% ушло в течение 2 дней»
5terminalКолено 0-3, дальше уходит не больше 10%, получено от 100 000 ₸ или от 2+ плательщиков«Получил 4,50 млн ₸ от 2 плательщиков, дальше ушло только 4%»
6peripheralУзел 4-го колена без исходящих: обрыв выборки, роль не определить«4-е колено: исходящие не собирались. Похожие узлы 1-3 колена пересылают дальше в 41% случаев»
7peripheralОстальные: пороги ролей не достигнуты«Вход 7 000 ₸ от 1 плательщика, выход 0 ₸. Признаков роли нет»

Откуда пороги: 5 плательщиков и 10 получателей примерно отделяют верхние 2-3% узлов по входу и выходу (медиана у узла 1 плательщик); 100 000 ₸ больше трёх медианных переводов (медиана 30 000 ₸); коридор 80-120% соответствует «пропускает, не удерживая» с допуском на комиссии и дробление.

Как это работает для двух ловушек. Узел 4-го колена не может стать terminal: правило 5 требует колено 0-3. Роль consolidator такой узел получил бы только при 5+ плательщиках; на выданных данных таких нет, и все 444 узла обрыва получили peripheral (строка 6). Для seed правило 5 срабатывает только при нуле исходящих: доля «дальше ушло» для них не считается.

role_score (уверенность 0..1): 0,5 ровно на пороге правила и растёт до 1,0 на «сильном» уровне (например, для distributor 60 получателей и больше). Для transit: 1,0 при пропуске ровно 100% и 0,5 на краях коридора, +0,1 за сквозной транзит; итог всегда ограничен 1,0. Для обрыва 4-го колена 0,4: роль честно не определена.

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

priority_score = прозрачная взвешенная сумма пяти компонент, каждая от 0 до 1 (pipeline/priority.py):

КомпонентаВесСмысл
role0,35вес роли: coordinator 1,0, consolidator 0,8, distributor 0,7, transit 0,5, terminal 0,4, peripheral 0,1
seeds0,20сколько из 81 seed имеют направленный путь к узлу: здесь сходятся деньги известных участников
volume0,20оборот узла на лог-шкале от 5 тыс. до 30 млн ₸
bridge0,15посредничество (betweenness): узел связывает части сети
signals0,10сквозной транзит, 3+ плательщика в один день, возвратный поток, повторяющийся маршрут

Seed умножаем на 0,85: они уже известны следствию, а фокус ТЗ смещается «с 81 курьера на реальные точки консолидации». Колонка why в top_nodes.csv объясняет позицию словами: роль, числа и главные причины.

Кластеры

Louvain (networkx.community.louvain_communities, seed=42, resolution=1.0) на неориентированной проекции графа, вес ребра = сумма переводов в обе стороны. Направление здесь сознательно не учитываем: кластер отвечает на вопрос «кто с кем связан деньгами», а направление потоков остаётся в ролях и на схеме. Узлы без связей собраны в кластер 0.

На выданных данных: 72 кластера, это 71 сообщество Louvain и кластер 0 (19 изолированных seed). 8 сообществ содержат 2+ seed, как и в примечании организаторов. Стабильность проверили на 5 разных seed алгоритма: число таких сообществ каждый раз 8. В clusters.csv у каждого кластера есть колонка stability: доля пар его узлов, которые остаются вместе при 4 других seed Louvain (медиана 1,0). Гипотеза кластера собирается из его состава, например: «ядро схемы: координатор связывает сбор (9 точек консолидации) и раздачу (5 распределителей)».

Насколько правила устойчивы

Пороги экспертные, поэтому мы проверили, не держится ли результат на одном числе. Пайплайн сдвигает каждый ключевой порог на шаг вверх и вниз и пишет в sensitivity.csv, сколько узлов сменило роль и сколько узлов топ-20 осталось в топе.

ПорогБазовое значениеПроверилиСменили рольТоп-20 сохранился
плательщиков у координатора54 и 613 и 485% и 100%
получателей у координатора54 и 61 и 0100% и 100%
плательщиков у точки консолидации54 и 625 и 1290% и 100%
получателей у распределителя108 и 1213 и 10100% и 100%
сумма у конечного получателя100 000 ₸50 000 и 200 000163 и 119100% и 100%
нижняя граница транзита80%70% и 90%21 и 8100% и 100%

Вывод: роли на границах порогов сдвигаются, как и должны, а список «кого смотреть первым» почти не меняется.

Как мы учли ловушки данных

Особенность из ТЗЧто сделали
444 узла 4-го колена без исходящихНикогда не ставим им terminal. Роль peripheral с флагом cutoff_depth4, role_score 0,4 и оценкой: какая доля похожих узлов 1-3 колена (по числу плательщиков и сумме входа) пересылает деньги дальше. Топ таких узлов попадает в next_requests.csv с запросом исходящих
Входящие seed заниженыДля seed не используем «отдал / получил», роли seed только по структуре. Топ seed попадает в next_requests.csv с запросом входящих
354 узла отдают больше, чем получилиНе считаем это аномалией. Пишем в evidence: «отдаёт больше, чем получил в выборке: есть источники вне данных». Топ таких узлов в next_requests.csv с запросом входящих
Порог 5 000 ₸Дробление ниже порога не видно: для топ-20 в next_requests.csv запрос полной выписки без порога
19 seed без рёбер, 12 только получатели19 изолированных seed в кластере 0 с флагом isolated; seed-получатели без исходящих могут быть terminal
16 слабосвязных компонентКластеры внутри компонент, схема показывает каждую компоненту отдельно
Нет атрибутов и разметкиТолько структура, суммы и даты. Никаких выдуманных полей. Правила объяснимы, а не обучены

Дополнительно (опции ТЗ)

ОпцияГде
Учёт обрыва 4-го коленароль, флаг cutoff_depth4, колонка forward_prob
Временные паттерныfast_out_share (сквозной транзит за 2 дня), max_payers_same_day (синхронные поступления)
Повторяющиеся маршруты и возвратные потокиroutes.csv (A→B→C минимум дважды за 2 дня), cycles.csv (циклы до 6 шагов), флаги recurring_route, cycle
Устойчивость сетиresilience.csv: блокировка топ-N по нашему приоритету и, для сравнения, по числу связей
Оценка полнотыnext_requests.csv: пробел в данных, какой запрос сделать, почему
Автогенерация карточки узлаполе card в graph.json, панель узла на экране, python -m pipeline.explain <gid>
AI-ассистент аналитикаapp/ (см. ниже)

Пример проверки: разбор трёх узлов

Так мы отвечаем жюри за минуту: роль следует из чисел в nodes_roles.csv. Карточку любого узла печатает команда python -m pipeline.explain <gid>: роль, правило с порогом и фактическим значением, потоки, крупнейшие контрагенты, сигналы и что проверить.

  1. 100000000331309100: coordinator, №1 в топе. Получает от 5 плательщиков 984 635 ₸ (порог 5) и рассылает 99 получателям 23,00 млн ₸ (порог 5). Через узел проходит самый большой поток в сети, к нему ведут пути от 7 seed, посредничество выше, чем у 99% узлов. Отдаёт в 23 раза больше, чем получил в выборке: у него есть источники вне данных, поэтому в next_requests.csv по нему запрос входящих переводов.
  2. 100000004015047100: consolidator. Получает от 9 плательщиков (порог 5), из них 3 seed, всего 919 104 ₸, а дальше отдаёт только 8%. Деньги собираются и остаются: классическая точка консолидации.
  3. 100000005075949100: peripheral с обрывом. 4-е колено, получил 2,23 млн ₸ от одного плательщика, исходящих в выгрузке нет. Наивное правило назвало бы его конечным получателем. Мы нет: его исходящие просто не собирали. Похожие узлы 1-3 колена пересылают деньги дальше в 41% случаев, поэтому первый запрос по нему: исходящие за июль.

Результаты на выданных данных

Роли: coordinator 18, consolidator 43, distributor 49, transit 66, terminal 418, peripheral 1 654 (из них 444 обрыв 4-го колена и 19 изолированных seed). Кластеров 72, сообществ с 2+ seed 8. Циклов длиной до 6: 1 541, устойчивых маршрутов A→B→C: 478.

Устойчивость сети (resilience.csv): крупнейшая связная часть до блокировки 1 877 узлов.

Заблокировать топ-NПо нашему приоритету: крупнейшая часть / доля оборотаПо числу связей: крупнейшая часть / доля оборота
101 554 узла / 17,3%1 428 / 23,1%
201 289 / 27,7%1 188 / 30,3%
301 086 / 33,8%916 / 38,6%

Вывод: чтобы разрезать сеть, сильнее работают узлы с максимумом связей. Наш приоритет решает другую задачу, «кого проверять»: он учитывает роль, деньги и сходимость seed. Поэтому аналитику мы даём оба взгляда. Доля оборота здесь означает долю июльских переводов с участием заблокированных клиентов. Это мера охвата сети, а не предотвращённый ущерб.

Экран

out/viewer.html собирается автоматически в конце пайплайна (модуль viewer/). Открывается двойным щелчком, работает без сервера и без интернета: библиотека vis-network лежит в viewer/vendor/. Если модуль экрана недоступен, пайплайн собирает запасной экран (pipeline/fallback_viewer.py) с тем же набором функций, так что must have 5 не зависит от одной части кода. Раскладка считается заранее в пайплайне (seed=42): кластеры как острова, крупнейший в центре, остальные по спирали без наложений. Схема: цвет узла = роль (или кластер, переключатель), размер = приоритет, стрелки = направление денег, толщина = сумма. Поиск по gid, карточка узла со всеми контрагентами, топ-20, фильтры, сводка по ролям. Ссылка viewer.html#gid=<gid> сразу открывает узел.

«Что если заблокировать». Кнопки «Топ-5», «Топ-10», «Топ-20» и «Заблокировать этот узел» в карточке. Экран прямо в браузере пересчитывает сеть без выбранных узлов и показывает «было → стало»: размер крупнейшей связной части, число фрагментов и долю оборота, которая шла через заблокированных. Для топ-20: 1 877 → 1 289 клиентов, 35 → 389 фрагментов, 27,69% оборота. Цифры совпадают с resilience.csv из пайплайна.

AI-ассистент аналитика (опционально)

app/ отвечает на вопросы на естественном языке по готовому out/graph.json и даёт ссылки на узлы схемы. Пайплайну ассистент не нужен: роли и выгрузки считаются без LLM.

Онлайн: https://graf-deneg-ai.vercel.app. Публикация отдельным проектом Vercel: python scripts/deploy_ai.py (в сборку идут только app/, static/, out/graph.json, out/viewer.html; ключ берётся из .env и передаётся Vercel как переменная окружения, в git не попадает). Запуск локально:

python -m uvicorn app.main:app --port 8000   # затем http://localhost:8000 (на Windows: _assistant.bat)

Как устроен (agentic AI с проверками в коде):

  • Инструменты по графу (app/graph_store.py): get_node, neighbors, top_nodes, cluster_info, common_receivers («кто собирает деньги с этих узлов»), paths, search. Модель OpenAI вызывает их сама через function calling и отвечает строго по схеме (Structured Outputs): текст, список gid, уверенность.
  • Проверка ссылок в коде, а не в промпте (enforce_gids): каждый gid в ответе должен существовать в графе и прийти из вызова инструмента в этом же ответе. Остальные gid вырезаются и попадают в meta.guardrails.
  • LLM-шлюз (app/llm.py): быстрая модель по умолчанию, эскалация на умную при уверенности ниже 0,6, кэш ответов, учёт токенов и стоимости, маскировка персональных данных (карты, ИИН, телефоны). Идентификаторы узлов похожи на номера карт, поэтому проверенные gid защищены от маскировки.
  • Черновик служебной записки по узлу («Черновик по », GET /api/draft/{gid}): роль, факты с числами, почему узел важен, какие данные запросить. Первая строка всегда «ЧЕРНОВИК. Гипотеза для проверки, не вывод о виновности». С ключом текст пишет LLM по данным инструментов, без ключа тот же черновик собирается шаблоном.
  • Без ключа типовые вопросы отвечаются правилами: «кто собирает деньги с <gid, gid>», «карточка ». GET /api/card/{gid} отдаёт готовую карточку из пайплайна без LLM. Под полем чата есть кнопки-подсказки для демо.
  • Ответ не зависает. На один ответ модели отведено 40 секунд на все раунды инструментов (OPENAI_TOTAL_TIMEOUT), без повторных попыток. Не уложилась: чат переходит в режим без модели, типовые вопросы отвечаются правилами. Кнопка «Без модели» под полем ввода делает это вручную.

Настройка (файл .env в корне, в git не попадает; значения ниже по умолчанию из app/config.py):

OPENAI_API_KEY=sk-...           # без ключа ассистент работает в режиме без модели
MODEL_FAST=gpt-5.6-luna         # быстрая модель
MODEL_SMART=gpt-5.6-sol         # умная модель для эскалации
REASONING_FAST=low              # необязательно: усилие рассуждения
OPENAI_TOTAL_TIMEOUT=40         # лимит на один ответ, секунды
BUDGET_USD=5                    # предел расходов процесса
RATE_LIMIT_PER_MIN=20           # вопросов в минуту с одного IP

Режим видно в интерфейсе («LLM включён» или «Без модели») и в GET /api/health (поле llm_enabled). Уверенность под ответом (например, 93%) это самооценка модели в схеме ответа, а не измеренная точность ролей. Если код удалил из ответа неподтверждённый gid, уверенность понижается до 30%. Ответы по правилам без модели получают фиксированную уверенность.

API: POST /api/ask {"question": "..."}, GET /api/top, GET /api/node/{gid}, GET /api/card/{gid}, GET /api/draft/{gid}, GET /viewer (экран).

Пример на выданных данных: вопрос «Кто собирает деньги с 100000007982728100, 100000007747786100, 100000008721637100?» даёт 100000004015047100: 549 384 ₸ от всех трёх, это точка консолидации из нашего разбора.

Чем решение отличается

  • Не чёрный ящик. Каждая роль следует из правила с порогом, каждое обоснование содержит числа. Жюри или аналитик проверяет любой узел за минуту: python -m pipeline.explain <gid>.
  • Проверяем сами себя. Пайплайн сдвигает каждый порог на шаг и показывает, что топ-20 сохраняется на 85-100% (sensitivity.csv), а кластеры держатся при других seed алгоритма (stability).
  • Честно работаем с дырами в данных. Обрыв 4-го колена не превращается в «конечного получателя»: мы оцениваем по похожим узлам, пересылают ли такие клиенты деньги дальше, и формируем конкретные запросы данных.
  • Следующий шаг расследования встроен в продукт. Инструмент говорит не только «кто», но и «что запросить», а справка сразу готова для руководителя.
  • ИИ с проверками в коде. Ассистент отвечает только по данным инструментов, а каждый gid в ответе проверяется кодом. Сами роли считаются без LLM, поэтому результат воспроизводим без платных сервисов.

Потенциал развития

  1. Пилот во Freedom: ежедневный или еженедельный прогон по свежей выгрузке, карточки узлов в системе кейсов AML-подразделения, обратная связь аналитиков по подтверждённым случаям. Эффект меряем на 20 закрытых делах: две очереди проверки, наш приоритет и сортировка по числу связей. Метрики: время разбора дела и доля направлений на углублённую проверку, которые подтвердились. Сейчас измерено только время расчёта: около 10 секунд на всю сеть.
  2. От правил к обучению: когда накопятся подтверждённые роли, пороги калибруются на них, а приоритет можно дообучить как ранжирование. Объяснимость сохраняется: правила остаются базой и проверкой.
  3. Новые сценарии на том же движке: сети дропов при мошенничестве, обналичивание, незаконные онлайн-казино, схемы с займами. Меняются только стартовые узлы и пороги в pipeline/config.py.
  4. Полнее картина: входящие переводы, межбанковские и карточные операции, связь с регулятором и другими банками. Чем полнее граф, тем меньше узлов на границе выборки.
  5. Масштаб: замерено до 174 501 узла, план до ~1 млн узлов в разделе ниже.

Технологии и архитектура

Python 3.10+, pandas, pyarrow, networkx, numpy, scipy. Экран: HTML + vis-network. Ассистент: FastAPI + OpenAI API.

Воспроизводимость проверили на двух наборах версий: pandas 2.2 + networkx 3.2 и pandas 3.0 + networkx 3.6. Роли, кластеры, приоритеты и порядок топа совпали полностью. Без scipy пайплайн тоже работает. Отдельно прогнали «чистую машину»: копия репозитория, новое виртуальное окружение, только pip install -r requirements.txt и одна команда пайплайна. Все выгрузки и экран собрались за 15 секунд, результат совпал с ноутбуком команды, все тесты прошли.

data/        исходные parquet организаторов (не меняются)
pipeline/    load -> features -> roles -> clusters -> priority -> layout -> export
  config.py    все пороги и веса
  features.py  метрики: степени, суммы, пропуск, seed выше по потоку, betweenness, время, циклы, маршруты
  roles.py     правила ролей, role_score, evidence, учёт обрыва 4-го колена
  clusters.py  Louvain и гипотезы кластеров
  priority.py  priority_score и обоснование топа
  extras.py    устойчивость сети, запросы для полноты, циклы
  robustness.py  стабильность кластеров и чувствительность к порогам
  cards.py     карточка узла; explain.py печатает её по gid
  report.py    аналитическая справка out/report.html и главная страница out/index.html
scripts/     scale_test.py: нагрузочный тест на синтетическом графе той же схемы
viewer/      экран: сборка самодостаточного out/viewer.html
app/         AI-ассистент: graph_store.py (инструменты), assistant.py (ответы и проверка gid), llm.py (шлюз), main.py (API)
tests/       проверки must have на настоящих данных
starter/     стартовый код организаторов, для справки

Тесты

python -m pip install -r requirements-dev.txt   # то же, что requirements.txt, плюс pytest
python -m pytest -q                              # все тесты, меньше минуты

tests/test_pipeline.py проверяет must have: 2 248 строк и все обязательные колонки, роли только из словаря, evidence с числами и не длиннее 200 символов, ни один узел обрыва не стал terminal, cluster_id согласован с clusters.csv, топ от 20 строк по убыванию, время меньше 5 минут, детерминизм (два прогона дают одинаковые роли и кластеры), в коде нет зашитых gid. tests/test_viewer.py проверяет сборку экрана без внешних ссылок, tests/test_assistant.py инструменты и проверку gid, tests/test_llm_offline.py LLM-шлюз против поддельного сервера OpenAI: цикл инструментов, эскалацию, откат при ошибке, кэш и маскировку, tests/test_assistant_llm_timeout.py лимит времени ответа модели. Всего 94 теста проходят без сети и ключа; ещё 2 теста экрана запускаются, если на машине есть Chrome.

Ограничения

  • Разметки ролей нет, поэтому правила экспертные: пороги объяснимы, но не откалиброваны на подтверждённых делах.
  • Выгрузка содержит только исходящие переводы от seed: входящие извне выборки не видны, полный баланс узла не посчитать.
  • Переводы меньше 5 000 ₸, межбанковские, наличные и крипта не видны. Узел может выглядеть конечным, а деньги ушли другим каналом.
  • Для узлов обрыва оценка «пересылает дальше» статистическая, по похожим узлам, а не по самому узлу.
  • Louvain не учитывает направление и зависит от seed алгоритма: фиксируем seed=42 и проверяли стабильность.
  • Встречные переводы A↔B встречаются часто (177 пар), поэтому в приоритет они не входят, только циклы из 3+ узлов.
  • Веса приоритета заданы экспертно и задокументированы. Их легко поменять в pipeline/config.py.

Масштабирование: замеры до 174 501 узла и план до ~1 млн

Проверено замером, а не только на словах. scripts/scale_test.py строит синтетический граф той же схемы (seed, обход исходящих на 4 колена, веерные распределители, точки сбора, транзакции от 5 000 ₸) и запускает пайплайн в режиме --core (роли, кластеры, приоритет, три CSV):

УзловРёберТранзакцийВремя пайплайна
2 248 (выданные данные, полный режим с экраном и справкой)3 1194 840около 10 с
50 06755 97089 62919 с
174 501195 289312 19071 с

Рост времени почти линейный: граф в 78 раз больше выданного укладывается в лимит ТЗ (5 минут) с запасом. Для графов больше 5 000 узлов пайплайн сам включает приближённые методы: betweenness по выборке из 100 стартовых узлов (seed=42) и предел перебора циклов. На выданных данных они не включаются, результат не меняется.

Что изменится при ~1 млн узлов:

  • Данные: parquet читать через DuckDB или Polars, признаки считать векторно (сейчас часть циклов по узлам на Python).
  • Граф: networkx заменить на igraph или graph-tool (C/C++), для кластера банков на Spark GraphFrames.
  • Betweenness: точный расчёт O(n·m) не пройдёт, берём приближённый по выборке источников или считаем внутри кластеров.
  • Кластеры: Leiden (igraph/leidenalg) вместо Louvain: быстрее и даёт связные сообщества.
  • Циклы: искать только внутри сильно связных компонент и только для топ-узлов, с ограничением длины.
  • Seed выше по потоку: обход в ширину из seed уже ограничен 4 коленами, это линейно по числу рёбер.
  • Экран: 1 млн узлов не рисуем целиком. Показываем граф кластеров и раскрываем кластер или окрестность узла по запросу.
  • Пороги ролей перевести в процентили распределения, чтобы они сами подстраивались под новые данные.

Данные и внешние сервисы

Данные: только архив организаторов (data/, описание полей в data/README_dataset.md). Внешних источников нет, атрибуты клиентов не достраиваются. Пайплайн и экран работают локально без интернета. Интернет нужен только AI-ассистенту для OpenAI API, и только если задан ключ в .env (ключ в git не попадает).

Как мы работали с AI-агентами

  • Claude (Anthropic) как штаб: разбор ТЗ, выбор подхода, пайплайн, тесты, README.
  • Codex (OpenAI) у двух участников: экран (viewer/) и AI-ассистент (app/) по общему контракту docs/CONTRACT.md.
  • Правила для агентов лежат в AGENTS.md: зоны ответственности, запрет на зашитые gid, формулировки как гипотезы.

Раскрытие (п. 6.4 Положения)

  • Стартовый код организаторов в starter/: загрузка parquet и проверки консистентности взяты за основу pipeline/load.py.
  • Наш заранее подготовленный шаблон (коммит setup: starter kit): скиллы и агенты для Codex (.agents/, .codex/), LLM-шлюз app/llm.py с маршрутизацией моделей и кэшем, app/pii.py, скрипты проверки. Логика кейса (пайплайн, роли, кластеры, приоритет, экран, ассистент по графу) написана на хакатоне.
  • Библиотеки: pandas, pyarrow, networkx, numpy, FastAPI, OpenAI SDK, vis-network 9.1 (лицензия Apache-2.0 или MIT, файл в viewer/vendor/).
  • Данные: выданы организаторами, используются только в рамках хакатона.
Звёзды
⭐ 0
Форки
0
Язык
HTML
Последний push
в прошлом месяце
Коммиты
30
Авторы
4
Файлы
98
Ветки
1

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

AmirlanSeit 20AbishDan 6Akezhan Omashev 2BebraPro2007 2

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

  • 2026-09-23 17:52Correct test count and improve README clarityAmirlanSeit
  • 2026-09-23 17:37docs: readme precisionAmirlanSeit
  • 2026-09-23 17:30Добавь одинаковую строку навигации вверху всех четырёх страниц, логику не меняй.AmirlanSeit
  • 2026-09-23 17:24ui: assistant layout, single scrollAmirlanSeit
  • 2026-09-23 17:17viewer: top-20 firstAmirlanSeit
  • 2026-09-23 17:06assistant: keep small numbers in answersAmirlanSeit
  • 2026-09-23 16:39outputs: rebuild with mobile fixesAmirlanSeit
  • 2026-09-23 16:39ui: consistent navigation buttonsAbishDan
Все коммиты (30)
  • 2026-09-23 16:24mobile: card scroll, report tableAbishDan
  • 2026-09-23 16:16assistant: 40s budget, low reasoningAmirlanSeit
  • 2026-09-23 16:02site: assistant online, pilot plan, wordingAmirlanSeit
  • 2026-09-23 15:41docs: demo steps for assistantAmirlanSeit
  • 2026-09-23 15:33docs: submission text, scale in READMEAmirlanSeit
  • 2026-09-23 15:31assistant: keep demo usable during API outagesAkezhan Omashev
  • 2026-09-23 15:26docs: demo script; report: grammarAmirlanSeit
  • 2026-09-23 15:18pipeline: large-graph mode, scale testAmirlanSeit
  • 2026-09-23 14:50pipeline: analyst report, landing pageAmirlanSeit
  • 2026-09-23 14:42assistant: draft memo, demo promptsAkezhan Omashev
  • 2026-09-23 14:40viewer: what-if blocking, summaryAbishDan
  • 2026-09-23 14:26tests: llm gateway offline; readme: assistant; windows run scriptsAmirlanSeit
  • 2026-09-23 14:16outputs: rebuilt with team viewer and assistantAmirlanSeit
  • 2026-09-23 14:09viewer: headerAbishDan
  • 2026-09-23 14:09assistant: real graphBebraPro2007
  • 2026-09-23 14:08outputs: rebuilt with team viewerAmirlanSeit
  • 2026-09-23 14:05assistant: graph tools and guardrailBebraPro2007
  • 2026-09-23 14:01viewer: real data, node cardAbishDan
  • 2026-09-23 13:55viewer: network mapAbishDan
  • 2026-09-23 13:51pipeline: roles, clusters, priority, cards, screenAmirlanSeit
  • 2026-09-23 13:25case: data, contract, new AGENTS.mdAmirlanSeit
  • 2026-09-23 13:01setup: starter kit hackalem-kit (pre-made, disclosed in README, rule 6.4)AmirlanSeit
Структура репозитория (98 файлов)
.agents/skills/checkpoint/SKILL.md
.agents/skills/deploy/SKILL.md
.agents/skills/evals/SKILL.md
.agents/skills/pitch/SKILL.md
.agents/skills/review/SKILL.md
.agents/skills/spec/SKILL.md
.codex/agents/judge.toml
.codex/agents/qa.toml
.codex/agents/reviewer.toml
.dockerignore
.env.example
.gitignore
.vercelignore
AGENTS.md
Dockerfile
README.md
_assistant.bat
_run.bat
app/__init__.py
app/assistant.py
app/config.py
app/graph_store.py
app/llm.py
app/main.py
app/pii.py
app/router.py
data/README_dataset.md
data/demo_cache.json
data/edges.parquet
data/nodes.parquet
data/transactions.parquet
docs/CASE.md
docs/CONTRACT.md
docs/DEMO.md
docs/SUBMISSION.md
docs/TZ.md
out/clusters.csv
out/cycles.csv
out/graph.json
out/index.html
out/next_requests.csv
out/nodes_roles.csv
out/report.html
out/resilience.csv
out/routes.csv
out/sensitivity.csv
out/summary.json
out/top_nodes.csv
out/viewer.html
pipeline/__init__.py
pipeline/__main__.py
pipeline/cards.py
pipeline/clusters.py
pipeline/config.py
pipeline/explain.py
pipeline/export.py
pipeline/extras.py
pipeline/fallback_viewer.py
pipeline/features.py
pipeline/fmt.py
pipeline/layout.py
pipeline/load.py
pipeline/priority.py
pipeline/report.py
pipeline/robustness.py
pipeline/roles.py
pipeline/run.py
requirements-dev.txt
requirements.txt
reviews/.gitkeep
scripts/check.py
scripts/check_key.py
scripts/deploy_ai.py
scripts/scale_test.py
specs/.gitkeep
starter/README.md
starter/requirements.txt
starter/starter.py
static/app.js
static/index.html
static/style.css
tests/conftest.py
tests/test_assistant.py
tests/test_assistant_llm_timeout.py
tests/test_llm_offline.py
tests/test_pii.py
tests/test_pipeline.py
tests/test_router.py
tests/test_viewer.py
vercel.json
viewer/__init__.py
viewer/app.js
viewer/build.py
viewer/make_sample.py
viewer/sample_graph.json
viewer/style.css
viewer/template.html
viewer/what_if.js

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

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

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