freeCodeCamp.org

How to Detect Financial Fraud Patterns with Knowledge Graphs: A Production Guide

8.5内容质量

TL;DR · AI 摘要

知识图谱能有效检测传统方法遗漏的财务欺诈模式,Neo4j实现的流水线可揭示多实体关联。

核心要点

  • 传统规则引擎无法检测25个供应商汇入同一账户的协同欺诈模式
  • Neo4j通过索引免费邻接指针实现多跳查询常数时间查找
  • 完整实现包含五种欺诈模式检测,可在fraud-detection-knowledge-graphs仓库获取

结构提纲

按章节快速跳转。

  1. 传统支付流程欺诈检测方法存在结构性缺陷,无法识别复杂关联模式。

  2. 表格数据难以高效追踪多实体关系,深度路径查询性能下降。

  3. Neo4j通过邻接指针实现多跳查询常数时间响应。

  4. 包含财务实体建模、预计算边优化和Cypher查询编写。

  5. 覆盖资金中介环、循环开票等复杂欺诈场景。

  6. 提供可扩展的图流水线部署方案和性能优化建议。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • 知识图谱欺诈检测
    • 传统方法局限
      • 孤立交易检查
    • Neo4j实现
      • 邻接指针优化
      • 预计算边
    • 欺诈模式
      • 资金中介环
      • 循环开票

金句 / Highlights

值得收藏与分享的关键句。

#知识图谱#Neo4j#欺诈检测#金融风控
打开原文

如何利用知识图谱检测财务欺诈模式:生产指南

2026年10月30日

/

#knowledge graph

Kamal Kishore

企业支付流程中的大多数反欺诈架构都是孤立地评估交易。

当发票到达时,自动化规则引擎会将其与静态阈值进行比对。它会检查金额是否在正常范围内、供应商是否在批准的供应商名单中,以及提交的IP地址是否熟悉。如果单个属性看起来合理,付款就会通过。

这种方法的问题在于结构性。复杂的欺诈手段,如洗钱团伙、循环开票和供应商碰撞,通常不会触发单个阈值的警报。一个壳公司开具4800美元的发票以低于5000美元的双签阈值,在孤立情况下看起来完全正常。但当25个看似独立的供应商都将付款路由到同一银行账户时,协调的模式才逐渐显现。

孤立交易的属性中不会出现警告信号。它存在于支付网络的拓扑结构中。

在关系型数据库中,追踪这些多实体关系需要跨供应商、发票、付款和银行账户表进行递归单元连接。但随着表增长到数百万行,深度路径遍历会迅速降低查询性能。图数据库通过将关系存储为一等公民的无索引邻接指针来解决这个问题,将多跳遍历转换为常数时间查找。

在本教程中,你将探索基于Neo4j构建的端到端关系型欺诈检测流水线架构。你将看到如何将金融实体建模为图,预计算派生边以优化查询延迟,并编写参数化Cypher查询来揭示五种核心采购欺诈模式。

完整的参考实现可在fraud-detection-knowledge-graphs仓库中找到。

我们将涵盖的内容:

  • 为什么表格规则会遗漏关系型欺诈
  • 先决条件
  • 如何建模金融图和摄入架构
  • 如何使用Cypher查询检测五种欺诈模式
  • 如何运行端到端流水线并验证结果
  • 生产架构:如何扩展图流水线
  • 关键架构教训和要点
  • 结论

为什么表格规则会遗漏关系型欺诈

在应付账款流程中,风险检查通常针对孤立记录进行:

code
# 交易级视图 —— 没有可疑之处
transactions = [
    {"vendor": "Alpha Corp", "amount": 5200.00, "date": "2025-08-01"},
    {"vendor": "Beta LLC", "amount": 4800.00, "date": "2025-08-02"},
    {"vendor": "Gamma Inc", "amount": 5100.00, "date": "2025-08-03"},
]
# 所有金额都在正常范围(1000美元–15000美元)。所有供应商有效。
# 日期是连续的。没有任何规则被触发。

当你将这些相同记录映射到互联图中时,隐藏的结构会立即显现:

code
# 图视图 —— 所有三个供应商都支付到同一账户
connections = {
    "ACC-MULE-001": ["Alpha Corp", "Beta LLC", "Gamma Inc"],
    "ACC-0042":     ["Delta Corp", "Epsilon LLC"],
    "ACC-0099":     ["Zeta GmbH"],
}
# ACC-MULE-001接收来自25多个供应商的付款。这是一个洗钱团伙。
# 收敛是红色警报。

底层数据没有变化。但当你在实体图中评估交易时,协调的漏斗会变得明显。

在关系型数据库中,连接这些点需要在不同表之间进行多次连接。在知识图谱中,这些连接直接以连接节点的边形式存储,使您可以通过单次遍历查询它们。

模式

是什么

传统工具无法捕捉到的内容

重复发票

相同的发票和供应商,金额几乎相同,日期相近

精确匹配会忽略细微差异

循环环

供应商之间相互开票形成封闭回路

每张发票单独看来都是合法的

资金中间人环

多个供应商向一个受控账户汇款

单笔支付本身没有异常

供应商共谋

看似独立的供应商共享底层属性

无法跨实体检测集群

支付异常

发票由部门权限范围外的人批准

基于角色的检查通过,但缺乏上下文检查

知识图谱不会取代规则引擎或机器学习模型。但它确实填补了这些工具无法触及的空白:关系。

先决条件

要跟随本教程,您需要:

  • 在您的机器上安装了Python 3.10或更高版本。
  • 安装Docker和Docker Compose以运行本地Neo4j实例。
  • 对Python和数据库概念有基本了解。

如何建模财务图和摄入模式

在运行欺诈查询之前,需要将数据结构化为连贯的属性图。参考数据集建模了财务系统中常见的六种实体类型:

实体

关键属性

角色

供应商

tax_id, name, country, city

开具发票的主体

发票

invoice_id, amount, date, category

所开具的账单

支付

payment_id, amount, date

所支付的款项

银行账户

account_id, routing_number, bank_name

资金流向的位置

员工

employee_id, name, department

批准支付的主体

组织

org_id, name, departments

结构层级

基础关系形成一个二分支付网络:

code
(Employee)-[:APPROVED]->(Payment)-[:LINKED_TO]->(Invoice)-[:ISSUED_BY]->(Vendor)
                                   |
                                   v
                            (Payment)-[:PAID_TO]->(BankAccount)

步骤1:如何使用已注入模式生成数据集

由于隐私和监管限制,真实世界的财务数据集难以获取。使用合成数据可以注入已知的欺诈模式,并系统性验证检测查询是否能发现这些模式。

数据集生成器创建约500个实体,嵌入五个已知模式:

  • 12对重复发票
  • 1个循环发票环(3个供应商)
  • 1个资金中间人环(25个供应商向1个账户汇款)
  • 1个供应商共谋集群(4个供应商)
  • 3个支付异常
code
from dataset.generate import FraudDatasetGenerator

gen = FraudDatasetGenerator(
    num_vendors=100,
    num_employees=30,
    num_organizations=5,
    seed=42,
)
output_dir = gen.save("dataset/output")

设置固定seed=42使数据集完全可复现。运行生成器每次都会产生完全相同的欺诈模式,为您提供可靠的查询测试基准:

code
供应商: 100
发票: 220 (200基础 + 20欺诈)
支付: 331 (300基础 + 31欺诈)
银行账户: 51 (50 + 1中间人账户)
员工: 30
组织: 5
欺诈模式: 5种类型

生成器还会导出一个真实情况清单文件ground_truth.json,记录所有注入的异常,这将成为您自动验证的黄金标准。

第 2 步:如何连接到 Neo4j 并创建约束

你可以使用 Docker Compose 启动一个本地 Neo4j 5.20 容器并启用 APOC 过程:

code
docker compose up -d

接下来,初始化数据库驱动并建立唯一性约束:

code
from graph.connection import FraudGraph

with FraudGraph() as graph:
    graph.create_constraints()

这些约束对查询性能至关重要。在 Neo4j 中,唯一属性约束会自动构建底层的 B 树索引。如果没有这些约束,每个 MERGE 操作都会强制对数据库中的所有节点进行未索引的全表扫描:

code
class FraudGraph:
    def __init__(self, uri=None, user=None, password=None):
        self.uri = uri or os.environ.get('NEO4J_URI', 'bolt://localhost:7687')
        self.user = user or os.environ.get('NEO4J_USER', 'neo4j')
        self.password = password or os.environ.get('NEO4J_PASSWORD', 'password')
        self.driver = None

    def connect(self):
        self.driver = GraphDatabase.driver(
            self.uri, auth=(self.user, self.password),
            max_connection_pool_size=50,
            connection_acquisition_timeout=30,
        )
        with self.driver.session() as session:
            session.run('RETURN 1')
        return self

    def __enter__(self):
        self.connect()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def create_constraints(self):
        with self.driver.session() as session:
            session.run("CREATE CONSTRAINT vendor_tax_id IF NOT EXISTS FOR (v:Vendor) REQUIRE v.tax_id IS UNIQUE")
            session.run("CREATE CONSTRAINT invoice_id IF NOT EXISTS FOR (i:Invoice) REQUIRE i.invoice_id IS UNIQUE")
            session.run("CREATE CONSTRAINT account_id IF NOT EXISTS FOR (a:BankAccount) REQUIRE a.account_id IS UNIQUE")
            session.run("CREATE CONSTRAINT employee_id IF NOT EXISTS FOR (e:Employee) REQUIRE e.employee_id IS UNIQUE")

第 3 步:如何将实体加载到图中

FraudGraphLoader 使用 Cypher 的 UNWIND 批量插入实体到参数化事务中:

code
from graph.loader import FraudGraphLoader

with FraudGraph() as graph:
    graph.create_constraints()
    loader = FraudGraphLoader(graph)
    loader.load_all(data)

加载顺序遵循外键依赖关系:首先加载组织,然后是供应商、银行账户和员工,最后加载发票和付款。

每个加载方法都遵循相同的模式:

code
UNWIND $data AS v
MERGE (vendor:Vendor {tax_id: v.tax_id})
SET vendor.name = v.name,
    vendor.country = v.country,
    vendor.city = v.city
WITH vendor, v
MATCH (org:Organization {org_id: v.organization})
MERGE (vendor)-[:BELONGS_TO]->(org)

MERGE 用于创建或匹配一个节点(通过约束实现 O(1) 查找),SET 用于附加属性,而关系上的 MERGE 用于连接节点。

第 4 步:如何为快速查询构建派生关系

图数据库的一个核心架构决策是决定是在查询时计算关系,还是在数据摄入期间将其构建出来。

确定两个供应商是否共享一个银行账户需要遍历一个四跳路径:

code
(Vendor)<-[:ISSUED_BY]-(Invoice)<-[:LINKED_TO]-(Payment)-[:PAID_TO]->(BankAccount)<-[:PAID_TO]-(Payment)-[:LINKED_TO]->(Invoice)-[:ISSUED_BY]->(Vendor)

在每次实时欺诈查询期间评估这个四跳路径会导致显著的计算开销。相反,可以在数据进入系统时物化派生边。这会在批量摄入期间略微增加写入延迟,但可以将多跳遍历转换为运行时检测查询中的直接单跳查找。

第一个派生关系是 SHARED_ACCOUNT_WITH,它将共享目标银行账户的供应商链接在一起:

code
MATCH (v1:Vendor)<-[:ISSUED_BY]-(i1:Invoice)<-[:LINKED_TO]-(p1:Payment)-[:PAID_TO]->(a:BankAccount)
MATCH (v2:Vendor)<-[:ISSUED_BY]-(i2:Invoice)<-[:LINKED_TO]-(p2:Payment)-[:PAID_TO]->(a)
WHERE id(v1) < id(v2)
MERGE (v1)-[:SHARED_ACCOUNT_WITH]->(v2)

过滤条件 WHERE id(v1) < id(v2) 确保关系以内部节点 ID 之间的单向边形式存储,这可以防止重复的反向边。

你还可以物化共享地理位置和银行路由前缀,以检测松散的共谋网络:

code
-- 同一城市和国家
MATCH (v1:Vendor), (v2:Vendor)
WHERE id(v1) < id(v2)
  AND v1.city = v2.city
  AND v1.country = v2.country
MERGE (v1)-[:SHARED_LOCATION]->(v2)

-- 具有相同路由前缀(前3位数字)的银行账户
MATCH (v1:Vendor)<-[:ISSUED_BY]-(i1:Invoice)<-[:LINKED_TO]-(p1:Payment)-[:PAID_TO]->(a1:BankAccount)
MATCH (v2:Vendor)<-[:ISSUED_BY]-(i2:Invoice)<-[:LINKED_TO]-(p2:Payment)-[:PAID_TO]->(a2:BankAccount)
WHERE id(v1) < id(v2)
  AND a1.account_id <> a2.account_id
  AND substring(a1.routing_number, 0, 3) = substring(a2.routing_number, 0, 3)
MERGE (v1)-[:SHARED_ROUTING_PREFIX]->(v2)

摄入完成后,你可以检查图拓扑以确认基础和派生关系已正确加载:

code
info = graph.get_info()
for label, count in sorted(info['nodes'].items(), key=lambda x: -x[1]):
    print(f"  {label}: {count} nodes")
for rel, count in sorted(info['relationships'].items(), key=lambda x: -x[1]):
    print(f"  {rel}: {count} relationships")

输出显示原始实体和生成的派生关系:

code
Invoice: 220 nodes
  Payment: 331 nodes
  Vendor: 100 nodes
  BankAccount: 51 nodes
  Employee: 30 nodes

  SHARED_ACCOUNT_WITH: 412 relationships
  SHARED_LOCATION: 387 relationships
  LINKED_TO: 331 relationships
  PAID_TO: 331 relationships
  ISSUED_BY: 220 relationships
  APPROVED: 331 relationships
  SHARED_ROUTING_PREFIX: 52 relationships

这些派生关系专为高速检测而设计。在关系型数据库中,发现共享账户需要跨多个表进行自连接。在图数据库中,它变成了预计算的边,将复杂的遍历转换为直接查找。

如何使用 Cypher 查询检测五种欺诈模式

一旦建立好模式和派生关系,就可以构建参数化 Cypher 查询,用于标记五种常见的采购欺诈模式。

模式 1:如何查找重复发票

欺诈者通常会在几天或几周内提交重复发票,假设应付账款团队会将它们作为常规重复费用处理。

基于规则的引擎通常能捕捉到完全匹配的发票,但可能会遗漏日期略有偏移的发票。在图数据库中,可以查询同一供应商在滚动日期窗口内开具的所有发票:

code
// graph/queries.py — detect_duplicates()
MATCH (v:Vendor)<-[:ISSUED_BY]-(i1:Invoice),
      (v)<-[:ISSUED_BY]-(i2:Invoice)
WHERE i1.invoice_id < i2.invoice_id
  AND i1.amount = i2.amount
  AND i1.date < i2.date
  AND duration.between(date(i1.date), date(i2.date)).days <= $max_days
RETURN i1.invoice_id AS invoice_a,
       i2.invoice_id AS invoice_b,
       v.name AS vendor,
       i1.amount AS amount,
       i1.date AS date_a,
       i2.date AS date_b,
       duration.between(date(i1.date), date(i2.date)).days AS days_apart
ORDER BY days_apart

关键点如下:

  • 条件 i1.invoice_id < i2.invoice_id 可防止返回重复的对,例如 (A, B) 和 (B, A)。
  • i1.amount = i2.amount 是欺诈信号:金额相同意味着可能是同一张发票被提交了两次。
  • 通过将搜索范围限定在由同一供应商 v 发票的范围内,查询利用了供应商的索引查找,然后再使用原生时间函数 duration.between() 评估日期间隔。

可以通过 Python 封装器执行此查询:

code
from graph.queries import FraudQueries

queries = FraudQueries(graph)
duplicates = queries.detect_duplicates(max_days_apart=14)
print(f"找到 {len(duplicates)} 对重复发票")
for dup in duplicates[:5]:
    print(f"  {dup['invoice_a']} ↔ {dup['invoice_b']} | "
          f"{dup['vendor']} | ${dup['amount']} | "
          f"{dup['days_apart']} 天间隔")

该查询检测到合成数据集中注入的所有 12 对重复发票,实现 100% 的召回率。

模式 2:如何揭露循环开票环

在循环开票模式中,三个或更多供应商相互之间形成闭环开票。每笔交易表面上看起来合法,但实际上没有实际的商品或服务交付。

由于已预先计算了 SHARED_ACCOUNT_WITH,环检测简化为在供应商子图中查找三个节点的三角形:

code
// graph/queries.py — detect_circular_schemes()
MATCH path = (v1:Vendor)-[:SHARED_ACCOUNT_WITH]-(v2:Vendor)
              -[:SHARED_ACCOUNT_WITH]-(v3:Vendor)
              -[:SHARED_ACCOUNT_WITH]-(v1)
WHERE id(v1) < id(v2) AND id(v2) < id(v3)
RETURN path,
       [n IN nodes(path) | {name: n.name, tax_id: n.tax_id}] AS vendors,
       length(path) AS hops
  • 约束条件 id(v1) < id(v2) AND id(v2) < id(v3) 消除了循环排列。如果没有这个条件,一个三个节点的环(A, B, C)会被返回三次。
  • 如果直接在未预计算边的原始四跳交易路径上运行此查询,找到三个节点的环需要遍历十二跳,这会导致大型数据集上的高 CPU 使用率。
code
circles = queries.detect_circular_schemes(min_cycle_length=3)
print(f"找到 {len(circles)} 个循环开票环")
for ring in circles:
    vendor_names = [v['name'] for v in ring['vendors']]
    print(f"  环 ({ring['hops']} 跳): {' ↔ '.join(vendor_names)}")

该查询以 100% 的召回率检测到循环开票环。

模式 3:如何检测资金中介网络

在资金中介网络中,多个空壳实体将资金汇入由犯罪集团控制的单一账户。单笔汇款金额较小,但目标账户的入度异常高。

可以通过对目标银行账户进行度数分析来发现这种模式:

code

// graph/queries.py — detect_money_mule_rings() MATCH (a:BankAccount)<-[p_paid:PAID_TO]-(p:Payment)-[:LINKED_TO]->(i:Invoice) -[:ISSUED_BY]->(v:Vendor) WITH a, collect(DISTINCT v) AS vendors, sum(p.amount) AS total_received, count(DISTINCT v) AS unique_vendors, count(p) AS payment_count WHERE unique_vendors >= $min_vendors RETURN a.account_id, a.routing_number, a.bank_name, unique_vendors AS vendor_count, payment_count, total_received AS total_amount, [vendor IN vendors | vendor.name] AS vendor_names ORDER BY unique_vendors DESC

code

您可以使用以下风险指南,根据入账连接评估账户:

供应商数量

风险等级

1–3

正常

4–9

调查

10+

可能为洗钱中介

在生产环境中,您可以添加总接收金额的阈值以减少误报:

WHERE unique_vendors >= 10 AND total_received > 50000

code

在Python中运行检测查询:

mules = queries.detect_money_mule_rings(min_vendor_count=5) for acct in mules: print(f" {acct['a.account_id']} ({acct['a.bank_name']}) | " f"{acct['vendor_count']} vendors | ${acct['total_amount']:.0f}")

code

该查询成功识别了注入的洗钱中介环(ACC-MULE-001),该环通过25个空壳供应商聚合付款。

### 模式4:如何识别供应商共谋集群

共谋的空壳公司通常通过使用不同的公司名称来隐藏其关联。但它们经常泄露元数据,例如共享的物理办公室套房或相同的银行分行路由前缀。

要识别这些集群,您可以同时评估多个派生关系,并计算复合结构风险评分:

// graph/queries.py — detect_vendor_collusion() MATCH (v:Vendor) OPTIONAL MATCH (v)-[:SHARED_ACCOUNT_WITH]->(v2) OPTIONAL MATCH (v)-[:SHARED_LOCATION]->(v3) OPTIONAL MATCH (v)-[:SHARED_ROUTING_PREFIX]->(v4) WITH v, collect(DISTINCT v2) AS shared_account_vendors, collect(DISTINCT v3) AS shared_location_vendors, collect(DISTINCT v4) AS shared_routing_vendors WITH v, size(shared_account_vendors) AS sc, size(shared_location_vendors) AS sl, size(shared_routing_vendors) AS sr WHERE (sc + sl + sr) >= $min_score AND (sc + sl + sr) > 0 RETURN v.name AS vendor, v.tax_id, (sc + sl + sr) AS risk_score, [n IN shared_account_vendors + shared_location_vendors + shared_routing_vendors | n.name] AS related_vendors, [x IN range(1, sc) | 'shared_account'] + [x IN range(1, sl) | 'shared_location'] + [x IN range(1, sr) | 'shared_routing'] AS flags ORDER BY risk_score DESC

code

- 在此处使用OPTIONAL MATCH至关重要。严格的MATCH会丢弃那些表现出共享路由和位置但尚未共享银行账户的供应商。

- 您可以根据业务环境调整风险评分。在大型商业枢纽中,共享城市是常见的,但同时共享路由前缀和办公室套房会将复合评分提升至调查阈值以上。

collusion = queries.detect_vendor_collusion(min_risk_score=2) for vendor in collusion[:10]: print(f" {vendor['vendor']} | Risk: {vendor['risk_score']} | " f"Flags: {vendor['flags']}")

code

多属性评分查询成功标记了四供应商共谋集群。

### 模式5:如何标记超出司法管辖区的付款批准

内部欺诈通常依赖员工在其部门权限范围外批准发票。例如,人力资源经理可能未经授权批准工程基础设施供应商的发票。

审批系统通常会确认审批人是否具有签署权限,但很少验证审批人所在部门是否对发票类别具有管辖权。图查询可以遍历完整的审批路径并评估部门权限:

code
// graph/queries.py — detect_payment_anomalies()
MATCH (inv:Invoice)<-[:LINKED_TO]-(p:Payment),
      (e:Employee)-[:APPROVED]->(p),
      (e)-[:WORKS_FOR]->(org:Organization)
WITH inv, p, e, org,
     CASE
       WHEN inv.category = 'procurement' AND
            e.department IN ['finance', 'operations', 'procurement']
            THEN 'ok'
       WHEN inv.category = 'payroll' AND
            e.department IN ['hr', 'finance']
            THEN 'ok'
       WHEN inv.category = 'travel' AND
            e.department IN ['operations', 'finance', 'hr']
            THEN 'ok'
       WHEN inv.category = 'marketing' AND
            e.department IN ['marketing', 'finance']
            THEN 'ok'
       WHEN inv.category = 'consulting' AND
            e.department IN ['finance', 'operations', 'procurement']
            THEN 'ok'
       WHEN inv.category = 'services' AND
            e.department IN ['finance', 'operations', 'procurement']
            THEN 'ok'
       WHEN inv.category = 'supplies' AND
            e.department IN ['operations', 'procurement', 'finance']
            THEN 'ok'
       WHEN inv.category = 'maintenance' AND
            e.department IN ['operations', 'procurement', 'finance']
            THEN 'ok'
       WHEN inv.category = 'software' AND
            e.department IN ['engineering', 'operations', 'finance']
            THEN 'ok'
       ELSE 'ANOMALY'
     END AS approval_status
WHERE approval_status = 'ANOMALY'
RETURN inv.invoice_id, inv.amount, inv.category,
       inv.vendor_name, inv.date,
       e.name AS approver, e.employee_id,
       e.department AS approver_department
ORDER BY inv.date DESC
  • 默认拒绝的 ELSE 'ANOMALY' 结构确保任何未明确授权的发票类别都会被拒绝。
  • 在企业架构中,应直接在图中使用 (:Department)-[:AUTHORIZED_FOR]->(:Category) 节点实现此策略逻辑,而不是在查询中硬编码值。这使合规团队可以在不部署新代码的情况下更新权限矩阵。
code
anomalies = queries.detect_payment_anomalies()
print(f"Found {len(anomalies)} anomalous approvals")
for a in anomalies:
    print(f"  Invoice {a['inv.invoice_id']}: "
          f"{a['e.name']} ({a['e.department']}) approved "
          f"{a['inv.category']} | ${a['inv.amount']}")

该查询能够以100%的召回率识别出所有3个注入的审批异常。

如何运行端到端流水线并验证结果

您可以将这些步骤编排为自动化命令行流水线:

code
python analysis/run_detections.py

端到端数据流包含六个独立阶段:

code
生成数据集 → 加载到Neo4j → 构建派生关系 → 运行5个查询 → 保存结果 → 验证

如何运行数据摄入和检测

主流水线脚本负责数据生成、模式创建、关系构建和查询执行:

code
# analysis/run_detections.py
from dataset.generate import FraudDatasetGenerator
from graph.connection import FraudGraph
from graph.loader import FraudGraphLoader
from graph.queries import FraudQueries

def run_pipeline(output_dir='analysis/output', force_regenerate=False):
    os.makedirs(output_dir, exist_ok=True)

    # 第一步:生成或加载数据集
    dataset_path = os.path.join(output_dir, 'data')
    if not os.path.exists(os.path.join(dataset_path, 'vendors.json')) or force_regenerate:
        gen = FraudDatasetGenerator(seed=42)
        gen.save(dataset_path)
    else:
        print(f'数据集已存在:{dataset_path}')

    # 加载JSON文件
    data = {}
    for name in ['vendors', 'invoices', 'payments', 'bank_accounts',
                  'employees', 'organizations', 'ground_truth']:
        with open(os.path.join(dataset_path, f'{name}.json')) as f:
            data[name] = json.load(f)

    # 第二步:加载到Neo4j
    graph = FraudGraph()
    graph.connect()
    graph.create_constraints()
    loader = FraudGraphLoader(graph)
    loader.load_all(data)
    loader.create_shared_account_relationships()
    loader.create_attribute_based_relationships()

    info = graph.get_info()
    print('图已加载:')
    for label, count in sorted(info['nodes'].items(), key=lambda x: -x[1]):
        print(f'  {label}: {count} 个节点')

    # 第三步:运行所有检测
    queries = FraudQueries(graph)
    results = queries.detect_all()

    # 第四步:保存结果
    with open(os.path.join(output_dir, 'detection_results.json'), 'w') as f:
        json.dump(results, f, indent=2, default=str)

    for pattern, items in results.items():
        print(f'  {pattern}: {len(items)} 个发现')
code
detect_all() 方法会运行所有五个查询并返回一个字典:

graph/queries.py

def detect_all(self): return { 'duplicate_invoices': self.detect_duplicates(), 'circular_schemes': self.detect_circular_schemes(), 'money_mule_rings': self.detect_money_mule_rings(), 'vendor_collusion': self.detect_vendor_collusion(), 'payment_anomalies': self.detect_payment_anomalies(), }

code

### 如何使用注入的真实数据验证召回率

验证脚本会将查询结果与真实数据文件进行对比以测量召回率:

analysis/validate.py

from collections import defaultdict

def validate(results_path, output_path=None): with open(results_path) as f: results = json.load(f)

with open(os.path.join(os.path.dirname(results_path), 'data', 'ground_truth.json')) as f: ground_truth = json.load(f)

gt_by_pattern = defaultdict(list) for pattern in ground_truth: gt_by_pattern[pattern['pattern']].append(pattern)

code

验证逻辑会评估每种模式类型的检测准确性:

for pattern_name, gt_patterns in gt_by_pattern.items(): found = results.get(pattern_name, []) gt_count = len(gt_patterns)

if pattern_name == 'duplicate_invoices': print(f"重复发票:检测到 {len(found)} 对,共 {gt_count} 对 — {len(found)/max(1,gt_count):.0%} 召回率")

elif pattern_name == 'money_mule_rings': matched = sum(1 for gt in gt_patterns for r in found if r.get('a.account_id') == gt['shared_account']) print(f"洗钱团伙:检测到 {matched} 个,共 {gt_count} 个 — {matched/max(1,gt_count):.0%} 召回率")

code

运行验证器对使用 seed=42 生成的数据集进行检测,得到以下结果:

重复发票:检测到 12 对中的 12 对 —— 100% 召回率 循环方案:检测到 1 个环中的 1 个环 —— 100% 召回率 洗钱团伙:检测到 1 个环中的 1 个环 —— 100% 召回率 供应商合谋:标记出 89 个高风险供应商(多属性评分) 支付异常:检测到 3 个异常中的 3 个 —— 100% 召回率

code

### 如何执行完整工作流程

以下是设置环境、运行流水线并验证结果的完整终端命令序列:

1. 启动 Neo4j

docker compose up -d

2. 安装依赖

pip install -r requirements.txt

3. 运行完整流水线

python analysis/run_detections.py

4. 验证结果

python analysis/validate.py

5. (可选) 使用新数据重新生成并重新运行

python analysis/run_detections.py --regenerate

code

## 生产架构:如何扩展图流水线

虽然参考原型在数百个节点上运行,但生产环境的欺诈检测引擎每天需要处理数百万笔交易。扩展此架构需要解决关键的操作瓶颈。

新交易 ↓ 阶段 1 — 确定性查询(快速,基于规则) ↓ (如果匹配任何模式) 阶段 2 — 基于图特征的机器学习评分 ↓ 风险评分 → 触发告警 / 通过 / 升级

code

### 如何实现两阶段检测架构

在在线支付路径中,对每笔入账交易同步运行全局循环检测查询会导致数据库连接池耗尽。

应将检测分为两个层级:

- 阶段 1(同步拦截):亚毫秒级 Cypher 查询,用于捕获已知模式。它们速度快是因为使用了索引属性、预构建关系和有限长度的模式。

- 阶段 2(异步图分析):对于未触发确定性规则的交易,计算基于图的特征并进行评分:

### 如何调整阈值和管理误报

检测系统不可避免会产生误报。在生产环境中,应使用合规分析师的反馈持续校准查询阈值:

根据误报率调整阈值

queries.detect_money_mule_rings(min_vendor_count=10) # 原为 5 queries.detect_duplicates(max_days_apart=7) # 原为 14 queries.detect_vendor_collusion(min_risk_score=3) # 原为 2

code

每次调整阈值后,可以重新运行验证以监控召回率影响:

python analysis/validate.py --input analysis/output/detection_results.json

code

### 如何缓解超节点扩展问题

图系统中常见的故障是“超节点”问题。合法的共享实体(例如大型企业房东、国家电力公司或内部工资账户)可能累积数万个入边。

当查询尝试在不进行过滤的情况下通过超节点时,遍历延迟会呈指数级增长。

可通过以下两种策略缓解:

- 度数限制:在遍历中添加度数剪枝:

MATCH (a:BankAccount)

WHERE size((a)<-[:PAID_TO]-()) < 1000

- 节点标签:为已知的高流量机构实体添加 :Whitelisted 或 :Utility 标签,并明确排除它们在合谋和洗钱检测中的扫描。

### 如何使用图特征进行机器学习

最有效的欺诈检测系统不会将图和机器学习视为相互竞争的范式。相反,图作为预测模型的高级特征存储:

- **度中心性**:衡量账户的连接性,以识别资金汇集点。
- **中介中心性**:识别在看似不相关供应商群体之间充当桥梁的实体。
- **PageRank 和社区检测**:量化网络中集群关联性及实体重要性。

仅依赖交易金额的机器学习分类器可能不会对 4900 美元的支付产生异常判断。但当该特征与图衍生特征结合——显示供应商具有高中介中心性且与已确认的欺诈团伙存在二级关联时,分类器能以高精度标记该交易。

## 关键架构教训与要点

构建和调优基于图的欺诈流水线突显了多个关键权衡:

首先,在关系型数据库中,模式设计优先考虑第三范式(3NF)以消除数据冗余。而在图数据库中,应围绕需要执行的遍历操作设计模式。如果查询需要反复遍历四跳以查找相关属性,应在数据摄入时将该关联直接建模为关系。

其次,唯一属性约束不仅是数据完整性保障,它们还提供了使节点查找达到 $O(1)$ 复杂度的底层哈希索引。省略约束会导致所有图合并操作退化为未索引的全表扫描。

第三,应精心管理规则库。从少量高信号模式(4-5 个)开始,每个模式都需要阈值校准和处置流程。同时添加数十个未经验证的启发式规则会为合规团队带来操作噪声和警报疲劳。

最后,建立分析师反馈闭环。检测查询的输出是假设而非最终结论。当合规分析师调查警报并确认欺诈时,应将该状态写回图中作为节点属性或标签(例如::ConfirmedFraud)。这会丰富邻域拓扑结构,并为下游机器学习模型训练提供干净的真实数据。

## 结论

在本教程中,你学习了为何实时关系检查会遗漏复杂欺诈模式,如何将应付账款数据建模为互联图,以及如何通过衍生边的实体化加速遍历延迟。

你还实现了五个 Cypher 查询,可检测重复计费、环形团伙、资金中间人、供应商合谋和未经授权的付款批准。

本文配套代码可在 github.com/kamalct/fraud-detection-knowledge-graphs 获取。所有数据均为合成数据且可完全复现。

推荐课程(合作伙伴推荐,1/4)

(新标签页中打开)

**广告**

- Udemy《Dynamics 365 Finance & Operations:财务部分1/3 MB310 D365 Studio》4.5 星(1,724 人评分)4小时9分钟 初学者 证书 查看课程(新标签页中打开)
- Udemy《Neo4j:使用 Cypher 的图数据库基础》Louis Sayers 4.6 星(5,652 人评分)4小时46分钟 初学者 证书 查看课程(新标签页中打开)
- Coursera《数据科学:图分析》Madecraft 4周(每周1小时)初学者 证书 查看课程(新标签页中打开)

- 在 Udemy 上的 SAP Uplatz 训练《应付账款》4.4(满分5分)(1,468 人评价)10小时42分钟 初级 证书 查看课程(在新标签页中打开)

更多 Neo4j 课程在 Class Central

→

推荐课程(来自合作伙伴,侧边栏第2个)

- 在 Udemy 上的《Neo4j:Cypher、GDS、GraphQL、LLM、用于 RAG 的知识图谱》Cramlays, Nitin Gujral 4.5(满分5分)(246 人评价)3小时 初级 证书 查看课程(在新标签页中打开)

- 在 Coursera 上的《银行运营与欺诈预防》EDUCBA 2周,每周2小时 初级 证书 查看课程(在新标签页中打开)

- 在 Coursera 上的《Neo4j:Cypher、GDS、GraphQL、LLM、用于 RAG 的知识图谱》Packt - 课程讲师 9周,每周1小时 中级 证书 查看课程(在新标签页中打开)

- 在 edX 上的《使用 Neo4j 理解图数据库》2周,每周2-4小时 初级 证书 查看课程(在新标签页中打开)

推荐课程(来自合作伙伴,侧边栏第3个)

- 在 Corporate Finance Institute 上的《支付技术基础》Esteban Santana 5(满分5分)(6 人评价)34分钟 初级 证书 查看课程(在新标签页中打开)

- 在 DataCamp 上的《使用 LangChain 和 Neo4j 构建图 RAG》Adam Cowley 4.8(满分5分)(125 人评价)3小时 中级 证书 查看课程(在新标签页中打开)

推荐课程(来自合作伙伴,侧边栏第4个)

阅读更多文章。

如果这篇文章对你有帮助,请分享。

免费学习编程。freeCodeCamp 的开源课程已帮助超过40,000人成为开发者。立即开始

推荐课程(来自合作伙伴)