О проекте
Команда: 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/.
Этапы и проверяемые результаты
- ETL. Сначала добавляются все 2248 узлов, затем 3119 направленных рёбер. Транзакции агрегируются по паре; с edges.parquet сверяются пары, суммы (абсолютный допуск 0.01 KZT) и количества. Проверяются типы, порог 5000, июль 2026, seed/depth и расстояния BFS. 97 одинаковых строк транзакций сохранены: без transaction_id нельзя доказать дубликаты.
- Метрики. in_deg/out_deg — уникальные контрагенты; in_kzt/out_kzt — суммы наблюдаемых потоков; pass_through=out/in. При нулевом входе — null в JSON/Parquet, пустая ячейка CSV; значения выше 1 сохраняются. 444 узла помечены truncated_by_depth.
- Louvain. Ненаправленная проекция с суммой встречных потоков; потоковая сводка по исходному направленному графу. resolution=1, пять стартов, выбор максимальной модульности. Ещё пять resolution проверяются отдельно. 90 кластеров, 8 с несколькими seed, 5 из 8 устойчивы по строгому минимуму Jaccard≥0.8. Минимальный ARI 0.9241. Число 8 не используется для подгонки. Методика.
- Роли. Упорядоченные правила из метрик; все узлы получают одну роль и числовое evidence ≤200 символов. Полные правила и смысл score.
- Ранжирование. 0.40×процентиль оборота + 0.30×процентиль числа связей + 0.30×(вес роли×role_score). Равенства разрешаются по gid. Топ 30.
- Экспорт. Точные схемы трёх CSV и полный JSON со строковыми gid, ролями, правилами, метриками и cluster_id.
- API. Граф, поиск gid, CSV, запуск child_process и статус. До успешного окончания нового батча сервер показывает предыдущий снимок. Одновременные запуски запрещены, тайм-аут 300 секунд, shell не используется.
- Экран. Стрелки, окраска ролей/кластеров, фильтры, поиск, топ, соседние узлы, карточка с порогами и обоснованием. Мобильная вёрстка и локальный Telegram SDK. Реальная публикация/бот не создавались по выбору пользователя; инструкция подключения.
Критерии ролей и пороги
Первое подошедшее правило определяет роль. Сначала защитные правила: обрыв depth=4/out_deg=0 и seed без исходящих → peripheral с пояснением.
| Роль | Условия после защитных правил | Базовый score | Узлов |
|---|---|---|---|
| coordinator | in_deg≥3, out_deg≥3, оборот≥Q95 = 1 441 669.75 KZT | 0.65 | 38 |
| distributor | out_deg≥10 | 0.75 | 35 |
| consolidator | не seed; in_deg≥3, pass_through≤0.5 | 0.70 | 88 |
| transit | не seed; есть вход и выход; 0.8≤pass_through≤1.2 | 0.60 | 69 |
| terminal | не seed; depth<4; out_deg=0; вход≥Q75 положительных входов = 166 819.70 KZT | 0.50 | 191 |
| peripheral | ни одно правило не подошло либо защитное правило | 0.25 / 0.10 | 1827 |
У активных seed score умножается на 0.8 из-за неполноты входящих. role_score — условная поддержка эвристики, не обученная вероятность и не вероятность виновности. Терминальный кандидат может иметь невидимые переводы за пределами выборки; coordinator — лишь гипотеза связующей функции.
Медиана входящей степени равна 1, её Q95=3; Q95 исходящей степени=5, Q99≈23.53. Поэтому вход от трёх плательщиков и выход к десяти получателям выделяют заметную структуру. Динамические денежные пороги сохраняются в role_rules_resolved.json. Ни одно правило не использует списки gid. Порядок разрешения пересечений и формула приоритета.
Выгрузки
| Файл в data/output | Схема/назначение |
|---|---|
| nodes_roles.csv | gid, role, role_score, cluster_id, priority_score, evidence — ровно 2248 строк |
| clusters.csv | cluster_id, n_nodes, n_seed, sum_kzt_internal, top_gids, hypothesis — ровно 90 строк |
| top_nodes.csv | rank, 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 при этом не обязательны.
Артефакты сдачи и демо
- Схема решения: данные → структура → роли → интерфейс.
- Пятиминутный сценарий и текущие примеры узлов.
- Правила ролей, методика кластеризации, инструкция Telegram.
- Реализация сверена с предоставленным DOCX «HackAlem AI_ Граф денег_ восстановление финансовой структуры организованной группы по транзакционной сети». Требования из документов рассматриваются как описание кейса; объём работ определяется сообщениями пользователя.
Авторы коммитов
История коммитов
- 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 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.