P4 Gotham
图分区与压缩:一张大图分成几块,块之间什么关系
社群发现看"局部谁是一伙",图分区看"整张图整体上分几块、每块是谁、块与块怎么往来"——多层折叠 Louvain 分层折叠取模块度最高层,小社区并入共享边权最大的邻居,压缩输出社区图,同参数二次调用命中缓存直返。看完这 3 个故事,你就能把一张几百节点的网盘成几块可解读的版图。
反洗钱分析师
情报研判专家
Louvain
多层折叠
社区压缩
结果缓存
共 3 个故事
能 / 不能速览
✅ 这个主题能做
- POST /analysis/graph/partition:多层折叠 Louvain 分区(层间 foldWeighted 折叠),取各层中模块度最高层划分
- 小社区合并:<min_community_size 的社区并入共享边权最大的邻居社区,孤立社区豁免
- 并行 Phase1:节点数 ≥64 时按节点分片 goroutine 并行局部移动(默认 parallel=4),结果确定性归并
- CompressGraph 压缩输出社区图:社区聚合节点 {community_id, size, top_labels} + 社区间聚合边 {from, to, weight}
- 参数幂等缓存:ga_partitions 表,id=归一化参数 JSON 的 SHA-256 前 16 字节,同参数二次调用直返缓存
- GET /analysis/graph/partitions 分区历史列表(created_at 倒序,最多 100 条)
⛔ 这个主题做不了
- 分区 / 时序接口全部挂在 admin 角色下,非 admin 一律 403
- 参数负值(levels / min_community_size / parallel)返回 422 VALIDATION_ERROR
- partition 是只读分析:分区结果不写回原图,压缩图是只读摘要
- 分区仍受 Louvain 简化版约束(迭代上限 10、社区数上限 50),复杂大图划分与标准 Louvain 可能有差异
- 缓存键是参数哈希不是图数据哈希:图结构变化后同参数仍命中旧缓存,需人工重跑覆盖
适用角色
本主题面向两类角色:
- 反洗钱分析师:核心使用者,用分区看整张网络的团伙版图,用压缩社区图看块与块的资金往来。
- 情报研判专家:调参合并小社区、回看分区历史,把结构分块与业务情报交叉印证。
分区接口与图分析其它算法一样挂在 admin 角色下,执行前请确认当前账号具备管理员权限;分区是"算法建议",最终研判仍需人工把关。
能力速览(能做什么)
多层折叠 Louvain
每一层跑 Louvain 后按社区折叠(foldWeighted 聚合社区内 / 社区间边权),再跑下一层,共迭代 levels 层;层间折叠能发现更大粒度的社群结构。
取模块度最高层
每层记录 LevelStat(level / communities / modularity),最终取各层中模块度最高的划分作为结果,而不是简单用最后一层。
小社区合并
成员数小于 min_community_size 的社区并入共享边权最大的邻居社区;无共享边的孤立社区豁免保持原样,避免强行并入断开社区破坏模块度。
并行 Phase1
节点数 ≥64 时按节点分片到多个 goroutine 并行计算局部移动(默认 parallel=4),小图走串行;平局按 size+成员字典序确定,结果不因并行抖动。
社区图压缩
CompressGraph 按分区结果把原图聚合成社区图:社区聚合节点(top_labels 按频次最多 5 条)+ 社区间聚合边(weight=原始边计数),前端用桑基图 / 柱状图呈现。
参数幂等缓存
ga_partitions 缓存分区结果,id=归一化参数 JSON SHA-256 前 16 字节;同参数(含零值等价形式)恒定命中同一键,二次请求直返缓存不重复执行。
调整指南(怎么调整)
- 改分块粒度:levels 控制多层折叠层数(默认 2),调大看更粗粒度的分块,但层级越高块越少。
- 改合并阈值:min_community_size 控制小社区合并(默认 3),调大把更小的社区并掉、调小保留边缘小团体。
- 改并行度:parallel 控制并行 goroutine 数(默认 4),节点数 <64 时并行不生效走串行。
- 改对比口径:每层 LevelStat 的 modularity 可以对比,选模块度最高的参数组合;partitions 历史按 created_at 倒序回看调参轨迹。
- 改缓存策略:同参数自动命中缓存,想强制重算就改一个参数(如 min_community_size 微调)或等人工重跑覆盖。
做得好的场景
图分区最擅长回答"整张网从结构上分几块":
- 团伙版图初判:几百节点的网络几秒钟盘成几块,替代人眼数节点、拍脑袋划圈子。
- 块间往来识别:压缩社区图直接给出块与块之间的边权重,跨块往来一眼可见。
- 散兵游勇归并:小社区阈值一调,噪声小团体自动并入边权最大的邻居,不干扰主视野。
- 调参可回溯:参数幂等缓存 + 历史列表,哪个参数组合模块度最高有据可查。
限制与不足
以下是明确的边界,使用前先知道:
- 仅管理员可用:partition / partitions 接口挂在 admin 角色下,非 admin 一律 403。
- 参数校验严格:levels / min_community_size / parallel 为负返回 422,全零给默认值。
- 只读不写图:分区不修改原图数据,压缩图是只读摘要,想落盘需要自行持久化。
- 算法简化版:仍受 Louvain 简化版约束(迭代 10 轮、社区数上限 50),复杂网络与标准 Louvain 可能有差异。
- 缓存不过期:缓存按参数幂等,图数据变化后同参数仍命中旧缓存,需人工重跑覆盖。
场景故事
故事 1
200 节点嫌疑网,多层折叠 Louvain 盘成三块版图
场景:多层折叠分区
角色:反洗钱分析师
耗时:约 4 分钟
- 背景
- 苏分析师手里的嫌疑网络图扩到了约 200 节点。社群发现只能看局部聚团,她要回答的是"整张图整体上分几块、每块是谁"。她调用分区接口,用默认参数(levels=2)跑一次多层折叠 Louvain。
- 传统做法对比
- 以前判断"这张网分几块"靠人眼盯着关系图数,200 节点数到头晕,两个分析师数出的块还不一样;现在算法按结构分层折叠自动划分,结果可复核、可引用。
- 角色
- 苏分析师(反洗钱分析师,需 admin 角色——分区接口仅管理员可执行;登录 admin / admin1,端口 18083)。
- 操作步骤
-
- POST /analysis/graph/partition(body 空或默认参数)
- 看 communities 的 nodeID→社区ID 分配
- 看 levels 各层统计与 modularity
- 看 compressed 社区图摘要(桑基图 / 柱状图)
- 系统响应
- 返回 PartitionResult:
{
"communities": { "p_101": 0, "p_102": 0, "o_55": 0,
"p_203": 1, "s_8": 1, "p_9": 2 },
"modularity": 0.4126,
"levels": [
{ "level": 0, "communities": 6, "modularity": 0.3812 },
{ "level": 1, "communities": 3, "modularity": 0.4126 }
],
"compressed": {
"nodes": [
{ "community_id": "c0", "size": 120, "top_labels": ["陈亮", "天海物流"] },
{ "community_id": "c1", "size": 55, "top_labels": ["周涛", "东海航运"] },
{ "community_id": "c2", "size": 25, "top_labels": ["张远", "天河贸易"] }
],
"edges": [ { "from": "c0", "to": "c1", "weight": 8 } ]
}
}
社区 size 合计 = 原节点数。
- 结果洞察
- 多层折叠把 200 节点聚成 3 大块:c0(120 节点,以陈亮 / 天海物流为核心的物流圈)、c1(55,周涛 / 东海航运)、c2(25,张远 / 天河贸易链路)。c0 与 c1 之间有 8 条跨块边(weight=8)——两个圈子"有往来但不算一伙"。最终模块度 0.41 明显高于第 0 层的 0.38,说明第 1 层折叠找到了更粗粒度的分块,系统取的是模块度最高层。
- 调整建议
- levels 调大看更粗的分块(层级越高块越少);想要更细粒度就调小 min_community_size;把 compressed 的社区图与中心度榜单结合,锁定"跨块桥梁节点"。
- 动手试一试
- 操作:POST /analysis/graph/partition(不传 body)。预期结果:返回 communities / modularity / levels / compressed 四件套,社区 size 合计等于原图节点数;levels 数组里模块度最高的层与最终 modularity 一致。
- 限制提示
- 分区接口仅 admin 可调(非 admin 403);参数负值 422;分区只读不改图;空图返回空 communities(非错误);仍受 Louvain 简化版社区数上限 50 约束。
故事 2
小社区合并 + 并行 Phase1:两三个人的散圈并进大圈子,孤立的不动
场景:小社区合并
角色:情报研判专家
耗时:约 4 分钟
- 背景
- 韩老师发现默认分区结果里有两个"两三人小社区"孤零零挂在图边缘,他怀疑是数据噪声干扰主视野。他把 min_community_size 调到 5,让系统把小于 5 人的社区并入共享边权最大的邻居社区,同时观察孤立社区是否被豁免。
- 传统做法对比
- 以前小团伙要么人肉拉进大团伙(拍脑袋分给谁),要么留着不管(干扰视野);现在阈值一调,算法按共享边权自动归并,合并依据可复核。
- 角色
- 韩老师(情报研判专家,需 admin 角色)。
- 操作步骤
-
- POST /analysis/graph/partition(body {"levels":2,"min_community_size":5,"parallel":4})
- 对比两次结果的 communities 数量
- 看被合并的小社区成员去了哪个社区
- 核对 modularity 变化
- 系统响应
- 返回:
{
"communities": { "p_101": 0, "p_102": 0, "p_9": 0 },
"modularity": 0.4033,
"levels": [
{ "level": 0, "communities": 5, "modularity": 0.3701 },
{ "level": 1, "communities": 3, "modularity": 0.4033 }
],
"compressed": {
"nodes": [
{ "community_id": "c0", "size": 122, "top_labels": ["陈亮"] },
{ "community_id": "c1", "size": 56, "top_labels": ["周涛"] },
{ "community_id": "c2", "size": 22, "top_labels": ["张远"] }
],
"edges": [ { "from": "c0", "to": "c1", "weight": 9 } ]
}
}
社区总数从 6 变为 3。
- 结果洞察
- min_community_size=5 时,两个 3 人小社区分别并入与它们共享边权最大的邻居(c0、c1),c0 从 120 变 122、c1 从 55 变 56;没有任何共享边的孤立社区被豁免保持原样,不会强行并入断开社区破坏模块度。并行 Phase1 在节点 ≥64 时按分片 goroutine 计算局部移动,平局按"成员数升序、同 size 取最小成员字典序"确定,结果不会因并行而抖动。
- 调整建议
- 阈值设太高会把真团伙也并掉——记住"并入"是结构归并不是业务定性;想保留边缘小团体把 min_community_size 调回 2 或 3;用模块度对比合并前后的质量。
- 动手试一试
- 操作:POST /analysis/graph/partition,body 传 min_community_size=5。预期结果:communities 数量比默认参数少,被并社区的成员出现在新社区里;传负值(如 {"min_community_size":-1})预期 422 VALIDATION_ERROR。
- 限制提示
- 归并是"结构合并"不是"业务定性",是不是真同伙仍需人工研判;无共享边的孤立社区豁免;并行仅在节点 ≥64 且 parallel ≥2 时启用,小图走串行。
故事 3
同参数二次调用命中缓存:ga_partitions 把分区结果记住了
场景:参数缓存
角色:反洗钱分析师
耗时:约 3 分钟
- 背景
- 苏分析师对同一张图反复调参跑分区:第一次 levels=3 跑完,她改了个无关参数又改回来。她想验证系统会不会因为参数一样就复用上次结果,而不是把多层 Louvain 再算一遍。
- 传统做法对比
- 以前每次跑算法都重新算,200 节点小图无所谓,万级图就慢,而且没有"这次结果是不是上次那份"的凭据;现在参数幂等命中缓存,响应时间从秒级降到毫秒级。
- 角色
- 苏分析师(反洗钱分析师,需 admin 角色)。
- 操作步骤
-
- POST /analysis/graph/partition(body {"levels":3})第一次执行
- POST /analysis/graph/partition(body 不传,零值等价 levels=2 / min_community_size=3 / parallel=4)
- 重复提交完全相同的参数再试一次
- GET /analysis/graph/partitions 查看历史列表
- 系统响应
- 分区历史列表返回 ga_partitions 记录:
[
{
"id": "3f2a9c1d0b4e5f7a",
"params": { "levels": 2, "min_community_size": 3, "parallel": 4 },
"result": { "communities": { … }, "modularity": 0.4126, "levels": [ … ] },
"modularity": 0.4126,
"created_at": "2026-08-29T10:30:00Z"
}
]
同参数第二次请求直接返回缓存结果(HTTP 200),不再执行多层 Louvain。
- 结果洞察
- 缓存键 = 归一化参数 JSON 的 SHA-256 前 16 字节 hex(32 字符):"不传 body"与"传 {levels:2,min_community_size:3,parallel:4}"归一化后等价,恒定命中同一键;保存走 OnConflict upsert,同参数覆盖。partitions 历史按 created_at 倒序(最多 100 条)列出每次分区的参数与模块度,调参轨迹可回看。
- 调整建议
- 调参时每次只改一个维度便于对比 modularity;用 partitions 历史找"哪个参数组合模块度最高";注意缓存按参数幂等,图数据更新后要人工重跑覆盖。
- 动手试一试
- 操作:同一参数连续 POST 两次。预期结果:第二次返回与第一次一致(缓存命中);GET /analysis/graph/partitions 能看到缓存记录及其 modularity。
- 限制提示
- 缓存 id 是参数哈希不是图数据哈希,图结构变化后同参数仍命中旧缓存(需人工重跑覆盖);缓存写失败仅 warn 不影响返回;无 DB 环境 Load / Save 降级不报错。
常见问题
分区和社群发现(community)有什么区别?
社群发现在每个连通分量内部跑 Louvain,看"局部谁是一伙";分区是更高粒度——多层折叠 Louvain 把社区再折叠成超节点,回答"整张图整体分几块、块与块怎么往来",并压缩输出社区图。两者可以搭配:先用分区看版图,再进某一块用社群发现看内部。
为什么我调用分区接口返回 403?
分区(partition / partitions)与时序(temporal)接口全部挂在 admin 角色下,只允许管理员执行。请确认当前登录账号是 admin(登录 admin / admin1),或让管理员代为执行。
参数可以随便传吗?
levels / min_community_size / parallel 传负值会返回 422 VALIDATION_ERROR;全传 0 时取默认值(2 / 3 / 4)。建议一次只调一个维度,便于对比模块度与分块数。
分区会修改图数据吗?
不会。partition 是只读分析:只读 ListNodes / ListEdges,分区结果不写回原图,压缩图是只读摘要。想落盘需要自行持久化结果。
缓存会过期吗?
不会自动过期。缓存键是"归一化参数"的哈希,同参数恒定命中;但图数据更新后旧缓存不会自动失效,需要人工重跑覆盖(或用不同参数触发新计算)。
主题小结
一句话:图分区把"整张网分几块"从拍脑袋变成可复核的算法结论——多层折叠 Louvain 分层折叠取模块度最高层,min_community_size 小社区并入共享边权最大邻居(孤立豁免),CompressGraph 压缩输出社区图,同参数命中 ga_partitions 幂等缓存直返。记住边界:仅 admin 可用、参数负值 422、只读不写图、缓存按参数不过期。结构给轮廓,人给定性。