如果你已经跟着上一篇的流程把分词、去停用词、词频统计跑通了,那你多半遇到了一个让人憋屈的现象:图表画了一堆,词数了一箩筐,但业务方问“所以用户到底在抱怨啥”的时候,你只能指着“客服”“商品”“快递”这几个高频词绕圈子。词频回答的是“什么词被反复提到”,回答不了“什么词真正有辨识度”。这次“文本数据分析(二)”就专门处理这个卡点。
这一篇会从“词频”进阶到“关键词”,把TF-IDF和TextRank的原理、代码、参数、坑全部过一遍,再顺手解决文本相似度计算和结果可视化的问题。内容偏实操,适合已经跑通分词流程、想进一步做真实业务分析的读者;哪怕你对算法细节不熟,跟着代码走也能把一套完整的文本分析流水线搭起来。
1. 为什么第二篇要解决“关键词提取”
1.1 词频统计的局限在哪儿
先用一个我实际处理过的场景说事。
某电商售后团队给我一批工单数据,大概几千条,内容比较乱,有投诉、有咨询、有退换货记录。我按常规流程走完分词、去停用词、统计词频之后,排名前五的词是:“客服”“问题”“客户”“商品”“解决”。这五个词能说明什么?最多说明“客服在解决客户商品问题”,这是一句不用跑数据也能猜到的废话。
问题不在流程,而在词频本身的天性。词频衡量的是“出现次数”,但一个词在文本里反复出现,未必代表它重要。反而是那些在某几条文本里集中出现、在整体语料里很少露面的词,才更像这批数据的“身份标记”。比如“屏幕划痕”这个词,在几千条工单里可能只出现在屏幕问题相关的几十条记录中,但它几乎能直接定位到一类具体的售后场景。这种词,词频统计很难给你找出来。
所以从“文本数据分析(一)”到“(二)”,核心转变就是把“出现得多”升级为“更能区分文本”。有了这个视角,后面所有方法都是围绕它展开的。
1.2 关键词提取的两条主流路线
关键词提取不一定要上深度学习模型。对大部分业务分析场景,两条经典路线足够用。
第一条是统计加权路线,代表算法是TF-IDF。它的逻辑是:一个词如果在某篇文本中出现次数多,又在整个语料库中很少出现,那它对这篇文本就很重要。这像是在说“稀缺即价值”,和“物以稀为贵”一个意思。
第二条是图算法路线,代表算法是TextRank。它的逻辑更偏“关系”:把文本里的词看成节点,词和词之间有共现关系就连接起来,然后迭代计算每个词的权重。权重高的词就是文本里的“中心词”,相当于在一个社交网络里找到了关键人物。
这两条路线各有适用场景,后面会单开章节细讲。先记住一个结论:TF-IDF更偏向“词和文档的区分度”,TextRank更偏向“词在文本结构里的枢纽地位”。理解到这个层面,你就知道什么时候该用谁了。
2. TF-IDF:用“稀缺性”给词做加权
2.1 公式拆成两半来理解
TF-IDF是两个部分的乘积,分别是TF(Term Frequency,词频)和IDF(Inverse Document Frequency,逆文档频率)。
TF部分很好理解,就是词在文档里出现的次数除以文档总词数。它的作用是反映“词在这篇文档里有多活跃”。如果一篇手机评论里反复出现“屏幕”和“划痕”,这两个词的TF值就会比较高。
IDF部分稍微绕一点,公式是:
IDF = log( 总文档数 / (包含该词的文档数 + 1) )
分母加1是为了防止某个词在所有文档里都没出现过导致除零。IDF的含义是:包含这个词的文档越少,IDF越大,说明这个词越“稀有”。比如“划痕”可能只有20条评论包含,而“手机”几乎每条都有,那“划痕”的IDF远大于“手机”。
把两部分乘起来,TF-IDF既保留了“在这篇文档里活跃”的语义,又惩罚了“到处都能见到”的背景词。你再看高频词“商品”“客服”,它们TF可能不低,但IDF非常低,综合权重也就被压下去了。这也是为什么TF-IDF在实际分析里比纯词频好用太多。
2.2 代码实现与关键参数
用Python做TF-IDF,不需要自己实现公式,Scikit-learn的TfidfVectorizer直接搞定。配合Jieba做中文分词,代码可以控制在二十行以内。
import jieba from sklearn.feature_extraction.text import TfidfVectorizer # 原始文本,每一条代表一个文档 corpus = [ "手机收到了,但屏幕有一道划痕,售后处理很慢,等了三天才回复,物流倒是挺快", "这款笔记本散热效果一般,打游戏风扇声音很大,退货流程倒是很顺利", "客服态度很好,但换货的快递一直没发出,系统显示已经三天了" ] def cut_text(text): # 用jieba精确模式分词,再用空格拼接 return " ".join(jieba.cut(text)) corpus_cut = [cut_text(text) for text in corpus] # 构造TF-IDF向量 vectorizer = TfidfVectorizer(max_features=500, ngram_range=(1, 2)) X = vectorizer.fit_transform(corpus_cut) # 输出每个词的权重,方便检查结果 for word, index in vectorizer.vocabulary_.items(): print(f"{word}: {X[0, index]:.4f}")先说几个我反复用到的参数。
max_features=500,限制特征词典大小,取词频靠前的500个词。语料量大时一定要设这个值,不然特征维度会爆炸,矩阵稀疏到后面根本跑不动。
ngram_range=(1, 2),意思是把单个词和相邻两个词的组合都算作特征。“屏幕划痕”作为一个二元词组,比单独的“屏幕”和“划痕”携带更多上下文信息。对中文短文本来说,开启bigram通常能提升关键词的语义完整度。
min_df和max_df也值得调。min_df=5表示词至少在5篇文档中出现才算特征,过滤只出现一两次的噪声词;max_df=0.8表示在超过80%的文档中都出现的词直接丢掉,这些基本是通用背景词。
2.3 我踩过的几个坑
第一个坑是停用词表不够全。TF-IDF虽然能降低背景词权重,但对“我们”“这个”“因为”这类纯语法词,效果有限。建议自己维护一份业务停用词表,把“亲”“亲亲”“麻烦”这类客服语料里的高频客套词也加进去,否则它们会占据大量特征空间。
第二个坑是没有过滤标点和数字。原始文本里常混着“187****1234”这种手机号,分词后可能变成单个数字或符号,直接进入特征词典。我在预处理阶段就会用正则把手机号、纯数字、网址、邮箱全部替换或移除,再进分词。
第三个坑是语料量太少时结果不稳定。TF-IDF里的IDF依赖“包含该词的文档数”,如果总共只有几十条文本,IDF的估计会非常随机。我的经验是:少于200条文本时,TF-IDF结果只能用来找感觉,不建议直接作为业务结论依据;最好先用TextRank和人工抽检交叉验证。
3. TextRank:用“投票关系”找关键节点
3.1 核心思想一句话讲透
TextRank最早受PageRank启发。PageRank的思路是,一个网页的重要程度由其他重要网页链接过来决定;链接越多且来源越重要,这个网页就越重要。
TextRank把这个逻辑搬到了文本里。它把每一个词当作一个网页,词与词之间的“链接”靠共现关系建立:在一个滑动窗口内同时出现的词,就有边连接。然后算法反复迭代,把权重在词与词之间传播,最后每个词得到一个稳定分数,排序靠前就是关键词。
举个例子。“屏幕有划痕但物流很快”,设置窗口大小为5,那么“屏幕”“划痕”“物流”“很快”这四个词可能都在一个窗口内,它们之间两两建边。迭代之后,“划痕”如果和很多重要词都发生共现,它的分数就会被抬起来。
这就解释了为什么TextRank能抓住一些TF-IDF抓不住的东西:它不只看词出现的多少,更看词“和谁一起出现”。在长文本里,这种共现关系能天然保留句子结构带来的语义上下文。
3.2 调用方式与常用参数
Jieba库内建了TextRank实现,不用自己搭图,调用起来比较省事。
import jieba.analyse text = "客服态度很好,但换货的快递一直没发出,系统显示已经三天了,用户要求尽快处理" # 提取top5关键词,显示权重 result = jieba.analyse.textrank( text, topK=5, withWeight=True, allowPOS=("ns", "n", "vn", "v", "a") ) for word, weight in result: print(f"{word}: {weight:.4f}")allowPOS是这里最值得说的参数。它限定了参与排序的词性范围,等于告诉算法“你只看名词、动词、形容词、地名,别把副词和助词拉进来”。不加的话,类似“很”“了”“就”这种词偶尔会混进结果,特别影响观感。
topK控制返回关键词数量。短文本取3到5个就够,长文本可以取10个以上。窗口大小是通过代码内部参数控制的,Jieba默认窗口是5,如果发现提取出的词比较散、不成句,可以把窗口调小到3,让词和词之间的联系更紧;如果希望看到更多关联组合,可以调大到8。
3.3 TF-IDF和TextRank怎么选
直接给结论,后面实战会再印证。
| 维度 | TF-IDF | TextRank |
|---|---|---|
| 核心逻辑 | 用文档间分布差异加权 | 用词共现关系迭代投票 |
| 数据要求 | 需要成规模的语料库 | 单篇文本也能计算 |
| 典型场景 | 多篇短文本横向对比筛选 | 单篇长文档摘要挖掘 |
| 结果特点 | 偏“稀有词”,区分度强 | 偏“上下文枢纽”,平滑规整 |
| 主要问题 | 依赖语料质量,短语料不稳 | 对长文效果好,短文本略泛 |
实操选择很直接:如果你手里是几千条客服工单、评论、问卷反馈,要做“这批数据整体在聊什么”,用TF-IDF更合适;如果是对某一条长微博或一篇新闻做独立的关键信息抽取,TextRank会更好。当然,两个一起用做交叉验证,是成本最低的稳健方案。
4. 文本相似度:给文本之间的“距离”量个尺
4.1 从关键词到向量
文本相似度是个绕不开的话题。做文本去重、评论聚类、相似问题归并,都需要回答同一个问题:“这两条文本像不像?”
答案的起点是把文本变成向量。没有向量,文本就只是一串字符,无法做数学运算。TfidfVectorizer把每篇文档映射成一个固定维度的向量,维度就是词典里的词项,每个位置的数值就是该词在这篇文档里的TF-IDF权重。于是几千条文本就成了一个矩阵,矩阵的每一行代表一篇文档。
这里有一个很多人刚接触时想不通的问题:为什么不让计算机直接比较字符是否相等?因为文本表达太灵活了,“屏幕有问题”和“屏幕坏了有瑕疵”字符不一样,但语义高度接近。向量化的意义,就是把这种语义层面的接近转换成几何层面的接近。
4.2 余弦相似度的计算实操
文本向量常用的相似度是余弦相似度。它的公式是两向量夹角的余弦值,取值范围在-1到1之间,越接近1越相似。
为什么不用欧氏距离?因为欧氏距离会受向量长度影响。两篇同样内容的文本,一个写500字,一个写2000字,TF-IDF向量的模长可能差很多,欧氏距离会认为它们不相似;余弦相似度只看“方向”,天然克服了文档长度差异。
代码实现非常简单:
from sklearn.metrics.pairwise import cosine_similarity # X是上一步得到的TF-IDF矩阵 similarity_matrix = cosine_similarity(X) # 看第0条和第1条的相似度 print(f"第0条和第1条相似度: {similarity_matrix[0][1]:.4f}") print(f"第0条和第2条相似度: {similarity_matrix[0][2]:.4f}")拿到相似度矩阵之后,你可以做两件事。
第一,找相似度超过阈值的文本对,把它们归为一类。比如客服系统里用户反复提交同一问题,通过相似度阈值0.8直接识别重复工单,减少人工重复处理。
第二,把相似度矩阵喂给聚类算法,先降维再用KMeans等无监督方法做主题分组。这样你不必靠人工读几千条记录,也能快速得到几个粗略的话题簇。
4.3 相似度分析里最容易被忽视的细节
短文本的相似度非常容易变形。两句都很短的话,比如“质量差”和“客服差”,分词后可能只有一个重叠词,相似度接近0,但实际都指向负面评价。这个问题在传统TF-IDF路线里几乎无解,只能通过扩充同义词表或者后续换用词向量来处理。
另一个细节是阈值怎么定。不要上来就拍板“相似度大于0.8就去重”。不同语料分布下的相似度分布差异很大,我的习惯是先统计一下相似度矩阵的分布,比如看看0.6、0.7、0.8分别有多少对文本,再结合业务抽检定阈值。盲目定阈值的结果,不是误杀就是漏网。
5. 可视化:让分析结果通俗可讲
5.1 词云:快速看重点
文本分析做完,如果只给业务方一张Excel词频表,对方大概率不会认真看。可视化是文本分析的关键一步,不是为了好看,是为了降低沟通成本。
词云是最直观的手段。把每个词的权重映射成词云中的字号,权重越大字号越大。用WordCloud库实现很顺手,但中文场景必须指定中文字体,否则所有字都会变成方框。
from wordcloud import WordCloud import matplotlib.pyplot as plt import jieba text = " ".join(jieba.cut(open("data.txt", encoding="utf-8").read())) wc = WordCloud( font_path="C:/Windows/Fonts/simhei.ttf", # 换成你自己的中文字体路径 max_words=100, width=800, height=600, background_color="white" ) wc.generate(text) plt.imshow(wc) plt.axis("off") plt.show()做词云有一个经验:不建议直接把原始文本丢进去,而是先跑一遍停用词过滤和关键词提取,只把得分靠前的词做成词云。否则你会得到一张被“客服”“问题”这种大词包围的图,信息量又回到词频统计的老路上。
5.2 关键词共现网络:看词与词的关系
词云只能看“哪些词重要”,看不出“它们之间什么关系”。如果需要分析话题结构,可以做一张关键词共现网络。
构建方法不复杂:在分词后的文本上设定一个滑动窗口,窗口内任意两个词算一次共现,统计共现次数。过滤掉低频词和背景词之后,用NetworkX建图,节点是词,节点大小代表权重,边代表共现关系,边的粗细代表共现强度。
看共现网络有个很直觉的作用:你能一眼看出哪些词经常抱团。比如“物流”“迟迟”“不发货”“退款”形成一个密集子图,那基本就能判断这批文本里存在一个“物流延迟导致退款”的话题簇。这种结构特征,词云很难直观呈现。
我通常会把共现网络图和词云放在同一份分析报告里:词云回答“主要有什么”,网络回答“哪些词总一起出现”。两份图一对照,给业务方讲结论的时候底气足很多。
6. 完整实操:从原始文本到分析结论
6.1 先把分析目标拆清楚
前面讲了一堆方法,但更重要的是一套流程怎么串起来。我用一个模拟场景演示,你拿去换自己的数据就能用。
场景:某平台客服收到一批关于“发货和退换货”的用户反馈,总量约800条。目标是快速定位最集中的问题类型,给运营一个优先级排序。
这一步的关键是先定义输出。我的输出不是“一些关键词”,而是一句话:“在发货环节,物流信息更新滞后是最集中的问题;在退换货环节,退款到账慢是最集中的问题。”有了这个目标,后面的技术选型就很清晰:先用TF-IDF做整体关键词,再按文本相似度分组细看,最后人工抽检验证。
6.2 预处理和特征提取
原始文本很脏,不能直接分词。我先统一做几件事:把所有换行符处理成空格,删除纯数字、网址、手机号、邮箱,把“客服”“用户”“您好”这类对话客套词加入停用词表。然后才进入jieba分词。
import re import jieba from sklearn.feature_extraction.text import TfidfVectorizer stopwords = set() for line in open("stopwords.txt", encoding="utf-8"): stopwords.add(line.strip()) def clean_text(text): text = re.sub(r"https?://\S+", "", text) text = re.sub(r"\d+", "", text) text = re.sub(r"\s+", " ", text) return text def cut_and_filter(text): words = jieba.cut(clean_text(text)) return " ".join(w for w in words if w.strip() and w not in stopwords) corpus_cut = [cut_and_filter(t) for t in feedback_list] vectorizer = TfidfVectorizer(max_features=800, ngram_range=(1, 2), min_df=3, max_df=0.7) X = vectorizer.fit_transform(corpus_cut)min_df=3在这里很有用,至少3条文本出现的词才保留,把只出现过一次的偶然噪声挡掉。max_df=0.7则把“客服”“问题”这类超过70%文本都有的背景词排除掉,比后面再手动剔除省事。
6.3 组合应用与结果解读
TF-IDF结果出来后,我拿Top20关键词给运营同事看,对方反馈“物流信息不更新”“退款慢”“发错货”这些核心词确实在Top里,说明方向对了。但Top20里也有几个比较模糊的复合词,比如“一直”“还是”,这些是停用词表没覆盖掉的口语化高频词,直接追加进停用词表重新跑。
接着把TF-IDF矩阵做余弦相似度,阈值先定0.6,找出彼此相似的文本簇。这时候TextRank登场,对每个聚类簇内部做二次提取,相当于“簇内关键词”。簇A的TextRank结果是“物流、不更新、三天、联系客服”,簇B是“退款、不到账、银行卡、已关闭”。两簇的差异一下就清楚了,再拉几条例句人工确认,结论基本能定稿。
整套流程下来,800条文本从清洗到输出结论,用时大概半小时,不需要训练模型,不依赖标注数据。这种“低成本快出活”的路线,是业务分析场景最需要的能力。
7. 常见问题与排查技巧
7.1 提取结果全是噪声词怎么办
先查停用词表,再把数字和标点过滤前置。如果都做了,噪声仍然多,大概率是语料太杂、话题太多。我的办法是先对文本做一次粗分类,比如按“发货”“售后”“质量”分桶,再在桶内各自提取关键词。小范围语料的关键词质量,通常远好过把所有文本混在一起硬提。
7.2 程序跑不动、内存溢出
特征维度是最常见的肇事者。TfidfVectorizer如果不设max_features,特征数可能膨胀到几十万维,矩阵稀疏度巨大但是内存占用仍然失控。另一个办法是分批读取,不要一口气把几百万条文本全部读进内存。数据量大时,直接用spark或者分批循环生成TF-IDF矩阵,结果差异不大。
7.3 分析结果不知道靠不靠谱
关键词提取没有标准答案,所以验证很重要。我习惯抽10到20条原始文本,人工判断提取出的关键词是否落在核心话题上,再算一个粗糙的准确率。如果准确率低于七成,就检查停用词表、min_df、max_df和分词效果,通常调一轮就能改善。还有一个技巧:把TF-IDF和TextRank两份结果都跑出来,取交集或合并,比单用其中一个稳定得多。
7.4 中文词云乱码
这是WordCloud最常见的坑。中文字体文件必须显式传入font_path,默认字体不支持中文。另外要注意操作系统路径差异,别直接抄网上的绝对路径,先确认本机字体文件真实存在。
7.5 相似度计算结果不符合直觉
如果一对明显是在说同一件事的文本,相似度却很低,先分词看结果。Jieba对某些专业词汇会切得过碎,比如“自动退款”被切成“自动”和“退款”,导致语义被拆散。解决方式是把这类业务词加进自定义词典,再分词重跑一遍。
我个人在实际操作中的体会是,文本数据分析做到“关键词提取+相似度分组+可视化”这一层,已经能覆盖绝大多数业务分析需求。很多人一上来就想上深度学习、微调大模型,实际连数据预处理都没做好,结果事倍功半。把TF-IDF和TextRank这两个基础方法用扎实,配合一套好用的停用词表,你在文本分析上的输出质量已经超过多数临时抱佛脚的团队。
最后再分享一个小技巧:把每次调优后的参数和结果记录下来,形成“语料类型+参数组合+效果评价”的对照表。下次再遇到类似文本,直接套用历史经验,不用重新踩一遍分词和阈值的坑。文本分析这东西,方法多是一回事,能不能稳定复现才是真正拉开差距的地方。