简介:networkx-1.11.zip 面向 Python 图论与复杂网络分析的学习者和开发者,提供一套可离线查阅、运行的经典 NetworkX 发行包,适合课程实验、算法复现以及旧项目依赖兼容等场景。压缩包共 594 个文件,约 1.47MB,其中 429 个 py 文件包含核心模块、算法实现与测试代码,117 个 rst 文件对应官方文档与教程,另有 txt 说明、bz2/gz 压缩的图数据样例、png 图片及 html/css 文档页面,便于按源码、文档和示例数据分层检索。已有 83 人学习或下载。借助包内完整的源码目录、文档说明和多种图格式样例,读者可理解图结构、最短路、中心性等功能的实现脉络,也能直接用于小规模网络数据实验与旧版本环境复现,减少从零搭建和查找资料的成本。
1. NetworkX 1.11 压缩包:为什么内网机器上还在用它算 PageRank
第一次在一台不联网的分析机上看到有人解压 networkx-1.11.zip,我以为是在做怀旧考古。后来发现那台机器的离线源里只有这一个图计算包,而上头的脚本写死了G.node[n]['weight']和nx.pagerank(G, weight='weight')这一套 1.x 的调用方式,换成新版 wheel 包当场抛 AttributeError。NetworkX 1.11 是 2016 年前后那一代版本,纯 Python、零编译依赖,解压装上就能建图、跑最短路、算 PageRank、导 GML,对没有外网、没有 C 工具链的环境格外友好。这份压缩包解决的从来不是「最新最好」,而是「让一串老脚本原地跑起来」。它适合三类人:维护历史分析管线的人、教学实验里需要固定 API 行为的人、以及要在隔离环境里复现旧结果的人。
2. 从 zip 到 import:解包、依赖与隔离环境的搭建顺序
拿到一个源码压缩包,最忌讳的动作是直接pip install然后祈祷。1.11 这个年代的包,setup.py里的依赖声明、metadata 格式、以及它对解释器版本的心理预期,都和今天的构建工具链有代差。先花两分钟看清楚包里有什么,比装完再回滚省事得多。
2.1 先看目录结构和依赖声明,再决定装在哪
zip 包解开后通常是单层目录networkx-1.11/,里面放着setup.py、networkx/(真正的包体)、doc/、examples/、tools/。examples/别急着删,那里面有不少 1.x 时期的算法调用样例,比在线文档更能反映这个版本的真实行为。
# 不解压,先看顶层结构,确认包名和版本目录 unzip -l networkx-1.11.zip | head -40 # 直接读 setup.py 里的依赖声明,重点看 install_requires unzip -p networkx-1.11.zip networkx-1.11/setup.py | grep -n -A6 "install_requires" # 确认包体是否完整(应该能看到 algorithms、generators、drawing 等子包) unzip -l networkx-1.11.zip | grep "networkx/algorithms/__init__.py"上面三段命令分别解决三个问题:包是不是官方结构、它到底依赖什么、包体有没有被裁剪过。install_requires那一段的输出会告诉你它依赖decorator,这是 1.x 系列绕不开的外部依赖,所有@not_implemented_for、@open_file这类装饰器都建在它上面。参数上没什么可调的,但这条信息决定了后面能不能一次装成——现代decorator的主版本改动过装饰器签名处理逻辑,和老代码里依赖decorator.decorator行为的写法不兼容,所以安装前要先把它压到 4.x。
提示:如果
unzip -p的输出里install_requires那段是空的或者只有一行setuptools,说明这份包被二次打包过,装之前先手动补decorator。
2.2 三条安装路径的取舍:pip 装本地包、setup.py 直装、解压后挂路径
路径选择取决于你所在的环境能不能联网、Python 版本多新、以及这台机器上还有没有别的项目共用同一个解释器。
第一条路最干净,虚拟环境加本地 wheel 构建:
# 建一个专用环境,别和主环境的包混在一起 python -m venv .venv-nx111 source .venv-nx111/bin/activate # Windows 下是 .venv-nx111\Scripts\activate # 先把 decorator 压到 4.x,避免装饰器签名解析报错 pip install "decorator<5" # 直接从 zip 安装,pip 会调用 setup.py 走 bdist_wheel pip install ./networkx-1.11.zippip install ./xxx.zip的实际动作是解压到临时目录、执行setup.py bdist_wheel、再安装产出的 wheel。参数上值得留意的是--no-build-isolation:新版 pip 默认在隔离环境里构建,会临时拉一套最新 setuptools,而 1.11 的setup.py用的是老式 metadata 写法,碰上过新的 setuptools 可能直接报错。加上--no-build-isolation让它复用当前环境里的构建工具,成功率会高一些。
第二条路是老办法,适合 pip 版本较旧或者构建环节始终过不去的场景:
unzip networkx-1.11.zip cd networkx-1.11 # 不走 wheel,直接 install,产物落到当前环境的 site-packages python setup.py install # 或者只装到用户目录,不动系统环境 python setup.py install --usersetup.py install的缺点是它不会生成RECORD文件,pip uninstall管不了它,卸载时得手动去 site-packages 里删目录。所以这条路只建议在一次性容器里用。
第三条路最土但最稳:解压后把父目录挂进PYTHONPATH,根本不装。
unzip networkx-1.11.zip -d /opt/libs export PYTHONPATH=/opt/libs/networkx-1.11:$PYTHONPATH这样做的价值在于完全隔离——同一台机器上不同脚本可以用不同的PYTHONPATH指向不同版本的 networkx,互不干扰。代价是每次启动都要带上这个变量,容易漏。我一般会把它写进项目自己的启动脚本里,而不是塞进.bashrc,免得污染全局。
至于解释器版本,1.11 那个年代的官方支持范围是 Python 2.7 和 3.3 以上那一批。在 3.8 之后的解释器上大部分功能能用,但会撞到collections里被移除的抽象基类名一类的问题——报错形式通常是AttributeError: module 'collections' has no attribute 'Iterable'之类,traceback的最后一行会精确指出是哪个文件在引用。看到这种报错不用怀疑包坏了,是解释器跑得比包快。稳妥做法是用 3.6 或 3.7 的解释器,或者干脆放进容器里。
2.3 装完立刻做的三行验证
装完不验证,等于没装。验证代码要同时覆盖「版本对不对」「图结构能不能建」「算法能不能跑通」这三层,别只import一下就完事。
# verify_nx111.py import networkx as nx import decorator # 第一层:版本必须是 1.11,别被环境里其他 networkx 顶掉 print("networkx:", nx.__version__) print("decorator:", decorator.__version__) assert nx.__version__.startswith("1.1"), "装错版本了,检查 sys.path 里有没有别的 networkx" # 第二层:建图 + 属性访问,验证 1.x 的 G.node 入口存在 G = nx.Graph() G.add_edge("a", "b", weight=2.0) G.add_edge("b", "c", weight=1.0) G.node["a"]["role"] = "source" # 1.x 的写法,2.x 会报错 assert G.node["a"]["role"] == "source" # 第三层:跑一个算法,确认数值链路正常 pr = nx.pagerank(G, alpha=0.85, weight="weight") print("pagerank:", {k: round(v, 4) for k, v in pr.items()}) assert abs(sum(pr.values()) - 1.0) < 1e-6, "概率向量没归一,算法实现异常"这段脚本的三段对应三个故障层次。第一段堵住「装了两个版本、import 到旧的」这类最常见的环境问题,assert只看主版本号前缀,够用且不挑具体 patch。第二段确认 1.x 特有的G.node字典入口存在,这一行在 2.x 上会直接抛AttributeError,是判断当前到底加载了哪个大版本最快的探针。第三段跑 PageRank 并断言概率和为 1,alpha=0.85是衰减因子,管的是随机跳转概率;weight="weight"表示用边上的weight属性做加权,缺了这个参数会退化成无权计算,数值对不上但不会报错,属于静默出错。
注意:验证脚本里
assert abs(sum(pr.values()) - 1.0) < 1e-6这个容差不要调得太松。1.11 的 PageRank 走的是幂迭代,默认tol是 1e-6 量级,容差放到 1e-3 就分辨不出「算法跑对了」和「迭代提前退出」了。
3. 1.x 与 2.x 的 API 断层:七处必须改写的调用点
1.11 和 2.x 之间的差异不是「多了几个函数」,而是几处容器类型的根本改动。照 2.x 文档写代码,在 1.11 上跑出来的错误信息往往指向一个看起来毫不相干的地方,因为断点发生在属性访问和迭代协议上。这一节把最容易踩的几处摊开讲。
3.1 节点与边的属性访问:G.node 和 G.edge 才是这个版本的入口
这是断层最深的一刀。1.x 里图对象把邻接结构和属性字典分开存放,G.node是「节点 → 属性字典」的映射,G.edge是「源 → 目标 → 属性字典」的嵌套映射,而G.nodes和G.edges是方法,返回列表。
import networkx as nx G = nx.Graph() G.add_edge("u", "v", weight=3.0) G.node["u"]["label"] = "user" # 正确:1.x 用 G.node 取属性字典 # 下面这行在 1.x 上会炸:'method' object is not subscriptable # print(G.nodes["u"]) # 想按 2.x 的习惯拿属性,只能绕道 node_attrs = dict(G.nodes(data=True)) # 返回 [(node, dict), ...] 的列表 print(node_attrs["u"]["label"]) edge_attrs = dict(((u, v), d) for u, v, d in G.edges(data=True)) print(edge_attrs[("u", "v")]["weight"])G.nodes(data=True)在 1.11 上返回的是一个列表,元素是(节点, 属性字典)二元组,用dict()一次性转成映射是成本最低的兼容写法。同理G.edges(data=True)返回三元组列表。参数上唯一要注意的是data的取值:data=True表示带上全部属性字典,data="weight"表示只带weight这一个字段,返回的三元组第三个元素是数值而不是字典,混用会直接抛类型错误。
G.edge在 1.11 上是G.adj的别名,两者指向同一个对象,写哪个都行。但别把它当成 2.x 的G.edges用——G.edge[u][v]["weight"]返回的是属性值,而G.edges[u, v]["weight"]在 2.x 里也是属性值,看起来像,实际一个是嵌套 dict 索引、一个是 EdgeView 索引,前者在键不存在时抛KeyError,后者抛KeyError但错误信息不同,迁移时靠错误信息分辨很费劲。
| 用途 | 1.11 写法 | 2.x 写法 | 备注 |
|---|---|---|---|
| 全部节点属性 | G.node | G.nodes | 前者是 dict,后者是视图 |
| 全部边属性 | G.edge或G.adj | G.edges | 前者嵌套 dict,后者视图 |
| 单个节点属性 | G.node[n] | G.nodes[n] | 1.x 上G.nodes[n]直接报错 |
| 节点列表 | G.nodes() | list(G.nodes) | 1.x 无参调用返回列表 |
| 边列表 | G.edges() | list(G.edges) | 同上 |
3.2 迭代器函数与容器类型:nodes_iter、edges_iter 与连通分量
1.x 同时提供了「返回列表」和「返回迭代器」两套接口,后者带_iter后缀。2.x 把列表版砍掉,只留下视图。这个设计差异在迁移时最坑的地方是:1.x 的代码里混用两套写法完全合法,但一旦把_iter版本带到 2.x 就会AttributeError。
import networkx as nx G = nx.Graph() G.add_edges_from([("a", "b"), ("b", "c"), ("d", "e")]) # 1.x 独有:迭代器版本,大图上省内存 for u, v in G.edges_iter(): pass for n in G.nodes_iter(): pass # 1.x 的 degree() 无参调用返回 dict,不是视图 deg = G.degree() print(type(deg), deg["b"]) # <class 'dict'> 2 # 连通分量返回的是生成器,直接打印只能看到一个 generator comps = nx.connected_components(G) comps = sorted((list(c) for c in comps), key=len, reverse=True) print(comps)G.degree()的返回值类型是 1.x 和 2.x 最容易埋雷的地方。1.x 无参调用返回dict,可以直接deg.get(node, 0)、可以直接序列化;2.x 返回DegreeView,json.dumps会直接失败。反过来,从 1.x 迁移到 2.x 的代码里如果写了isinstance(deg, dict)这种判断,会在新版本上悄悄失效。写兼容层时的稳妥做法是统一dict(G.degree())。
connected_components返回生成器这一点两代都一样,但生成器里的元素容器类型在不同小版本上有过调整。业务代码里别去猜,取到之后统一list()或set()转换一次,把不确定性关在转换那一行里。另外 1.11 还有一个connected_component_subgraphs(G),直接返回子图列表,2.x 已经把它删了,要自己用G.subgraph(c)构造。这个函数在老脚本里出现频率很高,迁移时搜一遍component_subgraphs基本不会漏。
3.3 图算法的位置与签名:PageRank、pandas 读入与布局的 seed
算法层面的差异主要在两个地方:模块路径和关键字参数。1.x 的算法分散在networkx.algorithms.*下面,顶层nx.命名空间做了大量 re-export,所以大多数时候感觉不到位置变化;但一旦用到某些偏门的,比如社区发现相关的,就得注意 1.11 里community子包只有k_clique_communities这一类,没有后来流行的 Louvain 实现。
import networkx as nx import pandas as pd # 1.11 里读 DataFrame 用的是 from_pandas_dataframe,2.0 起改名 df = pd.DataFrame({"src": ["a", "b"], "dst": ["b", "c"], "w": [1.0, 2.0]}) G = nx.from_pandas_dataframe(df, "src", "dst", edge_attr=["w"]) # 布局函数没有 seed 参数,这一点和 2.x 差别很大 import random random.seed(42) # 只能靠全局随机种子控制复现 pos = nx.spring_layout(G, iterations=50, weight="w", scale=1.0) print(pos["a"])from_pandas_dataframe的四个关键参数:source、target指定列名,edge_attr指定要带进边属性的列(可以传列表),create_using指定图类型(默认无向图,要建有向图得显式传nx.DiGraph())。这个函数在 2.0 被from_pandas_edgelist取代,参数名从edge_attr变成edge_attr保持一致,但函数名变了,老脚本里搜pandas_dataframe就能定位全部调用点。
spring_layout没有seed参数是 1.11 上一个实打实的坑。2.x 的布局函数签名里加了seed,很多人从新文档抄过来,在 1.11 上直接TypeError: spring_layout() got an unexpected keyword argument 'seed'。解决办法只有两个:用random.seed()在调用前固定全局状态,或者自己算好初始坐标通过pos=参数传进去。前者简单但影响整个进程的随机流,后者干净但要多写十几行。我一般选后者,因为分析脚本里往往还有别的随机过程,全局种子一设容易互相干扰。
4. 一条能跑通的最小工作流:建图、算指标、导出三段式
前面讲的是「别踩坑」,这一节把坑绕开之后拼一条完整链路。选一个最典型的场景:手头有一份 CSV 边表,需要算每个节点的影响力排名、找出最大的几个连通块、把结果导成下游能读的格式。整条链路只用 1.11 自带的函数,不引第三方图算法库。
4.1 从 CSV 边表建图并做去重、自环与权重合并
原始边表几乎不可能干净。重复边、自环、空字段、权重写成字符串,这四类问题在真实数据里出现率极高。建图阶段不做清洗,后面所有指标都会被污染,而且往往不会报错,只是数值慢慢偏掉。
# build_graph.py import csv import networkx as nx def build_graph(path): G = nx.Graph() skipped = {"self_loop": 0, "empty": 0, "bad_weight": 0} with open(path, "r", encoding="utf-8", newline="") as f: for row in csv.DictReader(f): u = (row.get("src") or "").strip() v = (row.get("dst") or "").strip() # 空字段和自环直接丢,自环会让度数和聚类系数失真 if not u or not v: skipped["empty"] += 1 continue if u == v: skipped["self_loop"] += 1 continue # 权重列可能是空串、可能是带千分位的字符串,统一兜底成 1.0 raw = (row.get("weight") or "").strip().replace(",", "") try: w = float(raw) if raw else 1.0 except ValueError: skipped["bad_weight"] += 1 w = 1.0 # 重复边不是覆盖,是累加:两条 0.5 的边应该合成 1.0 if G.has_edge(u, v): G[u][v]["weight"] += w G[u][v]["count"] += 1 else: G.add_edge(u, v, weight=w, count=1) print("nodes=%d edges=%d skipped=%s" % (G.number_of_nodes(), G.number_of_edges(), skipped)) return G这段代码的每个分支都对应一个具体的数据问题。自环过滤放在最前面,因为无向图上的自环会让G.degree()多算 2,而聚类系数的定义里对自环的处理各家不一致,留着纯属给自己找麻烦。权重兜底用try/except而不是isdigit()判断,是因为真实数据里会出现"1,234.5"这种带千分位的写法,isdigit()判不出来反而更危险。skipped字典记录三类丢弃数量,是为了让清洗过程可审计——如果empty数量占到总行数的三成,说明上游导出逻辑有问题,这时候应该回头查数据源,而不是继续往下算。
重复边合并的策略值得单独说。默认行为如果是「后写覆盖先写」,两条指向同一对节点的记录最终只留下一条,权重信息就丢了。改成累加并同时记count,等于把「这条边被观测到几次」这个信息保留下来,后面算加权指标时可以按count做置信度过滤。参数上唯一要注意的是G.has_edge(u, v)在 1.11 上是 O(1),大规模数据下不会成为瓶颈,放心用。
4.2 一次跑完 PageRank、连通分量与度中心性
建完图之后跑指标,最容易犯的错是逐个函数试、每次都在全图上重算一遍。正确做法是一次遍历把能拿的都拿下来,尤其是连通分量这种 O(V+E) 的操作,跑两遍纯属浪费。
# metrics.py import networkx as nx from build_graph import build_graph G = build_graph("edges.csv") # PageRank:alpha 是阻尼系数,max_iter/tol 决定迭代终止条件 pr = nx.pagerank( G, alpha=0.85, weight="weight", max_iter=200, tol=1e-08, nstart=None, ) # 连通分量:先按大小排序,取前若干个 comps = sorted((list(c) for c in nx.connected_components(G)), key=len, reverse=True) giant = G.subgraph(comps[0]) if comps else nx.Graph() # 度中心性在无向图上就是 degree 归一化,1.11 里手动算比调函数直观 n = float(G.number_of_nodes() - 1) or 1.0 deg_cent = {k: v / n for k, v in G.degree().items()} # 加权度:直接读边属性累加,避免 weight 被默认参数忽略 wdeg = {node: 0.0 for node in G.nodes()} for u, v, d in G.edges(data=True): wdeg[u] += d.get("weight", 1.0) wdeg[v] += d.get("weight", 1.0) print("components:", len(comps), "giant:", giant.number_of_nodes()) print("top pagerank:", sorted(pr.items(), key=lambda x: -x[1])[:5])nx.pagerank在 1.11 上是纯 Python 幂迭代实现,参数含义和 2.x 一致但实现路径不同,所以数值在小数点后若干位上会有差异——做历史结果对比时这一点必须考虑进去。alpha=0.85是标准衰减因子,调大有两点后果:收敛变慢、长尾节点权重被压低。max_iter=200比默认的 100 大一倍,是为了给中等规模图留余量;tol=1e-08比默认严两个数量级,代价是多迭代几轮。如果图规模上万节点、又只要一个粗略排名,把tol放宽到1e-06能省一半以上时间。
nx.connected_components返回生成器,sorted配合key=len一次性排完,注意这里必须先list(c)把每个分量转成列表——生成器里的元素如果直接拿去len(),行为在不同小版本上不完全一致,转一次最保险。G.subgraph(comps[0])得到的是原图的视图而非拷贝,1.11 里视图对原图的修改是双向可见的,如果后面要往巨连通块里加节点,记得先G.subgraph(...).copy()。
度中心性那段手动算是有意为之。nx.degree_centrality(G)在 1.11 里存在,但它内部会重新遍历一遍图,而G.degree()的结果我们后面还要用,复用一次省一次遍历。归一化分母用n-1,or 1.0兜住单节点图的除零。加权度同理,直接读d.get("weight", 1.0)而不是依赖函数的weight参数,好处是缺weight属性时不会静默按 1 处理——这里按 1 处理是显式写出来的,可控。
4.3 结果落盘:edgelist、GML 与可视化用的坐标文件
导出这一步看着简单,其实是最容易出兼容问题的地方。GML 会把节点标识转成字符串,edgelist 默认不带属性,坐标文件需要和节点顺序对齐,三个格式各有各的脾气。
# export.py import json import networkx as nx def export_all(G, prefix="out"): # edgelist 要显式声明 data,否则权重字段直接丢 nx.write_edgelist(G, prefix + ".edgelist", data=["weight", "count"], delimiter="\t") # GML 前先把节点 id 统一转成字符串,避免回读时类型漂移 H = nx.relabel_nodes(G, {n: str(n) for n in G.nodes()}) nx.write_gml(H, prefix + ".gml") # 坐标和指标一起落成一张宽表,下游画图直接读 pos = nx.spring_layout(G, iterations=100, weight="weight") pr = nx.pagerank(G, weight="weight") rows = [ {"node": k, "x": round(pos[k][0], 6), "y": round(pos[k][1], 6), "pagerank": round(pr[k], 8)} for k in G.nodes() ] with open(prefix + "_layout.json", "w", encoding="utf-8") as f: json.dump(rows, f, ensure_ascii=False, indent=2) # 一份纯文本摘要,方便人工核对 with open(prefix + "_summary.txt", "w", encoding="utf-8") as f: f.write("nodes\t%d\nedges\t%d\n" % (G.number_of_nodes(), G.number_of_edges()))write_edgelist的data参数必须显式给,1.11 里默认只写u v两列,边属性一概不落盘,很多人导完发现权重没了才回头查文档。delimiter="\t"是为了绕开节点名里含逗号的情况,制表符出现在节点名里的概率低得多。
GML 那一步用relabel_nodes把所有节点转成字符串再写,是拿血泪换来的经验。GML 规范里节点 id 是字符串类型,1.11 的write_gml对非字符串节点会做隐式转换,read_gml回读时默认relabel=True,拿到的节点 id 全是字符串。如果原图节点是整数 id,来回一趟类型就变了,下游按整数 key 查字典全部 miss。提前统一转字符串,至少让「进去是什么、出来是什么」这一条可预期。
坐标文件和指标表合并成一张宽表,是为了下游画图时不用再做 join。round(..., 6)和round(..., 8)这两个精度不是随手写的:坐标保留 6 位足够像素级定位,PageRank 保留 8 位是因为小图的概率值经常落在 1e-4 以下,截太狠会把排序差异抹平。ensure_ascii=False保证中文节点名不被转义成\uXXXX,方便肉眼检查。
5. 避坑与排查:这份老包里最容易翻车的五个地方
下面五条都是从实际报错里反推出来的。每条按「看到什么现象 → 为什么 → 怎么解」写,照着对号入座就行。
5.1 报 'method' object is not subscriptable
现象:代码跑到G.nodes[n]["weight"]或者G.edges[u, v]["weight"]的时候抛TypeError: 'method' object is not subscriptable,堆栈指向那一行,看不出更多信息。
原因:1.11 里G.nodes和G.edges是方法而不是属性,G.nodes[n]等价于先取方法对象再对它做下标访问,Python 层面直接拒绝。这个语法在 2.x 里完全合法,所以从新文档抄代码必然中招。
解决:把所有单个元素的属性访问改成G.node[n]和G.edge[u][v]。批量场景用dict(G.nodes(data=True))先转映射,再按下标取。如果代码要在两代版本上都跑,包一层get_node_attrs(G, n)函数,内部用hasattr(G, "node")判断走哪条分支——1.x 有G.node,2.x 没有,这个探针比版本号判断可靠。
5.2 导入阶段就挂在 decorator 相关的行上
现象:import networkx直接失败,traceback最后几行落在decorator.py或networkx/utils/decorators.py,错误类型五花八门,有TypeError、有AttributeError,甚至可能是SyntaxError。
原因:1.11 的装饰器工具链建立在decorator3.x/4.x 的签名解析行为上,现代decorator换了实现方式,对函数签名的处理不再兼容老写法。这类错误的特点是「看着像 networkx 坏了」,实际是依赖版本对不上。
解决:先确认decorator的主版本,pip show decorator看 Version 那一行。大于等于 5 就压下去:pip install "decorator<5"。压完重启解释器再试,importlib.reload不管用,因为模块加载失败时不会留下可重载的对象。
5.3 spring_layout 报 unexpected keyword argument 'seed'
现象:nx.spring_layout(G, seed=42)抛TypeError: spring_layout() got an unexpected keyword argument 'seed',而同一份代码换个环境就正常。
原因:seed参数是 2.x 才加到布局函数签名里的,1.11 的spring_layout只有dim、pos、fixed、iterations、weight、scale这几个参数,随机性完全由全局random状态决定。
解决:两条路。图省事就在调用前random.seed(42),但要注意这会重置整个进程的随机流,如果同一次运行里还有采样、划分训练集之类的操作,结果会跟着变。干净的做法是传pos=初始坐标:自己生成一组随机坐标,或者从上一轮的布局结果读进来,这样随机性被收进一个显式变量,复现时可控。
5.4 PageRank 抛 PowerIterationFailedConvergence
现象:nx.pagerank(G)跑着跑着抛networkx.exception.PowerIterationFailedConvergence: (PowerIterationFailedConvergence(...), 'power iteration failed to converge within 100 iterations'),图越大越容易出现。
原因:1.11 的 PageRank 是幂迭代实现,默认max_iter=100、tol=1e-06。图规模上去之后,谱间隙变小,收敛需要的迭代次数远超 100;另一种常见诱因是图里有大量悬挂节点(出度为 0),概率质量在迭代中来回震荡。
解决:先把max_iter提到 500 甚至 1000,同时把tol放宽到1e-05,两个方向一起调。如果还是不收敛,检查有没有孤立节点或悬挂节点,用dangling参数显式给出悬挂节点的概率分配方式。实在不行换nx.pagerank_scipy,它走稀疏矩阵乘,收敛行为更稳,代价是多一个 scipy 依赖。
5.5 GML 回读后节点 id 从整数变成字符串
现象:nx.write_gml(G, "a.gml")之后nx.read_gml("a.gml"),节点 id 从1, 2, 3变成了'1', '2', '3',下游按整数 key 查表全部KeyError。
原因:GML 格式规范里节点标识是字符串,1.11 的read_gml默认relabel=True,会用文件里的 label 字段作为节点 id,而 label 是字符串类型。写入时如果不做处理,整数 id 会被隐式转成字符串写进去。
解决:写入前用nx.relabel_nodes统一转成字符串,让「进去是 str、出来还是 str」这条链路可预期;或者在读取时传relabel=False,保留文件里的数值 id 并用label属性单独存标签。第二种做法要多写一层映射,但节点 id 类型不会漂。我一般选第一种,因为字符串 id 在跨语言交换时最省心。
6. 进阶用法:把 1.11 关进适配层,用黄金用例锁住数值
老版本真正难的地方不在装,而在「以后怎么办」。我的做法是加一层薄适配层,把所有版本相关的写法收进一个文件,业务代码只调这层暴露的函数。
# nx_compat.py import networkx as nx IS_V1 = hasattr(nx.Graph(), "node") # 1.x 有 G.node,2.x 没有 def node_attrs(G, n): return G.node[n] if IS_V1 else G.nodes[n] def degree_map(G): # 两代版本返回值类型不同,统一成 dict return dict(G.degree()) def components_sorted(G): return sorted((list(c) for c in nx.connected_components(G)), key=len, reverse=True)适配层的价值在于:升级版本时只需要改这一个文件,业务代码里不会散落着if version判断。IS_V1这个探针比读nx.__version__再解析字符串可靠,因为它检测的是真实的对象结构而不是元数据。
光有适配层不够,还得有黄金用例。挑一张十来节点的小图,把 PageRank、度中心性、连通分量的结果算一次存成基准,之后每次改环境都跑一遍断言:
import pytest from nx_compat import components_sorted def test_golden_pagerank(): import networkx as nx G = nx.Graph() G.add_weighted_edges_from([("a", "b", 1.0), ("b", "c", 2.0), ("a", "c", 0.5)]) pr = nx.pagerank(G, alpha=0.85, weight="weight", tol=1e-10, max_iter=500) # 容差不能设太紧:1.x 与 2.x 的实现路径不同,末位必然有差异 assert pr["b"] == pytest.approx(0.4023, abs=1e-3) def test_component_count(): import networkx as nx G = nx.Graph() G.add_edges_from([("a", "b"), ("c", "d"), ("e", "e")]) assert len(components_sorted(G)) == 3abs=1e-3这个容差是刻意的,不是偷懒。1.11 走幂迭代、2.x 走稀疏矩阵,两条路径在浮点末位上不可能完全一致,容差收到 1e-8 只会让测试变成环境检测器而不是正确性检查。真要做严格数值对齐,得把两边的tol和max_iter都拉到极致再比,成本远超收益。
从那以后我每次接手一个还挂在 1.x 上的分析项目,第一件事不是读业务代码,而是先把适配层和黄金用例建起来,再动别的。老包本身不可怕,可怕的是没人知道它现在算出来的数还对不确实。希望帮到你。
本文还有配套的精品资源,点击获取