简介:这份资源面向Python数据分析与文本挖掘的学习者,尤其是需要完成课程设计或入门NLP实战的学生。它围绕电商产品评论展开完整的情感分析流程:先用爬虫获取的原始评论数据,经pre_data.py完成分句、jieba分词与词性标注、停用词过滤等预处理;再由lda_model.py提取评论中的特征名词,对特征词前后的情感副词与情感词加权打分,构建以特征名词为列向量的评论得分DataFrame;最后通过PCA、皮尔逊相关分析提取特征,使用LR、SVM、Xgboost建模训练并预测销量排名。压缩包共10个文件,包含4个csv数据文件、3个py脚本、2个pyc缓存与1个md说明文档,整体约4.1MB,结构紧凑便于直接运行与二次修改。目前已有1817人学习下载,适合希望掌握LDA主题模型与情感分析全流程、积累建模与特征工程经验的读者参考。
1. 电商评论情感分析:LDA 能挖出什么别人看不到的东西
做电商运营或者数据岗的朋友大概率遇到过这种场景:老板丢过来一份几万条的评论数据,问“用户到底在骂什么、夸什么”,你打开 Excel 翻了二十页,满眼都是“质量不错”“物流太慢”“客服态度差”,靠人眼归类根本干不完。这时候 Python 加 LDA 主题模型就是一套能落地的方案——它不需要你提前标注正负面,也不需要训练分类器,直接把一堆中文评论丢进去,自动吐出若干主题簇,每个簇里带着高频词,你一眼就能看出“这个簇在讲物流”“那个簇在讲色差”。
这套东西适合谁?一是电商数据分析岗,手里有评论数据但没预算买商业舆情系统;二是 Python 入门到中阶的开发者,想找一个完整的小项目把 pandas、jieba、sklearn、pyLDAvis 串起来;三是做美团情感分析、影视情感分析这类课题的学生,需要一套可复现的代码框架。它解决的核心问题是:从非结构化中文文本里,用无监督方式提取可解释的主题,再叠加情感倾向判断,最终输出“哪个主题下负面占比最高”这种能直接给业务看的结论。
需要提前说清楚边界:LDA 做的是主题发现,不是情感分类。情感极性还得靠 SnowNLP 或者自己构建情感词典来补。两者结合才是完整的“评论数据情感分析”。下面从环境搭建一路讲到调参和可视化,代码全部可跑。
2. 环境搭建与中文评论数据预处理:从原始 CSV 到分词矩阵
2.1 安装依赖与中文分词库选型
先解决环境问题。Python 安装教程网上很多,不展开,直接说库的安装。核心依赖有五个:pandas 做数据读取和清洗,jieba 做中文分词,sklearn 提供 LDA 实现和 CountVectorizer,pyLDAvis 做主题可视化,SnowNLP 做情感打分。
pip install pandas jieba scikit-learn pyLDAvis snowNLP matplotlib如果你用的是 vscode python 环境配置或者 pycharm 配置 python 环境,直接在终端里跑上面这行就行。注意 pyLDAvis 依赖较老,如果报 numpy 版本冲突,先pip install "numpy<1.24"再装 pyLDAvis。
分词库为什么选 jieba 而不是 HanLP 或 THULAC?在这个场景下,jieba 的优势是轻量、加载快、支持自定义词典。电商评论里有大量专有名词比如“百亿补贴”“仅退款”“预售定金”,这些词不进词典就会被切碎,直接影响 LDA 的主题质量。HanLP 精度更高但模型大、启动慢,对于几万条评论的项目,jieba 加自定义词典完全够用。
2.2 评论数据清洗的四个关键步骤
拿到一份电商评论 CSV,典型字段是:评论 ID、用户昵称、评分、评论内容、时间。清洗流程分四步走。
第一步,去重和去空。同一用户刷屏、默认好评“此用户没有填写评价”这类要干掉。第二步,过滤短文本。少于 5 个字的评论信息量太低,留着只会增加噪声。第三步,去除特殊符号和 URL。第四步,处理评分字段,把 1-2 星标记为负面,4-5 星标记为正面,3 星单独放一边。
import pandas as pd import re def clean_comments(df, text_col='content', score_col='score'): # 去重 df = df.drop_duplicates(subset=[text_col]) # 去空和默认评价 df = df[df[text_col].notna()] df = df[~df[text_col].str.contains('此用户没有填写评价|默认好评', na=False)] # 过滤短文本 df = df[df[text_col].str.len() >= 5] # 去 URL 和特殊符号,只保留中文、英文、数字 df[text_col] = df[text_col].apply( lambda x: re.sub(r'http\S+|www\.\S+', '', str(x)) ) df[text_col] = df[text_col].apply( lambda x: re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9]', ' ', x) ) # 生成情感标签 df['sentiment'] = df[score_col].apply( lambda s: '负面' if s <= 2 else ('正面' if s >= 4 else '中性') ) return df.reset_index(drop=True) df = pd.read_csv('comments.csv') df = clean_comments(df) print(f'清洗后剩余 {len(df)} 条评论') print(df['sentiment'].value_counts())这段代码的逻辑说明:drop_duplicates按评论内容去重,防止刷单数据干扰主题分布。正则[^\u4e00-\u9fa5a-zA-Z0-9]把标点、emoji 全部替换成空格,保留中文和英文数字。情感标签用评分映射,这是最省事的弱标注方式,虽然不完美但比人工标注快得多。
参数方面,短文本阈值 5 是可以调的。如果你做的是外卖评论,很多有效评论就四五个字比如“送得太慢了”,那阈值降到 3。如果是数码产品评论,一般都比较长,阈值可以提到 8。
2.3 自定义词典与停用词表:决定 LDA 主题质量的黑匣子
这一步是很多人翻车的地方。jieba 默认词典对电商领域覆盖很差,“退货退款”会被切成“退货”“退款”两个词,“性价比高”会被切成“性价比”“高”。LDA 拿到这种分词结果,主题词就散了。
解决办法是加载自定义词典。我一般会从评论数据里先用 TF-IDF 跑一遍高频词,人工挑出明显该合并的词,写成一个userdict.txt,每行一个词,格式是“词 词频 词性”,词频和词性可以省略。
import jieba import jieba.analyse # 加载自定义词典 jieba.load_userdict('userdict.txt') # 停用词表:中文常见停用词 + 电商领域噪声词 stopwords = set() with open('stopwords.txt', 'r', encoding='utf-8') as f: for line in f: stopwords.add(line.strip()) # 补充电商场景专属停用词 stopwords.update(['东西', '感觉', '真的', '就是', '还是', '已经', '一个', '什么', '这个']) def tokenize(text): words = jieba.lcut(text) # 过滤停用词、单字、纯数字 return [w for w in words if w not in stopwords and len(w) > 1 and not w.isdigit()] df['tokens'] = df['content'].apply(tokenize) print(df['tokens'].head(3))停用词表是 LDA 的后悔药。不加停用词,主题词里全是“的”“了”“是”,pyLDAvis 图上一片糊。电商场景还要额外加“宝贝”“卖家”“买家”“收到”这类高频但无区分度的词。我一般会把停用词表维护到 200 词左右,覆盖通用停用词加领域噪声词。
分词完成后,用 sklearn 的 CountVectorizer 构建文档-词矩阵。这里有个关键选择:用 CountVectorizer 还是 TfidfVectorizer?LDA 本质上基于词频统计,用 CountVectorizer 更符合模型假设。TfidfVectorizer 会压制高频词,反而可能把“物流”“质量”这些真正重要的主题词权重拉低。
from sklearn.feature_extraction.text import CountVectorizer # 把 tokens 列表拼回字符串,CountVectorizer 需要字符串输入 df['tokens_str'] = df['tokens'].apply(lambda x: ' '.join(x)) vectorizer = CountVectorizer( max_df=0.8, # 出现在 80% 以上文档中的词丢掉 min_df=5, # 出现在少于 5 篇文档中的词丢掉 max_features=5000 # 最多保留 5000 个词 ) dtm = vectorizer.fit_transform(df['tokens_str']) print(f'文档-词矩阵形状: {dtm.shape}')max_df=0.8过滤掉过于普遍的词,min_df=5过滤掉太罕见的词,max_features=5000控制矩阵规模防止内存爆炸。这三个参数直接决定 LDA 的输入质量,后面调参章节还会展开。
3. LDA 主题建模实操:从困惑度选 K 值到主题词输出
3.1 LDA 在电商评论场景下的数学直觉
LDA 的全称是 Latent Dirichlet Allocation,中文叫潜在狄利克雷分配。名字吓人,核心思想其实很朴素:假设每篇文档是由多个主题混合而成的,每个主题又是若干词的分布。比如一条评论“质量很好但物流太慢”,它可能 60% 属于“产品质量”主题,40% 属于“物流服务”主题。LDA 要做的就是反过来,从所有文档的词分布中,推断出主题词分布和文档主题分布。
在电商评论场景下,这个假设是合理的。用户评论通常涉及产品质量、物流速度、客服态度、性价比、外观包装等几个维度,每个维度就是一个潜在主题。LDA 不需要你告诉它有几个维度,它会自己从词共现模式里学出来。
但 LDA 有两个前提假设需要注意:一是词袋假设,不考虑词序,“物流快”和“快物流”在 LDA 眼里一样;二是文档主题分布服从狄利克雷先验。这两个假设在短文本评论上会打折扣,所以后面要讲短文本的应对策略。
3.2 用困惑度和一致性曲线确定最优主题数 K
K 值是 LDA 最重要的超参数,代表你要提取多少个主题。K 太小,主题混在一起分不开;K 太大,主题碎成渣,每个主题只有两三个词。确定 K 值的标准做法是跑困惑度(Perplexity)和一致性(Coherence)曲线。
困惑度衡量模型对数据的拟合程度,越低越好。一致性衡量主题内部词的语义连贯性,越高越好。实际项目中我一般两个都看,以一致性为主,困惑度为辅。
from sklearn.decomposition import LatentDirichletAllocation import matplotlib.pyplot as plt perplexities = [] coherences = [] k_range = range(3, 12) for k in k_range: lda = LatentDirichletAllocation( n_components=k, max_iter=20, learning_method='batch', random_state=42 ) lda.fit(dtm) perplexities.append(lda.perplexity(dtm)) # sklearn 不直接提供 coherence,用近似方法:主题间词重叠度 top_words = set() for topic_idx in range(k): topic = lda.components_[topic_idx] top_idx = topic.argsort()[:-11:-1] top_words.update(vectorizer.get_feature_names_out()[top_idx]) coherences.append(len(top_words) / (k * 10)) # 越大说明主题间区分度越高 fig, ax1 = plt.subplots(figsize=(10, 5)) ax1.plot(k_range, perplexities, 'b-o', label='困惑度') ax1.set_xlabel('主题数 K') ax1.set_ylabel('困惑度', color='b') ax2 = ax1.twinx() ax2.plot(k_range, coherences, 'r-s', label='主题区分度') ax2.set_ylabel('主题区分度', color='r') plt.title('LDA 主题数选择曲线') plt.show()这段代码跑完,你会看到困惑度随 K 增大先降后升,存在一个拐点。主题区分度则随 K 增大而下降。综合来看,电商评论数据一般 K 取 5 到 8 比较合适。我做过的一个家电评论项目,K=6 时主题分别是:产品质量、物流配送、安装服务、性价比、外观设计、售后体验,业务方一看就懂。
注意max_iter=20是迭代次数,太小模型没收敛,太大会过拟合。learning_method='batch'适合数据量不大的场景,如果评论超过 10 万条,改成'online'做增量学习。
3.3 训练最终模型并输出每个主题的高频词
确定 K 值后,用完整数据训练最终模型,输出每个主题的 top 词和对应权重。
best_k = 6 lda_final = LatentDirichletAllocation( n_components=best_k, max_iter=50, learning_method='batch', random_state=42 ) lda_final.fit(dtm) feature_names = vectorizer.get_feature_names_out() for topic_idx, topic in enumerate(lda_final.components_): top_indices = topic.argsort()[:-16:-1] top_words = [(feature_names[i], round(topic[i], 2)) for i in top_indices] print(f'主题 {topic_idx + 1}:') print(' ' + ' | '.join([f'{w}({s})' for w, s in top_words])) print()输出结果里每个主题会带 15 个词和对应权重。权重越高说明该词对这个主题的代表性越强。拿到结果后,人工给每个主题起个业务名字,比如“主题 3”的高频词是“安装、师傅、上门、收费、预约”,那就命名为“安装服务”。
这里有个血泪经验:LDA 每次跑的结果会略有不同,因为初始化是随机的。random_state=42固定随机种子保证可复现。但如果你换了数据或者改了 K 值,主题编号会变,不要硬编码“主题 1 就是物流”,每次都要重新看词。
3.4 文档-主题分布:给每条评论打上主题标签
训练完模型后,用transform方法可以得到每条评论在各个主题上的概率分布。取概率最大的主题作为该评论的主主题。
doc_topic = lda_final.transform(dtm) df['topic'] = doc_topic.argmax(axis=1) df['topic_prob'] = doc_topic.max(axis=1) # 统计每个主题下的评论数量和平均情感倾向 topic_stats = df.groupby('topic').agg( 评论数=('content', 'count'), 正面占比=('sentiment', lambda x: (x == '正面').mean()), 负面占比=('sentiment', lambda x: (x == '负面').mean()) ).round(3) print(topic_stats)这张表是给业务方看的最核心产出。比如“物流配送”主题下负面占比 0.45,那就说明物流是重灾区,需要优先改进。topic_prob字段还能用来过滤低置信度评论,概率低于 0.5 的说明这条评论主题不明确,分析时可以剔除。
4. 情感分析与主题交叉:SnowNLP 打分与负面主题定位
4.1 SnowNLP 在电商评论上的表现与局限
SnowNLP 是一个基于朴素贝叶斯的中文情感分析库,snownlp.SnowNLP(text).sentiments返回 0 到 1 之间的情感分数,越接近 1 越正面。它的训练语料是电商评论,所以在这个场景下开箱即用效果还行。
但它有三个明显局限。第一,对反讽和双重否定识别差,“质量好得不得了”和“质量好什么好”可能打出相近分数。第二,对长文本效果下降,超过 100 字的评论打分不稳定。第三,领域迁移差,数码产品评论里的“运行流畅”和服装评论里的“面料流畅”它分不清。
所以我的做法是:SnowNLP 打分作为辅助信号,主情感标签还是用评分映射。两者交叉验证,评分和 SnowNLP 分数矛盾时,人工抽查。
from snownlp import SnowNLP def get_sentiment_score(text): try: return SnowNLP(text).sentiments except: return 0.5 # 异常文本返回中性 # 只对负面评论做 SnowNLP 打分,减少计算量 df['snownlp_score'] = df['content'].apply(get_sentiment_score) # 交叉验证:评分映射 vs SnowNLP df['snownlp_label'] = df['snownlp_score'].apply( lambda s: '正面' if s > 0.6 else ('负面' if s < 0.4 else '中性') ) # 看两者一致率 agreement = (df['sentiment'] == df['snownlp_label']).mean() print(f'评分映射与 SnowNLP 一致率: {agreement:.2%}')一致率一般在 70% 到 85% 之间。低于 70% 说明你的评分映射阈值有问题,或者数据里刷单评论太多。高于 85% 说明数据质量很好。
4.2 主题-情感交叉矩阵:定位负面评论集中在哪个主题
把主题标签和情感标签交叉,得到一张透视表。这张表能直接回答“用户最不满意的到底是哪个环节”。
cross = pd.crosstab(df['topic'], df['sentiment'], margins=True) cross_pct = pd.crosstab(df['topic'], df['sentiment'], normalize='index').round(3) # 合并主题名称 topic_names = {0: '产品质量', 1: '物流配送', 2: '客服态度', 3: '性价比', 4: '外观包装', 5: '售后体验'} cross_pct.index = cross_pct.index.map(topic_names) print(cross_pct)输出结果里,每一行是一个主题,列是正面/中性/负面占比。负面占比最高的主题就是优先改进项。我一般还会按负面评论数排序,因为占比高但总量少的主题优先级可以往后放。
4.3 用 pyLDAvis 做主题可视化与业务汇报
pyLDAvis 是 LDA 可视化的标准工具,生成一个交互式 HTML 页面,左边是主题气泡图,右边是词条分布。气泡大小代表主题在语料中的占比,气泡之间的距离代表主题相似度。
import pyLDAvis import pyLDAvis.sklearn pyLDAvis.enable_notebook() panel = pyLDAvis.sklearn.prepare(lda_final, dtm, vectorizer, mds='tsne') pyLDAvis.save_html(panel, 'lda_visualization.html')mds='tsne'用 t-SNE 做降维,比默认的 PCA 更能拉开主题间距。生成的 HTML 文件可以直接发给业务方,他们鼠标悬停就能看到每个主题的高频词和占比。
注意 pyLDAvis 对中文支持需要额外配置字体,否则词条显示乱码。在生成的 HTML 里找到<style>标签,加上font-family: "Microsoft YaHei", sans-serif;即可。
5. 避坑与排查:LDA 情感分析项目中最容易翻车的五个地方
5.1 主题词全是“的了吗呢”——停用词没清干净
现象:跑完 LDA,每个主题的 top 词里都有“的”“了”“是”“就”,业务方看了直摇头。
原因:停用词表不完整,或者分词后没有过滤单字。jieba 默认会把“的”切出来,CountVectorizer 的max_df设得太高没把它过滤掉。
解决:停用词表至少覆盖 200 个通用停用词,再加领域噪声词。分词函数里强制过滤len(w) <= 1的词。max_df设到 0.7 到 0.8 之间,让过于普遍的词自动被过滤。
5.2 主题之间高度重叠——K 值选大了或者语料太单一
现象:pyLDAvis 图上气泡挤在一起,两个主题的 top 词有七八个重复。
原因:K 值选大了,把本属于一个主题的内容强行拆成两个。或者语料本身主题就少,比如全是差评,那翻来覆去就是质量、物流、客服三个主题。
解决:先跑 K=3 到 K=10 的曲线,看主题区分度拐点。如果语料确实单一,不要硬凑 K 值,K=3 也能出好结果。另外可以尝试按评分分层建模,正面评论和负面评论分别跑 LDA,主题会更清晰。
5.3 短文本评论主题概率全都很低——LDA 在短文本上的天然缺陷
现象:doc_topic.max(axis=1)的值普遍在 0.3 以下,说明模型对每条评论的主题判断都不自信。
原因:LDA 依赖词共现,短文本词太少,共现信息不足。一条“物流太慢”只有四个词,LDA 没法从中推断出稳定的主题分布。
解决:三个方案。一是把同一用户的多次评论合并成一篇文档。二是用 BTM(Biterm Topic Model)替代 LDA,BTM 专门为短文本设计,基于词对共现建模。三是接受低置信度,只分析topic_prob > 0.5的评论,牺牲覆盖率换准确率。
5.4 SnowNLP 打分与评分严重矛盾——刷单评论在捣乱
现象:5 星好评的评论,SnowNLP 打分只有 0.2,内容写的是“质量差得离谱,懒得退了”。
原因:刷单评论,评分和内容不一致。这类数据不清理,会严重污染情感分析结果。
解决:构建矛盾检测规则。评分 >= 4 且 SnowNLP < 0.3,或者评分 <= 2 且 SnowNLP > 0.7 的评论,标记为可疑,单独输出人工复核。如果可疑评论占比超过 10%,说明数据源本身有问题,需要考虑换数据或者做降权处理。
5.5 每次跑出来的主题编号不一样——随机种子没固定
现象:今天跑出来主题 1 是物流,明天跑出来主题 1 变成了质量,报告没法写。
原因:LDA 的初始化是随机的,random_state没设或者设了但数据顺序变了。
解决:random_state固定为 42 或其他常数。同时固定数据输入顺序,df = df.sort_values('comment_id').reset_index(drop=True)。如果换了数据,主题编号必然变,不要硬编码主题名称,每次从 top 词重新映射。
6. 让 LDA 结果真正可用的三个进阶技巧
6.1 用主题词做业务规则引擎,而不是只做分析报告
LDA 跑出来的主题词表可以直接变成规则引擎的词典。比如“物流配送”主题的 top 词是“快递、发货、速度、包装、破损”,那就在客服工单系统里建一条规则:评论命中这三个以上词,自动打上“物流问题”标签,路由到物流部门处理。这比人工分类快得多,而且规则可解释。
logistics_words = {'快递', '发货', '速度', '包装', '破损', '物流', '配送', '到货'} quality_words = {'质量', '做工', '材质', '手感', '正品', '瑕疵', '色差'} def auto_tag(text): words = set(jieba.lcut(text)) tags = [] if len(words & logistics_words) >= 2: tags.append('物流问题') if len(words & quality_words) >= 2: tags.append('质量问题') return tags if tags else ['其他'] df['auto_tags'] = df['content'].apply(auto_tag) print(df['auto_tags'].value_counts().head(10))阈值设 2 而不是 1,是为了降低误报。命中一个词可能是偶然,命中两个以上才说明这条评论确实在讲这个主题。
6.2 主题数 K 的动态调整:按品类分别建模
不同品类的评论主题结构不一样。服装评论的主题可能是“尺码、面料、色差、版型”,数码评论的主题是“性能、续航、发热、屏幕”。用同一个 K 值跑所有品类,效果会打折扣。
我的做法是按品类分组,每组单独跑 K 值选择曲线,取各自最优 K。虽然计算量翻倍,但主题可解释性提升明显。一个家电客户的项目里,空调用 K=5,冰箱用 K=7,洗衣机用 K=6,业务方看报告时一眼就能对应上自己的品类。
6.3 用一致性指标做模型选择的自动化脚本
每次手动跑 K 值曲线太累,写个自动化脚本,输入文档-词矩阵,输出最优 K 值和对应主题词。
def auto_select_k(dtm, vectorizer, k_min=3, k_max=10): best_k, best_score = k_min, -1 results = {} for k in range(k_min, k_max + 1): lda = LatentDirichletAllocation( n_components=k, max_iter=30, learning_method='batch', random_state=42 ) lda.fit(dtm) # 计算主题间平均 Jaccard 距离作为区分度 topic_words = [] for t in range(k): top_idx = lda.components_[t].argsort()[:-11:-1] topic_words.append(set(vectorizer.get_feature_names_out()[top_idx])) jaccard_sum, pairs = 0, 0 for i in range(k): for j in range(i + 1, k): inter = len(topic_words[i] & topic_words[j]) union = len(topic_words[i] | topic_words[j]) jaccard_sum += 1 - inter / union pairs += 1 score = jaccard_sum / pairs results[k] = score if score > best_score: best_score, best_k = score, k return best_k, results best_k, scores = auto_select_k(dtm, vectorizer) print(f'推荐 K 值: {best_k}') for k, s in scores.items(): print(f'K={k}, 主题区分度={s:.3f}')这个脚本用 Jaccard 距离衡量主题间词重叠度,距离越大说明主题越独立。跑一遍几分钟,比手动试快得多。注意这个指标只是参考,最终 K 值还要结合业务可解释性来定。我一般会把这个脚本的输出和 pyLDAvis 图结合起来看,机器推荐 K=7,但图上 K=6 的主题更干净,那就选 6。
做这类项目最大的教训是:不要追求模型指标上的最优,要追求业务方能看懂、能行动。一个 K=5 但主题清晰可命名的模型,比 K=12 但主题模糊的模型有价值得多。我现在的习惯是,跑完 LDA 先不急着看困惑度,先把每个主题的 top 词打印出来,如果我自己都没法用一句话概括这个主题在讲什么,那就说明 K 值或者预处理有问题,回去调,别硬着头皮往下做。希望帮到你。
本文还有配套的精品资源,点击获取