☰
手机评论挖掘:LDA主题建模与情感分析预测销量排序
2026/10/10 3:52:14 网站建设 项目流程

简介:针对电商手机评论的文本挖掘入门项目,面向希望掌握中文文本分析流程的数据学习者,可完整覆盖数据预处理(清洗、分词、去停用词)、LDA主题模型提取特征词、情感极性判断与程度计算、回归模型预测销量排序四个环节。资源包共11个文件,含3个Python脚本、4个CSV评论数据集(原始评论文本)、2个说明文档和2个编译缓存文件,压缩包约4.1MB,结构轻量便于快速上手。已有173人学习下载,适合作为自然语言处理与机器学习综合应用的练习素材。通过运行代码可完整体验从原始评论到主题挖掘、情感量化再到销量预测的链路,便于对照理解每一步的原理与实现细节,并迁移到其他商品评论分析场景。代码注释完整,目录结构清晰,学习者可基于自带数据集直接运行调试。

1. 手机评论的文本挖掘到底能挖出什么:从「好评差评」到「销量排名」

电商运营盯评论,最常看的就是好评率。但好评率 98% 的机型,销量常常打不过好评率 92% 的机型,因为消费者真正传播的不是那个百分比,而是评论里反复出现的主题——屏幕、续航、手感、物流——以及这些主题上情绪的强弱。标题里这个压缩包,就是把「手机评论的文本挖掘」串成一条能跑的初体验链路:先用数据预处理把评论洗干净,用 LDA 模型从语料里提取特征词,再做情感极性判断和程度计算,最后把文本特征喂给回归模型,预测销量排序。适合电商或消费电子领域的数据分析师、想入门文本挖掘但不想只跑 demo 的工程师,以及需要给运营一个可解释销量归因的产品同学。不用一开始就吃透全部原理,照着这条链路跑一遍,等特征和销量排名对得上,再回头调参。

2. 数据预处理:把原始评论变成能喂给主题模型的干净语料

解压 zip 之后先别急着装库,先用一句话说清楚目标:LDA 吃进去的是词袋,情感计算吃进去的是词序列,回归吃进去的是特征矩阵。三者都从同一份原始评论来,所以预处理做得好不好,直接决定后面 LDA 主题是否漂移、情感分数是否可信、回归特征是否有区分度。这一段就是整个链路的地基。

2.1 解压后第一件事:先看数据形态,再定清洗规则

拿到压缩包后,我一般先把目录结构列出来看一遍。这类包通常会有三块东西:评论明细(评论内容、评分、评论时间、手机型号)、销量或榜单数据、若干词典文件。字段名各家不一样,假设你手头的评论表字段是 comment、score、model,先读进来看看长什么样。

import pandas as pd # 电商导出的评论数据,表头常见为:评论内容、评分、手机型号 df = pd.read_csv('phone_comments.csv', encoding='utf-8-sig') print(df.shape) print(df[['comment', 'score', 'model']].head(3).to_string())

read_csv的encoding参数值得多说一句:国内电商导出的 CSV 很多是 GBK 编码,如果读出来乱码或直接报 UnicodeDecodeError,就把编码换成 'gbk'。df.shape的用途是看样本量,评论少于两三千条时,后面 LDA 的主题数 K 要往小调,否则每个主题里都是噪声;head打印前几行是为了确认 comment 字段里有没有夹 HTML 标签、占位符、追评拼接这些脏数据。

此时不要急着写清洗正则,先看垃圾大多长什么样。常见情况是「此用户未填写评价内容」这类占位符、促销链接、以及「快递给力」这种跟手机本体无关的句子。占位符整行过滤,链接用正则删掉,物流这类词是否保留,取决于你后面想不想要「物流」这个主题——想保留就把「快递」「物流」加进领域词典,不想保留就丢进停用词表。

2.2 清洗、分词与去停用词:跑通最小预处理流水线

我给的最小流水线是四步:去 HTML 与链接、压缩重复字符、分词、去停用词。压缩重复字符这步很多人会漏,但电商评论里「好好好好好」「卡卡卡」特别多,不压缩的话 jieba 会把它们切成一堆无意义单字,直接污染词袋。

import re import jieba def clean_comment(text): text = re.sub(r'<[^>]+>', '', text) # 去掉 HTML 标签 text = re.sub(r'https?://\S+', '', text) # 去掉链接 text = re.sub(r'(\D)\1{2,}', r'\1', text) # “好好好好”压缩成“好” return text.strip() df['clean'] = df['comment'].astype(str).apply(clean_comment) stopwords = set() with open('cn_stopwords.txt', encoding='utf-8') as f: # 通用中文停用词表 stopwords = set(line.strip() for line in f) def tokenize(text): words = jieba.lcut(text) # 精确模式分词 keep = [] for w in words: if w in stopwords or w in (' ', '\n'): continue keep.append(w) return keep df['words'] = df['clean'].apply(tokenize) df['words_join'] = df['words'].apply(' '.join)

这段代码里我刻意没有加len(w) > 1这种过滤,因为「不」「卡」「烫」「沉」都是单字,却是后面情感判断和主题里的关键信号。去停用词时也注意不要把「不」「没」「很」「太」收进停用词表,否则情感计算的原料就没了。jieba.lcut默认精确模式,对「骁龙8Gen3」这类词条原始词典经常切错,所以下一步要把领域词塞进用户词典。

停用词表的来源不用纠结,顺手找一份通用中文停用词表就行,但一定要人工过一遍:把「手机」「评论」「购买」这类电商高频泛词留下还是删掉,取决于你想让 LDA 主题更聚焦于产品属性。泛词太多,主题会变成「手机、一个、就是」这种废话集合。

2.3 用户词典与否定词表:两个小文件解决分词与情感基线

手机评论里大量出现骁龙、天玑、曲面屏、快充、光学防抖,默认词典会切成「骁龙/8」「曲面/屏」,导致后面 LDA 里「骁龙」和「8」被当成两个词,主题直接散掉。常见做法是准备一个领域词典文件,逐行add_word。

import jieba domain_words = [ '骁龙8Gen3', '天玑9300', '曲面屏', '光学防抖', '屏下指纹', '无线充电', '灵动岛', '散热背夹' ] for w in domain_words: jieba.add_word(w, freq=100000) # 给大频率,强制优先切分 neg_words = {'不', '没', '无', '别', '莫', '未必', '不太', '不怎么'} degree_words = { '极其': 2.0, '非常': 1.8, '很': 1.5, '比较': 1.2, '稍微': 0.7, '有点': 0.6, '一般般': 0.5 } with open('domain_words.txt', 'w', encoding='utf-8') as f: f.write('\n'.join(domain_words)) with open('neg_words.txt', 'w', encoding='utf-8') as f: f.write('\n'.join(neg_words))

freq参数是个容易翻车的地方:给太小(比如默认一万)不一定能压过 jieba 的既有切分,给到十万以上基本稳定。否定词表和程度词表单独存成文件,是为了第 4 章情感模块直接复用,不用回头改代码。注意这里把「不太」「不怎么」直接收进否定词表,是因为它们的语义和单一「不」不一样,后面情感计算里要单独处理,不能简单乘负一就完事。

分词这事多少有点玄学,同样的「手感不错」,不同型号评论里可能被切成「手感/不错」或「手/感/不错」。所以跑完分词后,我习惯随机抽 20 条评论打印words_join出来肉眼检查一遍。分词结果稳定,LDA 和情感计算才稳定,这一步省不了。

3. LDA 模型获取特征词:从评论里自动捞出「大家在聊什么」

数据预处理做完,手头是干净的评论词序列。这个压缩包的第二个板块是用 LDA 模型获取特征词。这里要明确:LDA 给的不是「哪些词单独重要」,而是「哪些词经常一起出现」,每个主题就是一坨特征词,比如屏幕主题可能是「分辨率、曲面屏、亮度、绿边」,续航主题可能是「续航、充电、掉电、发热」。这比简单数词频高出整整一个层级。

3.1 为什么选 LDA:主题模型和 TF-IDF 的边界

TF-IDF 能告诉你「独特」的词,但词之间是什么关系它说不出来。「曲面屏」和「绿边」各自 TF-IDF 都很高,可你无法从 TF-IDF 知道它们其实在聊同一个问题——屏幕。LDA 把词聚成主题,输出两个分布:主题-词分布和文档-主题分布。前者就是我们说的特征词,后者可以直接当作回归模型的一列特征:这条评论有多少比例在聊屏幕、多少比例在聊续航。

LDA 在短文本上有个著名问题:单条手机评论往往只有十几个字,主题信号非常稀疏,直接训练会出现主题漂移,同一个词在不同 run 里被分到完全不同的主题。行业里常见做法是拼伪文档:把同一个手机型号、同一个时间窗口内的所有评论拼接成一篇长文档再训练。型号级别的 LDA 主题分布,正好对得上后面回归模型的粒度。这个取舍很重要,因为单条评论的文本太短,靠 LDA 硬挖特征词,基本是撞大运。

3.2 词袋矩阵与 LDA 训练:困惑度和一致性怎么选 K

训练 LDA 我用 gensim,流程是先把词序列转成词袋,再建词典、过滤极端词、训练模型。过滤这一步别跳过,低频词和高频泛词都会把主题搅浑。

from gensim.corpora import Dictionary from gensim.models import LdaModel, CoherenceModel # 按型号拼伪文档,降低短文本主题漂移 pseudo_docs = df.groupby('model')['words'].apply( lambda x: [w for doc in x for w in doc] ).tolist() dictionary = Dictionary(pseudo_docs) # 低频词至少出现 5 次,高频词出现超过 50% 语料就丢弃 dictionary.filter_extremes(no_below=5, no_above=0.5) corpus = [dictionary.doc2bow(doc) for doc in pseudo_docs] def train_lda(k): model = LdaModel(corpus=corpus, id2word=dictionary, num_topics=k, passes=10, random_state=42) cm = CoherenceModel(model=model, texts=pseudo_docs, dictionary=dictionary, coherence='c_v') return model, cm.get_coherence() for k in range(4, 9): model, coh = train_lda(k) print(f'K={k}, coherence={coh:.3f}')

no_below=5的意思是词至少在 5 篇伪文档里出现,否则视为低频噪声直接滤掉;no_above=0.5表示词不能在超过一半的文档里出现,否则像「手机」这种词没有区分度。passes=10是 LDA 迭代遍历整个语料的次数,太小模型不收敛,太大训练时间成倍涨。random_state=42这行千万别删,否则你每次跑出来的主题词都不一样,后面对结果没法交代。

选 K 只看困惑度会翻车,困惑度在短文本上经常忽悠人。我一般以主题一致性(coherence c_v)为主,再人工看主题词。K 从 4 到 8 跑一遍,选 coherence 最高且主题词能看出业务含义的那个数。注意 LDA 有随机性,同 K 跑三次取稳定结果,别拿一次好运的 coherence 当真。

3.3 特征词落业务:主题命名与词分布检查

模型训练完,用show_topic把每个主题的特征词打出来,人工命名。这一步是黑匣子开盖,也是整个文本挖掘里最需要判断力的地方。

for topic_id in range(model.num_topics): words = model.show_topic(topic_id, topn=10) print(f'Topic {topic_id}:', ' '.join(w for w, p in words))

主题命名我一般遵循三个原则:看前十个词里有没有核心名词;看词之间的共同诉求;主题词里出现「手机」「快递」「这家」这类泛词,说明 K 太小或 filter 太松。给个常见对照:主题词的「曲面屏、分辨率、亮度、绿边、发白」可以命名「屏幕」;「续航、掉电、发热、充电快」命名「续航与发热」;「物流、包装、发货、态度」命名「服务体验」。命名结果可以直接作为特征词的业务口径,后面给运营解释时不用讲 LDA,直接讲「屏幕、续航、服务」三个主题的占比变化就好。

主题-词分布表里,每个词后面跟着权重,一般只取前 10 个词看。权重最大的词就是主题标签,但真正说明问题的往往是第 4 到第 8 个词——比如「屏幕」主题里出现「绿边」,这就是负面信号,比「分辨率」更能预测销量下滑。

4. 情感极性判断与程度计算:把「不太满意」从「不满意」里分离

LDA 告诉我们大家在哪,情感计算告诉我们态度如何。这个压缩包的第三个板块是情感极性判断与程度计算。极性的意思是这段评论是正向还是负向,程度是这个情感有多强。「不满意」和「不太满意」都是负向,但程度差很多,如果只分正负,等于把「续航一般」和「续航尿崩」混在一起。这一步要输出的是一组连续的情感分数,而不是简单的 0/1 标签。

4.1 情感词典基准:极性分从哪里来

情感计算最稳的基线是词典法:准备一份正向情感词表和一份负向情感词表,把评论分词后逐个查表,命中正向加一分,命中负向减一分。不用上模型的原因很简单:标注样本不够时,词典法可解释、可干预,运营问起来你能说清楚为什么这条评论得负分。

# pos_words / neg_words 从公开中文情感词典整理而来 def sentiment_score(words): score = 0.0 for w in words: if w in pos_words: score += 1.0 elif w in neg_words: score -= 1.0 return score df['sentiment'] = df['words'].apply(sentiment_score)

这段是最朴素的基线,正负各一分。实际跑出来会有两个问题:一是「不太满意」会被算成「满意」减一分,得到 -1,和「非常不满意」一样;二是「续航还可以」里「可以」命中正向,但「还」这个程度词把语气削弱了,词典法却给了满分。所以下一步必须处理否定词和程度副词。

4.2 否定词与程度副词:两条规则把程度算出来

程度计算我用两条规则:情感词前一个位置是程度副词,按权重放大缩小;情感词前一到两个位置是否定词,极性反转或弱化。这里的关键是「不太」「不怎么」这类组合,拆开看是否定加程度,合起来其实是弱化负向。

weak_neg = {'不太': 0.5, '不怎么': 0.4, '没那么': 0.5} def sentiment_score_v2(words): score = 0.0 for i, w in enumerate(words): if w in pos_words: s = 1.0 if i > 0 and words[i-1] in neg_words: s *= -0.8 if i > 0 and words[i-1] in degree_words: s *= degree_words[words[i-1]] if i > 1 and ' '.join(words[i-1:i+1]) in weak_neg: s *= -0.5 score += s elif w in neg_words: score -= 0.2 # 孤立否定词给轻微负向,防止漏判 return score

窗口宽度取 2 是权衡结果:只往前看一个词,识别不了「不太满意」;往前看三个词,容易误伤,比如「没有想象中满意」里的「没有」其实修饰的不是「满意」。-0.8表示否定后极性反转但保留一点余地,因为「不满意」不等于绝对负向的「很差」;weak_neg的 0.5 表示「不太满意」负向程度只有「不满意」的一半左右。这些系数不是玄学,是拿 200 条人工标注试出来的,基本符合直觉。

程度副词表在前面预处理阶段已经存成文件,这里直接读进来用。注意「比较」和「稍微」的权重不同,「比较满意」是正向 1.2 倍,「稍微满意」只有 0.7 倍,虽然都正向,但对销量排序的贡献完全不同——这个区分度在回归里会体现出来。

4.3 评论级与属性级聚合:从词分数到型号特征

单条评论的情感分是浮动的,直接丢给回归模型太碎。常见做法是把情感分聚合到型号级,同时把 LDA 主题信息揉进来,形成「屏幕主题情感」「续航主题情感」这类属性级特征。属性级特征比评论级好用得多:同样是负向,「屏幕发绿」对销量的杀伤力远大于「物流慢」。

聚合后的特征初步如下,按型号计算,作为后面回归的输入。

特征名计算方式说明
sent_mean该型号所有评论情感分求平均整体情绪强度
neg_ratio情感分为负的评论数 / 总评论数负向评论占比
pos_ratio情感分为正的评论数 / 总评论数正向评论占比
topic_screen_neg屏幕主题文档的负向情感均值属性级负向信号
topic_battery_neg续航主题文档的负向情感均值属性级负向信号
comment_cnt评论总数销量体量的间接信号

这三个字段都是连续值,比「好评率」一个数信息量大得多。售价评论区经常出现「一分钱一分货」,这类词正面负面词典可能都命中,最终分接近零,没问题,因为聚合后它就不是主信号了。

5. 回归模型预测销量排序的避坑与做法:从特征构造到模型对照

前面几章产出的 LDA 主题权重、情感特征词分数,到这一章变成回归模型的输入,预测目标是销量排序。标题里写「回归模型」,实际落地思路是:用型号级别的文本特征回归销量(或排名分数),再按预测值排序,和运营用的销量榜对齐。先讲坑,是因为回归建模的坑大多集中在特征构造和评估方式上,模型本身反而不是重点。

5.1 标签与特征构造:销量排序怎么来,特征用哪些

标签构造是第一关。如果数据里直接有销量,回归目标就用销量,最后按预测销量排序;如果只有榜单排名,就把排名归一化到 0 到 1 之间当标签。我个人偏好用对数销量,因为销量分布通常长尾,头部型号销量是尾部的几十倍,直接回归原始销量会被大数带偏。

特征矩阵用型号粒度聚合:LDA 输出的文档-主题分布按型号求均值,得到每个型号在「屏幕」「续航」「服务」等主题上的权重;情感模块输出型号级情感均值、负向占比、属性级负向分数;再加评论数和平均评分。代码示意如下:

# topic_weights: 每行是一个型号的主题分布,来自 LDA 输出 feat = df.groupby('model').agg( sent_mean=('sentiment', 'mean'), neg_ratio=('sentiment', lambda x: (x < 0).mean()), comment_cnt=('sentiment', 'count'), avg_score=('score', 'mean') ).reset_index() feat = feat.merge(topic_weights, on='model', how='left') feat = feat.merge(sales_df[['model', 'log_sales']], on='model')

这里最容易犯的错误是特征泄漏:评论是消费者购买后写的,而销量是评论产生之后一段时间发生的,两者有时间差。用 3 月的评论预测 3 月的销量没问题,但用 3 月的评论预测 1 月的销量就是拿未来预测过去。实际操作要严格按时间窗口切分:T1 到 T2 的评论特征,对齐 T2 到 T3 的销量。

注意:特征泄漏是文本挖掘预测里最隐蔽的翻车点,模型指标再漂亮,窗口一对不齐就是自欺欺人。

5.2 岭回归与 GBDT 对照:小样本下的稳定选择

型号数量往往只有几十个,特征十几个,这个规模下第一个该试的是岭回归。岭回归的 L2 正则能压住特征之间的共线性,LDA 主题权重之间天然存在此消彼长的关系,用普通最小二乘很容易过拟合。

from sklearn.linear_model import Ridge from sklearn.model_selection import train_test_split from scipy.stats import spearmanr X = feat.drop(columns=['model', 'log_sales']) y = feat['log_sales'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42) model = Ridge(alpha=1.0) model.fit(X_train, y_train) pred = model.predict(X_test) rho, _ = spearmanr(y_test, pred) print(f'Spearman rho={rho:.3f}')

alpha是正则强度,我一般从 0.01、0.1、1、10 四个值里网格搜,选测试集 Spearman 最高的。别一上来就调大 alpha,太大所有预测值都往均值缩,排序反而没区分度。test_size=0.3在样本只有几十条时意味着测试集只有十几个型号,单次划分运气成分很大,建议跑多次取平均值。

GBDT 可以作为对照,样本量小的时候它更容易过拟合,但有时能抓到非线性关系。LightGBM 里num_leaves控制在 8 以下,min_child_samples设 10 以上,learning_rate0.02 左右,先把过拟合压住再谈精度。跑出来的结论经常是:岭回归和 GBDT 的 Spearman 差不多,但岭回归的系数可解释,能直接看出「屏幕负向情感」比「评论数」对销量排序影响更大,这个解释能力对运营汇报很重要。

5.3 避坑记录:回归预测里的 5 个高频问题

以下五条是按次数排序的真实踩坑,每一条都交过学费。

  1. 现象:预测分数全挤在均值附近,排序完全没有区分度。原因:特征方差被 LDA 归一化和情感聚合压得太平,或者岭回归 alpha 过大。解决:先看特征的标准差,如果某一列标准差接近 0,说明这个特征没有区分度,删掉;alpha 用网格搜,别拍脑袋定。

  2. 现象:评论数越多的型号,情感分越接近 0,负向占比反而失真。原因:大数定律,评论多了正负抵消。解决:不要只依赖情感均值,把comment_cnt直接放进特征,让模型自己去权衡;或者对评论数做分箱,在箱内再看情感分布。

  3. 现象:LDA 主题权重在同一批数据上跑两次,结果完全不同,回归特征跟着变。原因:LDA 训练没固定随机种子,或者伪文档构建不完整。解决:random_state固定,伪文档按型号构造后保存成中间文件,一次跑完不再重新训练。

  4. 现象:模型在训练集上指标很好,上线后排序惨不忍睹。原因:评论窗口和销量窗口没对齐,模型学到的是「当期评论预测当期销量」的假规律。解决:严格按时间窗切分,训练用 T1→T2 评论预测 T2→T3 销量,测试用更晚的窗口,模拟真实上线。

  5. 现象:MSE 很低,但排在最前面的型号完全不对,运营一看就说不靠谱。原因:MSE 衡量的是整体偏差,排序测的是相对顺序,两者根本不是一回事。解决:评估指标换成 Spearman 相关系数加 Top-K 命中率,这两个指标跟业务目标直接相关,MSE 只做参考。

6. 销量排序回归的离线验证技巧:用 Top-K 命中率代替 MSE

MSE 在排序任务上会骗人。预测值整体比实际高 0.3,但排序完全正确,MSE 很难看;预测值整体很准,但第一名第二名互换,MSE 反而漂亮。运营真正关心的是「销量榜前几名的型号,你有没有给筛出来」。所以我最后会把 Top-K 命中率当成核心离线指标。

def topk_hit_rate(y_true, y_pred, k=3): # 真实销量和预测销量各自取前 k 名,看重合度 true_topk = set(pd.Series(y_true).sort_values(ascending=False).head(k).index) pred_topk = set(pd.Series(y_pred).sort_values(ascending=False).head(k).index) return len(true_topk & pred_topk) / k

sort_values(ascending=False)表示销量高的排前面,head(k)取前 k 个型号,两个集合求交集再除以 k。K 取 3 还是 5 看业务,头部型号就是 K=3,覆盖主流型号就 K=5。这个指标比 MSE 直观多了,运营问起「准不准」,直接说「前 3 名能命中 2 个」,比「均方误差 0.3」好理解一百倍。

第二个验证技巧是滚动时间窗口。把数据按时间切成三段,T1→T2 做训练集,T2→T3 做验证集,每次只往前挪一个窗口,模拟真实的上线节奏。随机切分在这个场景里太乐观,因为同一个型号的相邻时期评论高度相似,随机切分等于让模型偷看了部分未来。

第三个技巧是检查主题-情感交叉特征是否真的在工作。全局情感均值对销量排序的解释力通常一般,因为评论里有大量跟产品无关的物流好评。把 LDA 的「屏幕」「续航」主题和情感分交叉后,往往能明显看到「屏幕负向得分高的型号,销量排名靠后」这个信号。这个信号一旦稳定,就是文本挖掘真正有价值的地方——不是预测对了排名,而是告诉运营到底是哪个属性在拖后腿。

我最早跑这个包的时候只盯 MSE,模型在测试集上 MSE 漂亮得很,Top-K 命中率却只有两成,差点把假信号当成真理交给运营。后来把 Spearman 和 Top-K 命中率做成固定检查项,才敢说这个初体验流程是真的能落地。回归预测销量排序不是玄学,是把文本信号翻译成排名,前提是验证方式跟着业务走。希望帮到你。

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

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

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

立即咨询