AAYMA
RU
Войти

Граф денег

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

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

О проекте

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

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

Суть

AML-инструмент: по выгрузке переводов от 81 известного курьера строит сеть из 2 248 клиентов, назначает каждому роль (точка сбора, транзит, распределитель, конечный получатель, координатор, периферия), делит сеть на кластеры и ранжирует клиентов по приоритету проверки с объяснением простым текстом.

Как устроено

Пайплайн читает nodes/edges/transactions.parquet, проверяет данные и строит направленный граф в NetworkX. Для каждого узла считаются метрики: число плательщиков и получателей, суммы, доля выхода, пути от исходных клиентов. Роли назначаются упорядоченными правилами с role_score 0–1, кластеры находит Louvain с фиксированным seed. Приоритет — взвешенная сумма сглаженных признаков (35% — число исходных клиентов с путём до узла, 30% — плательщики, 20% — входящая сумма, 15% — концентрация выхода) с понижающими множителями для известных клиентов и узлов 4-го колена. На выходе три CSV, API на FastAPI и интерфейс React + Cytoscape.js.

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

  • NetworkX
  • Louvain
  • правила назначения ролей по структуре графа
  • взвешенный приоритет со сглаживанием s/(s+1)
  • пути от известных клиентов
  • Cytoscape.js

Метрики

2 248 узлов, 91 кластер; роли: 265 точек сбора, 68 транзит, 118 распределителей, 936 конечных, 2 координатора, 859 периферийных; в топ-20 — 18 точек сбора

Особенности

Аккуратно учтены ограничения выгрузки: узлы на обрыве 4-го колена не считаются конечными получателями, для исходных клиентов с неполными входящими переводами ограничен score, а разрыв «выход больше входа» явно помечается. В README есть конкретный план масштабирования до ~1 млн узлов (Polars/DuckDB, CSR в igraph, Leiden по компонентам) и честная оговорка, что это гипотезы, а не обвинения. LLM не используется.

Стек

Python · FastAPI · pandas · PyArrow · NetworkX · React · TypeScript · Vite · Cytoscape.js · Docker · uv


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

README

README.md

Граф денег

Инструмент для AML-аналитика: по выгрузке переводов строит сеть, присваивает каждому клиенту роль и показывает, кого проверять первым и почему.

Интерфейс: список приоритетов, сеть переводов и карточка клиента

Зачем

Банку известны 81 клиент, связанные с незаконным оборотом наркотиков. Это нижний уровень схемы: курьеры, получавшие деньги. Выгрузка их исходящих переводов на четыре колена даёт сеть из 2 248 клиентов, и вручную понять, кто в ней собирает деньги, через кого они проходят и где оседают, — это часы работы на каждый узел.

Граф денег делает это за секунды:

  • присваивает каждому из 2 248 клиентов роль: точка сбора, транзит, распределитель, конечный получатель, координатор или периферия;
  • ранжирует клиентов по приоритету проверки и объясняет каждую позицию обычным текстом, например «Плательщиков — 8; дальше отправлено 23,9% входа»;
  • делит сеть на кластеры и формулирует гипотезу о назначении каждого;
  • показывает схему переводов с направлениями, поиском по gid и путями от известных клиентов до выбранного узла.

Фокус смещается с 81 уже известного курьера на тех, к кому их деньги сходятся. Известные клиенты намеренно опущены в приоритете, и в топ-100 их нет.

Все выводы — гипотезы для проверки, а не утверждение о виновности.

Запуск

Распакуйте архив с данными от организаторов в корень проекта:

data/nodes.parquet
data/edges.parquet
data/transactions.parquet

Docker: всё одной командой

docker compose up --build -d --wait

Откройте http://localhost:8000. CSV можно скачать из меню «Скачать CSV» в интерфейсе. Остановка: docker compose down.

Только расчёт CSV

Нужен uv. Он сам установит нужную версию Python и зависимости.

uv run python -m backend.pipeline

Команда читает data/, пишет три CSV в out/ и укладывается в секунды (лимит задания — 5 минут).

Веб-интерфейс без Docker

Нужны uv и Node.js 24:

uv sync --frozen
npm --prefix frontend ci
npm --prefix frontend run build
uv run uvicorn backend.app:app --port 8000

Откройте http://localhost:8000.

Если архива нет под рукой, python3 scripts/fetch_data.py скачает его по публичной ссылке организаторов и проверит контрольную сумму. Пути к данным и выгрузкам можно переопределить переменными из .env.example.

Результаты

Готовые выгрузки для выданного набора лежат в репозитории:

ФайлСодержимое
out/nodes_roles.csvВсе 2 248 узлов: gid, role, role_score, cluster_id, priority_score, evidence
out/clusters.csv91 кластер: размер, число исходных клиентов, внутренний оборот, топ-5 узлов, гипотеза
out/top_nodes.csv100 узлов для первоочередной проверки с обоснованием why

Распределение ролей: 265 точек сбора, 68 транзитных, 118 распределителей, 936 конечных получателей, 2 координатора, 859 периферийных. В топ-20 — 18 точек сбора, 1 распределитель и 1 периферийный узел.

Для демонстрации удобно открыть три узла:

gidЧто показывает
100000003115284100Точка сбора: 8 плательщиков, получено 2 160 500 ₸, дальше ушло 24%
100000002224132100Кандидат в координаторы: получает от двух точек сбора, отправляет в 3 раза больше, чем получил в выборке
100000006638021100Граница 4-го колена: исходящих нет, но это обрыв выгрузки, а не «деньги осели»

Как определяются роли

Правила проверяются по порядку, срабатывает первое подходящее. d — число разных плательщиков узла, o — число разных получателей, «доля выхода» — сколько от полученного узел отправил дальше.

Точка сбора в этой таблице — узел, который не является исходным клиентом, не лежит на 4-м колене, получает от 2 и более плательщиков и отправляет дальше не больше 80% полученного.

РольПравилоСмысл
coordinatorПолучает от 2 и более точек сбора и отправляет 2 и более получателям; не исходный клиент и не 4-е коленоУзел над точками сбора — кандидат в организаторы
consolidatorСам является точкой сбораСобирает деньги от нескольких участников и удерживает их
distributoro ≥ 5 и o ≥ 2·dВеерная рассылка на много получателей
transitДоля выхода от 0,8 до 1,2; есть и входящие, и исходящие; не исходный клиентПропускает деньги дальше, почти не удерживая
terminalЕсть входящие, нет исходящих; не исходный клиент и не 4-е коленоДеньги пришли и остались
peripheralНичего из перечисленногоПризнаков роли нет

role_score (0–1) показывает, насколько ярко выражены признаки. Например, у точки сбора он растёт с числом плательщиков и долей удержанных денег. Формулы для каждой роли приведены в методологии.

В карточке клиента role_score показан как «Сила признаков роли» в процентах. Это выраженность структурных признаков, а не вероятность нарушения. Приоритет проверки — число от 0 до 1, задающее только порядок проверки.

Приоритет проверки

Приоритет отвечает на вопрос «кого смотреть первым» и от роли не зависит:

ПризнакВесПочему важен
От скольких исходных клиентов есть путь до узла35%Деньги нескольких курьеров сходятся в одном месте
Число разных плательщиков30%Широта сбора
Сумма входящих переводов20%Масштаб
Насколько выход сосредоточен на одном получателе15%Деньги собраны и переданы одному адресату

Каждый признак сглажен, чтобы крупные значения не забивали остальные: s/(s+1), d/(d+3), v/(v+500 000). Итог умножается на 0,5 для исходных клиентов (они уже известны) и на 0,85 для узлов 4-го колена (данные о них обрезаны). Карточка клиента в интерфейсе раскладывает приоритет на эти составляющие.

Как учтены ограничения данных

Особенность выгрузкиЧто сделано
444 узла на 4-м колене не имеют исходящих из-за обрыва обходаОни не считаются конечными получателями и не могут быть точкой сбора или координатором; в карточке есть предупреждение
Входящие переводы исходных клиентов видны не полностьюДолю выхода для них не считаем, role_score ограничен 0,85, приоритет умножен на 0,5
Многие узлы отправили больше, чем получили в выборкеОбоснование предупреждает, что выход выше входа и баланс неполон
19 исходных клиентов без переводов, 16 несвязанных частей сетиИзоляты получают свой кластер и пометку «связей в выборке нет»
Нет переводов меньше 5 000 ₸ и атрибутов клиентовИспользуются только структура и суммы; ничего не достраивается

Пути от исходных клиентов показывают, что узел достижим по наблюдаемым переводам. Это не доказывает, что до него дошли те же самые деньги.

Как устроено

flowchart LR
    P[3 файла parquet] --> V[Проверка данных]
    V --> G[Направленный граф]
    G --> M[Метрики: плательщики, получатели, суммы, доля выхода, пути от исходных клиентов]
    G --> C[Кластеры Louvain]
    M --> R[Роли и приоритет]
    C --> R
    R --> O[3 CSV]
    R --> A[FastAPI]
    A --> U[React + Cytoscape]
backend/pipeline.py   расчёт: загрузка, граф, роли, приоритет, кластеры, CSV
backend/app.py        API и раздача интерфейса
frontend/src/         интерфейс: список приоритетов, граф, карточка клиента
scripts/fetch_data.py загрузка архива данных
tests/                тесты расчёта и API

Пайплайн детерминирован: у Louvain фиксированный seed, и повторный запуск даёт те же CSV. Интерфейс получает данные из того же расчёта, что и выгрузки. Контракт API описан в docs/api-contract.md, устройство компонентов — в docs/architecture.md.

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

Сейчас весь граф строится в памяти через NetworkX. Это нормально для тысяч узлов, но не для миллиона. Что пришлось бы изменить:

  • Хранение и агрегация. Считать степени и суммы колоночно в Polars или DuckDB прямо по parquet. Граф держать в компактном виде (CSR в igraph или graph-tool), а не в словарях Python.
  • Пути от исходных клиентов. Вместо хранения путей для каждой пары «клиент → узел» считать только число достижимых исходных клиентов одним проходом по коленам. Примеры путей строить по запросу.
  • Кластеризация. Запускать Louvain или Leiden отдельно для каждой связной компоненты, крупные компоненты обрабатывать параллельно.
  • API. Заранее построить индекс смежности, чтобы окрестность узла отдавалась без полного прохода по рёбрам.
  • Интерфейс. Не отрисовывать всю сеть: только окрестность узла или кластеры, свёрнутые в один узел.

Проверки

uv run pytest -q
uv run ruff check backend tests scripts
npm --prefix frontend test
npm --prefix frontend run lint
npm --prefix frontend run typecheck

Стек и источники

Python (FastAPI, pandas, PyArrow, NetworkX), React, TypeScript, Cytoscape.js, Vite. Решение работает локально, не использует LLM, платные сервисы и внешние API. Версии и лицензии всех зависимостей — в docs/third-party.md. При разработке использовался Codex; для запуска он не нужен.

Данные обезличены (gid — синтетический идентификатор) и предоставлены организаторами HackAlem только для хакатона. Исходные parquet в репозиторий не включены.

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

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

Sergey 11

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

  • 2026-09-23 17:46docs: clarify role strength and priority scaleSergey
  • 2026-09-23 17:36fix: restore role evidence in top list rationaleSergey
  • 2026-09-23 17:32fix: remove repetition from CSV explanationsSergey
  • 2026-09-23 17:27docs: rewrite README around analyst workflowSergey
  • 2026-09-23 17:18fix: align role evidence and ship reproducible CSV artifactsSergey
  • 2026-09-23 16:54fix: prioritize new consolidation candidates with seed pathsSergey
  • 2026-09-23 16:30fix: simplify graph navigation and client detailsSergey
  • 2026-09-23 16:10fix: improve responsive and accessible graph workspaceSergey
Все коммиты (11)
  • 2026-09-23 15:27feat: clarify transaction graph neighborhoodsSergey
  • 2026-09-23 15:02feat: add explainable transaction graph workspaceSergey
  • 2026-09-23 14:26chore: initialize project environment and case READMESergey
Структура репозитория (46 файлов)
.dockerignore
.env.example
.gitignore
.python-version
Dockerfile
README.md
backend/__init__.py
backend/app.py
backend/pipeline.py
compose.yaml
docs/api-contract.md
docs/architecture.md
docs/images/workspace.jpg
docs/methodology.md
docs/third-party.md
frontend/eslint.config.js
frontend/index.html
frontend/package-lock.json
frontend/package.json
frontend/public/favicon.svg
frontend/src/App.tsx
frontend/src/Graph.tsx
frontend/src/NodeCard.tsx
frontend/src/RoleTag.tsx
frontend/src/api.ts
frontend/src/graphLayout.test.ts
frontend/src/graphLayout.ts
frontend/src/main.tsx
frontend/src/selection.test.ts
frontend/src/selection.ts
frontend/src/styles.css
frontend/tsconfig.app.json
frontend/tsconfig.json
frontend/tsconfig.node.json
frontend/vite.config.ts
out/clusters.csv
out/nodes_roles.csv
out/top_nodes.csv
pyproject.toml
scripts/fetch_data.py
tests/conftest.py
tests/test_api.py
tests/test_methodology.py
tests/test_official_data.py
tests/test_pipeline.py
uv.lock

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

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

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