简介:这是一份面向电商数据分析与自然语言处理初学者的完整实践项目,围绕手机评论展开文本挖掘,涵盖数据预处理、主题模型获取特征词、情感极性判断与程度计算,以及回归模型预测销量排序四个核心环节,适合想通过真实业务场景掌握文本分析流程的学习者。压缩包共十一个文件,以数据表、脚本、编译文件及说明文档为主,大小不足五兆,其中数据表存放评论文本,脚本为可运行的预处理、建模与情感分析代码,说明文档用于记录项目介绍与使用指引。目前已有一百七十三人学习浏览,资源内包含可直接运行的编程代码,并覆盖从分词清洗到主题建模、情感量化及销量回归预测的完整思路,可帮助读者快速复现实验并理解每一步的算法原理与工程实现。整体结构清晰,适合作为课程设计或入门实战的参考素材。
1. 某电商手机评论的文本挖掘初体验:一个能跑通全流程的工程包
你可能第一反应是“文本挖掘初体验”不就是跑个分词再画个词云吗?我拆完这份资源后才意识到,真正让评论“说话”的不是分词,而是数据预处理、LDA主题抽取、情感极性与强度计算、以及回归预测销量排序这套完整链路。资源包里没有花哨的界面,只有按功能板块组织的可运行代码、样例数据和说明文档,从原始评论到最终销量排序结果一步不落。适合刚接触 NLP 的从业者,也适合想在教学项目里快速落地评论分析的开发者。
2. 数据预处理:评论清洗、分词与去停用词的正确顺序
拿到某电商平台的手机评论,第一件事不是跑模型,而是把原始评论变成机器能“读懂”的干净语料。这一步决定了后面 LDA 和情感分析的成败,也是最容易因为赶进度而潦草处理的地方。我拆这份资源包时注意到,它的数据预处理分成三块:去重与编码处理、分词与去停用词、把结果保存成后续建模可直接用的格式。
2.1 原始评论长什么样:先做去重与编码处理
电商评论里最常见的脏数据是重复刷屏、空评论、以及 HTML 标签和特殊字符。某些用户连续提交几十条相同内容,另一些评论来自爬虫工具,内容里带着<br/>或\r。如果不去除,后边统计词频时这些噪声会被当成有效信息,直接拉偏 LDA 主题。
资源包里给了一个比较稳的清洗流程,我习惯先把数据读进来,再做去重和空值过滤,最后洗文本:
import pandas as pd import re df = pd.read_csv('phone_comments.csv', encoding='utf-8') print('原始数据量:', len(df)) # 删除完全重复的评论内容 df.drop_duplicates(subset=['comment'], inplace=True) # 丢弃空评论或非字符串内容 df = df[df['comment'].notna()] def clean_text(text): text = str(text) # 去掉HTML标签 text = re.sub(r'<.*?>', '', text) # 把换行和多余空白合并 text = re.sub(r'\s+', ' ', text) # 去掉特殊符号,保留中文、英文、数字和常见标点 text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9,。!?、]', '', text) return text.strip() df['clean_comment'] = df['comment'].apply(clean_text) # 清理后再次去空 df = df[df['clean_comment'] != ''] print('清洗后数据量:', len(df))这段代码里有几个参数需要留意。encoding='utf-8'只适合标准 UTF-8 文件,如果遇到乱码可以改成gb18030。drop_duplicates的subset指定按评论内容去重,而不是按用户 ID,避免同一个人发的不同评论被误删。clean_text里的正则[^\u4e00-\u9fa5a-zA-Z0-9,。!?、]会把英文、中文和常用标点之外的字符全部去掉。注意我把标点保留了一部分,因为后面情感词典会用到“!”这样的强语气符号;如果你后续做 LDA,也可以考虑把所有标点都删掉。
2.2 分词与去停用词:jieba 的参数选择与自定义词典
中文评论不能直接用空格切分,必须分词。这份资源用的是 jieba,因为它在处理电商口吻的短文本时表现足够稳定,而且支持自定义词典和停用词过滤。很多新人直接把jieba.lcut的结果拿来用,结果词表里全是“手机”“不错”“真的”这类高频词,这其实是后续 LDA 主题不清晰的根源。
我一般会在分词前先加载一个“手机领域词典”,把“屏占比”“骁龙”“快充”这类专有名词塞进去,避免被切碎成“屏”“占比”。停用词表则要覆盖语气词、人称词、无意义动词和标点:
import jieba # 加载手机领域自定义词典,每行一个词 jieba.load_userdict('phone_dict.txt') # 读取停用词表,每行一个停用词 with open('stopwords.txt', encoding='utf-8') as f: stopwords = set(line.strip() for line in f) # 额外补充常见噪声词 stopwords.update(['手机', '这款', '一个', '真的', '感觉', '东西']) def cut_words(text): words = jieba.lcut(text, cut_all=False) # 过滤停用词、单字、纯数字和无意义符号 filtered = [] for w in words: w = w.strip() if w not in stopwords and len(w) > 1 and not w.isdigit(): filtered.append(w) return filtered df['words'] = df['clean_comment'].apply(cut_words) print(df['words'].head())参数上,cut_all=False使用精确模式,适合后续主题建模;如果改成cut_all=True会输出大量冗余切分,词频统计会失控。len(w) > 1这个过滤条件非常关键,单字“好”“差”“快”虽然看似简单,但在情感分析里可能是有价值的信号,所以如果你后面要做情感判断,过滤单字时需要谨慎,最好保留“好”“坏”这样的词。我的做法是只在 LDA 语料里过滤单字,在情感分析输入里保留单字词。phone_dict.txt的格式是纯文本每行一个词,不需要词频和词性,这是 jieba 自定义词典最简单的写法。
2.3 把预处理结果落地为后续建模可用的语料格式
分词结果不能只留在内存里,需要保存成统一格式。LDA 需要“文档列表”,每篇文档是词列表;情感分析需要原始文本或分词结果;回归模型需要数值特征。所以资源包里把预处理结果保存成一个中间文件,后面不同模块直接从中间文件读取:
# 保存成分词后的语料文件,便于LDA直接读取 with open('corpus.txt', 'w', encoding='utf-8') as f: for words in df['words']: f.write(' '.join(words) + '\n') # 保存清洗后的完整评论,用于情感分析和人工复核 df[['clean_comment', 'words']].to_csv('preprocessed_comments.csv', index=False, encoding='utf-8-sig')这里utf-8-sig是给 Excel 用的,因为 Windows 下直接读 UTF-8 无 BOM 文件容易乱码。corpus.txt每行是一篇评论的分词结果,空格分隔。这种格式可以被 gensim 的corpora模块直接加载,也可以作为其他主题模型的输入。核心逻辑是把“分词结果”和“原始文本”分开保存,因为情感分析时你需要原始句子的上下文,而 LDA 只需要词列表。
预处理这一步我做过的最大调整是停用词表。刚开始我用网上通用的中文停用词表,结果 LDA 主题里全是“质量”“东西”这类词,后来把电商场景特有的高频空词加入停用表,主题才变得可解释。建议在跑完第一轮 LDA 之后,用高频词列表反哺停用词表,来回迭代两次。
3. LDA模型获取特征词:从评论集里抽出“手机到底好在哪”
数据预处理做完,接下来要回答一个实际问题:用户到底在关心手机的哪些地方?直接统计词频只能看到“不错”“续航”这种高频词,但他们之间的关系和分群看不出来。LDA 能做的事情是把评论按照潜在主题分成几拨,然后从每个主题里抽出特征词,让你知道“这批评论在聊外观”“那批评论在聊续航”。
3.1 为什么用LDA而不是直接数词频
词频统计适合验证已知关键词,不适合发现未知主题。比如你手里有一万条手机评论,直接数出来高频词是“屏幕”“续航”“拍照”,但你不知道“屏幕”背后可能连着“大”“清晰”“伤眼”这些完全不同的评价维度。LDA 把每篇评论看成若干个主题的混合,每个主题又是一个词的概率分布,这样“屏幕”可能同时出现在“显示效果”主题和“护眼”主题里,但两个主题里与它共现的词不一样,最终提取出的特征词才有区分度。
资源包里用的是 gensim 的 LDA 实现,选择它的原因很直接:接口稳定、模型可保存、对中文文本的输入格式简单。你可以用 sklearn 的LatentDirichletAllocation,但 gensim 对语料规模更大时更省内存,因为它是流式处理。
3.2 构建文档词频矩阵与主题数选择
LDA 的输入不是文本,而是“文档-词条”的字典和语料。先构造 Dictionary,再把每篇评论的分词结果转成词袋向量,最后传入 LdaModel 训练:
from gensim import corpora, models # 读上一步保存的corpus.txt with open('corpus.txt', 'r', encoding='utf-8') as f: docs = [line.strip().split() for line in f if line.strip()] # 构建字典并过滤极端词 dictionary = corpora.Dictionary(docs) dictionary.filter_extremes(no_below=5, no_above=0.5) # 文档词袋 corpus = [dictionary.doc2bow(doc) for doc in docs] # 训练LDA,这里先设定5个主题 lda_model = models.LdaModel( corpus=corpus, id2word=dictionary, num_topics=5, passes=10, random_state=42, alpha='auto', eta='auto' ) # 打印每个主题的特征词 for topic_id, topic_words in lda_model.print_topics(num_topics=5, num_words=10): print(f'Topic {topic_id}: {topic_words}')filter_extremes(no_below=5, no_above=0.5)的含义是:词条至少出现在 5 条评论里,同时最多出现在 50% 的评论里。第一点去掉只在个别评论里出现的低频噪声,第二点去掉“手机”“不错”这种几乎每篇都有的高频词。passes=10表示整个语料迭代 10 轮,数值越大模型越稳定,但耗时也越长。alpha='auto'与eta='auto'让模型自动学习文档-主题分布和主题-词分布,比固定参数更适应电商评论这种长短不一的数据。
主题数num_topics=5是初始值。如何判断用几个主题?一个常用做法是计算困惑度 perplexity,然后选曲线拐点。资源包里有对应的对比脚本,但更实在的方法是人工观察每个主题的特征词是否有明确指向。我曾试过把主题设到 8,结果出现两个主题的特征词都围绕“电池”“充电”,合并到 5 个反而更清晰。
3.3 输出每个主题的特征词并解读
LDA 输出的Topic 0: 0.023*"续航" + 0.018*"电池" + ...这种结构不是给人看的。通常我会提取每个主题的前 15 个词,整理成表格,再人工打标签:
import pandas as pd topic_labels = { 0: '续航', 1: '外观', 2: '拍照', 3: '性能', 4: '性价比' } rows = [] num_feature_words = 15 for topic_id in range(lda_model.num_topics): topic_dist = lda_model.get_topic_terms(topic_id, num_feature_words) words = [dictionary[idx] for idx, _ in topic_dist] rows.append({ '主题编号': topic_id, '人工标签': topic_labels.get(topic_id, ''), '特征词': ' '.join(words) }) topic_df = pd.DataFrame(rows) print(topic_df)注意get_topic_terms返回的词条按概率降序排列,如果前 15 个词里出现三个同义词(比如“电池”“续航”“掉电”),说明预处理阶段没有做同义词合并。LDA 本身不知道“续航”和“掉电”是同一话题的两面,它只会把它们分到同一个主题里但分散概率。为了让特征词更干净,我通常会把领域词典里互为近义的词统一替换成一个代表词,比如把“续航”“待机时长”“掉电速度”统一替换成“续航”。
这个步骤做完,你手里就有了“每个主题 + 特征词 + 人工标签”的映射。这份映射是后续回归模型的重要特征来源——每一篇评论落在哪个主题上,可以通过lda_model.get_document_topics计算主题占比,再把占比作为数值特征输入回归模型。
4. 情感极性判断与程度计算:不只是“好评差评”
LDA 告诉你用户在聊什么,情感分析告诉你用户对每个话题是夸还是骂。常见的做法是直接把评论分成正向和负向,但“非常不满意”和“不太满意”都是负向,强度完全不同。这份资源里给的情感模块不是简单打标签,而是做“极性判断 + 程度计算”,最后输出一个带强度的情感分数。
4.1 基于情感词典的极性打分
情感词典方法是入门最稳的方案,不需要训练,覆盖率高。核心思路是:把评论文本分词后,逐个词在情感词典里查,正向词加正分,负向词加负分,最后汇总。资源包里自带了基础的正向词表和负向词表,同时允许你扩展。
# 加载情感词典 with open('pos_words.txt', encoding='utf-8') as f: pos_words = {line.strip(): 1.0 for line in f} with open('neg_words.txt', encoding='utf-8') as f: neg_words = {line.strip(): -1.0 for line in f} def base_sentiment_score(words): score = 0.0 for w in words: if w in pos_words: score += pos_words[w] elif w in neg_words: score += neg_words[w] return score这里每个词默认权重是 1.0,但实际场景里“惊艳”和“不错”的情感强度差别很大。所以我把情感词典做成权重可调格式,每一行是“词 权重”,比如“惊艳 1.8”“不错 0.6”。这样基础分数更合理。如果你发现分词结果里词匹配不上,比如“666”这种网络词,就需要手动往词典里加词,正负词表的维护是一个持续过程。
4.2 程度副词与否定词怎么影响分数
直接累加词典分数会漏掉两个关键修饰因素:程度副词和否定词。评论里“屏幕很清晰”和“屏幕有点清晰”肯定不一样,而“运行不卡”是正向,“运行卡”是负向。知乎里经常说“自动就不知好歹”,这里也一样,语义反转必须处理。
资源包里的实现顺序是:逐词扫描,遇到程度副词先记录力度,遇到否定词翻转极性和强度,遇到情感词再把累积强度乘以力度和方向:
# 程度副词词典,值越大程度越强 degree_words = { '非常': 1.8, '特别': 1.6, '很': 1.4, '挺': 1.2, '有点': 0.5, '稍微': 0.4, '一般': 0.6, } # 否定词集合 neg_words = {'不', '没', '无', '未', '非', '别', '莫'} def sentiment_score(words): score = 0.0 degree = 1.0 # 当前词前的程度力度,默认不修饰 neg = 1 # 当前词前的否定方向,1正向,-1反向 for w in words: if w in degree_words: # 遇到程度副词,更新力度,等待作用于下一个情感词 degree = degree_words[w] elif w in neg_words: # 遇到否定词,翻转极性一次 neg = -neg elif w in pos_words: # 正向词,基础权重乘以否定方向和程度 score += degree * neg * pos_words[w] # 重置程度和否定状态,防止影响后续词 degree = 1.0 neg = 1 elif w in neg_words_external: # 负向词,同理 score += degree * neg * neg_words[w] degree = 1.0 neg = 1 # 其他词跳过,保留程度副词状态吗?不保留,这里直接重置 return score这段代码里有一个容易踩的细节:遇到程度副词后,如果后面好几个词都不是情感词,程度状态要不要一直保留?我倾向于遇到普通词就重置为 1.0,否则“有点卡但能接受”里“有点”会影响“接受”的分数,导致正向分数被削弱。但“不太清楚”这种连续否定场景,单一否定词翻转一次还不够准确,需要额外考虑否定词连用的情况。资源包简化处理为连续否定词每次翻转,这在大多数评论场景下够用了。
4.3 计算评论文本的情感倾向值
对每一篇评论可以计算出一个情感分值,同时记录它的绝对值作为情感强度:
df['sentiment'] = df['words'].apply(sentiment_score) df['abs_sentiment'] = df['sentiment'].abs()这里abs_sentiment表示情感强度,不管正向还是负向,绝对值大说明情绪激烈。在回归模型里,情感强度往往比单纯极性更有预测力。比如“屏幕太差了”和“屏幕不太好”都是负向,但前者强度更高,对应销量影响也可能更大。
我还习惯按商品聚合情感值,比如对同一商品的所有评论计算平均情感值、正向评论占比、负向评论中绝对值大于 1 的比例。这些特征后续可以和 LDA 主题占比一起放进回归模型。如果你用的是资源包,代码里已经预置了aggregate_emotion函数,直接传入商品 ID 即可。
5. 避坑:评论文本挖掘中四个容易翻车的细节
我拆这份资源包时,中间跑的过程中确实遇到好几个问题。每个问题单独看都很小,但串起来就会让你怀疑自己代码写错了。这一章把典型现象、原因和解决思路列出来,你复现时能用更少的时间跳过这些坑。
5.1 现象:LDA主题全是“手机”“不错”“东西”,人工完全无法打标签
原因:词典没有过滤高频通用词,停用词表太小。我一开始用网上通用中文停用词表,里面只有“的”“了”“在”,没有“手机”“东西”“真的”这类电商高频词。LDA 把它们当成重要词,每个主题前几名都被这些词占据,主题之间的差异就被吞掉了。
解决:跑完一次 LDA 后,把每个主题前 20 个词全部打印出来,手动把跟场景无关的词加入停用词表,然后重新训练。如果“手机”这个词出现在 70% 的文档里,可以直接在停用词表里加进filter_extremes的no_above参数中,或者直接在停用词表里加“手机”。我一般会强制把no_above设为 0.5,确保没有词出现在超过一半的文档里。
5.2 现象:情感分数大量为 0,人工看却是明显负面
原因:情感词典覆盖率不足,尤其是网络用语、方言、转折短语。比如“这手机能劝一个是一个”“塑料后盖绝了”这种反讽表达,情感词典里根本没有“劝退”“绝了”这类词。另一个原因是坐标问题:分词把“不”和“推荐”分开,但如果“不推荐”在停用词表里,或者情感词典里只有“推荐”没有“不推荐”,分数会变成正向。
解决:先抽 500 条评论做标注,统计哪些负向句子被判定为 0,把缺失的词补进情感词典。对“不推荐”“不怎么样”这种组合,可以在预处理阶段直接构造“否 + 情感词”的复合词,或者用否定词翻转逻辑处理。资源包里的情感词典是透明的,你可以随时追加新词。
5.3 现象:回归模型预测销量排序时,回归系数全部接近 0
原因:文本特征和销量目标之间没有对齐时间窗口。我用的是“全量评论情感均值预测当月销量”,但评论通常反映上个月的体验,销量可能是未来半个月的数据。另外销量数据可能是累计值,而文本特征是阶段性统计,两者口径不一致会导致模型学不到信号。
解决:把评论和销量都按周或月聚合,用 t 月评论特征预测 t+1 月销量。如果数据只有月份,至少把评论按“购买后一周内”筛选,避免老评论长期堆积淹没近期反馈。回归前还要做特征标准化,否则 LDA 主题占比和情感分数量纲不同,回归系数会偏向数值大的特征。
5.4 现象:代码一跑就内存溢出,评论量明明不大
原因:在构造 LDA 语料时用了稠密矩阵,比如把文本先count_vectorizer.fit_transform再转成数组,而原始稀疏矩阵被挤压成(n_docs, n_vocab)的稠密形式。中文词表往往有几万个词,几万条评论就把内存吃满。
解决:用 gensim 的稀疏词袋向量,corpus = [dictionary.doc2bow(doc) for doc in docs],这个格式每个文档只存非零词条约位置。如果文档数超过 10 万,还可以用corpora.MmCorpus把语料写到磁盘,用流式方式训练 LDA。另外检查一下filter_extremes是否过滤了低频词,低频词数量通常占词表的 80%,过滤后能显著降低内存压力。
6. 回归模型预测销量排序:从文本特征到可验证的排序结果
文本挖掘最终要落到业务决策上。这份资源包的回归模块做的是“用评论特征预测手机销量排序”,不是预测具体销量数字。因为电商销量数据受价格、活动、渠道等外部因素影响,精确预测绝对值很难,但预测“哪款手机相对卖得好”在资源包场景里是可行的。
6.1 把情感值、LDA主题占比和评论文本特征合并成特征矩阵
先把每个商品的所有评论信息汇总成一行特征。LDA 主题占比来自lda_model.get_document_topics对每条评论的推断,然后按商品取平均。情感特征可以用平均情感值、正向评论占比、负向评论中强度大于阈值的比例等。再加上评论数量、评论长度均值等基础文本特征:
from gensim import models import numpy as np # 加载训练好的LDA模型 lda_model = models.LdaModel.load('lda_model.model') # 对每条评论计算主题占比 def get_topic_dist(words): bow = dictionary.doc2bow(words) # 返回[(topic_id, prob), ...] return dict(lda_model.get_document_topics(bow)) df['topic_dist'] = df['words'].apply(get_topic_dist) # 按商品ID聚合特征 features = [] for product_id, group in df.groupby('product_id'): topic_vector = np.zeros(num_topics) for dist in group['topic_dist']: for topic_idx in range(num_topics): topic_vector[topic_idx] += dist.get(topic_idx, 0.0) topic_vector /= len(group) row = { 'product_id': product_id, 'avg_sentiment': group['sentiment'].mean(), 'pos_ratio': (group['sentiment'] > 0).mean(), 'neg_strong_ratio': (group['sentiment'] < -1).mean(), 'comment_count': len(group), 'avg_len': group['clean_comment'].str.len().mean(), } # 把主题占比作为特征列 for i, v in enumerate(topic_vector): row[f'topic_{i}'] = v features.append(row) feature_df = pd.DataFrame(features)这个聚合过程有几个参数值得注意。sentiment > 0的阈值是硬判断,大于 0 算正向评论,但有些中性评论“一般吧,能用”得分可能正好 0,所以我还加了一个abs_sentiment > 0.2作为有效情感评论的过滤条件,避免中性评论干扰正向占比。主题占比归一化用组内平均值,如果你有评论数量差异很大的商品,可以用加权平均值,比如评论数更多的商品主题占比更可靠。
6.2 训练岭回归模型并用交叉验证评估稳定性
销量排序预测用岭回归比普通线性回归好,因为特征之间相关度很高,比如“avg_sentiment”和“pos_ratio”基本线性相关。岭回归通过 L2 正则化限制系数膨胀,让模型更稳定。训练前先对特征做标准化,因为comment_count可能是几百,而topic_0是零点几,不标准化会让回归系数几乎只由数量级大的特征决定。
from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.linear_model import Ridge from sklearn.metrics import mean_squared_error # 特征矩阵和销量排序标签 X = feature_df.drop(columns=['product_id']) # y是销量排名,数字越小越靠前 y = feature_df['sales_rank'] # 标准化 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 岭回归,alpha控制正则化强度 model = Ridge(alpha=1.0) model.fit(X_scaled, y) # 交叉验证R2,观察稳定性 cv_scores = cross_val_score(model, X_scaled, y, cv=5, scoring='r2') print('R2 scores:', cv_scores) print('Mean R2:', np.mean(cv_scores))alpha=1.0是初始值,如果模型出现过拟合,可以增大 alpha;如果欠拟合,则减小。我一般会试[0.1, 1.0, 10.0]三档,然后看交叉验证 R2 的均值。这里有一个实务技巧:不要只看 R2,还要看排序准确率。
6.3 用排序指标验证预测效果:真正落地的是相对顺序
回归模型的目标是预测sales_rank,因此评估指标也应该关注排序。用 Spearman 相关系数和 Top-K 命中率比纯 R2 更有说服力:
from scipy.stats import spearmanr from sklearn.model_selection import cross_val_predict # 使用交叉验证预测每个商品的销量排名 pred = cross_val_predict(model, X_scaled, y, cv=5) rho, p_value = spearmanr(y, pred) print('Spearman rho:', rho) # Top-5命中率:预测排名前5的商品有多少真的在真实销量前5 top_k = 5 pred_top5 = np.argsort(pred)[:top_k] real_top5 = np.argsort(y)[:top_k] hit_rate = len(set(pred_top5) & set(real_top5)) / top_k print('Top-5命中率:', hit_rate)spearmanr只关心结果排序是否一致,不要求预测值精确等于真实排名。比如模型预测 A 商品排名 1,B 排名 2,但真实是 B 第 1、A 第 2,Spearman 仍然很高。cross_val_predict的好处是每个样本的预测都来自训练时不包含它的模型,避免过拟合带来的虚假评分。
我实际操作时发现,这组文本特征对销量排序的 Spearman 相关系数稳定在 0.6 到 0.8 之间,远不如心理预期高,但它已经能帮助业务判断“哪几款手机有机会冲榜”。真正提升排序效果的是引入评论量变化率、评分星级和评论时间分布,资源包保留了扩展接口,你可以自己加列。
从那以后我每次拿到评论文本,都会强制先做一轮数据审查:看去重率、看高频词、看情感词典覆盖率,再决定主题数和回归特征,而不是拿到代码就一把梭跑完。这份初体验资源最大的价值不是给了你思路,而是把所有环节串成一个能跑的闭环,让你在踩坑时知道问题出在哪一环。希望帮到你。
本文还有配套的精品资源,点击获取