О проекте
Команда: RmT
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Рабочий MVP
Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)
Суть
Локальный объяснимый AML-пайплайн по графу внутрибанковских переводов. Даёт роль, role_score и evidence каждому из 2248 клиентов, выделяет кластеры с гипотезами и формирует TOP для проверки. Есть веб-интерфейс и опциональный LLM-ассистент.
Как устроено
Три Parquet-файла обрабатываются одной командой run.py и превращаются в три CSV (nodes_roles, clusters, top_nodes). Роли назначаются по формальным правилам, validate.py и verify_mvp.py сверяют числа в объяснениях. Локальный Python-сервер отдаёт UI с направленной схемой и поиском по GID. Опциональный OpenAI-агент через Responses API вызывает только строго типизированный query_graph из белого списка операций чтения, а числа и финальный текст возвращает Python.
Ключевые решения
- формальные правила ролей
- кластеризация графа
- function calling (query_graph)
- SHA-256 manifest provenance
- направленные пути
Метрики
2248 узлов, 68 кластеров, top-30, 3119 рёбер в UI; расчёт укладывается в лимит 5 минут
Особенности
out/manifest.json связывает SHA-256 входных данных, аналитического кода и результатов, поэтому устаревший расчёт нельзя открыть как актуальный. LLM выбирает только типизированный запрос к графу и не генерирует числа. В UI честно различаются режимы: OpenAI настроен, OpenAI проверен, локальный разбор.
Стек
Python 3.14 · Parquet · JavaScript · HTML/CSS · unittest
Модели
gpt-4.1-mini (OpenAI Responses API)
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-0ac3a7a7-rmt.
README
README.mdFreedom AML — граф денег
Локальный объяснимый анализ четырёхколенного графа внутрибанковских переводов. Пайплайн назначает роль каждому из 2 248 клиентов, выделяет сообщества и отвечает на рабочий вопрос AML-аналитика: кого проверять первым и почему.
Для технического отбора: что проверять по ТЗ
Кейс Freedom / HackAlem AI: «Граф денег: восстановление финансовой структуры организованной группы по транзакционной сети». Пользователь — AML-аналитик. Основной вопрос: кого из наблюдаемых клиентов проверять первым и почему. Вход — разовая четырёхколенная выгрузка внутрибанковских переводов; онлайн-поток по ТЗ не требуется.
Предоставленная выборка: 81 seed, июль 2026, только исходящий обход до четырёх колен, порог включения транзакций 5 000 KZT. Глубины 0–4 содержат соответственно 81 / 472 / 462 / 789 / 444 узла. Все результаты ниже ограничены этой наблюдаемой сетью, а не полным банковским оборотом клиентов.
Обязательный сценарий сдачи: исходные Parquet → команда в терминале → три CSV в out/ → интерфейс просмотра. Загружать полученные CSV обратно на сайт не нужно. Кнопка загрузки Parquet на сайте — дополнительный способ вызвать тот же пайплайн, а AI-ассистент — опциональная функция, не замена расчёту и не условие запуска.
| Must have из ТЗ | Реализация и способ проверки |
|---|---|
| Один запуск от Parquet до трёх CSV, менее 5 минут на исходном объёме | python run.py; измерение расчёта в out/manifest.json, контроль выгрузок в конце запуска |
| Роль, role_score и evidence у всех 2 248 узлов | python validate.py --expected-nodes 2248; все GID сохранены, включая изоляты; обязательные поля заполнены, evidence содержит числа и не длиннее 200 символов |
| Формальные объяснимые критерии ролей | Раздел «Формальные правила ролей» ниже; поиск произвольного GID → «Роль и признаки»; python verify_mvp.py сверяет численные объяснения всех узлов |
| Кластеризация и гипотеза по каждому кластеру | out/clusters.csv: размер, число seed, внутренний объём, лидеры, гипотеза; раздел интерфейса «Кластеры» |
| Ранжированный TOP ≥20 и направленная схема с поиском | out/top_nodes.csv содержит 30 строк на исходной выборке; на сайте есть поиск GID, стрелки, цвет по роли/кластеру и полный список прямых связей |
Обязательные артефакты: исходники в репозитории, этот README, три CSV в out/, диаграмма в разделе «Схема решения», сценарий живого пятиминутного демо в DEMO.md. Число кластеров не задано ТЗ: наши 68 — результат описанных ниже правил, а не заранее зашитый ответ.
Требования и подготовка
- Проверено: CPython 3.14.0, Windows x64, зависимости из
requirements.lock.txt, обычный CPU без GPU. Node.js, npm, Docker, база данных, облако и платные API для запуска не нужны. - Нужен браузер с JavaScript и свободный локальный порт 8000 (порт можно изменить).
- Распакуйте репозиторий в доступную для записи папку. Команды ниже выполняются из её корня, где лежит
run.py. Распаковывать содержимое.venvс другого компьютера не нужно: окружение создаётся заново. - Три исходных файла включены в папку
data/. Если получаете их отдельно от организаторов, положите их туда с точными именамиnodes.parquet,edges.parquet,transactions.parquet. Данные кейса предназначены для использования в рамках хакатона; не публикуйте их за его пределами без разрешения. - Для первой установки Python-библиотек нужен доступ к репозиторию пакетов либо заранее подготовленные пакеты для вашей ОС/Python. После установки обязательный расчёт и просмотр полностью локальны и не используют сеть. Для повторного запуска без установки есть
--skip-install. - Проверка нового виртуального окружения выполнена на том же Windows-компьютере. Другие ОС и версии Python не заявляются как проверенные; отдельная машина техотбора ещё не тестировалась. Подробности: VERIFICATION.md.
Windows / PowerShell: запуск с нуля
Установите Python 3.14 x64 с pip и Windows Python Launcher. Откройте PowerShell в папке проекта:
py -3.14 --version
py -3.14 -m venv .venv
.\.venv\Scripts\python.exe run.py
Первые две команды проверяют Python и создают изолированное окружение. Последняя команда — единственный запуск пайплайна по ТЗ: устанавливает зафиксированные зависимости, обрабатывает все три исходных файла, формирует выгрузки и проверяет их. Активировать окружение и менять PowerShell ExecutionPolicy не требуется.
Если py не найден, но python --version показывает 3.14.x, вместо py -3.14 -m venv .venv используйте python -m venv .venv. Не смешивайте pip одного интерпретатора с python другого.
Успешный запуск завершается кодом 0 и сообщением для исходной выборки:
Output validation: OK - 2248 nodes, 68 clusters, top-30, 3119 UI edges
Затем запустите просмотр уже рассчитанных результатов:
.\.venv\Scripts\python.exe server.py --use-prebuilt
После сообщения Freedom AML ready откройте http://127.0.0.1:8000/ui/. Терминал должен оставаться открытым; остановка сервера — Ctrl+C. Сайт использует локальный Python-backend: двойной клик по ui/index.html и python -m http.server не заменяют запуск сервера.
Для проверки откройте второй PowerShell в корне проекта:
.\.venv\Scripts\python.exe validate.py --expected-nodes 2248
.\.venv\Scripts\python.exe verify_mvp.py --server http://127.0.0.1:8000
.\.venv\Scripts\python.exe -m unittest discover -s tests -v
Ожидается успешная валидация, совпадение скачиваемых CSV с out/, проверка объяснений всех узлов и OK для тестов. Проверка --expected-nodes 2248 относится только к исходной выборке, не к новым наборам другого размера. Тесты создают свои временные данные, не заменяют активный пользовательский датасет и не вызывают платный LLM API.
В остальных разделах python означает интерпретатор этого же окружения; на Windows без активации заменяйте его на .\.venv\Scripts\python.exe.
Linux / macOS: эквивалентные команды, без заявления о проверке
Если установлен Python 3.14 с venv и pip, схема запуска такая же. Наличие бинарных пакетов зависит от ОС и архитектуры; этот вариант не проходил отдельный тест в рамках текущей проверки.
python3.14 -m venv .venv
.venv/bin/python run.py
.venv/bin/python server.py --use-prebuilt
Один запуск и повторный расчёт
Проверенное окружение: Python 3.14, Windows x64. Положить три исходных parquet-файла в data/ и выполнить из корня проекта:
python run.py
Команда устанавливает зафиксированные зависимости из requirements.lock.txt, пересчитывает все обязательные выгрузки и запускает механическую проверку результата. Первая установка библиотек требует сети; дальнейший расчёт — нет. Рекомендуется отдельное виртуальное окружение. Повторный запуск без установки зависимостей:
python starter.py --data ./data --out ./out
python validate.py
Для интерфейса с backend, загрузкой данных и ассистентом:
python server.py
Открыть http://127.0.0.1:8000/ui/. Или одной командой установить зависимости, сформировать файлы и запустить приложение: python run.py --serve. При готовом окружении: python run.py --serve --skip-install. Остановить сервер: Ctrl+C. Обычный http.server больше не подходит: интерфейс использует API.
run.py --serve считает данные один раз, затем сервер использует out/. python server.py сначала обновляет out/; python server.py --use-prebuilt только проверяет уже готовые результаты. out/manifest.json связывает SHA-256 исходных файлов, аналитического кода и результатов: изменённый или устаревший расчёт нельзя открыть как актуальный. В браузере вводить CSV не нужно. Исходный граф берётся из data/, роли и приоритеты — из out/. После загрузки новых Parquet через UI активной становится отдельная проверенная папка .runtime/datasets/<version>/out/; скачивание CSV всегда относится к активному снимку.
Расчёт, загрузка и локальные сценарии ассистента работают без API-ключа, GPU и интернета. OpenAI — отдельная необязательная функция. Сервер доступен только на loopback: это локальный прототип, не многопользовательское банковское развёртывание.
Подключение OpenAI
Задать OPENAI_API_KEY в окружении процесса сервера и перезапустить сервер. В PowerShell ключ можно ввести скрыто, без записи значения в историю команд:
$amlSecret = Read-Host 'OpenAI API key' -AsSecureString
$env:OPENAI_API_KEY = [System.Net.NetworkCredential]::new('', $amlSecret).Password
$env:OPENAI_MODEL = 'gpt-4.1-mini'
.\.venv\Scripts\python.exe server.py
Модель можно заменить через OPENAI_MODEL на доступную вашему проекту с поддержкой Responses/function calling. .env.example — только памятка: .env автоматически не загружается. Ключ не должен попадать в UI, git или чат. Если порт 8000 занят, сначала остановите прежний процесс либо используйте --port 8001.
В интерфейсе честно различаются «OpenAI · настроен» (наличие ключа, не проверка доступа), «OpenAI · проверенные расчёты» (успешный вызов) и «Локальный разбор · без LLM». При таймауте/ошибке API отображается предупреждение и используется локальный сценарий. Live-вызов OpenAI требует действующего ключа и тарифицируется провайдером; тесты его не вызывают.
Модель выбирает строго типизированный query_graph через Responses API. Исполнение — только белый список чтения: обзор, профиль, TOP/роль, кластер, направленный путь, общие прямые получатели, дневная активность, контрагенты, ограничения. Произвольный код, SQL, запись данных и действия с клиентами недоступны. GID модели сверяются с вопросом/выбранным клиентом и текущим датасетом. Числа и финальный текст возвращает Python, а не свободная генерация. Это ограниченный query-agent, не автономный расследователь. Каждый вопрос независим; история показана в UI, но не отправляется модели.
В OpenAI передаются вопрос и выбранный GID; сами Parquet и полный граф не передаются. Вопрос также может содержать финансовые сведения — не вводите данные, передачу которых ваша организация не разрешает. Используется store=false; это не обещание нулевого хранения данных провайдером.
Новые данные и непрерывная работа
Основной способ из ТЗ: остановите сервер, сохраните нужные предыдущие результаты и замените все три файла в data/ на согласованный новый набор. Затем выполните python run.py --skip-install и python server.py --use-prebuilt. Команда перезаписывает результаты в out/. Если ранее использовался импорт через UI, сервер может восстановить сохранённый импорт; выберите «Загрузить данные» → «Вернуть исходные данные», чтобы открыть текущий набор из data/.
Можно рассчитать внешний каталог без замены data/: python starter.py --data ./other-data --out ./other-out, затем python validate.py --data ./other-data --out ./other-out. Это CLI-выгрузка: сервер не переключается на произвольную папку other-out автоматически. Для показа этого набора используйте основной способ выше или импорт исходных Parquet через UI. Один набор из трёх CSV недостаточен для сайта: нужны также исходные рёбра/транзакции и дополнительные результаты пайплайна.
Ожидаемая схема входа (лишние колонки не используются):
| Файл | Обязательные колонки |
|---|---|
nodes.parquet | gid — int64, depth — целое 0–4, is_seed — bool; depth=0 совпадает с is_seed=True |
edges.parquet | src, dst — int64; sum_kzt — положительная конечная сумма; n_tx — положительное целое; depth — целое 1–4; одна строка на направленную пару |
transactions.parquet | src, dst — int64; date — дата; sum_kzt — положительная конечная сумма одной операции |
GID уникальны в nodes и неотрицательны; все концы рёбер присутствуют в nodes. Агрегация transactions по (src, dst) должна совпадать с edges по парам, количеству и сумме (допуск 0,01 KZT). Передавать GID через float нельзя: 18-значные идентификаторы потеряют точность. В браузере они хранятся строками. Количество строк и период нового набора могут отличаться от исходных, но ограничения четырёхколенного формата сохраняются.
Кнопка «Загрузить данные» принимает три файла в схеме ТЗ. Период, объём, количество узлов и транзакций берутся из файлов, не зашиты в интерфейс. Есть проверка типов, пропусков, уникальности, GID, положительных сумм, допустимой глубины, согласованности пар/сумм/количества операций. Даты имеют дневную точность. Изолированные узлы и пустой список переводов допустимы, пустой nodes — нет.
Лимиты локальной версии: 25 МБ файлов суммарно; 10 000 узлов; 100 000 рёбер; 500 000 транзакций; 128 МБ распакованного Parquet на файл; 240 секунд на расчёт. Лимиты защищают рабочую станцию, но не являются обещанием скорости для любой структуры графа. Больше — отдельный масштабируемый backend.
Пересчёт идёт в отдельном процессе. Пока он работает, предыдущий граф и ассистент доступны. Только после валидации результатов атомарно переключается целый снимок данных; при ошибке предыдущий снимок сохраняется. Один импорт одновременно, до двух запросов ассистента одновременно. UI проверяет версию раз в 4 секунды в активной вкладке, сбрасывает старую беседу при смене данных и не отображает запоздалые ответы предыдущего снимка. CSV привязаны к версии: устаревшая ссылка вернёт 409 вместо выгрузки другого датасета.
Загруженные файлы и результаты хранятся локально в .runtime/datasets/<version>/, активный снимок восстанавливается при перезапуске. «Вернуть исходные данные» переключает на data/, не удаляет загруженные файлы. Автоочистки нет: папка может расти. .runtime исключён из git, но не зашифрован; для реальных банковских данных нужны отдельные контроль доступа и политика хранения. Сервис работает, пока запущен Python; автозапуск после перезагрузки ОС не настроен.
Проверки
python -m unittest discover -s tests -v
python validate.py --expected-nodes 2248
python verify_mvp.py
# Дополнительно, при запущенном сервере и исходном датасете:
python verify_mvp.py --server http://127.0.0.1:8000
Тесты покрывают другой период/размер сети, 18-значные GID, одиночный изолят и пустые транзакции, повреждённые файлы, неверные типы/суммы, сценарии ассистента, таймаут и неподтверждённые GID от модели, фоновый импорт, конкурирующий импорт, сохранение предыдущего снимка, восстановление после перезапуска, устаревший экспорт и запрет cross-origin записи. OpenAI мокируется: реальную доступность модели эти тесты не подтверждают.
Дополнительные тесты сверяют слагаемые роли и приоритета с CSV, проверяют FIFO без повторного использования объёмов, границу месяцев, истечение двухдневного окна, направленный seed-путь, воспроизводимость и неизменяемые выгрузки активного снимка. verify_mvp.py проверяет объяснения всех узлов, а с --server — также побайтовое совпадение трёх скачиваемых CSV с out/ и реальные досье API. Итоги последнего прогона — в VERIFICATION.md, сценарий выступления — в DEMO.md.
Обязательные результаты
out/nodes_roles.csv— 2 248 строк:gid,role,role_score,cluster_id,priority_score,evidenceи дополнительные объясняющие признаки;out/clusters.csv— строка на кластер: размер, число seed, внутренний оборот, лидеры и гипотеза назначения;out/top_nodes.csv— 30 узлов по убыванию приоритета с числовым обоснованием;out/graph_data.js— все 2 248 узлов и 3 119 рёбер для поиска и построения связей произвольного GID.out/analysis.json— дополнительные объяснения, альтернативные роли, временные совпадения и чувствительность рейтинга; не меняет обязательную схему CSV.out/manifest.json— контрольные суммы и измеренное время аналитического пайплайна без установки зависимостей.
На предоставленном датасете полный пересчёт занимает около трёх секунд после установки библиотек. Алгоритм детерминирован (seed=42).
Фиксированные обязательные колонки CSV:
| Файл | Колонки по ТЗ |
|---|---|
nodes_roles.csv | 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 | rank, gid, role, priority_score, why |
role использует словарь ТЗ: consolidator, transit, distributor, terminal, coordinator, peripheral. Оба скора лежат в [0, 1]; role_score — эвристическая сила гипотезы, а не калиброванная вероятность. rank начинается с 1, приоритет убывает; при равенстве используется возрастающий GID. top_gids содержит GID через ;. Файлы имеют кодировку UTF-8 с BOM и разделитель ,; дополнительные колонки nodes_roles не удаляют обязательные. GID в табличных редакторах импортируйте как текст, чтобы не округлить идентификатор.
Для новых мини-наборов меньше 20 узлов TOP включает все имеющиеся узлы; требование ≥20 выполняется на предоставленном объёме. Все числа 2 248 / 68 / 30 выше относятся к исходному кейсу, не зашиты в расчёт.
Если запуск не удался
| Симптом | Что проверить / сделать |
|---|---|
py / python не найден | Установить Python 3.14 x64; открыть новый терминал; проверить версию. Затем создать .venv заново на этой машине |
ModuleNotFoundError | Использовать Python из созданного .venv; выполнить .\.venv\Scripts\python.exe -m pip install -r requirements.lock.txt. --skip-install применим только после установки |
No matching distribution или ошибка установки пакетов | Сверить Python 3.14, разрядность и доступ к репозиторию пакетов; не менять версии зависимостей вслепую. Использование другой ОС/архитектуры требует отдельной проверки |
| Не найдены Parquet или скрипт | Выполнять команды в распакованном корне проекта; проверить три точных имени внутри data/, а не расположение файлов на рабочем столе автора |
| Ошибка схемы, сумм или числа операций | Передать все три файла из одной выгрузки; сверить таблицу входных колонок выше. Не обходить проверку и не исправлять финансовые суммы наугад |
Порт занят / WinError 10048 | Не останавливать неизвестный процесс. Выбрать другой порт: python server.py --use-prebuilt --port 8001; открыть http://127.0.0.1:8001/ui/ |
| Нет manifest / результаты изменились | Запустить python run.py --skip-install, затем python server.py --use-prebuilt. После копирования репозитория пересчитывать результаты, а не полагаться на старые контрольные суммы |
| Сайт показывает другой ранее загруженный набор | «Загрузить данные» → «Вернуть исходные данные». Сервер восстанавливает последний успешный UI-импорт из .runtime, если он есть |
| После CLI-пересчёта сайт остался прежним | Сервер держит неизменяемый снимок в памяти. Остановить его Ctrl+C, запустить заново с --use-prebuilt и обновить страницу |
verify_mvp.py --server сообщает о разных датасетах | Открыть на сервере исходный набор из data/; убедиться, что URL/порт относится именно к проверяемой копии проекта |
| Ошибка CSV 409 / датасет изменился | Обновить страницу и скачать заново: ссылка экспорта привязана к версии активных данных |
| Нет связи с backend | Проверить, что процесс server.py работает, адрес и порт совпадают. Не открывать HTML напрямую с диска |
| Нет API-ключа / OpenAI недоступен | Это не блокирует сдачу: расчёт, CSV, граф и локальные сценарии работают без LLM. Проверять основной кейс без обязательного подключения OpenAI |
Ни приложение, ни эта инструкция не требуют прав администратора, отключения защиты ОС или раскрытия API-ключа. При передаче проекта не включайте .venv/, .runtime/, ключи и персональные настройки: передаются исходники, ui/, tests/, инструкции, зависимости, разрешённые данные кейса и обязательные выгрузки. Не ограничивайте пакет тремя CSV: для повторного запуска нужен весь аналитический код.
Схема решения
flowchart LR
A[3 parquet-файла] --> B[Проверка схемы<br/>и консистентности]
B --> C[Направленный<br/>взвешенный граф]
B --> D[Временные ряды<br/>по дням]
C --> E[Степени, суммы, tx,<br/>PageRank, посредничество,<br/>сочленения и циклы]
C --> G[Louvain-кластеры]
G --> E
D --> E
E --> F[Скоры шести ролей]
F --> H[Priority score]
E --> H
H --> I[3 CSV + объяснения + manifest]
G --> I
I --> J[Локальный API<br/>проверенный снимок]
A --> J
J --> K[Граф, досье, TOP,<br/>экспорт и ассистент]
Направление, суммы и количество переводов сохраняются при расчёте ролей. Ненаправленная проекция применяется только для Louvain и поиска articulation points.
Признаки и нормализация
P(x) ниже — percentile rank признака среди узлов текущей выборки (2 248 в исходном кейсе): (rank_average(x) - 1) / (N - 1), от 0 до 1; совпадающие значения получают средний ранг, при N=1 возвращается 0. Такая нормализация не позволяет одному экстремально крупному переводу задавить остальные сигналы. Перцентили разных датасетов не являются общей абсолютной шкалой риска.
Основные признаки:
in_deg,out_deg— уникальные плательщики и получатели;in_kzt,out_kzt— наблюдаемые суммы;in_tx,out_tx— количество отдельных транзакций;same_day_ratio— доля наблюдаемого входа, для которой в тот же день есть исходящий объём; значение ограничено единицей;flow_balance = min(in_kzt, out_kzt) / max(in_kzt, out_kzt);upstream_seed_count— число seed-ветвей, достигающих узла по рёбрам с возрастающей глубиной;betweenness— приближённое посредничество на направленном графе с детерминированной выборкой до 300 узлов; кратчайший путь измеряется числом переходов (weight=None), а не суммой: сумма перевода не интерпретируется как расстояние;cross_cluster_count— число соседних Louvain-сообществ;pagerank— PageRank, взвешенный поsum_kzt;in_cycle, встречные рёбра и articulation point — признаки возвратных потоков и структурной связности.
same_day_ratio = Σday min(in_day, out_day) / in_kzt, с ограничением [0, 1] и нулём при отсутствии входа. active_days — число дней хотя бы с одной операцией; both_direction_days — число дней с входом и выходом. out_concentration = Σrecipient (amount_to_recipient / out_kzt)², при отсутствии выхода — 0. in_cycle устанавливается для узлов сильно связной компоненты размером >1 или самоперевода; это структурный цикл за период, не доказательство хронологического возврата одной суммы.
Выгрузка содержит pass_through = out_kzt / in_kzt; при in_kzt=0 используется техническое значение 0, означающее отсутствие наблюдаемого знаменателя, не доказанное удержание денег. Напрямую на этом отношении роль seed не строится.
Формальные правила ролей
Для каждой допустимой роли считается скор 0–1. Основная роль — максимальный допустимый скор, role_score — его значение.
Недопустимая по условиям роль получает 0. При равенстве скоров действует фиксированный порядок: consolidator → transit → distributor → terminal → coordinator → peripheral. Изолированные узлы обрабатываются отдельным правилом peripheral. Пороги и веса — явные эвристики команды; размеченных ролей для обучения и оценки accuracy в кейсе нет.
consolidator
Кандидат должен быть не seed и иметь не менее трёх наблюдаемых плательщиков.
0.29·P(in_deg) + 0.20·P(in_kzt) + 0.16·P(in_tx)
+ 0.12·(1-P(out_deg)) + 0.13·P(upstream_seed_count) + 0.10·P(pagerank)
Высокий скор означает много независимых входов и сравнительно мало направлений выхода.
distributor
Кандидат должен иметь минимум трёх получателей.
0.31·P(out_deg) + 0.22·P(out_kzt) + 0.17·P(out_tx)
+ 0.10·(1-out_concentration) + 0.10·P(betweenness)
+ 0.10·P(upstream_seed_count)
Низкая концентрация означает, что исходящая сумма распределена между несколькими получателями, а не одним каналом.
transit
Кандидат должен иметь вход и выход и не быть seed. Seed исключён, потому что его входящие данные системно неполны.
0.30·same_day_ratio + 0.24·flow_balance
+ 0.15·min(both_direction_days,3)/3 + 0.13·P(betweenness)
+ 0.10·P(active_days) + 0.08·(1-|P(in_deg)-P(out_deg)|)
Точность дат — один день, поэтому это признак транзита, а не утверждение о причинной связи переводов.
terminal
Сначала считается материальность входа:
M = 0.38·P(in_kzt) + 0.28·P(in_tx) + 0.20·P(in_deg) + 0.14·P(active_days)
- наблюдаемый сток на depth 0–3:
0.48 + 0.52·M; - сток на depth 4:
0.20 + 0.45·Mи наблюдаемость 0.45.
Поэтому одноразовый получатель на четвёртом колене обычно остаётся peripheral, а материальный повторяющийся поток может стать только гипотезой terminal с пониженным эвристическим скором, не оценкой вероятности. В evidence прямо указывается обрыв выборки.
coordinator
Нужны одновременно вход и выход, а также хотя бы один структурный признак: две seed-ветви, articulation point или связь минимум с двумя кластерами.
0.27·P(betweenness) + 0.23·P(upstream_seed_count)
+ 0.18·P(cross_cluster_count) + 0.12·P(pagerank)
+ 0.10·P(in_deg) + 0.10·P(out_deg) + 0.08·is_articulation
Скор ограничивается единицей. Роль означает кандидата на координирующую функцию и требует проверки аналитиком.
peripheral
0.30 + 0.55·(1-max(P(in_tx),P(out_tx))) + 0.15·I(total_degree≤1)
Изолированные seed получают peripheral с role_score=1: данных достаточно, чтобы уверенно сказать лишь об отсутствии наблюдаемой роли.
Приоритет проверки
Приоритет отделён от уверенности в роли. Сначала считается:
raw = 0.24·role_risk + 0.14·role_score + 0.22·activity
+ 0.20·structural + 0.10·temporal_or_cycle + 0.10·observability
priority_score = P(raw)
Где activity — максимум percentile сумм и количества операций, structural — максимум PageRank, betweenness и числа seed-ветвей. Веса ролей: coordinator 1.00, consolidator 0.92, distributor 0.86, transit 0.72, terminal 0.55, peripheral 0.08. Изолированные узлы получают нулевые raw score и priority_score.
Точно: activity = max(P(in_kzt), P(out_kzt), P(in_tx), P(out_tx)); structural = max(P(pagerank), P(betweenness), P(upstream_seed_count)); temporal_or_cycle = max(same_day_ratio, 0.65·I(in_cycle)). Коэффициент observability равен 0.45 для depth=4 без наблюдаемого выхода, иначе 0.70 для seed, иначе 1.00. Это эвристическая поправка, не измеренная доля полноты данных: невидимые входы возможны у всех узлов.
evidence формируется детерминированно, содержит фактические степени, суммы, tx и/или временные признаки и не превышает 200 символов.
Кластеризация
Louvain запускается с фиксированным seed на ненаправленной проекции. Сила связи:
log(1 + sum_kzt) · (1 + 0.15·log(1 + n_tx))
Так крупная сумма учитывается, но не поглощает структуру сети. Изоляты получают собственные кластеры. sum_kzt_internal считается только по рёбрам, у которых обе вершины находятся внутри кластера. Гипотеза строится из преобладающих ролей и формулируется как направление проверки.
Ограничения и осторожность выводов
- У всех узлов возможны невидимые входящие потоки; у seed проблема особенно сильна.
out_kzt / in_kzt > 1не является самостоятельной аномалией. - 444 узла depth=4 цензурированы обходом. Отсутствие исходящих не доказывает оседание денег.
- Переводы ниже 5 000 KZT, другие банки, наличные, остатки до июля и операции вне периода не наблюдаются.
depth— глубина обнаружения, а не уровень в преступной иерархии.- Нет разметки ролей и атрибутов клиента. Результат — объяснимая гипотеза для AML-проверки, а не вывод о виновности.
- В графе 16 компонент без изолятов и ещё 19 изолированных seed; они не объединяются искусственно.
Интерфейс
Доступны граф, таблица всех клиентов, карточки кластеров и экран ассистента. Поиск сохраняет точность 18-значных GID. Для любого узла интерфейс строит читаемый ego-граф: до 24 крупнейших прямых контрагентов и до 20 соседних узлов. Полный снимок узлов/рёбер отдаётся /api/state; ограничивается число одновременно нарисованных элементов. Все прямые связи и дни доступны через /api/node/<gid>. На карте явно указано, сколько контрагентов показано. Контрагенты ассистента — TOP по сумме, а не полный список всех переводов.
Карточка показывает роль, приоритет, потоки, число контрагентов, кластер, глубину и числовое обоснование. Направление денег показано стрелками. CSV выгружаются из интерфейса.
Переключатель «Цвет узлов» меняет окраску карты по ролям или кластерам. В режиме кластеров легенда перечисляет только сообщества видимого фрагмента; номер кластера также доступен в подсказке узла и досье. Фильтр по роли работает в обоих режимах.
Проверяемое досье
- «Роль и признаки»: фактические числа, вклады в выбранную роль, все шесть альтернатив с условиями допустимости, отличие от второго места и следующий шаг проверки. Формулы расчёта и объяснения используют один модуль
role_model.py, а не два расходящихся набора правил. При разнице меньше 5 п.п. UI явно отмечает близость гипотез. В сворачиваемом блоке показаны все шесть вкладов в приоритет. - «Движение по дням»: вход/выход по активным дням и сопоставление дневных объёмов с задержкой 1–2 календарных дня. FIFO расходует каждый входящий и исходящий объём в этом расчёте не более одного раза; вход текущего дня добавляется только после сопоставления выхода, истёкшие объёмы исключаются. Это дополнительный описательный признак, не доказанная трассировка денег и не новое правило роли. У seed отдельно отмечается неполнота входа. Внутридневной порядок неизвестен.
- «Цепочка переводов» / «Путь от seed»: один кратчайший направленный путь длиной до четырёх рёбер, суммы и число операций за период. Поиск multi-source BFS идёт по полному наблюдаемому графу. Это не сумма, которая прошла сквозь всю цепочку, и не хронологическая последовательность. При нескольких равных путях используется стабильный порядок GID. Seed имеет путь длины 0; недостижимый узел получает явное сообщение.
Чувствительность рейтинга
По очереди изменяем каждый из шести весов приоритета на −20% и +20%, после чего нормируем сумму весов. Получаем 12 сценариев, сравниваем состав TOP-20 с базовым и диапазон мест каждого клиента. На исходной выборке минимальное пересечение — 17 из 20. Роли, граф и признаки фиксированы. Эта проверка не измеряет accuracy, не даёт доверительный интервал и не заменяет стресс-тест удаления узлов: последнее остаётся опциональным развитием.
Масштабирование до миллиона узлов
Текущая реализация NetworkX рассчитана на хакатонный объём. Для графа порядка миллиона узлов:
- хранить признаки и рёбра в партиционированном Parquet;
- перейти на igraph/graph-tool или распределённый GraphFrames/Neo4j GDS;
- использовать приближённые PageRank и betweenness, Leiden/Louvain по компонентам;
- считать роли векторизованно по батчам, а временные признаки — SQL/Polars/Spark;
- отдавать интерфейсу ego-граф выбранного узла через API, не весь граф целиком;
- выполнять инкрементальный пересчёт затронутых компонент при поступлении нового периода.
Формулы ролей и формат evidence при этом сохраняются, поэтому объяснения остаются сопоставимыми.
Авторы коммитов
История коммитов
- 2026-09-23 17:22v.0.1.7.1 updated rmТимур Ларин
- 2026-09-23 17:05v.0.1.7Тимур Ларин
- 2026-09-23 16:27фулл 1Тимур Ларин
- 2026-09-23 15:57новые допилы2.0Тимур Ларин
- 2026-09-23 15:33новые допилыТимур Ларин
- 2026-09-23 15:06Добавил структуру проекта, UI и скриптыТимур Ларин
- 2026-09-23 13:49Добавил стартовые файлыТимур Ларин
Структура репозитория (32 файлов)
.env.example .gitignore DEMO.md README.md VERIFICATION.md analysis_report.py data/edges.parquet data/nodes.parquet data/transactions.parquet graph_agent.py out/analysis.json out/clusters.csv out/graph_data.js out/manifest.json out/nodes_roles.csv out/top_nodes.csv provenance.py requirements.lock.txt requirements.txt role_model.py run.py server.py starter.py tests/test_analysis.py tests/test_workspace.py ui/app.js ui/index.html ui/style.css ui/workspace.css validate.py verify_mvp.py структура/структура.txt
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.