AAYMA
RU
Войти

Граф денег

AML-инструмент: Python-батч по трём parquet строит граф, считает метрики, кластеры Louvain, объяснимые роли и приоритеты; Express отдаёт результат…

Рабочий MVP
Команда «BSSID» 4💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: BSSID
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Рабочий MVP

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

Суть

AML-инструмент: Python-батч по трём parquet строит граф, считает метрики, кластеры Louvain, объяснимые роли и приоритеты; Express отдаёт результат, Sigma.js показывает граф и карточки клиентов.

Как устроено

ETL сверяет транзакции с рёбрами и проверяет seed, depth и порог 5000. Метрики: степени, потоки, pass_through, флаг обрыва на depth=4. Louvain считается на ненаправленной проекции, 5 стартов; устойчивость оценивается через Jaccard и ARI. Роли назначаются упорядоченными правилами, приоритет считается как 0.40×процентиль оборота + 0.30×процентиль связей + 0.30×вес роли×score. Сигналы по кластерам проверяются через OR, точный тест Фишера и поправку Benjamini–Hochberg. Карточки дополнены FIFO-сопоставлением входа и выхода за 1–2 дня и размером SCC-циклов. Загрузка своих parquet обрабатывается в памяти без записи на диск.

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

  • Louvain
  • Jaccard/ARI cluster stability
  • Fisher exact test
  • Benjamini–Hochberg
  • odds ratio
  • FIFO temporal matching
  • strongly connected components
  • rule-based roles

Метрики

90 кластеров, 8 с несколькими seed, 5 из 8 устойчивы (Jaccard≥0.8), min ARI 0.9241; 540 сравнений, 7 с q<0.05; батч ~3 с

Особенности

Строгая статистика сигналов по кластерам: ORs, доверительные интервалы и FDR-коррекция вместо эвристик. Загрузка обрабатывается в памяти, запись на диск запрещена через Python audit hook. Интерфейс на RU/KK/EN с мобильной вёрсткой и Telegram SDK.

Стек

Python · pandas · pyarrow · Node.js · Express · Sigma.js · Telegram Web App SDK


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

README

README.md

Граф денег — локальный инструмент AML-аналитика

Проект реализует этапы 1–8: один Python-батч читает три parquet, строит направленный граф, рассчитывает метрики, Louvain, объяснимые роли и приоритеты; Express отдаёт проверенный результат, Sigma.js показывает связи и карточки. Никакой LLM, внешнего обогащения, GPU или облачного кластера нет.

Разделы приложения

Слева — навигация, загрузка файлов, язык и тема; справа — основной раздел. На узком экране навигация располагается сверху.

  • Поток — граф, расширенная карточка клиента, дневная активность и мини-схема «плательщики → клиент → получатели». При отсутствии одной стороны схема состоит из двух столбцов; изолированный клиент показан один. Число связанных клиентов учитывает уникальных соседей без самого клиента. Карточка доступна для каждого GID.
  • Проверка клиентов — все 2248 клиентов (или весь загруженный набор), частичный/точный поиск GID, фильтр роли, сортировка по приоритету/GID. Постраничный просмотр 50/100/500 строк или «Все». В строке — роль, приоритет, факты и компоненты оценки; карточки и графа здесь нет.
  • Кластеры и сигналы — размер, число seed, внутренний оборот и сравнение всех шести ролей внутри/вне каждого кластера. Доли со счётчиками, OR, приближённый 95% ДИ и глобально скорректированный q-value. На текущих данных 540 сравнений, 7 имеют q<0.05; это сигналы для проверки, не подтверждённые нарушения. Статистическая методика.
  • Методика — краткая формула, правила и денежные пороги; длинные пояснения раскрываются по запросу.

Полный интерфейс, объяснения строк и гипотезы сигналов доступны на русском, казахском и английском. Светлая/тёмная тема и язык сохраняются в localStorage; клиентские данные туда не записываются. Три CSV скачиваются для текущего набора, с прежними фиксированными схемами и текстами аналитического пайплайна.

Сообщества нумеруются с 1 во всех разделах, JSON и CSV. Сверху показаны количество сообществ / число сообществ с несколькими seed и число узлов на границе наблюдения (depth=4, out_degree=0): сейчас 90 / 8 и 444 соответственно.

Дополнительные признаки карточки «Поток»

Расчёт в python/node_details.py одинаков для батча и загрузки в память. JSON каждого клиента дополнен:

  • seed_ancestors_4: число различных seed, от которых есть направленный путь длиной 1–4 шага. Сам клиент исключён, повторные пути не увеличивают число.
  • activity: наблюдаемые суммы входящих/исходящих по календарным дням. Пропущенные дни имеют нулевой оборот; даты и значения доступны также текстом под графиком.
  • max_daily_payers: максимум числа различных плательщиков за день.
  • cycle_size: размер сильно связной компоненты с циклом; петля даёт 1, отсутствие цикла — 0. Это структурный признак, а не доказательство кругового движения одной суммы.
  • matched_1_2_days_kzt: FIFO-сопоставление входящих объёмов с исходящими через 1–2 календарных дня. Ни входящие, ни исходящие суммы не используются повторно; суммы того же дня и старше двух дней исключены. Временное совпадение не устанавливает идентичность денег.

Роли и формула приоритета не изменены. В раскрывающемся блоке показаны фактические три слагаемых: 0.30×вес роли×сила правила, 0.40×процентиль оборота, 0.30×процентиль связей. Для изолированных узлов все вклады равны 0. Новые признаки — контекст для проверки, без вклада в приоритет. У seed и узлов на границе отношение выхода к входу в карточке помечено «Не используется»; силу правила нельзя читать как вероятность виновности. Числа из примера карточки не зашиты в код.

Загрузка в память

Кнопка «Загрузить свои 3 parquet-файла» принимает вместе nodes.parquet, edges.parquet, transactions.parquet. Имена и обязательные поля проверяются на сервере, parquet разбирается в Python из BytesIO. Передача — JSON/base64 через HTTP и stdin/stdout дочернего процесса. В worker включены -B/PYTHONDONTWRITEBYTECODE и запрет операций записи/создания временных файлов через audit hook. Ни исходники, ни вычисленные результаты загрузки не записываются на серверный диск; CSV создаются строками в памяти и сохраняются только по явной команде скачивания в браузере.

Свои данные не обязаны иметь ровно 2248 узлов или даты июля 2026. Обязательны исходная схема, положительные суммы, depth 0–4, корректные seed, совпадение транзакций с рёбрами и достижимость по заявленной глубине. Лимиты: 8 МиБ/файл, 10 000 узлов, 50 000 рёбер, 200 000 транзакций, 128 МиБ распакованных данных/файл; тайм-аут 300 секунд. Считываются только обязательные столбцы. Неизменяемый исходный набор сохраняется на сервере; результат загрузки хранится в текущей вкладке до перезагрузки. Другие вкладки его не получают. «Вернуться к исходным данным» восстанавливает исходный просмотр. Пересчёт исходного набора кнопкой — отдельный существующий дисковый батч.

Одна команда запуска

Требуются Python 3.12+ и Node.js 22+ (с npm). Из корня проекта:

python start.py --setup

Первый запуск устанавливает отсутствующие зависимости, выполняет батч, собирает локальный JS и запускает экран на http://127.0.0.1:3000. Загрузка зависимостей при подготовке отдельно разрешена пользователем. Установка окружения не входит в измерение времени расчёта и зависит от сети. Последующие запуски полностью офлайн:

python start.py

Остановить сервер: Ctrl+C. Для одного батча без сервера:

python start.py --batch-only

Для первой установки только Python-части: python start.py --setup --batch-only. Python-зависимости закреплены в python/requirements.txt, npm — в server/package-lock.json (также есть pnpm-lock.yaml). server/.env.example описывает PORT/HOST/PYTHON и необязательный HTTPS.

На текущем компьютере Python находится по пути:

& 'C:/Users/Pascal/.cache/codex-runtimes/codex-primary-runtime/dependencies/python/python.exe' start.py

В песочнице Codex чтение установленного PyArrow и запуск дочерних процессов могут потребовать разрешения; это ограничение среды Codex, не приложения. На чистой машине команда предполагает уже установленные Python и Node, а не установку операционной системы и языковых сред. Измеренный полный батч: около 3 секунд, лимит — 300 секунд.

Структура

data/raw/                 nodes.parquet, edges.parquet, transactions.parquet, README.md
data/output/              проверенные выгрузки текущего запуска
python/etl.py             этап 1: загрузка, сверка и граф
python/metrics.py         этап 2: степени, потоки, коэффициент, обрыв depth=4
python/clustering.py      этап 3: Louvain и устойчивость
python/roles.py           этапы 4–6: правила, ранжирование, схемы CSV
python/run_pipeline.py    единый батч этапов 1–6
python/signals.py         OR, интервалы, Fisher и Benjamini–Hochberg
python/memory_pipeline.py обработчик parquet в памяти
python/config/            контрольные числа, Louvain, пороги ролей
server/                   Express API и локальная сборка Sigma.js
public/                   адаптивный экран и локальный Telegram SDK
tests/                    Python-тесты
server/test/              API-тесты
docs/                     методика, диаграмма, демо, Telegram, снимки экрана
start.py                  запуск расчёта и интерфейса одной командой

Загруженная ранее папка wer/ (React/Vite) сохранена, но не нужна для этого интерфейса. Старый каталог out/ первого этапа не используется. Корень hded/ соответствует согласованному money-graph/.

Этапы и проверяемые результаты

  1. ETL. Сначала добавляются все 2248 узлов, затем 3119 направленных рёбер. Транзакции агрегируются по паре; с edges.parquet сверяются пары, суммы (абсолютный допуск 0.01 KZT) и количества. Проверяются типы, порог 5000, июль 2026, seed/depth и расстояния BFS. 97 одинаковых строк транзакций сохранены: без transaction_id нельзя доказать дубликаты.
  2. Метрики. in_deg/out_deg — уникальные контрагенты; in_kzt/out_kzt — суммы наблюдаемых потоков; pass_through=out/in. При нулевом входе — null в JSON/Parquet, пустая ячейка CSV; значения выше 1 сохраняются. 444 узла помечены truncated_by_depth.
  3. Louvain. Ненаправленная проекция с суммой встречных потоков; потоковая сводка по исходному направленному графу. resolution=1, пять стартов, выбор максимальной модульности. Ещё пять resolution проверяются отдельно. 90 кластеров, 8 с несколькими seed, 5 из 8 устойчивы по строгому минимуму Jaccard≥0.8. Минимальный ARI 0.9241. Число 8 не используется для подгонки. Методика.
  4. Роли. Упорядоченные правила из метрик; все узлы получают одну роль и числовое evidence ≤200 символов. Полные правила и смысл score.
  5. Ранжирование. 0.40×процентиль оборота + 0.30×процентиль числа связей + 0.30×(вес роли×role_score). Равенства разрешаются по gid. Топ 30.
  6. Экспорт. Точные схемы трёх CSV и полный JSON со строковыми gid, ролями, правилами, метриками и cluster_id.
  7. API. Граф, поиск gid, CSV, запуск child_process и статус. До успешного окончания нового батча сервер показывает предыдущий снимок. Одновременные запуски запрещены, тайм-аут 300 секунд, shell не используется.
  8. Экран. Стрелки, окраска ролей/кластеров, фильтры, поиск, топ, соседние узлы, карточка с порогами и обоснованием. Мобильная вёрстка и локальный Telegram SDK. Реальная публикация/бот не создавались по выбору пользователя; инструкция подключения.

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

Первое подошедшее правило определяет роль. Сначала защитные правила: обрыв depth=4/out_deg=0 и seed без исходящих → peripheral с пояснением.

РольУсловия после защитных правилБазовый scoreУзлов
coordinatorin_deg≥3, out_deg≥3, оборот≥Q95 = 1 441 669.75 KZT0.6538
distributorout_deg≥100.7535
consolidatorне seed; in_deg≥3, pass_through≤0.50.7088
transitне seed; есть вход и выход; 0.8≤pass_through≤1.20.6069
terminalне seed; depth<4; out_deg=0; вход≥Q75 положительных входов = 166 819.70 KZT0.50191
peripheralни одно правило не подошло либо защитное правило0.25 / 0.101827

У активных seed score умножается на 0.8 из-за неполноты входящих. role_score — условная поддержка эвристики, не обученная вероятность и не вероятность виновности. Терминальный кандидат может иметь невидимые переводы за пределами выборки; coordinator — лишь гипотеза связующей функции.

Медиана входящей степени равна 1, её Q95=3; Q95 исходящей степени=5, Q99≈23.53. Поэтому вход от трёх плательщиков и выход к десяти получателям выделяют заметную структуру. Динамические денежные пороги сохраняются в role_rules_resolved.json. Ни одно правило не использует списки gid. Порядок разрешения пересечений и формула приоритета.

Выгрузки

Файл в data/outputСхема/назначение
nodes_roles.csvgid, role, role_score, cluster_id, priority_score, evidence — ровно 2248 строк
clusters.csvcluster_id, n_nodes, n_seed, sum_kzt_internal, top_gids, hypothesis — ровно 90 строк
top_nodes.csvrank, gid, role, priority_score, why — 30 строк
graph.jsonВсе узлы, рёбра, роли, кластеры, метрики и правила
nodes_analysis.parquetПолная таблица с деталями классификации и ранжирования
nodes_metrics.csv/parquetРасширенная таблица метрик текущего расчёта
clusters_detailed.csv, clusters.jsonДополнительные потоки и устойчивость кластеров
clustering_diagnostics.jsonПовторы Louvain и проверки resolution
signals.jsonТаблицы 2×2, доли, OR, 95% ДИ, p/q для всех кластеров и ролей
role_rules_resolved.jsonКонфиг и вычисленные денежные пороги
validation.json, pipeline_validation.jsonПроверки, распределение ролей, фактическое время
demo_nodes.mdТри автоматически выбранных примера для демонстрации

Также сохранены edge_list.csv/parquet, nodes.parquet, nodes_clusters.parquet и graph.graphml. gid в Parquet/CSV — int64; в JSON — строки, так как они превышают точность JavaScript Number. Для Excel импортировать gid как текст. top_gids — JSON-массив строк внутри поля CSV.

API и запуск вручную

python python/run_pipeline.py --data data/raw --out data/output
node server/build.js
node server/index.js

Если python не находится в PATH, указать PYTHON в server/.env. Конфиги можно заменить через --expected, --clustering-config, --roles-config.

  • GET /api/graph — проверенный снимок и итоговый отчёт.
  • GET /api/nodes/:gid — точный строковый gid, метрики, правило и инцидентные рёбра.
  • GET /api/clusters, GET /api/top — сводки.
  • GET /api/dataset — согласованный набор графа, кластеров, сигналов и порогов.
  • GET /api/signals — все статистические сравнения.
  • POST /api/upload — три base64 файла в files: [{name, data}], токен сеанса; результат и CSV в теле ответа, без записи на диск и изменения серверного снимка.
  • POST /api/pipeline — запуск; X-Run-Token из GET /api/session обязателен в локальном режиме.
  • GET /api/pipeline/status — idle/running/succeeded/failed и журнал.
  • GET /api/download/nodes_roles.csv (аналогично двум другим CSV).

HOST по умолчанию 127.0.0.1; при публичном HOST пересчёт выключен. Локальный токен защищает от случайного межсайтового запуска, не заменяет корпоративную аутентификацию. API считает в отдельную папку data/run-, затем заменяет output только после успеха, предыдущая версия остаётся в data/output.previous-. Не запускать CLI параллельно с API в ту же папку. Ошибочные запуски не должны использоваться как новые результаты; проверять status=passed.

Проверки

python -m unittest discover -s tests -v
node --test server/test/api.test.js

Проверены 24 Python-теста и 5 API-тестов. Дополнительно проверены отсутствие повторного использования сумм, окно 1–2 дня, уникальные seed-предки, циклы, изолированные клиенты и совпадение метрик батча с загрузкой в память. Новые тесты сверяют известную таблицу Фишера, BH, OR/ДИ, нулевые ячейки, отсутствие группы сравнения, чужие даты/размер набора, ошибочные имена/колонки/агрегаты. API-тест настоящей загрузки сверяет хеши файлов python/ и data/ до/после: изменений нет. Для API-тестов нужен Python в PATH или переменная PYTHON.

Браузерная проверка server/browser-check.cjs (опциональный локальный Playwright, Edge) проверяет все 2248 строк, фильтр, сортировку, поиск, отсутствие карточки/графа в проверке клиентов, OR/ДИ/q, раскрытие методики, три языка, две темы, реальную загрузку и три скачивания CSV. Проверены мобильная ширина и отсутствие внешних запросов. Скриншоты в docs/screenshots. Для раздела «Поток» требуется WebGL; остальные разделы не создают WebGL-контекст. Сначала запустить батч и сервер.

Ограничения и сверка описания данных

Все выводы — гипотезы для углублённой проверки. Нет ground truth, accuracy не заявляется. Отсутствуют ФИО, возраст, организации, остатки, типы операций; эти атрибуты не восстанавливаются и не выдумываются. Наблюдаемые денежные отношения не доказывают передачу одних и тех же средств.

444 узла на четвёртом колене не получают terminal автоматически. 19 seed изолированы, ещё 12 только получатели — все 31 сохранены. У seed входящие неполны; коэффициент выше 1 у 354 узлов не трактуется как самостоятельная аномалия. Один месяц, один банк и порог суммы не дают полного баланса даже у не-seed.

Уточнение против исходного описания: 35 слабосвязных компонент включают 16 с рёбрами и 19 изолированных seed. Крупнейшая — 1877 узлов; вне неё 371 узел, из них 352 неизолированных. Оборот по данным 365 890 012.01 KZT, контрольное описание округлено до целого. Конфигурация не заменяет эти результаты зашитыми значениями.

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

Текущий NetworkX рассчитан на предоставленный граф. Для миллиона узлов/десятков миллионов рёбер переносить хранение и проходы в CSR/CSC (igraph либо другой компактный движок), читать parquet столбцами и агрегировать DuckDB с выгрузкой на диск. Степени/суммы считаются линейно по рёбрам; для порогов подходят приближённые квантили. Louvain/Leiden выполнять в компактном движке, отдельно по слабосвязным компонентам; число повторов ограничивать бюджетом, устойчивость проверять на выборках и значимых сообществах. Эквивалентность весов и направлений нужно перепроверить.

Python-объекты NetworkX и монолитный JSON создают большой накладной расход памяти. Выгружать таблицы по разделам и версии снимков, gid индексировать в SQLite/DuckDB либо локальной БД. В API выдавать окрестность выбранного gid, агрегаты сообщества и пагинированные топы. На экран передавать тысячи видимых узлов, а не миллион сразу; раскрывать кластеры по запросу. Порог 5 минут для такого размера здесь не обещается: требуется отдельный замер на заданном числе рёбер и памяти. Облако и GPU при этом не обязательны.

Артефакты сдачи и демо

Звёзды
⭐ 0
Форки
0
Язык
Python
Последний push
в прошлом месяце
Коммиты
11
Авторы
3
Файлы
101
Ветки
1

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

Kaisin0LL 6DmDanial 4Codex 1

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

  • 2026-09-23 17:19Add files via uploadDmDanial
  • 2026-09-23 16:22Add files via uploadDmDanial
  • 2026-09-23 17:55Add complete Money Graph analytics application and verified data exportsCodex
  • 2026-09-23 17:50Create GraphKaisin0LL
  • 2026-09-23 17:44Delete analysis.pyKaisin0LL
  • 2026-09-23 17:42Delete top_nodes.csvKaisin0LL
  • 2026-09-23 17:42Delete waitpls.txtKaisin0LL
  • 2026-09-23 17:41Delete app.pyKaisin0LL
Все коммиты (11)
  • 2026-09-23 17:34Add files via uploadKaisin0LL
  • 2026-09-23 15:24Add files via uploadDmDanial
  • 2026-09-23 14:37Add files via uploadDmDanial
Структура репозитория (101 файлов)
.gitignore
Graph
README.md
data/output/clustering_diagnostics.json
data/output/clusters.csv
data/output/clusters.json
data/output/clusters_detailed.csv
data/output/demo_nodes.md
data/output/edge_list.csv
data/output/edge_list.parquet
data/output/graph.graphml
data/output/graph.json
data/output/nodes.parquet
data/output/nodes_analysis.parquet
data/output/nodes_clusters.parquet
data/output/nodes_metrics.csv
data/output/nodes_metrics.parquet
data/output/nodes_roles.csv
data/output/pipeline_validation.json
data/output/role_rules_resolved.json
data/output/signals.json
data/output/top_nodes.csv
data/output/validation.json
data/raw/README.md
data/raw/edges.parquet
data/raw/nodes.parquet
data/raw/transactions.parquet
docs/architecture.png
docs/architecture.svg
docs/clustering.md
docs/demo.md
docs/roles.md
docs/screenshots/clients-dark.png
docs/screenshots/clients-light.png
docs/screenshots/clients-mobile-kk.png
docs/screenshots/flow-light.png
docs/screenshots/flow-mobile.png
docs/screenshots/flow-seed-details.png
docs/screenshots/method-light.png
docs/screenshots/mobile.png
docs/screenshots/node.png
docs/screenshots/overview.png
docs/screenshots/signals-light.png
docs/screenshots/signals-mobile-en.png
docs/signals.md
docs/telegram.md
package_submission.py
public/app.bundle.js
public/app.js
public/flow-details.js
public/i18n.js
public/index.html
public/style.css
python/__init__.py
python/clustering.py
python/config/clustering.json
python/config/expected.json
python/config/roles.json
python/etl.py
python/memory_pipeline.py
python/metrics.py
python/node_details.py
python/requirements.txt
python/roles.py
python/run_pipeline.py
python/signals.py
python/verify_outputs.py
server/.env.example
server/browser-check.cjs
server/build.js
server/index.js
server/package-lock.json
server/package.json
server/pnpm-lock.yaml
server/pnpm-workspace.yaml
server/test/api.test.js
start.py
tests/__init__.py
tests/test_clustering.py
tests/test_etl.py
tests/test_memory_upload.py
tests/test_metrics.py
tests/test_node_details.py
tests/test_roles.py
tests/test_signals.py
wer/.gitignore
wer/README.md
wer/eslint.config.js
wer/index.html
wer/package-lock.json
wer/package.json
wer/public/favicon.svg
wer/public/icons.svg
wer/src/App.css
wer/src/App.jsx
wer/src/assets/hero.png
wer/src/assets/react.svg
wer/src/assets/vite.svg
wer/src/index.css
wer/src/main.jsx
wer/vite.config.js

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

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

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