О проекте
Команда: Max
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Доведённый продукт
Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)
Суть
Пайплайн AML-анализа транзакционной сети (2 248 узлов): роли, кластеры и приоритет проверки с объяснимыми правилами, команда LLM-агентов для исследования и веб-интерфейс.
Как устроено
Parquet-данные превращаются в направленный взвешенный граф NetworkX; считаются признаки (PageRank, betweenness, временные признаки, атрибуция), Louvain-кластеризация, роли по правилам с порогами и RuleTrace, второй проход для координаторов, приоритет. LLM-агенты (планировщик, калибровщик порогов с симуляцией, следователь с function calling, критик, редактор с проверкой чисел, ревизор, аналитик) не присваивают роли, а только направляют исследование; есть офлайн-режим с теми же CSV. Интерфейс — статичный фронтенд с vis-network или Gradio/pyvis.
Ключевые решения
- NetworkX
- PageRank
- betweenness centrality
- Louvain
- rule-based roles с RuleTrace
- function calling
- multi-agent
- threshold calibration simulation
Особенности
Принцип «агенты исследуют, движок правил решает»: редактор может переписать объяснение только если каждое число выводится из RuleTrace; журнал аудита всех вызовов агентов; прогон ~90 с и ~$0.21; 83 теста.
Стек
Python · pandas · pyarrow · NetworkX · SciPy · PyYAML · Gradio · pyvis · vis-network · pytest
Модели
OpenAI
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-e26f0e66-max.
README
README.mdГраф денег (Money Graph)
Восстановление финансовой структуры организованной группы по транзакционной сети. Трек 2 — Финансы. Кейс HackAlem AI.
Языки: Русский · English · Қазақша
📦 Итоговые файлы для жюри — три обязательные выгрузки и схема решения
./agent_run.sh
Одна команда: установка зависимостей → расчёт → проверка выгрузок → отчёт по времени и стоимости → открытие интерфейса в браузере.
Быстрый старт — выберите один вариант
[!IMPORTANT]
🔑 Нужен ли ключ API?
Нужен ключ? A — запустить всё с нуля, вместе с ИИ-агентами ✅ Да, нужен ключ OpenAI API. Впишите его в .envдо запуска.B — просто посмотреть результаты, полученные на хакатоне ❌ Нет. Ни ключа, ни аккаунта, ни интернета, настраивать нечего. C — пересчитать всё вообще без ИИ ❌ Нет. Получаются те же три CSV, те же роли и тот же порядок. Если ключа нет и вы просто хотите увидеть результат работы инструмента, переходите сразу к варианту B — он работает сразу.
A · Запустить всё с нуля, вместе с ИИ-агентами
Впишите ключ в .env и выполните ровно эти две команды:
cp .env.example .env && echo "OPENAI_KEY=sk-ЗАМЕНИТЕ" >> .env
./agent_run.sh
Скрипт всё установит, запустит полную команду агентов, проверит три обязательные выгрузки, напечатает время и стоимость и откроет интерфейс. Около 90 секунд и примерно $0.21 на текущей модели.
B · Просто посмотреть результаты, полученные на хакатоне
Ключ не нужен, обращений к модели нет, настраивать нечего — открываются закоммиченные результаты как есть:
./agent_run.sh --use-existing
C · Пересчитать всё вообще без ИИ
Те же три CSV, те же роли и тот же порядок — меняются только формулировки:
./agent_run.sh --offline
Какой интерфейс?
По умолчанию — собственный фронтенд. Это статическая страница, которая раздаётся локально, сама открывается в браузере и не требует интернета.
./agent_run.sh # собственный фронтенд (по умолчанию)
./agent_run.sh --ui gradio # Gradio, умеет публиковать общедоступную ссылку
./agent_run.sh --ui none # без интерфейса, только выгрузки
./agent_run.sh --no-browser # не открывать браузер
Оба интерфейса читают одни и те же выгруженные файлы, поэтому всегда показывают одинаковые числа. Оба открываются по-русски, переключение на English и қазақша — в один клик.
Сценарий демонстрации:
docs/demo_script.md— сценарий живой демонстрации с реальными номерами счетов.
Содержание
- Описание решения и его назначения
- Описание архитектуры
- Используемые технологии
- Инструкции по установке
- Инструкции по запуску
- Необходимые зависимости
- Параметры окружения
- Порядок проверки основного сценария работы
- Критерии ролей
- Приоритет проверки
- Кластеризация
- Ограничения данных и как они учтены
- Схемы выгрузок
- Воспроизводимость
- Формулировки и приватность
- Ограничения подхода
- Масштабирование до ~1 млн узлов
- Структура репозитория
1. Описание решения и его назначения
Задача
Аналитику AML банка второго уровня правоохранительные органы передают список клиентов, получавших деньги от наркоторговли, — нижний уровень цепочки. Кто стоит выше: кто собирает эти деньги, через кого их перемещают и кто в итоге ими управляет — восстанавливается вручную, часами на один узел.
Что делает решение
На входе — выгрузка исходящих переводов 81 известного клиента на глубину 4 колена (июль 2026, внутрибанковские переводы от 5 000 ₸): 2 248 узлов, 3 119 рёбер, 4 840 транзакций.
На выходе — ответ на один вопрос: кого из 2 248 клиентов проверять первым и почему. Конкретно:
- каждому узлу присвоена роль из словаря, уверенность в роли (0–1), кластер и приоритет проверки (0–1);
- к каждой роли приложено обоснование с числами — теми самыми, которые сравнивало правило;
- три CSV-выгрузки фиксированной схемы;
- веб-интерфейс, где по номеру счёта за секунды видно роль, сработавшее правило и карту движения денег.
Для кого
Аналитик финансового мониторинга. Сценарий: получить список gid → загрузить выгрузку → открыть список приоритетов → прочитать обоснование по каждой позиции → сформировать перечень клиентов на углублённую проверку и запрос в правоохранительные органы.
Ключевое свойство
Решение не является «чёрным ящиком». Каждая роль — результат формального правила с порогом, и это правило показывается в интерфейсе вместе с числами, которые оно сравнивало. ИИ-агенты участвуют, но не присваивают роли (см. раздел 2).
2. Описание архитектуры
Главный принцип
Агенты ведут исследование. Движок правил выносит решение.
Агенты решают, что изучать, насколько глубоко и какими должны быть пороги.
Модуль roles.py определяет, какая роль, сравнивая метрику с порогом, и
формирует RuleTrace — структуру с сработавшим правилом, использованными
значениями и порогами. Требование ТЗ «роль без объяснимого правила не
засчитывается» выполняется по построению.
Конвейер
загрузка → профилирование данных
→ [ПЛАНИРОВЩИК: какие проходы нужны на этих данных]
→ граф → признаки → временные признаки → атрибуция
→ [КАЛИБРОВЩИК: пороги ролей, с симуляцией и сохранением]
→ роли (проход 1) → кластеризация → роли (проход 2: координатор)
→ приоритет → обоснования
→ [СЛЕДОВАТЕЛЬ: досье] → [КРИТИК: аргументы против]
→ [РЕДАКТОР: формулировки] → [РЕВИЗОР: запросы данных]
→ выгрузки
Полная схема: docs/diagram.md.
Агенты и их полномочия
| Агент | Что решает | Что его ограничивает |
|---|---|---|
| планировщик | какие проходы запускать на этих данных и насколько глубоко | жёсткий лимит в config.yaml, который он не может превысить |
| калибровщик | все пороги ролей, с письменным обоснованием каждого | обязан остаться в диапазоне, выведенном из перцентилей; предложение симулируется на реальных данных и отклоняется, если обнуляет роль или отдаёт одной роли больше половины сети; результат сохраняется для воспроизводимости |
| следователь | что изучать вокруг каждого приоритетного счёта (многошаговый цикл вызова функций) | каждое утверждение обязано исходить из результата функции; упомянутые gid должны существовать; обязан предложить правдоподобное безобидное объяснение |
| критик | что не так с итоговым списком: артефакты сбора, чувствительность порогов, слепые зоны | может ссылаться только на показанные ему счета; не может ничего понизить — вывод носит рекомендательный характер |
| редактор | только формулировки | каждое число в переписанной фразе обязано присутствовать в RuleTrace, иначе результат отбрасывается |
| загрузчик | какая колонка входного файла чему соответствует | ответ перепроверяется по самим данным до принятия |
| ревизор | как сформулировать запросы недостающих данных | сами пробелы вычисляются из графа |
| аналитик | какой запрос к графу отвечает на вопрос пользователя | отвечает только из вывода функций; протокол вызовов показывается рядом с ответом |
Ни один из них не присваивает роль, оценку, кластер или место в списке. У
каждого есть детерминированный запасной путь: ./agent_run.sh --offline
формирует те же три CSV с теми же ролями.
Механизм проверки формулировок
roles.pyприсваивает роль и формируетRuleTrace.evidence.pyзаполняет шаблон из этой структуры — всегда корректно.- Редактор может переписать фразу. Переписанное принимается, только если
проходит ограничение длины, список запрещённых слов и проверку чисел:
каждое число в тексте обязано выводиться из
RuleTrace. - Отклонения подсчитываются и попадают в
run_trace.md.
Журнал аудита
output_files/agent_log.md фиксирует план каждого агента, каждый вызов функции
с аргументами и результатом, каждое отклонённое решение с причиной и каждый
переход на запасной путь. В задачах комплаенса невозможность показать, как
получен вывод, сама по себе является проблемой, поэтому журнал — часть
результата, а не отладочный файл.
3. Используемые технологии
| Слой | Технологии | Зачем |
|---|---|---|
| Язык | Python 3.10+ (проверено на 3.13) | рекомендован организаторами |
| Данные | pandas, pyarrow, numpy | чтение parquet/CSV, векторные расчёты |
| Графы | networkx 3.7, scipy | направленный взвешенный граф, PageRank, betweenness, Louvain, компоненты связности |
| Конфигурация | PyYAML | все пороги и веса вне кода |
| Интерфейс | собственный фронтенд (HTML/CSS/чистый JS, vis-network вендорится локально) | по умолчанию: без сборки, без фреймворка, без сети |
| Интерфейс (альт.) | gradio 5.50, pyvis 0.3.2 | второй интерфейс, умеет публиковать общедоступную ссылку |
| Агенты | openai (совместимый клиент), python-dotenv | ИИ-слой; опционален во время выполнения |
| Тесты | pytest | 83 теста: контракт выгрузок, формулировки, агенты, интерфейс |
Намеренно не используется библиотека построения графиков. Единственная диаграмма в интерфейсе нарисована на CSS: иначе её отображение зависит от совпадения версий JS-пакета и фронтенда Gradio.
Не требуется: облако, GPU, платные сервисы. Интернет нужен только для внешнего LLM API, и только если он включён.
4. Инструкции по установке
Быстрый путь (рекомендуется)
git clone https://github.com/BAITC-Hacks/hack-e26f0e66-max.git
cd hack-e26f0e66-max
./agent_run.sh
Скрипт сам найдёт Python 3.10+, создаст .venv, установит зависимости из
requirements.txt, создаст .env из шаблона и продолжит расчёт. Отдельная
установка не нужна.
Ручной путь
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # ключ можно не заполнять
Проверка установки
.venv/bin/python -m pytest -q # ожидается: 83 passed
Требования к машине: обычный ноутбук. Расчёт занимает менее 2 секунд без ИИ-слоя и около 30–90 секунд с ним, при лимите ТЗ в 5 минут.
5. Инструкции по запуску
Одна команда
./agent_run.sh
Выполняет шесть шагов: окружение → зависимости → проверка модели → расчёт → проверка выгрузок → веб-интерфейс с публичной ссылкой.
Режимы
./agent_run.sh # полный запуск с ИИ-слоем (нужен .env)
./agent_run.sh --offline # без обращений к модели; выгрузки идентичны
./agent_run.sh --use-existing # пропустить расчёт, открыть интерфейс на прошлом результате
./agent_run.sh --data ./my_export # свои файлы вместо data/
./agent_run.sh --recalibrate # перезапустить агента-калибровщика
./agent_run.sh --no-app # остановиться после проверки выгрузок
./agent_run.sh --no-share # интерфейс только на localhost
./agent_run.sh --port 7861
Коды возврата: 0 — успех, 1 — сбой расчёта, 2 — выгрузки отсутствуют или
не соответствуют схеме, 3 — проблема окружения. Пригодно для CI.
По частям
.venv/bin/python run.py # только расчёт: data/ → output_files/
.venv/bin/python run.py --only-profile # только отчёт по данным
.venv/bin/python app/app.py --share # только интерфейс
make run | make offline | make existing | make test
Что создаётся в output_files/
| Файл | Содержимое |
|---|---|
nodes_roles.csv | обязательно. Строка на каждый из 2 248 счетов: gid, role, role_score, cluster_id, priority_score, evidence |
clusters.csv | обязательно. Строка на кластер: размер, число seed, внутренний оборот, топ-gid, гипотеза |
top_nodes.csv | обязательно. 30 счетов по убыванию приоритета с текстовым обоснованием |
node_features.parquet | все метрики и rule_trace каждого узла — это читает интерфейс |
graph_edges.parquet | рёбра, чтобы интерфейс никогда не обращался к data/ |
profile_report.md | профиль данных: все заявленные факты проверены, перцентили |
agent_log.md | журнал аудита ИИ-агентов |
review_notes.md | аргументы критика против итогового списка |
data_requests.md | чего не хватает и что запрашивать дальше |
dossiers.json | досье следователя по приоритетным счетам |
ingest_report.json | какой файл чем прочитан и какие колонки как распознаны |
extras_report.md / extras.json | опциональные анализы: возвратные потоки, повторяющиеся маршруты, устойчивость, флаги аномалий |
web_data.json | всё, что читает собственный фронтенд, на трёх языках |
run_trace.md / .json | время по этапам, токены, стоимость |
6. Необходимые зависимости
Полный список с закреплёнными версиями — requirements.txt.
Набор разрешён установщиком и проверен, а не выписан вручную: gradio
ограничивает pandas версиями ниже 3.0, поэтому весь стек стоит на линии
pandas 2.x.
pandas==2.3.3 pyarrow==25.0.1 numpy==2.5.3
networkx==3.7 scipy==1.18.1 PyYAML==6.0.3
gradio==5.50.0 pyvis==0.3.2
openai==3.19.0 python-dotenv==1.2.3
pytest==9.1.1
Замечания:
scipyобязателен:networkxпередаёт ему расчёт взвешенного PageRank.gradioзакреплён ниже 6.0:app/app.pyнаписан под API 5.x.openaiиpython-dotenvнужны только для ИИ-слоя. Без них расчёт и интерфейс работают полностью, агенты переходят на детерминированный путь.
Перед изменением любой версии следует перепроверить набор:
pip install --dry-run -r requirements.txt
7. Параметры окружения
Файл .env
Создаётся автоматически из .env.example и не попадает в
репозиторий. Поддерживаются три варианта.
1. OpenAI
OPENAI_KEY=sk-...
2. Свой или сторонний OpenAI-совместимый сервер — vLLM, Ollama, llama.cpp, TGI, LM Studio, OpenRouter, Together, внутренний шлюз:
MONEYGRAPH_BASE_URL=http://localhost:8000/v1
MONEYGRAPH_MODEL=Qwen/Qwen3-32B-Instruct
MONEYGRAPH_API_KEY=local # многие локальные серверы ключ игнорируют
Дополнительно в config.yaml: llm.api: chat и пустой
llm.reasoning_effort, если сервер не принимает этот параметр. Тарифы модели
стоит добавить в llm.pricing — для локальной модели поставить 0, и отчёт
честно покажет $0.00 вместо «не тарифицировано».
3. Без модели вообще
MONEYGRAPH_NO_LLM=1
Либо ./agent_run.sh --offline. Требование ТЗ «воспроизведение без платных
сервисов» выполняется.
Переменные окружения
| Переменная | Назначение | По умолчанию |
|---|---|---|
OPENAI_KEY / OPENAI_API_KEY / MONEYGRAPH_API_KEY | ключ API; берётся первый непустой | — |
MONEYGRAPH_BASE_URL | адрес OpenAI-совместимого сервера | API OpenAI |
MONEYGRAPH_MODEL | имя модели, переопределяет config.yaml | gpt-6-sol |
MONEYGRAPH_NO_LLM | 1 — полностью детерминированный запуск | не задана |
MONEYGRAPH_SHARE | 1 — публичная ссылка интерфейса | 0 |
MONEYGRAPH_HOST / MONEYGRAPH_PORT | адрес и порт интерфейса | 127.0.0.1:7860 |
Файл config.yaml
Все пороги, веса, лимиты и тарифы, каждый с комментарием о происхождении.
Ключевые разделы: roles (пороги ролей), priority (веса приоритета),
clustering, llm (модель, лимиты, тарифы), agents (включение каждого
агента), tracing (лимит времени), viewer, input (синонимы колонок).
Текущая модель: gpt-6-sol, reasoning_effort: low. Полный запуск с
ИИ-слоем — около 53 вызовов и 76 000 токенов, примерно $0.21. Лимит
llm.budget.max_usd действует на один запуск и равен $2.00, то есть
запас десятикратный. Тарифы gpt-6-luna и gpt-6-astra также прописаны —
смена модели занимает одну строку.
Файл config.calibrated.yaml
Пороги, выбранные агентом-калибровщиком, с обоснованием каждого. Файл
коммитится намеренно: благодаря ему запуск на машине проверяющего
воспроизводит ровно те числа, что были выбраны здесь. Перезапуск агента —
./agent_run.sh --recalibrate.
8. Порядок проверки основного сценария работы
Ниже — последовательность, которую можно выполнить целиком, не заглядывая в код.
Шаг 1. Развернуть и запустить
git clone https://github.com/BAITC-Hacks/hack-e26f0e66-max.git
cd hack-e26f0e66-max
./agent_run.sh --offline --no-app
Режим --offline выбран намеренно: он не требует ключа, ничего не стоит и
проверяет именно детерминированное ядро.
Ожидается:
[1/6] Python environment ✓
[2/6] Dependencies ✓
[3/6] Agent layer ! offline mode
[4/6] Pipeline ✓ pipeline finished in 2s
[5/6] Deliverables
nodes_roles.csv 2248 ok
clusters.csv 88 ok
top_nodes.csv 30 ok
✓ all three required CSVs are present and valid in output_files/
Шаг 2. Проверить обязательные выгрузки
wc -l output_files/nodes_roles.csv # 2249 (2248 строк + заголовок)
head -2 output_files/nodes_roles.csv # gid,role,role_score,cluster_id,priority_score,evidence,...
head -4 output_files/top_nodes.csv # rank,gid,role,priority_score,why
Схема проверяется автоматически на шаге 5 скрипта: наличие файлов, порядок
обязательных колонок, отсутствие пустых значений, уникальность gid, не менее
20 строк в top_nodes.csv.
Шаг 3. Проверить объяснимость (must-have 3 из ТЗ)
Сценарий «жюри называет три произвольных gid — команда за минуту объясняет роль». Из терминала:
.venv/bin/python - <<'PY'
import json, pandas as pd
f = pd.read_parquet("output_files/node_features.parquet").set_index("gid", drop=False)
for gid in pd.read_csv("output_files/top_nodes.csv").gid[:3]:
tr = json.loads(f.at[gid, "rule_trace"])
print(f"\ngid {gid} → {tr['role']}")
print(f" правило: {tr['gate']}")
print(f" значения: {tr['metrics']}")
print(f" пороги: {tr['thresholds']}")
PY
Каждый узел возвращает сработавшее правило, сравнённые значения и пороги. То же самое показывается в интерфейсе на вкладке «Карточка счёта».
Шаг 4. Проверить воспроизводимость
.venv/bin/python -m pytest -q -m slow
Тест запускает расчёт дважды и сравнивает хеши CSV — они обязаны совпасть. Второй тест подтверждает, что выгрузки формируются без доступа к модели.
Шаг 5. Открыть интерфейс
./agent_run.sh --use-existing
Интерфейс откроется на http://127.0.0.1:7860 и выдаст публичную ссылку.
Стартовый язык — русский; English и қазақша — справа вверху. При выборе
қазақша показывается предупреждение о машинном переводе.
Первая вкладка «О решении» объясняет остальные восемь и содержит порядок демонстрации из трёх шагов.
Проверка основного сценария в интерфейсе:
- «Кого проверять первым» — список из 30 счетов, отсортированный по приоритету, с колонкой обоснования. Клик по строке раскрывает карточку.
- «Карточка счёта» — ввести любой gid (например, из
top_nodes.csv). Должны появиться: роль, сработавшее правило с числами, карта связей со стрелками направления денег, таблицы плательщиков и получателей. - «Как принято решение» — пороги, выбранные агентом, с обоснованиями; досье; аргументы критика против результата; полный журнал агентов.
- «Время и стоимость» — время по этапам против лимита в 300 секунд.
Шаг 6 (необязательно). Проверить ИИ-слой
cp .env.example .env && $EDITOR .env # вписать OPENAI_KEY
./agent_run.sh
В выводе видны действия планировщика, калибровщика, следователя (параллельно), критика и ревизора, а также итог по токенам и стоимости.
Шаг 7 (необязательно). Проверить работу на других данных
./agent_run.sh --data /путь/к/своей/выгрузке
Читаются .parquet, .csv, .tsv, .json, .jsonl, .xlsx. Результат
разбора колонок виден на вкладке «Ваши данные» и в ingest_report.json.
9. Критерии ролей
Все пороги — в config.yaml с указанием распределения, из которого они
получены. MAX_DEPTH читается из данных, а не задан константой.
| Роль | Правило | Порог | Обоснование |
|---|---|---|---|
consolidator (точка сбора) | in_deg >= min_payers | 5 | p95 = 3, p99 = 6. Порог 5 отбирает 51 узел (2,3 %) — «необычно много разных плательщиков», а не «больше одного». Допустима на любом колене: сбор виден со стороны входа. |
distributor (распределитель) | out_deg >= min_recipients и out_deg >= fan_ratio × max(in_deg,1) | 10, ×3 | p95 = 5, p99 = 24. Порог 10 даёт 64 узла; множитель снижает до 56 и не даёт записать в распределители узел, который одновременно много собирает. |
transit (транзит) | обе стороны потока наблюдаемы, pass_ratio ∈ [0.8, 1.2], in_deg < 5, out_deg < 10 | 0.8–1.2 | в полосе 70 узлов (72 с учётом seed, которые исключены). Уверенность повышается, если ≥ 50 % суммы уходит в течение 2 дней. |
terminal (конечный получатель) | исходящие прослежены, in_sum > 0, out_sum <= max_pass × in_sum | 0.1 | никогда не присваивается на MAX_DEPTH. Роль широкая по природе данных, поэтому имеет низкий вес в приоритете. |
coordinator (координатор) | проходит ворота consolidator и n_key_payers >= 2 и seed_reach >= p90 | 2, p90 | точка сбора, собирающая с других точек сбора. Нужны все три условия (см. ниже). 10 счетов, 0,4 %. |
cutoff (обрыв обхода) | depth == MAX_DEPTH и ни одно правило не сработало | — | документированное расширение словаря, включается use_extended_roles. Фиксированная уверенность 0,7. |
peripheral (периферия) | ничего не сработало | — | включает 19 изолированных seed. Уверенность выражает уверенность в отсутствии сигнала: 0,9 при ≤ 1 ребре, 0,6 при нескольких. |
Приоритет при совпадении: coordinator > consolidator > distributor > transit > terminal > cutoff > peripheral. Остальные сработавшие правила
сохраняются в secondary_roles.
Уверенность (role_score): 0.5 + 0.5 × clip((метрика − порог) / (сильный − порог), 0, 1), затем ×0.8 на MAX_DEPTH (исходящие неизвестны) и
×0.8, если сторона входа ненадёжна.
Seed-клиенты никогда не получают transit и terminal: их вход занижен
устройством выгрузки, соотношение бессмысленно. Могут быть distributor,
consolidator или peripheral.
Почему координатору нужны все три условия
Стартовое правило из ТЗ (n_key_payers >= 2 И seed_reach >= 5) на этих данных
присваивало роль 264 счетам — 11,7 % графа, из них 103 имели роль
terminal, то есть деньги у них заведомо остаются. Это противоположность
управляющему узлу. Две причины:
seed_reach >= 5отбирает 71 % графа. Диапазон метрики здесь p50 = 7, максимум 15: крупнейшая компонента содержит 46 seed, которые питают почти всё ниже по потоку. Абсолютный порог по метрике, диапазон которой целиком определяется данными, не несёт информации. Теперь это перцентиль (p90), который пересчитывается под любые входные данные.- Получать платежи от двух сборщиков — не то же самое, что быть точкой сбора. Координатор обязан сам проходить ворота consolidator. Одно это условие снижает счёт с 264 до 10, и все десять — точки сбора, что и соответствует определению: собирает с других сборщиков.
Конвейер предупреждает, если coordinator превышает 2 % графа.
Фактическое распределение на данных организаторов
| Роль | Счетов | |
|---|---|---|
terminal | 1 100 | широкая по природе данных: большинство — обычные листья, у которых просто не было переводов выше порога 5 000 ₸ |
peripheral | 540 | |
cutoff | 444 | ровно заявленное число узлов 4-го колена без исходящих |
transit | 67 | |
distributor | 46 | |
consolidator | 41 | |
coordinator | 10 | верхний уровень, 0,4 % графа |
Полный расчёт: 1,6 с при лимите 300 с.
10. Приоритет проверки
Взвешенная сумма перцентильных рангов, а не сырых значений: оборот охватывает четыре порядка величины, поэтому ранжирование до взвешивания — это то, что делает веса осмысленными.
| Компонент | Вес | Смысл |
|---|---|---|
| вес роли | 0.30 | coordinator 1.0 · consolidator 0.9 · distributor 0.6 · transit/terminal 0.5 · cutoff 0.3 · peripheral 0.05 |
seed_kzt_attributed | 0.25 | сколько денег от seed правдоподобно прошло через узел |
seed_reach | 0.20 | сколько независимых цепочек до него дотягиваются |
in_deg | 0.15 | сколько разных плательщиков |
| плотность seed в кластере | 0.10 | насколько насыщено окружение |
Две документированные поправки:
- Известные seed × 0.5. Правоохранительным органам эти 81 клиент уже известны; ценность — в том, кто над ними, поэтому статус seed понижает приоритет.
- Сборщики на границе обхода × 1.1 (с ограничением 1.0).
consolidatorилиcoordinatorна MAX_DEPTH — наиболее вероятное место продолжения цепочки и лучший кандидат на запрос данных 5-го колена.
top_nodes.csv содержит 30 счетов. Колонка why называет компоненты, которые
действительно определили место, — они берутся из сохранённых вкладов, а не
угадываются.
11. Кластеризация
Louvain (seed=42, разрешение из конфигурации) на неориентированной
проекции с весами log1p(sum_kzt). Отбрасывание направления — реальная
уступка, и она заявлена открыто: выделение сообществ требует
неориентированного графа, а все роли, метрики и стрелки, показываемые
аналитику, используют направленный.
Мелкие компоненты никогда не поглощаются крупными кластерами: всё, что
меньше min_cluster_size, становится отдельным кластером, каждый изолированный
seed — одиночным. Нумерация по убыванию размера с разрешением ничьих по
наименьшему gid, поэтому она стабильна между запусками.
Устойчивость измеряется, а не предполагается: Louvain перезапускается с 5 разными сидами, и по каждому кластеру считается доля пар узлов, остающихся вместе. На данных организаторов — среднее 0,95, 78 кластеров с устойчивостью ≥ 0,80.
11a. Опциональные анализы (ТЗ §8)
Все четыре реализованы, детерминированы и занимают около 0,1 с. Ни один из них не меняет роль, оценку или место в списке — это флаги и контекст рядом со счётом, ровно как требует ТЗ.
| Анализ | Что даёт |
|---|---|
| Артефакт обрыва обхода | в ядре: роль cutoff, отсутствие terminal на MAX_DEPTH, повышение приоритета сборщиков на границе |
| Временные паттерны | median_lag_days, fast_pass_share (по сумме, а не по количеству), max_payers_same_day для синхронного сбора |
| Возвратные потоки и маршруты | направленные циклы в пределах длины и повторяющиеся цепочки A→B→C с передачей в течение трёх дней. Перебор ограничен, и отчёт пишет «не менее N», а не выдаёт за итог число, которое не считал |
| Устойчивость сети | удаление топ-5/10/20 по приоритету с измерением крупнейшей компоненты, числа компонент и дальности прослеживания денег — против базовой линии случайного удаления, потому что «сеть распадается» без неё ничего не значит |
| Флаги аномалий | суммы чуть выше порога отчётности (как выглядит дробление с видимой стороны), круглые суммы и счета, экстремальные для своего колена |
Текущий запуск: 57 счетов с флагами, ≥200 циклов, ≥50 повторяющихся маршрутов; удаление топ-20 сокращает крупнейшую компоненту на 3,4 % против 1,7 % при случайном удалении — вдвое больший ущерб.
12. Ограничения данных и как они учтены
| Заявленное ограничение | Как учтено |
|---|---|
| Обрыв на 4-м колене (444 узла без исходящих) | outflow_observed = depth < MAX_DEPTH. Эти узлы никогда не получают terminal — это и есть решение проблемы 444 ложных «стоков». Им присваивается роль cutoff, уверенность умножается на 0,8, а если узел ещё и собирает — его приоритет повышается: точка сбора на границе видимых данных лучший кандидат на запрос 5-го колена. |
| Только исходящие переводы | pass_ratio считается только там, где обе стороны надёжны, иначе NaN с записанной причиной — никогда не тихий ноль. |
| Занижен вход у seed | seed получают pass_ratio = NaN и не допускаются к ролям transit и terminal. Их сырое отношение доходит до 53 — это артефакт выгрузки, а не поведение. |
| Порог 5 000 ₸ | дробление сумм ниже порога невидимо. Зафиксировано как пробел и превращено в конкретный запрос в data_requests.md. |
| 19 seed отсутствуют в рёбрах, 12 только получают | все присутствуют как узлы, роль peripheral, обоснование прямо это указывает. Никогда не отбрасываются: в выгрузке обязана быть строка на каждого участника. |
| 16 компонент + 19 изолированных seed | компоненты не сливаются. Всё, что меньше min_cluster_size, — отдельный кластер, каждый изолированный seed — одиночный. |
| Нет атрибутов клиентов | используются только структура, суммы и даты. Правила формулировок запрещают выдумывать что-либо ещё. |
| Нет разметки ролей | ничего не сверяется с эталоном, ни один вывод не претендует на достоверность. Пороги обоснованы перцентилями, а не подгонкой. |
13. Схемы выгрузок
nodes_roles.csv — строка на счёт, по возрастанию gid.
| Колонка | Тип | Смысл |
|---|---|---|
gid | int64 | идентификатор клиента |
role | str | одна из семи ролей |
role_score | float | уверенность в роли, 0–1 |
cluster_id | int64 | номер кластера |
priority_score | float | приоритет проверки, 0–1 |
evidence | str | обоснование с числами, ≤ 200 символов |
Далее идут контекстные колонки (depth, is_seed, степени, обороты,
pass_ratio, seed_reach, seed_kzt_attributed, secondary_roles, два флага
наблюдаемости). Лишние колонки разрешены ТЗ; шесть обязательных идут первыми в
заданном порядке.
clusters.csv — cluster_id, n_nodes, n_seed, sum_kzt_internal, top_gids, hypothesis. sum_kzt_internal суммирует рёбра с обоими концами внутри
кластера; top_gids — пять счетов с наибольшим приоритетом через ;.
top_nodes.csv — rank, gid, role, priority_score, why, 30 строк,
ранги 1..N.
Выгрузки формируются на английском языке: их схему жюри проверяет
механически, а список запрещённых слов определён по-английски. Интерфейс
переводит обоснования на лету, пересобирая их из rule_trace.
14. Воспроизводимость
- Пороги и веса — в
config.yaml, каждый с указанием распределения. В коде магических чисел нет. - Ни один gid не зашит в код. Тест ищет в
src/иapp/литералы из 5+ цифр и падает на всём, что не является объявленным значением конфигурации. - Один сид (
seed: 42) управляет Louvain, выборкой для оценки устойчивости и раскладкой карт. Тест запускает расчёт дважды и сравнивает хеши файлов. - Пороги, выбранные агентом, сохраняются, а не пересэмплируются.
config.calibrated.yamlкоммитится, поэтому запуск на машине проверяющего использует ровно те числа, которые выбрал калибровщик здесь.--recalibrate— явное согласие их изменить. - Версии закреплены в
requirements.txt. Облако, GPU и платные сервисы не требуются. - Заявленные факты проверяются, а не принимаются на веру: раздел 1
profile_report.mdсверяет каждое число из ТЗ с файлами и громко сообщает о расхождениях.
15. Формулировки и приватность
Каждая порождаемая строка — evidence, why, hypothesis, текст запросов
данных, любой ответ агента — сформулирована как гипотеза для проверки
(«признаки сбора», «характерно для транзита», «кандидат на проверку»), а не как
утверждение о человеке. Слова criminal, guilty, launderer, organizer is,
confirmed внесены в чёрный список в config.yaml, проверяются в валидаторе
каждого агента, ещё раз перед записью выгрузок и в тестах.
Данные анонимизированы: gid — синтетический идентификатор. Имена, возраст,
пол, доход и организации не используются, а промпты агентов прямо запрещают их
выдумывать.
16. Ограничения подхода
- Атрибуция денег от seed — эвристика. Деньги обезличены: после смешения
двух входящих потоков ни одна выгрузка не скажет, какой тенге куда ушёл.
seed_kzt_attributed— оценка подверженности сверху, а не бухгалтерский факт. - Пороги обоснованы, но не оптимальны. Они выведены из наблюдаемых перцентилей и намеренно круглые. Эталона нет, подгонять не к чему, а подгонка до десятых только выглядела бы точнее.
- Роль
terminalширокая. Ей соответствует половина графа, потому что у большинства листьев просто не было исходящих выше порога. Формально верно и почти неинформативно — отсюда низкий вес в приоритете. - Направление отбрасывается при кластеризации. Заявлено выше; влияет только на группировку.
- Один месяц, один банк. Ничто здесь не отличает устойчивую структуру от совпадения в пределах одного месяца.
- Проверки не было. При отсутствии разметки любой вывод — средство расстановки приоритетов для аналитика, а не установленный факт.
17. Масштабирование до ~1 млн узлов
В порядке того, что упрётся первым:
- Betweenness убирается первым. Точный расчёт — O(VE), и это уже самый
дорогой этап. На 1 млн узлов: приближение по выборке (
k≈1000опорных точек) либо отказ — он не участвует ни в одном правиле, только в контексте. - Графовая библиотека.
networkxхранит Python-объект на узел. Переход наpython-igraphилиgraph-tool(ядра на C, ускорение в 10–100 раз) не затрагивает логику правил, которая читает только скаляры по узлу. - Louvain → Leiden. Leiden гарантирует связность сообществ и быстрее на
больших графах; есть в
igraph. Оценка устойчивости перешла бы на выборку компонент вместо полного перезапуска. - Атрибуция становится разреженной линейной алгеброй. Текущее
распространение по раундам — это произведение разреженной матрицы на вектор:
scipy.sparseсMAX_DEPTH+1умножениями либо одинspsolveдля(I − αA).seed_reachпревращается в произведение битовых множеств по компонентам. - Ввод-вывод. DuckDB или Polars поверх parquet с проталкиванием предикатов, чтобы загрузка никогда не материализовала весь кадр.
- Параллелизм по компонентам. Граф уже несвязен — здесь 16 компонент. Признаки, роли и кластеризация локальны для компоненты.
- Пороги уже выведены из перцентилей, поэтому пересчитываются автоматически. Именно поэтому в отчёте они записаны как перцентили, а не как подогнанные константы.
- Интерфейс перестаёт загружать граф целиком. Предрасчитанный бэкенд — Neo4j или индекс смежности в parquet — отвечающий на запросы окрестности по требованию. Контракт интерфейса уже «читать только выгруженное», поэтому меняется только слой хранения.
- Слой агентов почти не меняется. Он уже ограничен не размером графа, а числом изучаемых узлов: короткий список плюс лидеры кластеров.
18. Структура репозитория
.
├── agent_run.sh # ОДНА команда: установка → расчёт → проверка → интерфейс
├── run.py # точка входа расчёта
├── config.yaml # пороги, веса, агенты, лимиты, тарифы
├── config.calibrated.yaml # пороги, выбранные агентом (генерируется, КОММИТИТСЯ)
├── requirements.txt # закреплённые версии
├── .env.example # три варианта настройки модели
├── Makefile # make run | offline | existing | test
├── task_guidelines.md # рабочее техзадание
├── final_deliverables/ # итоговые файлы для жюри (генерируется)
├── data/ # входные файлы организаторов (только чтение)
├── starter/ # стартовый код организаторов (только чтение)
├── src/moneygraph/
│ ├── schema.py # поиск файлов и распознавание колонок
│ ├── io.py # канонический Dataset и происхождение данных
│ ├── profile.py # отчёт по данным
│ ├── graph.py # DiGraph по всем gid
│ ├── features.py # степени, обороты, центральности
│ ├── temporal.py # задержка, скорость транзита, синхронный сбор
│ ├── attribution.py # seed_reach, seed_kzt_attributed
│ ├── roles.py # движок правил и RuleTrace
│ ├── clustering.py # Louvain и оценка устойчивости
│ ├── priority.py # взвешенная сумма перцентилей
│ ├── evidence.py # шаблоны и проверка формулировок
│ ├── i18n.py # три языка интерфейса и обоснований
│ ├── extras.py # опциональные анализы (ТЗ §8)
│ ├── webexport.py # собирает web_data.json для фронтенда
│ ├── export.py # выгрузки
│ ├── trace.py # время, токены, стоимость
│ ├── pipeline.py # только оркестрация
│ └── agents/
│ ├── base.py # каркас агента: план → действие → проверка → запасной путь
│ ├── orchestrator.py # команда агентов и agent_log.md
│ ├── llm.py # клиент провайдера с трассировкой и лимитами
│ ├── tools.py # детерминированные функции по графу
│ ├── calibrator_agent.py # выбирает пороги ролей
│ ├── investigator_agent.py# многошаговые досье
│ ├── critic_agent.py # аргументы против списка
│ ├── ingest_agent.py # незнакомые схемы данных
│ ├── narrator_agent.py # RuleTrace → текст, с проверкой
│ ├── analyst_agent.py # вопросы на естественном языке
│ └── review_agent.py # запросы недостающих данных
├── web/ # собственный фронтенд — интерфейс ПО УМОЛЧАНИЮ
│ ├── index.html · styles.css · app.js · serve.py
├── app/app.py # интерфейс Gradio (en / ru / kk)
├── docs/
│ ├── diagram.md # схема решения (Mermaid)
│ ├── demo_script.md # сценарий живой демонстрации
│ └── case_brief.docx # описание кейса от организаторов
├── output_files/ # результаты расчёта
└── tests/test_outputs.py # 83 теста
Авторы коммитов
История коммитов
- 2026-09-23 17:41upload: finalAkylbek Maxutov
- 2026-09-23 17:38upload: finalAkylbek Maxutov
- 2026-09-23 17:23upload: fixesAkylbek Maxutov
- 2026-09-23 16:58upload: fixesAkylbek Maxutov
- 2026-09-23 16:31upload: fixesAkylbek Maxutov
- 2026-09-23 15:45upload: visualizationAkylbek Maxutov
- 2026-09-23 14:43upload: core_functionalityAkylbek Maxutov
- 2026-09-23 13:56upload: starter and initial filesAkylbek Maxutov
Структура репозитория (83 файлов)
.env.example .gitignore CLAUDE.md Makefile README.en.md README.kk.md README.md agent_run.sh app/app.py config.calibrated.yaml config.yaml data/README.md data/edges.parquet data/nodes.parquet data/transactions.parquet docs/case_brief.docx docs/demo_script.md docs/diagram.md final_deliverables/README.md final_deliverables/clusters.csv final_deliverables/diagram.md final_deliverables/nodes_roles.csv final_deliverables/top_nodes.csv output_files/agent_log.md output_files/clusters.csv output_files/data_requests.md output_files/dossiers.json output_files/extras.json output_files/extras_report.md output_files/graph_edges.parquet output_files/ingest_report.json output_files/node_features.parquet output_files/nodes_roles.csv output_files/profile_report.md output_files/review_notes.md output_files/run_trace.json output_files/run_trace.md output_files/top_nodes.csv output_files/web_data.json pytest.ini requirements.txt run.py src/moneygraph/__init__.py src/moneygraph/agents/__init__.py src/moneygraph/agents/analyst_agent.py src/moneygraph/agents/base.py src/moneygraph/agents/calibrator_agent.py src/moneygraph/agents/critic_agent.py src/moneygraph/agents/ingest_agent.py src/moneygraph/agents/investigator_agent.py src/moneygraph/agents/llm.py src/moneygraph/agents/narrator_agent.py src/moneygraph/agents/orchestrator.py src/moneygraph/agents/review_agent.py src/moneygraph/agents/tools.py src/moneygraph/attribution.py src/moneygraph/clustering.py src/moneygraph/deliverables.py src/moneygraph/evidence.py src/moneygraph/export.py src/moneygraph/extras.py src/moneygraph/features.py src/moneygraph/graph.py src/moneygraph/i18n.py src/moneygraph/io.py src/moneygraph/pipeline.py src/moneygraph/priority.py src/moneygraph/profile.py src/moneygraph/roles.py src/moneygraph/schema.py src/moneygraph/temporal.py src/moneygraph/trace.py src/moneygraph/webexport.py starter/README.md starter/requirements.txt starter/starter.py task_guidelines.md tests/conftest.py tests/test_outputs.py web/app.js web/index.html web/serve.py web/styles.css
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.