ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

图数据挖掘实战:K-core、Truss、Clique与ECC算法解析

2026/10/4 1:10:30 拓冰建站 浏览量
图数据挖掘实战:K-core、Truss、Clique与ECC算法解析 作为一个常年泡在图数据里的开发者我越来越觉得“稠密子图”这套东西是被低估的宝藏。你去看社交网络里寻找核心用户、风控场景里识别团伙、生物网络里找功能模块翻来覆去用的就是那几招K-core、truss、clique再加上一个用于衡量节点“离中心有多远”的ECC离心率。这四个概念单独拎出来都不难但真正把它们串起来解决一个实际问题很多人会卡住。这篇文章我就以一次典型的图数据挖掘任务为例把这些概念从原理到代码到坑完整捋一遍。这篇文章适合正准备入图谱、搞图挖掘的开发者也适合已经在用NetworkX但只会调包、不太清楚每个指标背后逻辑的朋友。我会从“为什么要用K-core而不是直接找clique”讲起再给出可复现的Python代码最后聊一聊我在实际项目里踩过的那些坑。1. 图数据挖掘里绕不开的四个概念ECC、K-core、Truss、Clique1.1 先从一次实际需求说起上个月有个朋友找我帮忙他们运营一个垂直领域的知识社区想找出“真正把社区撑起来的那批人”然后把运营资源倾斜过去。手里有一张用户之间的关注关系图节点大概五万个边大概四十万条。第一反应肯定是跑社区发现算法但试了Louvain之后发现分出来的社区太大、太碎根本没法直接用来圈人。后来我换了个思路不划分社区而是计算每个节点的core number和truss number把“在多大程度上处于一个稠密子图里”这个属性提取出来再叠加一些业务规则最后圈出来三千多人运营反馈这批人确实是内容贡献和互动的主力。这个案例基本上就是图数据挖掘里最经典的一类问题如何在复杂网络里找到有结构的局部稠密区域。而解决这类问题绕不开的就是ECC离心率、K-coreK核、core number核数、truss number桁架数、clique团这几个概念。1.2 ECC离心率衡量节点在网络中的“位置感”ECC的全称是Eccentricity图论里的定义很简单一个节点到图中所有其他节点的最短距离的最大值就是这个节点的离心率。换句话说它回答了一个问题从你出发最远要跳多少步才能摸到网络里任何一个角落。离心率越小说明这个节点越靠近网络中心离心率越大说明它越边缘。这里必须插一句ECC这个缩写太容易撞车了——有人会以为说的是椭圆曲线加密Elliptic Curve Cryptography还有人会联想到SAP ERP系统里的ECC模块。本文里所有ECC都是指图论中的离心率别搞混了。这两个完全不是一码事你在搜索引擎里输入“ECC”看到的结果大概率是加密算法但在图数据挖掘的语境下它就是离心率。离心率的价值在于它能快速告诉你网络的“直径”和“半径”。整个图的直径就是所有节点离心率的最大值半径就是最小值。直径决定了信息在网络中传播的最坏情况需要多少跳半径则能帮你找到“最核心的发起节点”。在实际项目中ECC常用于寻找关键传播源、评估网络连通效率但它的计算代价比较高因为它本质上要求全源最短路径这部分后面我会详细讲。1.3 K-core剥离出来的“层层核心”K-core是图数据挖掘里应用最广泛、性价比最高的稠密子图指标。它的定义是反复删除度数小于k的节点后剩余的子图。一个直观的理解方式就是“剥洋葱”先把那些只挂了1条边的节点全删掉剩下的子图就是1-core再删掉当前度数小于2的节点剩下的就是2-core不断重复这个过程剩下的就是k-core。core number核数是每个节点最终能留存到的最大k值。举个生活化的例子一个微信群如果群主把“一周内发言少于一次的人”全部踢掉剩下的就是1-core再把“发言少于3次的人”踢掉剩下的就是2-core。一个用户能活到第几轮筛选他的“core number”就是几。K-core最吸引人的地方在于它的计算复杂度极低几乎是线性的O(VE)。这意味着哪怕图有上亿条边跑一遍K-core分解也就是几秒钟的事。这个特性让K-core成为大规模图分析的默认首选工具。1.4 Truss number用三角形卡住“熟人圈”Truss是K-core的加强版。K-core只盯着节点的度数也就是你有多少个邻居但根本不管你的邻居之间认不认识。Truss则提出了更严格的要求一条边能属于k-truss当且仅当它至少被包含在k-2个三角形里。翻译成人话就是两个人之间的关系要算“稳固”不能只看他们各自朋友多还得看他们有多少共同朋友。共同朋友越多这段关系嵌入的三角形就越多这条边就越“牢靠”。truss number是边层面的指标它表示这条边最多能处于多大的truss中。这个指标在社交网络里特别有意思。朋友圈里有那么一种人好友上千但你要是看他跟哪些好友有共同交集可能寥寥无几。在K-core分解里这种人分数很高但在truss分解里会原形毕露——因为他的边根本没有三角形支撑。所以如果你要识别的是“真正的熟人圈子”truss往往比K-core靠谱得多。1.5 Clique最纯粹的稠密子图Clique团是稠密子图定义的“终极形态”一个团里的任意两个节点之间都有边相连。也就是说团里的每个人都直接认识每个人。K5表示5个节点两两相连K50表示50个节点全部互连。团又分为极大团和最大团。极大团是指没法再往里加任何一个节点还能保持“两两相连”性质的团最大团则是全图包含节点数量最多的那个团。找最大团是个经典的NP完全问题理论上当图规模变大时精确求解会爆炸式增长。所以实际工程里很少直接在全图上跑最大团更多是用极大团枚举配合剪枝或者在K-core、truss筛出来的子图上再跑团搜索。这四个概念放在一起看其实是一条层层递进的“稠密度”光谱。ECC衡量的是节点位置的全局属性K-core是从度数角度近似稠密truss用三角形约束了更严格的局部结构而clique是最强约束的完全子图。理解它们之间的区别和联系是做好图数据挖掘的第一步。2. 为什么主流方案是K-core和Truss而不是直接找Clique2.1 计算复杂度的现实抉择如果你去读图挖掘的论文会发现很多早期工作都围绕最大团展开但工程实践里却很少直接用团来做核心筛选。并不是团这个定义有问题而是计算代价太不可控了。找一个最大团是NP完全问题这意味着随着图规模增大计算时间可能指数级上升。哪怕是在一个只有几千个节点的中规模图上暴力枚举所有极大团也可能让你等到怀疑人生。而K-core分解是线性复杂度的Truss分解虽然比K-core贵一些但通过巧妙的三角形枚举也能在近线性到O(m^1.5)之间完成。这个复杂度差距直接决定了你在生产环境里能用哪个。我之前在一个百万节点的风控图上试过K-core分解跑了不到1秒Truss分解跑了大概2分钟而要找所有极大团——跑了一个小时没出结果最后我直接kill掉了。所以真实工程里的选择逻辑很简单能用K-core就用K-core需要更严格的结构就上truss而clique只在特定场景下作为二次精选的手段。2.2 Truss比K-core更适配社交语义K-core和truss的差异在语义层面也很明显。K-core只看“朋友数量”不看“朋友圈重叠”。这种特性决定了它在某些场景下会失效。举个例子一个明星账号关注了1000个人这1000个人也都回关了他K-core算出来这个明星的核心程度可能非常高。但这些人彼此之间可能完全不认识这个明星并没有嵌入到任何实际的“圈子”里。如果你用K-core来识别真实的核心用户就会误判。而Truss不会犯这个错误。它要求一条边必须嵌在多个三角形里三角形的含义就是“你朋友的朋友也是你的朋友”这在社交网络里高度对应熟人关系的闭合。所以做社交网络分析、社群挖掘、意见领袖识别时我通常优先跑truss再用K-core作为基线做对比。两条腿走路出来的结果才不容易被业务挑战。2.3 ECC和Clique的合理定位ECC虽然计算代价高但它提供的全局视角是K-core和truss给不了的。K-core和truss都是“局部密度”指标它们不关心一个节点在整个网络里处于中心还是边缘。而ECC恰好补上了这个视角一个节点可能core number不高但它正好处在连接两个社区的桥梁位置这种节点的ECC会比较小对网络的连通性至关重要。Clique则适合在局部使用。比如你已经通过truss筛出了一个结构极其紧密的子图接下来想看这个子图里是否包含更大的完全子图这个时候跑团搜索就有意义了。或者你在处理蛋白质相互作用网络想找功能模块完全子图往往对应着稳定的蛋白质复合物。把Clique和K-core、truss结合而不是取代才是正确的打开方式。3. 实操用Python完成K-core、Truss、Clique和ECC的完整计算3.1 环境准备与工具选型工欲善其事必先利其器图算法这块我强烈建议优先考虑NetworkX。它虽然在大规模图计算上性能一般但算法覆盖极全把K-core、truss、clique、ECC这些指标全都封装好了适合验证思路和学习原理。生产环境的大规模图可以考虑igraph或更底层的GraphScope但本文的操作部分用NetworkX就够了逻辑在哪个库都是通用的。安装很简单一条命令搞定pip install networkx matplotlib除了NetworkX我还会用matplotlib来画一些直观的图帮助理解结果。下面所有代码建议在Jupyter Notebook里跑方便边跑边看。3.2 构造一个带有明显层次结构的演示图为了把四个指标讲清楚我构造了一个小型社交网络图。这个图模仿了一个真实场景两个核心圈子通过一个中间人连接外围挂着一些半活跃用户再远处还有几个孤立节点。import networkx as nx import matplotlib.pyplot as plt G nx.Graph() # 社区A5人小团体内部关系紧密接近完全图 community_a [A1, A2, A3, A4, A5] for i in range(len(community_a)): for j in range(i 1, len(community_a)): G.add_edge(community_a[i], community_a[j]) # 社区B4人团体关系较紧密但不是完全图 community_b [B1, B2, B3, B4] B_edges [(B1, B2), (B2, B3), (B3, B4), (B4, B1), (B1, B3)] G.add_edges_from(B_edges) # 桥梁节点连接A社区和B社区 G.add_edge(A1, Bridge) G.add_edge(Bridge, B2) # 外围节点只连接核心社区但不构成三角形 peripheral [P1, P2, P3, P4, P5] G.add_edge(A2, P1) G.add_edge(A2, P2) G.add_edge(A3, P3) G.add_edge(A3, P4) G.add_edge(B4, P5) # 边缘节点挂在更外围 G.add_edge(P1, Outlier1) G.add_edge(P2, Outlier2) # 独立节点仅通过单边连接 G.add_edge(Outlier2, Isolated1) print(节点数:, G.number_of_nodes()) print(边数:, G.number_of_edges())这个图设计得不算复杂但结构层次很丰富社区A是标准的K5社区B有一个三角形和一个四边形组合桥梁节点连接两个社区外围节点是典型的“高朋友数、低共同朋友数”边缘节点则是纯粹的挂载节点。每个节点的指标差异会比较明显方便我们解读。3.3 计算K-core和core numberNetworkX里计算core number只需要一行代码。不过强烈建议你理解一下背后的实现原理因为它在很多图算法里都是基础组件。core_number nx.core_number(G) print(节点 - core number) for node, core in sorted(core_number.items(), keylambda x: x[1], reverseTrue): print(f{node}: {core})运行结果大致会是A社区的5个节点core number等于4B社区除了边比较弱的B4等于2之外其余等于2或3Bridge节点因为连接了A1和B2但它们的共同邻居为空core number只有1外围节点P1到P5是1Outlier和Isolated是0或1。这个结果说明什么K-core只能告诉你“这个节点周围有多少条边”它完全无法区分A2和P1的本质区别——P1虽然也是A2的邻居但P1处在一个没有闭合结构的稀疏区域里。实践中把K-core当筛选条件时一般这样用# 筛选出core number 3的子图通常就是最核心的骨架 core_subgraph nx.k_core(G, k3) print(3-core子图节点:, sorted(core_subgraph.nodes()))如果你的图足够大这个操作可以把数百万节点快速收敛到几百个核心节点计算代价极低是图粗筛的利器。3.4 计算Truss numberTruss在NetworkX里的计算接口有两种。一种是直接得到边层面的truss number另一种是提取k-truss子图。推荐直接算边上的truss number信息量更丰富# NetworkX没有直接给出边上truss number的现成函数 # 我们可以通过遍历不同k值来逼近每条边的最大truss数 def compute_truss_number(G): truss_num {} max_k 0 # 逐步提高k值直到子图为空 k 3 while True: sub nx.k_truss(G, k) if sub.number_of_edges() 0: break for u, v in sub.edges(): truss_num[(u, v)] k max_k k k 1 # 不在任何trussk3中的边truss number视为2 for u, v in G.edges(): if (u, v) not in truss_num and (v, u) not in truss_num: truss_num[(u, v)] 2 return truss_num truss_number compute_truss_number(G) print(边 - truss number) for edge, val in sorted(truss_number.items(), keylambda x: x[1], reverseTrue): print(f{edge}: {val})这段代码的思路是不断升级k值每轮提取k-truss子图看哪条边能撑到最高级别的truss。A社区里的边会得到很高的truss number因为K5里每条边都在3个三角形里所以它能达到truss number5。B社区里的对角线B1-B3在两个三角形里truss number是4而B2-B3只在一个三角形里truss number是3。Bridge节点连接A1和B2的那条边虽然Bridge自身的度数不低但这条边的两个端点没有共同邻居不在任何三角形里所以truss number就是2。这就是truss和K-core最典型的差异K-core会给Bridge打一个不错的分数但truss直接把它关在门外。3.5 计算Clique和ECCClique的枚举函数在NetworkX里也封装好了。我们还是用同一个图看看能搜出哪些极大的团重点观察A社区K5是否作为一个整体出现cliques list(nx.find_cliques(G)) print(极大团数量和大小分布:) for clique in sorted(cliques, keylen, reverseTrue): print(sorted(clique), 大小, len(clique))你会看到A社区整体作为一个大小为5的极大团出现B社区则会拆成若干大小为3的三角形团。这个结果直观地展示了极大团枚举在结构清晰的小图上的表现。但注意一旦图变大这个枚举的耗时就会迅速上涨所以别真拿它去跑大规模图。ECC的计算需要全源最短路径在NetworkX里直接调用即可ecc nx.eccentricity(G) print(节点 - ECC(离心率)) for node, val in sorted(ecc.items(), keylambda x: x[1]): print(f{node}: {val}) print(图的半径:, nx.radius(G)) print(图的直径:, nx.diameter(G))结果会很有意思A社区的节点离心率最小因为它们在图的中心区域Outlier和Isolated节点的离心率最大。Bridge节点的离心率处在中间它虽然不在任何一个紧密社区里但在全局拓扑中承担了连接作用它的ECC数值恰好反映了这一点。有些版本的NetworkX在计算离心率时要求图是连通的。如果图不连通会直接报错。这个坑我后面会详细讲。为了演示你可以在算之前把孤立节点临时去掉或者只在最大连通分量上计算。3.6 可视化把指标画出来光看数字不够直观用颜色和大小的编码能把结构看懂。下面这段代码把图布局画出来节点颜色深浅对应core number节点大小对应对数化的度数pos nx.spring_layout(G, seed42, k0.8) plt.figure(figsize(12, 8)) node_colors [core_number[n] for n in G.nodes()] node_sizes [300 * (1 G.degree(n)) for n in G.nodes()] nodes nx.draw_networkx_nodes( G, pos, node_colornode_colors, cmapplt.cm.viridis, node_sizenode_sizes, vmin0, vmax5 ) nx.draw_networkx_edges(G, pos, alpha0.3) nx.draw_networkx_labels(G, pos, font_size10) plt.colorbar(nodes, labelcore number) plt.axis(off) plt.title(图结构可视化颜色core number大小度数) plt.show()看到图基本就能理解为什么“度数高不等于核心”P1、P2的度数也不算低但颜色很浅而A社区五个节点颜色是最深的黄色。把core number和ECC两个属性叠加分析你甚至可以做一个简单的“节点角色划分”——核心成员core高、ECC小、桥梁节点core中等、ECC中等、外围节点core低、ECC大。这个划分思路可以直接延续到真实业务里。4. 常见问题与排查技巧实录4.1 大图性能优化思路如果你要处理的是千万级甚至亿级边的图NetworkX默认的单机实现会非常吃力。这时候通常会换用igraph的coreness函数或者干脆上Spark GraphX、GraphScope这类分布式框架。但在此之前有几个优化手段可以大幅降低计算压力我记得在一次真实项目里用这套组合拳把K-core的计算时间从几分钟降到了十几秒第一步先做连通分量分析只取最大的连通分量进行计算小于阈值的分量直接忽略。第二步用粗化抽样。如果业务上能接受近似结果可以先对图做子采样比如只保留度数在前20%的节点诱导子图在抽样图上算K-core再把结果映射回原图。第三步编码优化。NetworkX是纯Python实现对性能极其敏感。生产环境中可以考虑用Graph-tool或者igraph它们的C底层实现比NetworkX快一到两个数量级。Truss的计算比K-core还要重因为它要枚举三角形。在大图上跑truss之前建议先用K-core筛掉外围节点再跑能减少很多无谓计算。我在实际项目里通常的做法是先跑3-core或4-core在这个子图上再计算truss效果非常明显。4.2 不连通图的ECC计算问题ECC计算有个前提是要求图连通。如果图包含多个连通分量nx.eccentricity会直接抛异常。这是因为不连通的两个节点之间最短距离是无穷大离心率也就没有意义了。我的建议是分场景处理如果你想度量的是整个网络的全局拓扑那就只在最大连通分量上计算其他节点标记为“非主连通分量节点”单独分析。如果你关心的是每个连通分量内部的局部结构那就对每个分量分别计算再按分量大小加权汇总。千万不要图省事直接删掉所有小分量那样会丢失信息比如那些只包含两个节点的边可能在业务上是非常重要的关联。4.3 结果解读的常见误区用K-core和truss圈核心节点时最容易犯的错误是“唯指标论”。core number高只能说明这个节点周围有足够多的邻居形成一个紧密结构但它代表不了业务重要性。一个普通员工可能和部门里十几个同事都有协作边core number很高但真正跨部门推动项目的负责人core number反而不一定高。我一般在指标圈人后会再叠加两个业务过滤器第一个是“内容贡献度”比如发帖量、回复量、交易金额这些业务侧数值和结构指标做加权求和第二个是“跨社区连接数”如果一个节点连接了多个不同的K-core社区它在业务上往往是关键的传播枢纽比单纯在单个社区里core number高的节点更有运营价值。再就是K值的选择问题。很多人会拍脑袋选个K3或K5这不科学。我习惯的做法是画出core number的分布直方图找到分布里的“拐点”。比如core number从2到3断崖式下跌说明3-core就是你的“核心层”4-core就是“决策层”。这种基于数据分布确定阈值的方法比拍脑袋靠谱得多。4.4 Truss number与K-core混淆的实战教训最后分享一个真实的翻车案例。早期做社交网络用户分层时我直接用K-core筛选“高价值用户”结果筛出来的用户里混进了一批“点赞狂魔”——他们关注了几百个人但几乎不产生真实互动更不参与讨论。这批用户在K-core上分数很高但没有任何实质的圈子归属。后来改成用truss number做核心筛选情况立刻好转。因为共同好友少的人无论如何都凑不出三角形直接出局。这个教训让我意识到K-core和truss的语义差异在真实业务中非常致命。简单总结一下我的经验法则如果业务关系强调“数量”比如商品共现网络、引用网络用K-core如果业务关系强调“信任强度”或“圈层归属”比如社交好友、资金往来优先用truss。而ECC则作为全局视野的补充专门用来找出那些连接不同圈子、位置关键的“桥梁型节点”。图数据挖掘从来不缺花哨的算法缺的是对基础指标语义的精准把握。K-core、truss、clique、ECC这四个工具本质上是从不同角度回答“什么东西是重要的”这个问题。K-core告诉你哪里人多truss告诉你哪里关系铁clique告诉你哪里完全连通ECC告诉你哪里位置核心。把它们的输出组合起来才能构建出对图结构的立体理解。上面这套代码和思路直接改改数据就能复用到你手头的项目里剩下的就靠你在真实数据上去体会它们的差异了。