简介:面向社交网络分析与Python爬虫实践学习者,这份资源以新浪微博转发数据为对象,完整演示从模拟登录、网页解析到网络图与时间图绘制的项目流程,适合入门数据采集和关系网络分析的实战训练。压缩包共16个文件,以6个Python脚本为核心,配合3个编译生成的pyc文件、3个文本记录、1份CSV数据集、2张PNG结果图和1份Markdown说明,分别承担登录、编码、解析、绘图等模块功能;其中网络图清晰呈现转发链条,时间图帮助发现传播规律,整包大小仅485KB,轻量便捷。目前已有775人学习,资源目录结构清晰,便于按主程序、辅助模块与输出结果逐层对照复现。借助该案例,既可掌握微博数据抓取、关系网络建模与可视化的完整方法,也可通过原始CSV数据验证节点聚合效果,并学习多文件Python工程的拆分与排错思路,是入门社交网络分析的一次扎实演练。
1. 新浪微博转发网络分析:从一条微博到一张传播地图
打开微博,一条消息被转发了十万次。这十万次转发不是一条直线,而是一张充满分叉和层级的有向网络——有人转给粉丝,粉丝再转给粉丝的粉丝,形成一个多级的传播树。做社交网络分析,就是把这棵树的形状、关键节点、圈层结构都量化出来。用 Python 处理新浪微博的转发数据,可以回答几个非常实际的问题:谁是让这条微博扩散出去的关键中转站?信息在哪个圈层里停留最久?转发层级到了第几层就衰减到几乎为零?本文适合刚接触社交网络分析、手里有微博数据或者正准备采集转发数据的从业者,全程用可运行的 Python 代码,把从清洗到出图的主流程走一遍。
真正做过这个方向的人都知道,微博的转发数据比想象中脏得多:时间格式不统一、用户 ID 和昵称错位、被删除的中间节点让网络断链。这些坑不提前处理,后续算出来的中心度全是错的。本文会先讲数据怎么清洗和建模,再做图指标与社区发现,最后把避坑经验和可视化技巧一次性说清楚。
2. 数据准备:转发数据采集与清洗的最小可用方案
2.1 字段设计与存储:做分析前先想清楚要什么
做转发网络分析,最怕拿到数据后才发现关键字段没有采集。我一般在采集阶段就固定四个核心字段:源微博 ID、发布时间、用户 ID、转发关系。其中转发关系用「谁转发了谁」表达,具体来说就是一条转发记录里,需要记录转发者 uid、被转发者 uid、转发时间、转发层级。如果数据来自爬虫,还需要保留原始微博的 mid,因为后续要按 mid 把同一条微博的所有转发记录聚合到一起。
存储上直接用 CSV 就能跑通,字段不要多,五列足够:
source_mid, source_uid, repost_uid, repost_time, repost_level这里source_mid是原始微博的 ID,source_uid是原始微博作者的 ID,repost_uid是转发者的 ID,repost_time是转发动作发生的时间,repost_level是这条转发在传播树里的深度。repost_level非常关键,没有它,后续没法算传播深度分布。
2.2 清洗四步:去重、补全、格式统一、层级校验
实际数据里最常见的脏数据是重复转发记录。用户连续转两次,爬虫可能抓到两条;用户转发后又删除,数据里也会留下僵尸记录。先做去重,按source_mid + repost_uid去重,保留最早的一条。
import pandas as pd df = pd.read_csv('weibo_repost_raw.csv') # 去重:同一用户对同一条微博的重复转发,只保留最早的一条 df = df.sort_values('repost_time') df = df.drop_duplicates(subset=['source_mid', 'repost_uid'], keep='first') # 时间字段统一为 datetime 类型,后续按时间窗口计算才靠谱 df['repost_time'] = pd.to_datetime(df['repost_time']) # 用户 ID 统一为字符串,避免 ID 过长被当浮点数处理丢精度 df['repost_uid'] = df['repost_uid'].astype(str) df['source_uid'] = df['source_uid'].astype(str) # 缺失层级填 1,代表直接转发原始微博 df['repost_level'] = df['repost_level'].fillna(1).astype(int) print(df.shape) print(df['repost_level'].value_counts().sort_index())这段代码的逻辑是先按时间排序再按用户去重,保证留下来的最早转发是用户的首次动作。时间转成 datetime 类型是为了后续能按小时做传播速度分析,用户 ID 转字符串则是必须的——微博的 uid 超过 JavaScript 的安全整数范围,用 pandas 默认读入会变成浮点数,尾部几位直接丢成 0,后面匹配全是垃圾数据。
层级字段用 fillna(1) 是因为部分转发记录没有爬到转发链接的父节点,这时按直接转发处理是最保守的选择。如果层级数据大量缺失,不要勉强修复,更合理的做法是回到采集端重新解析转发页面里的「转自 @某用户」这个标签。
2.3 断链检测:转发关系里的中间人丢了怎么办
转发数据最头疼的问题是中间节点缺失。A 转发了 B,B 转发了原始微博,但爬虫没抓到 B 的这条转发记录,网络里就出现 A 直接指向原始微博的假边。这种断链会让传播深度被低估,也会让一些中转节点的重要性被高估。
检测断链的办法是看repost_level的跳变:如果一条转发记录声称是第 3 层,但它的父节点(第 2 层的某条记录)在数据里不存在,这条记录就可以被标记为断链。清洗时有两种选择:丢弃它,或者把它的层级改为 1。我的习惯是保留但降级为 1,原因很简单——数据量本身就不大,丢弃会让网络密度进一步下降。
# 构建转发父子关系:每条转发需要记录它转自哪条中间微博 # 这里假设采集时已经拿到了 parent_uid 字段 if 'parent_uid' in df.columns: df['parent_uid'] = df['parent_uid'].astype(str) known_uids = set(df['repost_uid']) | set(df['source_uid']) # 父节点不在已知用户集合里的,视为断链 broken_mask = ~df['parent_uid'].isin(known_uids) print(f'断链记录数: {broken_mask.sum()}') # 断链记录降级为直接转发 df.loc[broken_mask, 'repost_level'] = 1参数说明:parent_uid是采集阶段额外加的一个字段,表示这条转发记录的父节点用户 ID。如果没有采集这个字段,就退回到用repost_level跳变来做断链检测。注意断链处理不是越激进越好,把断链降级为第 1 层会人为抬高原始微博的度,后面计算入度时要心里有数。
3. 用 NetworkX 构建转发网络并计算核心指标
3.1 有向图建模:为什么不能把转发关系当成无向边
微博的转发关系天然是有向的:信息从原始微博流向转发者,再从转发者流向下一级转发者。如果把 A 转发 B 当成无向边,社区发现会把两个完全不相关的粉丝群体强行并到一起,中心度计算结果也会失真。构建图这一步要用 DiGraph,边的方向是「被转发者 -> 转发者」。
import networkx as nx G = nx.DiGraph() # 添加边:被转发者指向转发者,方向代表信息流向 for _, row in df.iterrows(): source = row['source_uid'] target = row['repost_uid'] if G.has_edge(source, target): G[source][target]['weight'] += 1 else: G.add_edge(source, target, weight=1) print(f'节点数: {G.number_of_nodes()}, 边数: {G.number_of_edges()}') print(f'网络密度: {nx.density(G):.4f}')这段代码用邻接的权重累加处理同一用户多次转发的情况。微博的实际场景里,同一用户不太可能反复转发同一条微博,但测试数据里可能出现,加上权重字段总归保险。nx.density对有向图非常不友好,真实转发网络的密度通常在 0.001 以下,这个数值只适合做横向对比,别拿它当绝对衡量标准。
3.2 中心度计算:入度、出度与 PageRank 的互补视角
在转发网络里,入度代表这个用户被多少不同的人转发了,出度代表这个用户转发了多少不同的人。入度极高的节点是内容源头或中间层大 V,出度极高的节点是活跃的传播者。只算度不够,因为微博的传播是级联的,一个节点哪怕入度只有 5,只要这 5 个节点的粉丝量足够大,它的实际影响力可能超过入度 50 的节点。所以还要叠加 PageRank 或 HITS 算法。
# 入度与出度 in_degrees = dict(G.in_degree()) out_degrees = dict(G.out_degree()) # PageRank:默认权重是边权重,alpha 取 0.85 是常规设置 pagerank = nx.pagerank(G, alpha=0.85) # 整合成表格 df_node = pd.DataFrame({ 'uid': list(G.nodes()), 'in_degree': [in_degrees[n] for n in G.nodes()], 'out_degree': [out_degrees[n] for n in G.nodes()], 'pagerank': [pagerank[n] for n in G.nodes()] }) df_node = df_node.sort_values('pagerank', ascending=False) print(df_node.head(10))参数说明:alpha=0.85是 PageRank 的标准阻尼系数,意味着一个用户有 15% 的概率随机跳转到任意其他节点。做微博数据时,alpha 可以适当提高到 0.9,因为信息沿转发链传播的特性比网页链接更强,随机跳转的概率理论上更低。别盲调,0.85 到 0.9 之间通常差异不大,但如果网络中强连通分量很小,调高 alpha 反而会让 PageRank 集中在少数几个节点上。
3.3 转发层级分布与传播深度衰减
把清洗后的repost_level按层级统计数量,能得到一条衰减曲线。绝大多数微博在第 2 层到第 3 层就衰减完了,能到第 5 层以上的微博占比极低。这个分布直接决定分析策略:如果数据集中在第 1 层和第 2 层,那么社区发现和传播路径的价值就被压缩了,分析重心应该放在源头影响力的拆解上。
level_dist = df['repost_level'].value_counts().sort_index() level_ratio = level_dist / level_dist.sum() for level, ratio in level_ratio.items(): print(f'第 {level} 层级占比: {ratio:.2%}') # 平均转发深度(加权平均) avg_depth = (df['repost_level'].astype(int) * 1.0).mean() print(f'平均传播深度: {avg_depth:.2f}')平均传播深度是一个很直观的指标,低于 1.5 说明信息几乎只在第一层完成传播,高于 2.5 说明存在明显的多级裂变。单条微博的转发层级分布和平均深度,在做事件对比分析时比单纯看转发总量有意义得多——转发总量可以被炸弹号刷出来,但传播深度是刷不出来的。
3.4 强连通分量:找到转发网络里的「信息回流圈」
有向图里的强连通分量,表示一组节点之间两两都能通过转发链到达对方。在新浪微博的真实场景里,强连通分量通常对应互粉互转的小圈子。这个组件的规模如果异常偏大,比如占了全图节点的一半以上,那这个网络很可能是刷量形成的,正常的信息传播不会出现大面积的多向连通。
wcc = nx.weakly_connected_components(G) scc = nx.strongly_connected_components(G) # 只看规模排前五的弱连通分量 wcc_sizes = sorted([len(c) for c in wcc], reverse=True)[:5] scc_sizes = sorted([len(c) for c in scc], reverse=True)[:5] print(f'弱连通分量规模 Top5: {wcc_sizes}') print(f'强连通分量规模 Top5: {scc_sizes}')弱连通分量的规模分布可以用来判断网络是否碎片化。如果最大的弱连通分量只占全图节点的 20%,说明这次传播覆盖的是多个互不相连的粉丝圈层,壁垒很强;如果占比超过 70%,说明信息跨圈层流动顺畅。这两个解读方向是写分析结论时最有价值的部分。
4. 社区发现与传播路径分析:Louvain 算法落地
4.1 社区发现选型:为什么 Louvain 比 Girvan-Newman 更适合微博数据
社交网络分析里做社区发现,常见的有 Girvan-Newman 和 Louvain 两种思路。Girvan-Newman 通过反复计算边介数并移除边来分裂社区,时间复杂度高,微博十万级节点的网络直接跑不动。Louvain 基于模块度优化,迭代速度极快,百万级边也就几十秒,是目前处理微博这类大规模网络的主流选择。如果网络规模小于 5000 个节点,Girvan-Newman 还能用,但当节点超过这个量级,直接用 Louvain 是唯一的合理路径。
import community as community_louvain # 注意:Louvain 社区发现一般作用于无向加权图 # 转发网络需要先把有向图转成无向图,权重取两个方向的转发次数之和 G_undirected = G.to_undirected() # 用 resolution 参数控制社区粒度,1.0 是默认值 partition = community_louvain.best_partition(G_undirected, resolution=1.0) df_node['community'] = df_node['uid'].map(partition) community_size = df_node.groupby('community').size().sort_values(ascending=False) print(community_size.head(10))参数说明:resolution=1.0对应标准模块度,调大这个值会让社区划分得更细碎,调小则会让社区更聚合。做微博转发网络时,我一般先用 1.0 跑一轮看看社区规模分布,如果最大社区占比超过 50%,就把 resolution 调到 1.2 到 1.5 之间再试。社区划分粒度没有绝对标准,取决于报告要看哪个粒度:看品牌圈层用粗粒度,看跨圈层传播用细粒度。
G.to_undirected()这一步会丢失方向信息,但社区发现本来就不依赖方向,它关注的是人群聚合程度。如果对方向敏感,比如要区分意见领袖和追随者,那应该用第 3 章的 PageRank 结果,而不是社区发现。
4.2 传播路径提取:从级联数据里还原信息主干
一条微博的传播不是树状图上的所有分支都重要。提取传播路径,最直接的方法是按转发层级把链路串起来。每条转发记录能定位到它的父节点,把所有转发记录按「原始微博 → 第 1 层 → 第 2 层」串联,就能得到完整的转发级联。
# 构造级联:每个节点记录自己转自谁 def build_cascade(df, source_mid): sub = df[df['source_mid'] == source_mid].copy() cascade = {} # 初始节点:原始微博作者 root = sub.iloc[0]['source_uid'] cascade[root] = [] # 按转发层级遍历,把每个用户挂到它的父节点下面 for _, row in sub.sort_values('repost_time').iterrows(): parent = row.get('parent_uid', None) if parent is None or parent not in cascade: # 没有父节点信息,或父节点缺失,挂到根 cascade.setdefault(row['repost_uid'], []).append(root) else: cascade.setdefault(row['repost_uid'], []).append(parent) return cascade这个函数返回的是一个字典,key 是用户 ID,value 是它的父节点列表。多数情况下一个用户只有一个父节点,但数据异常时可能出现多个,所以用列表存储。parent_uid这个字段如果采集阶段没有,就需要从转发页面的「转自」信息里二次解析,这一步跑不了捷径。
传播路径提取之后,能做的事很多:对比不同时段转发的路径长度差异、找出传播树的「枢纽」节点、识别转发链中断的位置。在做报告时,我一般会把 Top10 传播路径单独导出成 CSV 给业务方看,它们通常对这个比中心度指标更感兴趣。
4.3 关键传播者识别:K-core 与 PageRank 的配合使用
识别关键传播者,不要把 PageRank 当成唯一指标。PageRank 偏向源头内容型节点,K-core 分解则能看出谁在网络的深层结构里。K-core 的值表示节点处在多深的核心圈层,K 值越高的节点,越是网络骨架的一部分。两者配合使用:PageRank 高但 K-core 低,说明这个节点是「孤胆英雄」,影响力强但不嵌入圈子;PageRank 高且 K-core 也高,才是真正的圈层枢纽。
# 计算无向图的 K-core core_numbers = nx.core_number(G_undirected) # 按 K-core 值统计节点分布 core_dist = pd.Series(core_numbers).value_counts().sort_index() print(core_dist) # 找 K-core 大于等于 10 的节点 high_core_nodes = [n for n, k in core_numbers.items() if k >= 10] print(f'K-core >= 10 的节点数: {len(high_core_nodes)}')K-core 的阈值怎么定?先看 distribution,如果 K-core 最高的节点只有 5,那定 10 就找不到人。先跑一遍分布再定阈值,比拍脑袋定一个数要靠谱得多。K-core 与 PageRank 交叉筛选出来的节点集,通常只有个位数到几十个,这些就是要人工核查的重点对象。
5. 踩坑记录:微博转发网络分析的 5 个高频翻车现场
5.1 mid 与 uid 的类型精度丢失
现象:用 pandas 读 CSV 后,用户 ID 变成了类似 1.23457e+15 的科学计数法,后续和图里的节点对不上,所有节点都匹配失败。
原因:微博 uid 是 19 位数字,远超 Excel 和 pandas 默认 float64 的精确表示范围。读入时系统自动转成 float,末尾几位被截断。
解决:读取时直接指定 dtype:
df = pd.read_csv('weibo_repost_raw.csv', dtype={ 'source_uid': str, 'repost_uid': str, 'source_mid': str, 'parent_uid': str })如果数据已经读坏了,别试图用 round 修复,已经丢的位数找不回来,重新回源头导出。这个坑是所有微博数据处理里最隐蔽的,因为它不出报错,只是静默地错。
5.2 转发时间与发布时间混淆
现象:用转发时间减发布时间算传播速度,得到一堆负数。
原因:爬虫采集时把「原始微博发布时间」误解为「当前转发时间」,或者时间字段解析时把时区弄混了,导致部分转发时间早于发布时间。
解决:清洗时统一用 UTC 时间戳做中间态,转北京时间再展示。同时做一次合理性校验,转发时间早于发布时间的记录直接标记为异常:
df['pub_time'] = pd.to_datetime(df['pub_time'], utc=True) df['repost_time'] = pd.to_datetime(df['repost_time'], utc=True) invalid = df[df['repost_time'] < df['pub_time']] print(f'时间异常记录数: {len(invalid)}')5.3 未转发的原始微博不在数据里
现象:构建网络后,图的节点数远小于预期,原始微博作者根本没出现在图里。
原因:采集时只抓了转发列表,没有把原始微博本身作为一个节点加入网络。这会导致所有转发节点的入度从原始微博用户身上丢失,第一层结构全断。
解决:在构建图之前,把原始微博作为一个节点插入:
all_uids = set(df['repost_uid']) | set(df['source_uid']) G.add_nodes_from(all_uids)5.4 微博被删除导致中间节点缺失
现象:传播深度分布断层,大量转发集中在第 1 层和第 3 层,第 2 层几乎为空。网络里出现很多「跳跃转发」。
原因:部分中间层用户删除了微博,或被平台处理,导致爬虫采集到的转发关系链断裂。
解决:检测层级跳变,把断链记录降级为直接转发。这个方法在 2.3 写了,但实际做的时候要注意:断链如果超过总量 20%,数据质量就不适合做深度分析,只能做粗粒度的用户影响力排序,别硬撑。
5.5 网络密度极低导致 Louvain 社区切分失败
现象:运行 community_louvain.best_partition 后,所有节点被分到同一个社区,或社区数量极端少。
原因:转发网络太稀疏,很多节点只有一条边,模块度计算时无法形成有效社区结构。或者图构建错误,把大量孤立节点也加了进来。
解决:先过滤掉度为 0 的节点,只看实际参与转发的节点。如果还是不行,降低 resolution 到 0.8 左右,放宽社区聚合的阈值。如果依然一个社区,那这个数据就是「事件性传播」而不是「圈层性传播」,说明信息扩散靠的大节点单点播音,不是圈层互动。
6. 可视化与结论输出:让分析结果真正被业务用起来
6.1 交互式网络图:用 PyVis 替代静态布局
NetworkX 自带的 matplotlib 绘图在节点上千后一塌糊涂,标签互相覆盖,调参到崩溃。做交互式网络图,PyVis 是个好选择,直接输出 HTML,可以缩放、拖动、点击看节点属性。
from pyvis.network import Network # 取社区划分后的 Top 300 节点,控制可视规模 top_nodes = df_node.nlargest(300, 'pagerank')['uid'].tolist() G_sub = G_undirected.subgraph(top_nodes) net = Network(height='600px', width='100%', bgcolor='#ffffff', font_color='#333333') net.from_nx(G_sub) # 根据 K-core 值设置节点大小 for node in net.nodes: uid = node['id'] node['value'] = core_numbers.get(uid, 1) * 3 node['title'] = f'uid: {uid}<br>PR: {pagerank.get(uid, 0):.4f}<br>社区: {partition.get(uid, -1)}' net.save_graph('weibo_network.html')这个图的节点大小与 K-core 挂钩,鼠标悬停能看到 PageRank 和社区编号。PyVis 的参数里,height控制画布高度,font_color是标签颜色,节点value控制半径。输出 HTML 后可以直接发给非技术背景的合作方,双击节点还能看邻居。
可视化的核心原则:只画 Top 节点。全图一万个节点画出来没有任何阅读价值,把 PageRank 前 300 的节点画清楚,传播主骨架就出来了。剩下的节点是噪声,不是信息。
6.2 传播最快的 6 小时:用时间窗切片验证传播路径
做转发传播分析,只看最终转发总量远远不够,需要看信息是多久触达最大范围的。按小时统计转发量,能明显看到一条微博的爆发期通常在前 6 小时。把爆发期和非爆发期的传播路径拆开对比,会发现一个现象:爆发期的传播路径高度依赖前 3 层的几个关键节点,非爆发期的转发则分散得多。
df['hour'] = df['repost_time'].dt.hour hour_dist = df.groupby('hour').size() # 找出转发量最高的连续 6 小时窗口 hourly = df.set_index('repost_time').resample('1H').size() top_window_start = hourly.sort_values(ascending=False).index[0] window = hourly.loc[top_window_start:top_window_start + pd.Timedelta(hours=5)] print(f'峰值小时: {top_window_start}') print(f'窗口总转发量: {window.sum()}')这个时间窗切片能直接验证前面的传播路径分析:如果爆发期的路径存在明显的上游汇聚节点,优化策略就有了方向;如果转发是均匀爆发,那就说明是推广资源位带动的,和网络结构无关。
6.3 结论落地的三层输出框架
拿到图指标、社区划分和传播路径后,该怎么输出?我的习惯是三层结构。第一层是一句话结论:这次传播的核心特征是跨圈层扩散还是圈内共振。第二层是证据:用 Top10 关键传播者列表、社区划分结果、传播深度分布来支撑。第三层是动作:品牌方应该维护哪些关键中转节点、下一次投放应该针对哪个社区做内容定制。
这套分析流程我在多个模拟项目里反复用过,模拟项目 X 是某品牌的舆情事件,某图像处理 Demo 则是直接把微博转发逻辑套用在图片分享社区的数据上。两者数据形态不同,但方法和参数完全通用——这才是社交网络分析的可迁移性所在。
最后说一个自己的习惯:每次拿到新的转发数据,先不跑算法,先做 5 分钟的描述性统计——转发总量、参与用户数、平均层级、时间跨度。这四个数能筛掉一半以上不值得深挖的数据。数据没有传播深度的时候,什么社区发现、传播路径、关键节点分析,全都是在给噪声做拟合。把好数据跑出好结论不稀奇,能识别出不值得分析的数据才是真的省时间。希望这篇笔记能帮你少走一段弯路。
本文还有配套的精品资源,点击获取