☰
三国人物关系词频分析词云图:从文本到洞察的完整落地路径
2026/10/12 13:39:39 网站建设 项目流程

简介:这是一份面向Python文本分析与数据可视化学习者的实战资源包,围绕三国人物关系与词频展开,适合想掌握中文分词、词频统计与词云图生成的中级学习者练手。包内共16个文件,以txt文本、xml配置、png图片为主,另含py脚本、ttf字体、doc报告等,压缩包约6.73MB,涵盖原始语料、停用词表、词频结果与可视化成品。资源完整呈现了从TXT读取、jieba分词、去停用词与标点、Counter词频统计到wordcloud词云图生成的流程,并附人物社交关系网络图与设计报告,便于对照理解文本预处理与可视化参数设置。目前已有959人学习下载,读者可借此掌握中文文本清洗、词频排序与词云定制方法,并参考报告梳理三国主要人物互动格局,快速复现一套可迁移的文本分析小项目。

1. 三国人物关系词频分析词云图:从文本到洞察的完整落地路径

拿到「三国人物关系词频分析词云图.zip」这个标题,很多人第一反应是:不就是把三国文本扔进词云工具跑一张图吗?但真正动手就会发现,事情远没那么简单。人物关系词频分析的核心难点不在词云生成,而在于如何从非结构化的三国文本中准确提取人物共现关系,并把关系强度量化成可比较的数值。词云图只是最终的可视化呈现,前面的分词、人名识别、共现统计、权重计算才是决定成败的关键环节。这套方案适合有基础 Python 能力、想做文本挖掘实战的开发者,也适合需要快速产出人物关系图谱的内容分析人员。接下来我会把整个流程拆开,从数据准备到词云定制,每一步都给出可复现的代码和参数说明。

2. 数据准备与人物词典构建:别让分词毁在第一步

2.1 三国文本的获取与清洗策略

做人物关系词频分析,第一步不是写代码,而是搞定文本。三国相关文本来源主要有三类:一是《三国演义》原文(约 64 万字),二是《三国志》史书文本,三是各类三国题材的评论、弹幕或同人文本。我一般优先用《三国演义》原文,因为人物对话密集、关系描述丰富,共现信号强。文本获取后要做的清洗包括:去除章节标题中的「第X回」格式标记、统一全角半角标点、去掉空白行和页眉页脚残留。清洗脚本不复杂,但漏掉一步就会在分词阶段引入噪声。

import re def clean_sanguo_text(raw_text): # 去除章节标题行,如「第一回 宴桃园豪杰三结义」 text = re.sub(r'第[一二三四五六七八九十百千]+回.*?\n', '\n', raw_text) # 全角标点转半角 text = text.replace(',', ',').replace('。', '.').replace('!', '!') # 去除多余空白 text = re.sub(r'\s+', ' ', text) # 去除括号内注释性内容 text = re.sub(r'[\((].*?[\))]', '', text) return text.strip() with open('sanguo.txt', 'r', encoding='utf-8') as f: raw = f.read() cleaned = clean_sanguo_text(raw) print(f'清洗后文本长度: {len(cleaned)} 字符')

这段代码的逻辑很直接:先用正则干掉回目标题,因为标题里的人物名会和正文重复计数;然后统一标点,避免分词器把「,」和「,」当成不同 token;最后压缩空白。参数方面,正则里的[一二三四五六七八九十百千]+覆盖了三国演义全部回目编号,如果你的文本是《三国志》或其他版本,需要根据实际标题格式调整。清洗后建议打印字符数做 sanity check,正常《三国演义》清洗后应该在 60 万字符左右,偏差超过 10% 说明清洗规则有问题。

2.2 人物词典的构建与分词器配置

通用分词器(如 jieba 默认模式)会把「诸葛亮」切成「诸葛」和「亮」,把「司马懿」切成「司马」和「懿」,这对人物关系分析是致命的。解决办法是构建自定义人物词典,强制分词器把人名当作完整 token。三国核心人物大约 100-150 个,我一般从三个来源汇总:一是维基百科三国人物列表,二是原文中高频出现的二字、三字名词,三是手动补充的别名(如「孔明」对应「诸葛亮」,「孟德」对应「曹操」)。词典格式是每行一个词,后面可以跟词频和词性标签。

import jieba # 构建人物词典 person_dict = [ '诸葛亮', '司马懿', '曹操', '刘备', '孙权', '关羽', '张飞', '赵云', '周瑜', '吕布', '貂蝉', '袁绍', '董卓', '黄忠', '马超', '姜维', '孔明', '孟德', '玄德', '仲谋', '云长', '翼德', '子龙', '公瑾', '奉先', '本初', '仲颖', '汉升', '孟起', '伯约' ] # 加载自定义词典 for name in person_dict: jieba.add_word(name, freq=1000, tag='nr') # 别名映射表 alias_map = { '孔明': '诸葛亮', '孟德': '曹操', '玄德': '刘备', '仲谋': '孙权', '云长': '关羽', '翼德': '张飞', '子龙': '赵云', '公瑾': '周瑜', '奉先': '吕布', '本初': '袁绍', '仲颖': '董卓', '汉升': '黄忠', '孟起': '马超', '伯约': '姜维' } def segment_with_person(text): words = jieba.lcut(text) # 别名归一化 normalized = [alias_map.get(w, w) for w in words] return normalized tokens = segment_with_person(cleaned) print(f'分词后 token 数: {len(tokens)}')

这里的关键参数是jieba.add_word的freq设为 1000,这个值要足够高才能覆盖默认词典里的拆分倾向。tag='nr'是人名标记,方便后续过滤。别名映射表必须手动维护,因为「孔明」和「诸葛亮」在原文中交替出现,不归一化会导致同一个人被统计成两个实体。分词后建议抽样检查前 200 个 token,确认没有明显的人名被切碎。如果发现「诸葛」和「亮」还是分开的,说明词典加载顺序有问题,要在jieba.lcut之前完成所有add_word调用。

3. 人物共现关系提取:从句子窗口到关系矩阵

3.1 共现窗口的设定与滑动统计

人物关系的核心假设是:如果两个人名在同一个句子或同一个段落窗口内频繁出现,他们之间就存在较强的关系。这里有两个关键参数:窗口类型(句子级 vs 段落级)和窗口大小(固定 token 数 vs 自然句边界)。我的经验是,句子级窗口适合捕捉直接互动(如对话、对战),段落级窗口适合捕捉间接关联(如同时被提及)。实际操作中我一般用「句子级 + 5 token 滑动窗口」的组合:先按句号、问号、感叹号切分句子,再在每个句子内用大小为 5 的滑动窗口统计共现。

from collections import defaultdict import math def extract_cooccurrence(tokens, person_set, window_size=5): # 按标点切分句子 sentences = [] current = [] for t in tokens: if t in '.!?;': if current: sentences.append(current) current = [] else: current.append(t) if current: sentences.append(current) cooc = defaultdict(int) person_freq = defaultdict(int) for sent in sentences: # 统计单人物频次 for t in sent: if t in person_set: person_freq[t] += 1 # 滑动窗口共现 for i in range(len(sent)): if sent[i] not in person_set: continue for j in range(i+1, min(i+window_size, len(sent))): if sent[j] in person_set and sent[j] != sent[i]: pair = tuple(sorted([sent[i], sent[j]])) cooc[pair] += 1 return cooc, person_freq person_set = set(person_dict) - set(alias_map.keys()) cooc_matrix, freq = extract_cooccurrence(tokens, person_set, window_size=5) print(f'共现对数量: {len(cooc_matrix)}') print(f'人物频次 top10: {sorted(freq.items(), key=lambda x: -x[1])[:10]}')

这段代码的逻辑分三层:先按标点切句,保证共现不会跨句误统计;然后在每个句子内统计单人物频次;最后用滑动窗口统计共现对。window_size=5意味着两个人名之间最多隔 4 个其他 token 就算共现。这个值调大,关系网络会更密集但噪声也多;调小则可能漏掉长句中的关联。我试过 3、5、8 三档,5 在三国文本上平衡最好。pair = tuple(sorted(...))保证 (A,B) 和 (B,A) 被统计为同一对。输出后要检查 top10 人物频次,正常应该是曹操、刘备、诸葛亮、关羽、孙权这个量级,如果出现「将军」「丞相」这类官职词,说明词典还需要补充过滤。

3.2 关系强度量化:PMI 与归一化共现

原始共现次数有个问题:高频人物(如曹操)和任何人共现次数都高,但这不代表关系强。需要用 PMI(点互信息)或归一化共现来修正。PMI 的公式是log(P(A,B) / (P(A)*P(B))),值越高说明两人关联越超出随机预期。但 PMI 对低频对不友好,容易给出虚高分数。我一般用「归一化共现 + 最小频次阈值」的组合:先过滤掉共现次数小于 3 的对,再用cooc(A,B) / sqrt(freq(A)*freq(B))计算关系强度。

def compute_relation_strength(cooc, freq, min_cooc=3): strength = {} for (a, b), c in cooc.items(): if c < min_cooc: continue # 归一化共现强度 score = c / math.sqrt(freq[a] * freq[b]) strength[(a, b)] = { 'cooc': c, 'score': round(score, 4), 'freq_a': freq[a], 'freq_b': freq[b] } return strength relations = compute_relation_strength(cooc_matrix, freq, min_cooc=3) # 按强度排序输出 top20 top_relations = sorted(relations.items(), key=lambda x: -x[1]['score'])[:20] for (a, b), info in top_relations: print(f'{a} - {b}: 共现{info["cooc"]}次, 强度{info["score"]}')

min_cooc=3是经验阈值,低于 3 次的共现大概率是偶然同句,不值得分析。归一化公式里sqrt(freq[a]*freq[b])是几何平均,比算术平均更能抑制高频人物的影响。输出结果里,正常应该看到「刘备-诸葛亮」「曹操-关羽」「孙权-周瑜」这类强关联对排在前列。如果出现「曹操-刘备」强度异常高,要检查是不是把「曹刘」这种并称当成了共现,需要在分词阶段把并称也加入词典或做特殊处理。这个矩阵可以直接导出成 CSV,后续词云和网络图都用它作为输入。

4. 词云图生成与人物关系可视化:参数调优与避坑指南

4.1 词云图生成的完整代码与参数详解

词云图的核心参数有三个:字体、蒙版形状、词频权重。中文字体必须显式指定,否则会显示成方块。蒙版形状决定词云的轮廓,常见做法是用三国地图或人物剪影。词频权重直接用上一步的关系强度或人物频次。我一般用wordcloud库,配合matplotlib输出。

from wordcloud import WordCloud import matplotlib.pyplot as plt from PIL import Image import numpy as np # 准备词频字典:用人物频次作为权重 word_freq = {name: count for name, count in freq.items() if count >= 5} # 加载蒙版图片(需为黑白二值图,白色区域不填充) mask = np.array(Image.open('sanguo_mask.png')) wc = WordCloud( font_path='SimHei.ttf', # 中文字体路径 width=1200, height=800, # 输出尺寸 background_color='white', # 背景色 mask=mask, # 蒙版 max_words=100, # 最多显示词数 max_font_size=150, # 最大字号 min_font_size=10, # 最小字号 colormap='viridis', # 配色方案 random_state=42, # 随机种子,保证可复现 prefer_horizontal=0.7 # 水平排列比例 ) wc.generate_from_frequencies(word_freq) plt.figure(figsize=(15, 10)) plt.imshow(wc, interpolation='bilinear') plt.axis('off') plt.savefig('sanguo_wordcloud.png', dpi=200, bbox_inches='tight') plt.show()

font_path必须指向系统中实际存在的中文字体文件,Windows 一般在C:/Windows/Fonts/下,Linux 需要手动安装。mask参数要求图片是黑白二值图,白色区域会被忽略,黑色区域填充文字。max_words=100控制显示词数,太多会拥挤,太少信息量不足。random_state=42保证每次生成的布局一致,方便调试。prefer_horizontal=0.7让 70% 的词水平排列,剩余垂直排列,视觉上更自然。如果输出图片里人物名大小差异不明显,说明词频区分度不够,可以考虑对频次取对数后再传入。

4.2 人物关系网络图的补充可视化

词云图擅长展示人物频次,但关系强度用网络图更直观。我一般用networkx配合matplotlib画关系网络,节点大小对应人物频次,边粗细对应关系强度。这样一张图就能同时看到「谁重要」和「谁和谁关系近」。

import networkx as nx G = nx.Graph() # 添加节点,大小按频次 for name, count in freq.items(): if count >= 5: G.add_node(name, size=count) # 添加边,权重按关系强度 for (a, b), info in relations.items(): if info['score'] > 0.1: # 过滤弱关系 G.add_edge(a, b, weight=info['score']) pos = nx.spring_layout(G, k=2, seed=42) node_sizes = [G.nodes[n]['size'] * 20 for n in G.nodes()] edge_widths = [G[u][v]['weight'] * 5 for u, v in G.edges()] plt.figure(figsize=(16, 12)) nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color='lightblue', alpha=0.8) nx.draw_networkx_edges(G, pos, width=edge_widths, alpha=0.5, edge_color='gray') nx.draw_networkx_labels(G, pos, font_size=10, font_family='SimHei') plt.axis('off') plt.savefig('sanguo_network.png', dpi=200, bbox_inches='tight') plt.show()

spring_layout的k参数控制节点间距,调大更分散,调小更紧凑。seed=42同样是为了可复现。边过滤阈值score > 0.1需要根据实际数据分布调整,可以先画全量图再决定砍在哪里。节点大小乘以 20 是缩放因子,避免频次差异导致节点过大或过小。这张图配合词云图一起看,基本能把三国人物关系的核心结构呈现出来。

5. 避坑与常见问题排查:那些让我翻车的细节

5.1 人名识别不全导致关系遗漏

现象:关系网络图里只有曹操、刘备几个核心人物,大量次要人物(如「张辽」「徐晃」)完全没出现。原因:自定义词典只覆盖了主角,分词器把次要人名切碎,导致共现统计时这些人物被忽略。解决:从原文中提取所有二字、三字高频名词,人工筛选出人名补充进词典。我一般用jieba.analyse.extract_tags先跑一遍关键词,再对照三国人物列表补全。词典至少覆盖 100 人以上,否则关系网络会严重失真。

5.2 共现窗口过大引入虚假关系

现象:曹操和貂蝉的关系强度异常高,但原文中两人几乎没有直接互动。原因:滑动窗口设成了 10,导致同一段落里相隔很远的两个人名也被统计为共现。解决:把window_size降到 5 以内,并优先用句子边界切分。如果必须用段落级窗口,建议加一个「中间不能有其他人名」的约束,避免 A-C-B 被误判为 A-B 共现。

5.3 词云图中文显示为方块

现象:生成的词云图里所有中文都是方框,英文和数字正常。原因:font_path指向的字体文件不存在,或者字体本身不包含中文字形。解决:确认字体文件路径正确,Windows 用C:/Windows/Fonts/simhei.ttf,Linux 用fc-list :lang=zh查找可用中文字体。如果还是不行,换用SourceHanSansCN或NotoSansCJK这类开源中文字体。

5.4 关系强度分数区分度低

现象:所有人物对的强度分数都在 0.3-0.5 之间,看不出明显差异。原因:归一化公式对高频人物的抑制不够,或者共现次数本身分布太均匀。解决:改用 TF-IDF 思路,对共现次数取对数后再归一化,或者直接对分数做 min-max 标准化拉到 0-1 区间。另一个办法是只保留每个节点的 top3 强关系,砍掉长尾边,让核心结构更突出。

5.5 蒙版图片导致词云布局异常

现象:词云图里文字全部挤在角落,或者大量空白区域。原因:蒙版图片不是二值图,或者白色区域和黑色区域搞反了。解决:用 PIL 把蒙版转成灰度图,再用阈值二值化,确保白色是背景、黑色是填充区。如果蒙版太复杂(如精细地图轮廓),建议先做形态学膨胀,让填充区域更连续。

6. 进阶技巧:用 TF-IDF 加权和动态阈值提升关系识别精度

前面用的归一化共现有个局限:它只考虑全局频次,不考虑人物在不同章节的分布差异。如果两个人只在某一回集中出现,他们的关系可能被高估。进阶做法是引入 TF-IDF 思路:把每个人物看作一个「文档」,共现对看作「词」,计算共现对在人物上下文中的重要性。具体来说,对每个人物 A,统计它与其他所有人物的共现分布,然后对每个共现对 (A,B) 计算TF(A,B) * IDF(B),其中 TF 是 A 视角下 B 的共现占比,IDF 是 B 的全局稀有度。这样能抑制「曹操-刘备」这种高频但泛泛的关系,突出「关羽-颜良」这种低频但关键的对决关系。

def tfidf_relation(cooc, freq, person_set): # 构建每个人物的共现分布 person_cooc = defaultdict(dict) for (a, b), c in cooc.items(): person_cooc[a][b] = c person_cooc[b][a] = c total_pairs = len(cooc) tfidf_scores = {} for person, neighbors in person_cooc.items(): total = sum(neighbors.values()) for other, c in neighbors.items(): tf = c / total # IDF: 包含 other 的共现对越少,IDF 越高 idf = math.log(total_pairs / (1 + sum(1 for (x, y) in cooc if other in (x, y)))) tfidf_scores[(person, other)] = tf * idf return tfidf_scores tfidf_rel = tfidf_relation(cooc_matrix, freq, person_set) # 对每个节点取 top3 关系 top_per_person = defaultdict(list) for (a, b), score in tfidf_rel.items(): top_per_person[a].append((b, score)) for person in top_per_person: top_per_person[person] = sorted(top_per_person[person], key=lambda x: -x[1])[:3] # 输出示例 for person, rels in list(top_per_person.items())[:5]: print(f'{person}: {[(r[0], round(r[1], 3)) for r in rels]}')

这段代码的核心是tf和idf的计算。tf = c / total表示在人物 A 的所有共现中,与 B 的共现占比。idf用log(total_pairs / (1 + 包含 other 的对数)),包含 other 的对越少,IDF 越高,说明这个关系越独特。最后对每个人物只保留 top3 关系,避免网络图过于密集。实际跑下来,「关羽-颜良」「张飞-严颜」这类特定对手关系会明显浮上来,而「曹操-刘备」这种泛泛之交会被压下去。这个技巧在分析人物关系时特别有用,因为它能区分「经常一起出现」和「关系真正紧密」。

最后说个习惯:我每次跑完词云和网络图,都会把 top20 关系对导出来人工过一遍,看看有没有明显违背常识的。有一次发现「诸葛亮-司马懿」强度排到了第一,检查后发现是原文中「诸葛」和「司马」被当成了两个人名分别统计,修正词典后才恢复正常。数据挖掘的坑,一半在代码,一半在词典。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询