简介:这份资源是哈尔滨工业大学计算机课程实验中的社交网络分析项目,面向高校计算机相关专业学生及需要完成课程设计的学习者,帮助其将数据挖掘、图论与算法实现等理论落地为可运行代码。压缩包为zip格式,整体约1.74MB,内含源码与说明书,源码可用于阅读和调试算法实现,说明书则梳理实验目的、方法与结论,便于对照理解整体思路。实验覆盖社交网络分析的核心内容,包括数据预处理、图的遍历、社区检测、中心性分析以及数据可视化等环节,读者可借助源码掌握NetworkX等工具的使用方式,并理解度中心性、介数中心性等指标的计算逻辑。目前已有153人学习,适合希望提升编程实践、数据分析与问题解决能力,并需要一份完整课程实验参考的学习者。
1. 从一份课设压缩包说起:社交网络分析到底在算什么
很多人第一次拿到「哈尔滨工业大学计算机课程实验-社交网络分析-内含源码和说明书.zip」这类压缩包,第一反应是解压、找 README、跑 main。但真跑起来才发现,社交网络分析不是把图读进来算个度就完事——它要回答的是「谁和谁关系近」「信息从哪条路径扩散最快」「哪个节点删掉会让整个网络裂开」。这套实验通常围绕图论基础、中心性指标、社区发现、传播模型四条主线展开,配套源码多半是 Python + NetworkX,说明书负责交代数据集格式和实验步骤。它适合两类人:一是正在做计算机课程实验、需要一份能跑通参考实现的学生;二是想快速补齐图分析工程能力、把 NetworkX 用进真实业务的开发者。下面按「数据怎么进 → 指标怎么算 → 社区怎么分 → 传播怎么模拟 → 坑在哪」的顺序拆开讲,每一步都给可复现的命令和参数。
2. 把图读进来:数据集格式与 NetworkX 建图的最小闭环
2.1 先确认边表长什么样,再决定用哪张图
社交网络分析实验的数据集,九成以上是边表(edge list)或邻接表。边表每行两个节点 ID,用空格或逗号分隔;邻接表则是「节点: 邻居1 邻居2」。拿到数据先别急着read_edgelist,用head和wc -l看一眼规模和分隔符,否则后面报的错全是格式问题。
# 看前 5 行,确认分隔符是空格还是逗号 head -5 edges.txt # 统计边数,估算图规模 wc -l edges.txt # 看是否有表头或注释行 grep -n "^#" edges.txt | head逻辑说明:head决定后续delimiter参数;wc -l让你心里有数——几千条边和几百万条边,建图策略完全不同。参数上,如果文件带表头,read_edgelist要加comments='#'或先skiprows。常见翻车点是分隔符写成','但实际是制表符,结果整行被当成一个节点 ID。
2.2 用 NetworkX 建图并做第一轮体检
确认格式后,建图本身只有几行,但体检不能省。孤立节点、自环、重复边这三样不处理,后面中心性算出来全是噪声。
import networkx as nx # 读边表,nodetype=int 保证节点 ID 是整数而非字符串 G = nx.read_edgelist('edges.txt', delimiter=' ', nodetype=int, create_using=nx.Graph()) # 体检:节点数、边数、是否连通、有无自环 print('节点数:', G.number_of_nodes()) print('边数:', G.number_of_edges()) print('自环数:', nx.number_of_selfloops(G)) print('连通分量数:', nx.number_connected_components(G)) # 去掉自环和孤立节点 G.remove_edges_from(nx.selfloop_edges(G)) G.remove_nodes_from(list(nx.isolates(G)))逻辑说明:create_using=nx.Graph()建无向图,如果实验要求有向图就换nx.DiGraph()。nodetype=int很关键,不指定的话节点是字符串,后面按 ID 排序会得到字典序而非数值序。参数上,delimiter必须和 2.1 看到的一致。体检输出里,连通分量数大于 1 说明图不连通,算全局指标前要么取最大连通子图,要么在报告里注明。
提示:最大连通子图用
G.subgraph(max(nx.connected_components(G), key=len)).copy(),直接切片会返回视图,后续修改会污染原图。
2.3 小图和大图的建图策略要分开
实验数据通常不大,但如果你拿真实社交数据练手,几百万边用read_edgelist会吃满内存。常见做法是先用pandas读边表去重,再转成 NetworkX;或者直接用scipy.sparse存邻接矩阵,只在需要图算法时转回来。
import pandas as pd import networkx as nx # 大图:pandas 去重后再建图,省内存 df = pd.read_csv('edges.txt', sep=' ', header=None, names=['src', 'dst']) df = df.drop_duplicates() G = nx.from_pandas_edgelist(df, 'src', 'dst')逻辑说明:drop_duplicates去掉重复边,from_pandas_edgelist比逐行add_edge快一个量级。参数上,names要和列数对上,否则 pandas 会把第一行当表头。这一步的边界是:如果边表超过内存,就得换graph-tool或分块读,NetworkX 本身不适合十亿级边。
3. 中心性指标怎么选:度、介数、接近度的适用边界
3.1 三个中心性各回答什么问题
度中心性(degree)看谁连接多,介数中心性(betweenness)看谁在最短路径上卡位,接近中心性(closeness)看谁到所有人平均距离短。实验里通常要求三个都算,但报告里要能说清哪个指标对应哪个业务含义。度中心性适合找活跃用户,介数适合找信息枢纽,接近适合找传播起点。
import networkx as nx deg = nx.degree_centrality(G) btw = nx.betweenness_centrality(G, k=500, normalized=True) clo = nx.closeness_centrality(G) # 按介数排序取前 10 top_btw = sorted(btw.items(), key=lambda x: x[1], reverse=True)[:10] print(top_btw)逻辑说明:degree_centrality是度除以 n-1,betweenness_centrality的k参数是采样节点数,大图必须设,否则 O(nm) 跑不动。normalized=True让结果落在 0 到 1 之间便于比较。参数上,k=500是经验值,图越大可以越小,但太小会导致介数估计不稳。常见误用是拿介数中心性去衡量「影响力」,其实介数高只说明它在路径上,不代表它能影响别人。
3.2 介数中心性的采样与精度权衡
介数中心性是实验里最容易跑崩的一步。精确算法对每个节点做一次 BFS,复杂度 O(nm),几千节点还能忍,上万节点就得采样。
# 精确算法,小图用 btw_exact = nx.betweenness_centrality(G, normalized=True) # 采样算法,大图用,k 越大越准 btw_approx = nx.betweenness_centrality(G, k=200, normalized=True, seed=42) # 对比两者排名相关性 import numpy as np exact_rank = [n for n, _ in sorted(btw_exact.items(), key=lambda x: x[1], reverse=True)] approx_rank = [n for n, _ in sorted(btw_approx.items(), key=lambda x: x[1], reverse=True)] print('Top10 重合数:', len(set(exact_rank[:10]) & set(approx_rank[:10])))逻辑说明:seed固定采样随机性,保证实验可复现。k的取值没有公式,一般取min(500, n),然后看 Top10 重合数是否稳定。参数上,如果重合数低于 7,说明 k 太小,要往上加。这一步的坑是:采样后不同节点被选中的概率不同,直接比较绝对数值没意义,只能比排名。
3.3 中心性结果的归一化与可视化
算完指标要出图,否则实验报告没说服力。NetworkX 自带draw,但节点一多就糊成一团,常见做法是按中心性调整节点大小和颜色。
import matplotlib.pyplot as plt pos = nx.spring_layout(G, seed=42, k=0.15) node_size = [deg[n] * 3000 for n in G.nodes()] node_color = [btw[n] for n in G.nodes()] plt.figure(figsize=(12, 8)) nx.draw_networkx_edges(G, pos, alpha=0.2) nx.draw_networkx_nodes(G, pos, node_size=node_size, node_color=node_color, cmap='YlOrRd') plt.axis('off') plt.savefig('network.png', dpi=150, bbox_inches='tight')逻辑说明:spring_layout的k控制节点间距,太小会挤在一起,太大会散开。node_size乘 3000 是经验系数,让度大的节点明显更大。cmap='YlOrRd'让介数高的节点偏红。参数上,seed固定布局,dpi决定输出清晰度。常见翻车是节点数超过 500 还画标签,结果全叠在一起,这时候要么只标 Top20,要么换pyvis做交互图。
4. 社区发现:Louvain 与标签传播的落地差异
4.1 Louvain 的模块度优化逻辑
社区发现实验通常要求对比两种算法。Louvain 通过两阶段迭代最大化模块度 Q,先局部移动节点,再聚合社区成超节点。它的优点是快且结果稳定,缺点是分辨率限制——小社区可能被合并。
import networkx as nx import community as community_louvain # python-louvain 包 # Louvain 社区发现 partition = community_louvain.best_partition(G, resolution=1.0, random_state=42) # 模块度 mod = community_louvain.modularity(partition, G) print('模块度 Q:', mod) # 每个社区的大小 from collections import Counter sizes = Counter(partition.values()) print('社区数:', len(sizes), '最大社区:', max(sizes.values()))逻辑说明:best_partition返回节点到社区 ID 的字典。resolution大于 1 倾向更多小社区,小于 1 倾向更少大社区。random_state固定随机种子。参数上,resolution=1.0是默认值,实验里可以试 0.5 和 2.0 看社区数变化。常见误用是拿模块度当唯一评价标准,模块度高不代表社区有业务意义。
4.2 标签传播的随机性与多次投票
标签传播(LPA)比 Louvain 更快,但结果不稳定,每次跑可能不一样。实验里通常跑多次取众数,或者固定随机种子。
from networkx.algorithms.community import label_propagation_communities # LPA 返回的是社区集合的迭代器 communities = list(label_propagation_communities(G)) print('社区数:', len(communities)) # 转成节点到社区 ID 的字典 lpa_partition = {} for cid, nodes in enumerate(communities): for n in nodes: lpa_partition[n] = cid逻辑说明:label_propagation_communities不接受seed参数,所以结果天然随机。要稳定就多跑几次,用Counter统计每个节点最常出现的社区。参数上,LPA 没有可调参数,这也是它不如 Louvain 可控的地方。边界是:LPA 适合超大图快速粗分,Louvain 适合需要可复现结果的实验报告。
4.3 两种算法的对比表与选择建议
| 维度 | Louvain | 标签传播 LPA |
|---|---|---|
| 时间复杂度 | O(n log n) | O(n) |
| 结果稳定性 | 高(固定种子) | 低(随机) |
| 可调参数 | resolution | 无 |
| 模块度 | 通常更高 | 通常更低 |
| 适用场景 | 实验报告、需复现 | 超大图、快速粗分 |
选择建议:课程实验优先 Louvain,因为结果可复现、模块度可写进报告;如果数据规模大到 Louvain 跑不动,再用 LPA 做初筛。两者都跑一遍对比社区数,是实验报告里常见的加分项。
5. 传播模型模拟:SI、SIR 与独立级联的参数设置
5.1 SIR 模型的三个参数怎么定
SIR 模型把节点分成易感 S、感染 I、恢复 R 三态。关键参数是感染概率 beta 和恢复概率 gamma,基本再生数 R0 = beta/gamma。R0 大于 1 疫情扩散,小于 1 自然消亡。
import networkx as nx import random def sir_simulation(G, beta=0.3, gamma=0.1, initial_infected=5, steps=50): # 初始化状态 status = {n: 'S' for n in G.nodes()} infected = random.sample(list(G.nodes()), initial_infected) for n in infected: status[n] = 'I' history = [] for _ in range(steps): new_infected = [] new_recovered = [] for n in G.nodes(): if status[n] == 'I': # 恢复 if random.random() < gamma: new_recovered.append(n) # 感染邻居 for nb in G.neighbors(n): if status[nb] == 'S' and random.random() < beta: new_infected.append(nb) for n in new_infected: status[n] = 'I' for n in new_recovered: status[n] = 'R' history.append(sum(1 for s in status.values() if s == 'I')) return history逻辑说明:beta控制单次接触感染概率,gamma控制恢复速度。initial_infected是初始感染人数,steps是模拟轮数。参数上,beta=0.3, gamma=0.1对应 R0=3,属于强扩散。常见翻车是 beta 设太大,一轮全感染,曲线没有形状;或者 gamma 设太大,感染还没扩散就恢复完了。
5.2 独立级联模型与影响力最大化
独立级联(IC)模型里,每个刚激活的节点有一次机会以概率 p 激活邻居。它更贴近信息传播场景,常和影响力最大化实验一起出现。
def ic_simulation(G, p=0.1, seeds=None, steps=50): if seeds is None: seeds = random.sample(list(G.nodes()), 5) active = set(seeds) newly_active = set(seeds) history = [len(active)] for _ in range(steps): next_active = set() for n in newly_active: for nb in G.neighbors(n): if nb not in active and random.random() < p: next_active.add(nb) if not next_active: break active |= next_active newly_active = next_active history.append(len(active)) return history逻辑说明:p是单次激活概率,seeds是初始种子节点。newly_active保证每个节点只尝试激活邻居一次。参数上,p=0.1是常见起点,p 越大传播越快。边界是:IC 模型假设激活机会只有一次,和 SIR 可以反复感染不同,实验报告里要写清用的是哪个模型。
5.3 传播模拟的重复实验与置信区间
单次模拟结果随机性大,实验里通常跑 100 次取均值和标准差。
import numpy as np results = [sir_simulation(G, beta=0.3, gamma=0.1)[-1] for _ in range(100)] print('最终感染规模均值:', np.mean(results)) print('标准差:', np.std(results)) print('95% 置信区间:', np.percentile(results, [2.5, 97.5]))逻辑说明:重复实验消除单次随机性,np.percentile给出置信区间。参数上,100 次是平衡精度和耗时的经验值,图大可以降到 30 次。常见误用是只跑一次就下结论,结果换个种子完全不一样。
6. 避坑与排查:课设跑不通时先看这五条
6.1 现象:read_edgelist报节点 ID 转换失败
原因:边表里有非整数节点 ID,比如用户名或带字母的编号,但代码写了nodetype=int。解决:先head看数据,如果 ID 是字符串就去掉nodetype=int,或者用nodetype=str显式指定。
6.2 现象:介数中心性跑了一小时没结果
原因:图节点数上万,精确介数复杂度 O(nm),内存和时间都扛不住。解决:加k=200走采样算法,或者先取最大连通子图缩小规模。如果实验要求精确值,就换小数据集。
6.3 现象:Louvain 社区数和预期差很多
原因:resolution参数没调,默认 1.0 可能把该分开的社区合并了。解决:试resolution=0.5和2.0,看社区数变化,报告里注明用的分辨率。另外确认图是无向的,Louvain 对有向图支持不好。
6.4 现象:SIR 模拟曲线一直是 0 或直接爆掉
原因:beta和gamma比例失衡。R0 = beta/gamma 小于 1 时疫情自然消亡,曲线贴地;beta 接近 1 时一轮全感染,曲线直上直下。解决:先算 R0,控制在 1.5 到 3 之间,再微调steps让曲线完整。
6.5 现象:可视化图节点全挤在一起
原因:spring_layout的k参数太小,或者节点数太多。解决:调大k到 0.3 以上,节点超过 500 就只画最大连通子图,或者换pyvis做交互。标签只标 Top20,别全标。
7. 进阶技巧:用模块度增量和 R0 反推实验结论
跑通基础流程后,真正拉开实验报告差距的是「用指标反推结论」。两个具体技巧:一是用 Louvain 的模块度增量判断社区划分是否值得再调分辨率,二是用 SIR 的 R0 反推传播阈值。
模块度增量这样看:先跑resolution=1.0记下 Q 值,再跑 0.8 和 1.2,如果 Q 变化小于 0.01,说明分辨率已经到平台期,再调没意义;如果变化大于 0.05,说明还有优化空间,可以继续二分逼近。这个技巧在实验报告里写出来,比单纯贴一个 Q 值有说服力得多。
import community as community_louvain for res in [0.6, 0.8, 1.0, 1.2, 1.5]: part = community_louvain.best_partition(G, resolution=res, random_state=42) q = community_louvain.modularity(part, G) print(f'resolution={res}, Q={q:.4f}, 社区数={len(set(part.values()))}')逻辑说明:resolution从小到大扫,观察 Q 和社区数的变化趋势。参数上,步长 0.2 是平衡精度和耗时的选择。如果 Q 在某点后下降,说明分辨率过大把社区切碎了。
R0 反推传播阈值:SIR 模拟里,R0 = beta/gamma。如果实验要求找「传播临界点」,就固定 gamma,从小到大扫 beta,看最终感染规模在哪跳变。跳变点对应的 beta 就是阈值,和理论值 1/gamma 对比,能验证模拟是否正确。
import numpy as np gamma = 0.1 for beta in np.arange(0.02, 0.3, 0.02): results = [sir_simulation(G, beta=beta, gamma=gamma)[-1] for _ in range(30)] print(f'beta={beta:.2f}, R0={beta/gamma:.2f}, 最终感染={np.mean(results):.1f}')逻辑说明:np.arange扫 beta,每个 beta 跑 30 次取均值。参数上,步长 0.02 足够看出跳变。理论阈值是 beta = gamma = 0.1,模拟里感染规模应该在这附近明显上升。如果模拟阈值和理论差很远,检查图是否连通、初始感染数是否太少。
我自己的习惯是:每跑完一个模型,先把参数和结果存成 CSV,再画图。这样调参时不用重跑,直接读表对比。血泪经验是,实验报告里最容易被问的不是「你用了什么算法」,而是「你为什么选这个参数」——把扫描过程留下来,比事后编理由靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取