简介:一份面向外卖平台用户评价情感分析的Python实战资源包,适合具备基础Python语法、希望掌握文本分类与情感倾向分析流程的初学者或课程项目使用者。资源包含设计思路报告(word)、可直接运行的源码、评论数据集(csv)以及正负向候选词表,报告完整描述了将读取前4000条评论归为正向、后8000条归为负向并分别写入文件的情感倾向分类思路。压缩包共12个文件,主要涵盖py脚本、csv数据、docx报告、md说明、txt词表及png结果图,其中png展示正向/负向高频词与分类结果,整体大小约3.23MB,结构紧凑便于下载学习。目前已获得852人学习浏览,适合用于外卖评论情感分析实践、毕业设计参考或数据分析入门练习,读者可参照源码快速复现完整流程,并借助设计报告理解特征选择与分类器评价细节。
1. 外卖用户评价情感倾向性分析:拿到这份Python资源后我能做什么
外卖用户评价情感倾向性分析,说白了就是把“味道不错”“送得太慢”这类评论文本自动归类成正向和负向,而不是靠人一条条去看。这份Python资源把完整链路打包成了一个zip,里面有设计思路报告docx、可直接运行的code.py、评论数据review.csv,还有正负向候选词和高频词结果图。你拿到手能做的不只是跑通代码,还能看懂样本怎么切分、情感分类器怎么训练、词云怎么生成。它适合正在做课程设计、毕业设计,或者想用Python完成“数据清洗—文本分类—可视化”全流程的人。下面的拆解按我实际复现的顺序来,文档、代码、数据分别怎么用,以及哪里容易翻车,都会讲到。
2. 拆解zip包:文件结构、设计报告与实际运行逻辑
2.1 压缩包里都有什么:从设计报告到数据文件
解压后先把文件分成四类:文档、代码、数据、中间产物。文档类有“emotionaltendencies 设计思路报告.docx”、“README.md”、“简短思路.md”;代码类是“code.py”;数据是“review.csv”;中间产物是“pos_candi.txt”和“neg_candi.txt”,外加“res”文件夹里的四张图片。对于一个课程设计级别的项目,这个结构非常标准,比很多只丢一个大文件夹的压缩包装得清楚。
我习惯先看“简短思路.md”,因为它最短,能快速告诉我项目主线。看完之后再看“设计思路报告.docx”,里面一般会写需求分析、流程图、算法选型、测试结果,这部分可以直接改成自己的实验报告,省掉大量从零写文档的时间。不过要注意,报告里的代码截图大概率是旧版本,真正运行还是要以自己的环境为准。
“review.csv”是原始评论数据,格式不外乎评论内容、评分、店铺名、用户ID。最让我意外的是“pos_candi.txt”和“neg_candi.txt”这两个文件,它们不是结果输出,而是候选情感词表,作用类似词典:把“绝绝子”“拉胯”这样的外卖圈口语词提前喂给分词器,避免被拆碎。打开看,里面都是一行一个词,编码是UTF-8。如果你的记事本打开是乱码,说明系统默认编码不是UTF-8,后面会讲怎么处理。
“res”目录里的“正向结果.png”“负向结果.png”是分类结果的可视化,“正向高频.png”“负向高频.png”是词云图。整个项目用到的第三方库基本是pandas、jieba、scikit-learn、wordcloud、matplotlib这一套,不需要GPU,笔记本就能跑。对新手来说,最花时间的往往是环境配置,比如在PyCharm里配置Python解释器,或者在VSCode里配置Python环境,都得保证用的是同一个解释器,否则在终端里pip安装的库,到IDE里import会找不到。我一般先把命令行切到项目根目录,再执行下面的命令:
pip install pandas jieba scikit-learn wordcloud matplotlib逻辑说明:这一行把数据读取、分词、建模、画图需要用到的库一次装齐。如果安装时提示“No module named pip”,说明Python没有加入PATH,需要在安装Python时勾选“Add Python to PATH”,或者直接用python -m pip install。
参数说明:pip install后面可以跟多个包名,空格隔开,可以一次装完。如果网络慢,可以换成阿里云镜像源,但不建议用全局代理之类的方式,容易引入奇怪的问题。
下面用一个表格汇总文件位置和用途:
| 文件 | 类型 | 我的理解 |
|---|---|---|
| emotionaltendencies 设计思路报告.docx | Word | 完整实验报告,可直接改写成课程设计提交物 |
| README.md / 简短思路.md | Markdown | 项目说明和主线流程,先读后者 |
| code.py | Python | 主程序,负责读取、切分、分类、可视化输出 |
| review.csv | CSV | 待分析的原始外卖评论 |
| pos_candi.txt / neg_candi.txt | 纯文本 | 正向/负向候选情感词,用于分词与词典打分 |
| res/正向结果.png | 图片 | 正向样本分类结果图 |
| res/负向结果.png | 图片 | 负向样本分类结果图 |
| res/正向高频.png | 图片 | 正向高频词词云 |
| res/负向高频.png | 图片 | 负向高频词词云 |
这里有个细节:pos_candi.txt和neg_candi.txt可能同时服务于两个阶段。第一阶段作为jieba的自定义词典,让口语词不被拆开;第二阶段作为情感词典,给每句评论打正负分。如果你的数据里出现了“包装严实”“骑手态度好”这种明显的外卖评论词,但候选词表里没有,可以用记事本追加,每行一个,保存为UTF-8。
2.2 review.csv怎么变成正负样本:读取前4000条和后8000条的逻辑
根据“简短思路.md”和code.py的运行行为,这个资源的核心切分逻辑非常直接:把review.csv按行号切,前4000条写成正向样本文件,后8000条写成负向样本文件。也就是说,原始数据很可能已经预先分成两个部分,前面是好评,后面是差评,中间没有单独标label列。这样做的好处是代码简单,课程设计演示足够;坏处是一旦数据顺序不是这样,后续训练全崩。
我在复现时会把这一步单独拆出来跑,方便观察中间结果:
import pandas as pd # 读取原始评论数据;如果提示中文乱码,把 encoding 换成 'gbk' df = pd.read_csv('review.csv', encoding='utf-8', header=0) # 前4000条视为正向,后续8000条视为负向(假设数据本身有序) pos_df = df.iloc[:4000].copy() neg_df = df.iloc[4000:4000 + 8000].copy() print(pos_df.shape, neg_df.shape, df.shape)这段代码的逻辑是:iloc[:4000]取的是行索引0到3999,一共4000条;iloc[4000:4000+8000]取的是第4000条到第11999条。如果review.csv总行数等于12000,那么neg_df其实可以用更简单的df.iloc[4000:]写法。我故意写成4000+8000,是因为原描述里明确说了“后8000条”,这样即使前面还有其它数据,也能按这个区间切。
参数说明:header=0表示第一行是列名,如果CSV没有表头,这里会直接把第一条评论当成列名,必须改成header=None。encoding='utf-8'是大多数数据的默认值,但如果文件是Windows下用Excel另存的,通常是GBK,读进来会报UnicodeDecodeError,这时候改成encoding='gbk'即可。
切分完这只是正负样本数组,还需要给它们加上标签才能训练模型。常见做法是直接构造一列:
pos_df['label'] = 1 # 正向 neg_df['label'] = 0 # 负向 data = pd.concat([pos_df, neg_df], axis=0) data = data.sample(frac=1, random_state=42).reset_index(drop=True)这里concat是把两个DataFrame上下合并,sample(frac=1)是把所有行重新打乱,random_state=42让打乱结果可复现。如果不打乱,模型训练时前面全是正向,后面全是负向,交叉验证结果会非常不稳定。reset_index(drop=True)是为了让行索引从0重新连起来,避免后续train_test_split时索引残留。
我一般会顺手把切分结果保存成两个csv,做数据备份:
pos_df.to_csv('positive_comments.csv', index=False, encoding='utf-8-sig') neg_df.to_csv('negative_comments.csv', index=False, encoding='utf-8-sig')注意encoding='utf-8-sig'和utf-8的区别:前者会在文件开头写入BOM,Excel打开不会乱码,而pandas再读的时候也不需要改配置,所以这个写法更适合给不熟悉编码的人看。
到这里,数据集的基本形态就有了:一列是评论文本,一列是0/1标签。如果你拿着这份资源只是为了完成课程设计,跑到这一步其实已经可以交差一半。但这里要踩一下刹车:直接按行号切分对原始数据有很强假设,一旦review.csv被重新排序,或者换了一批数据,这个逻辑立刻失效。我自己的习惯是先跑df['star'].value_counts(),如果有评分列,就按评分阈值切,后面第4章会专门讲这个坑。
另外,运行code.py时,如果直接从终端执行python code.py,程序会先打印正负样本数量,再打印分类报告,最后在res目录生成图片。我第一次跑的时候没有在项目根目录打开终端,导致文件路径找不到,报FileNotFoundError。所以建议所有相对路径都以项目根目录为基准,或在IDE里把Working directory设置为项目根目录。这个习惯能帮你省掉很多莫名其妙的路径报错。
3. 情感分类链路:分词、TF-IDF与朴素贝叶斯的参数调法
3.1 外卖评论的分词:jieba库与自定义候选情感词
中文评论没有空格,直接套英文的split不现实。外卖评论又特别口语化:“绝绝子”“踩雷”“送餐贼慢”这些词,通用模型不一定认识。所以需要jieba分词,并且把pos_candi.txt、neg_candi.txt里的候选词加载进来,让“绝绝子”被当成整体而不是被拆成“绝/绝/子”。如果分词错误,后续不管是TF-IDF还是情感词典打分,都会被带偏。
先看最基础的分词代码:
import jieba # 加载自定义候选词,每行一个词 jieba.load_userdict('pos_candi.txt') jieba.load_userdict('neg_candi.txt') def cut_comment(text: str) -> list: # 精确模式分词,适合短文本 return jieba.lcut(text.strip())逻辑说明:load_userdict会把自定义词加入词典,并保证这些词在分词时优先被识别。比如“真香”不会被拆成“真/香”,“拉胯”不会被拆成“拉/胯”。lcut是jieba的精确模式切分函数,返回一个词列表。这里用strip()去掉首尾空格,避免把换行符带进特征。
参数说明:load_userdict要求文件每行一个词,不能有逗号或分号,文件编码必须是UTF-8,否则会报UnicodeDecodeError。如果你在Windows下用记事本另存,记得编码选UTF-8而不是默认的ANSI。
美团、饿了么评论里常有“骑手态度好”“包装严实”“漏撒”这些场景词。如果这些词不在候选词表里,也建议追加进去。我第一次跑的时候没有加载自定义词,结果“不好吃”被分成“不好/吃”,情感倾向直接判断反了。这是血泪经验:外卖评论的情感词往往藏在连续的名词+形容词里,不提前喂词典,分词质量会非常难看。
顺便说一下停用词问题。分词之后,词表里大量是“的”“了”“就”“都”这类功能词,它们对情感分类没有贡献。我一般会准备一个停用词集合,在特征提取前过滤:
stopwords = set(['的', '了', '是', '我', '你', '它', '很', '非常'])逻辑说明:这个集合可以根据数据自己扩充。注意“很”和“非常”其实是程度词,在某些表情感分析里应该保留,但在二分类“好评/差评”任务里,它们几乎不携带正负倾向,所以我会先过滤,避免它们占据TF-IDF的有效维度。
3.2 特征工程:TF-IDF与朴素贝叶斯的搭配
分词之后得到的是文本,但模型只认数字。常见做法是把词列表转成TF-IDF向量。TF-IDF的核心思想是:一个词如果在少数几个类别里频繁出现,它的权重就高;如果所有评论里都出现,权重就被压低。外卖评论属于短文本,词表不会太大,用TF-IDF加朴素贝叶斯是成熟组合,比直接上深度学习稳定得多。
特征工程代码:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report # 切分训练集和测试集,stratify保证正负比例一致 X_train, X_test, y_train, y_test = train_test_split( data['comment'], data['label'], test_size=0.2, random_state=42, stratify=data['label'] ) # 中文TF-IDF,max_features=5000避免维度爆炸,ngram_range=(1,2)保留“不新鲜” vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2), min_df=2) X_train_vec = vectorizer.fit_transform(X_train) X_test_vec = vectorizer.transform(X_test) # 多项式朴素贝叶斯,适合词频/词袋类特征 model = MultinomialNB(alpha=1.0) model.fit(X_train_vec, y_train) y_pred = model.predict(X_test_vec) print(classification_report(y_test, y_pred, target_names=['负向', '正向']))逻辑说明:先做train_test_split,再把训练集和测试集分别向量化。注意fit_transform和transform的区别:fit_transform会从训练集学习词表,transform只做转换,测试集只能用transform,绝不能再用fit或fit_transform,否则会把测试集里的词混进词典,导致评估结果虚高。
参数说明:max_features=5000是一个折中值,如果评论总量小于10000,设2000也可以;ngram_range=(1,2)能捕捉“不太行”这类否定组合,但维度会变高,内存不够时可以去掉;min_df=2表示只有出现在至少2条评论里的词才会被保留,用来过滤只在一条评论里出现的噪音名词。alpha=1.0是拉普拉斯平滑系数,一般保持默认。
如果数据量特别小,比如只有几百条评论,这时候max_features要降到1000,甚至直接改用情感词典打分。我见过有人拿50条样本硬跑朴素贝叶斯,结果训练集F1是0.9,测试集只有0.5,区别就在于词表太大导致过拟合。建议先用classification_report看宏平均F1,而不是盯准确率。
3.3 评估结果可视化:res目录里的PNG是怎么来的
资源里“正向结果.png”“负向结果.png”不是截图,而是程序跑出来的图表。最常用的是混淆矩阵,它能清楚看出“正向被误判成负向”的数量。对于课程设计,这张图是报告里的核心证据。
画图代码:
import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay cm = confusion_matrix(y_test, y_pred) disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['负向', '正向']) disp.plot(cmap='Blues') plt.savefig('res/confusion_matrix.png', dpi=150) plt.show()逻辑说明:confusion_matrix把真实标签和预测标签交叉统计,四个格子分别代表TN、FP、FN、TP。ConfusionMatrixDisplay负责把矩阵画成带色块的热力图。保存时dpi=150,打印到报告里不会发虚。如果只想看预测概率分布,可以用model.predict_proba画直方图,观察正负向样本的得分重叠程度。
参数说明:cmap可以换成'Blues'、'Greens'等,display_labels顺序要和分类报告里的顺序一致。保存路径最好先判断目录存在,用os.makedirs('res', exist_ok=True),否则首次运行会报找不到res目录。这里提一句,zip包里的“正向结果.png”“负向结果.png”可能来自旧数据,你跑新数据后应该用新图片替换,保留旧图反而容易被导师看出拼凑痕迹。
交叉验证也是验证模型稳定性的常用手段。常见做法是直接用cross_val_score,把5折平均F1作为最终指标:
from sklearn.model_selection import cross_val_score scores = cross_val_score(model, X_train_vec, y_train, cv=5, scoring='f1_macro') print(scores.mean())逻辑说明:这里用的X_train_vec是fit_transform完的训练集,cross_val_score会在内部再划分5份,轮流当验证集。scoring='f1_macro'把正负两个F1做平均,比准确率更能反映分类器在小类上的表现。如果得分低于0.7,就该检查样本切分或特征参数。
4. 避坑与常见问题:五个让评价分析翻车的细节
4.1 数据读取阶段的两个坑
坑一是CSV乱码。现象:用Excel打开review.csv一切正常,用pandas读进来之后所有中文变成“锟斤拷”或“�”。原因:文件实际编码是GBK或GB2312,read_csv却用了默认的utf-8。解决:先确认文件编码再读。我一般这样兜底:
try: df = pd.read_csv('review.csv', encoding='utf-8') except UnicodeDecodeError: df = pd.read_csv('review.csv', encoding='gbk', errors='ignore')逻辑说明:先试UTF-8,失败就换GBK。errors='ignore'会跳过非法字节,但可能丢字,只适合临时排查。更稳妥的是把数据转成标准编码:
df.to_csv('review_utf8.csv', index=False, encoding='utf-8-sig')参数说明:utf-8-sig带BOM,Excel打开不会乱码,pandas读它也不需要额外指定。从那以后,我拿到任何csv第一件事就是查编码,而不是急着打开。
坑二是pandas把第一行评论当成列名。现象:df.head()显示的第一行是一条评论“这家店味道不错”,而不是“user_id,comment,star”这类字段。原因:review.csv本身没有表头,read_csv默认把第一行当列名。解决:手动指定header=None,再按列数给names:
df = pd.read_csv('review.csv', header=None, names=['user_id', 'shop', 'comment', 'star'])逻辑说明:names的列表长度必须和实际列数一致,否则pandas会报错。如果不知道列数,先不写names读一次,用df.shape[1]看有几列,再补齐。
4.2 模型训练与表现阶段的三个坑
坑三是直接按前4000条切正负样本,结果模型非常偏。现象:classification_report里负向F1很高,正向F1不到0.3。原因:review.csv的顺序未必是“好评在前、差评在后”,可能原始数据按时间或用户ID排序,导致前4000条里混着大量差评。解决:先看评分分布,再按评分切:
print(df['star'].value_counts()) pos_df = df[df['star'] >= 4] neg_df = df[df['star'] <= 2]逻辑说明:评分4到5星归正向,1到2星归负向,3星中性弃掉。如果review.csv里确实没有评分列,只能靠关键字规则切,那我不建议直接上分类器,老老实实用词典打分更稳妥。判断标准很简单:凡是让你“赌顺序”的切分方式,都有翻车风险。
坑四是分词后高频词全是停用词。现象:生成的词云里最大的是“的”“了”“很”,完全看不出外卖特征。原因:统计词频前没有过滤停用词,也没有过滤单字。解决:在统计之前加过滤条件:
word_list = [w for w in cut_comment(text) if w not in stopwords and len(w) > 1]逻辑说明:len(w)>1会把“香”“慢”这类单字情感词也过滤掉。如果是要做情感分析,单字词要谨慎,我一般只在词云里过滤,在模型特征里保留,因为“慢”在配送场景里可能是负向信号。如果是要做词云展示,这个过滤会让图更干净。
坑五是词云里的中文全部变成小方块。现象:程序不报错,但生成的PNG里全是方框。原因:WordCloud默认字体不支持中文。解决:指定中文字体路径,Windows下用simhei.ttf,Linux下用文泉驿微米黑:
wc = WordCloud(font_path='C:/Windows/Fonts/simhei.ttf', background_color='white')逻辑说明:font_path不指定时,PIL用默认字体渲染,无法覆盖中文编码。指定黑体后,中文才能正常显示。如果在Linux服务器上跑,先执行fc-list :lang=zh查看有哪些中文字体,再选一个路径填进去。这个问题不算难,但几乎每个人都会遇到一次。
5. 高频词与词云:把分析结果变成好看的图(进阶技巧)
5.1 从分类结果到高频词:统计正向和负向评论的关键词
模型训练完之后,除了分类准确率,我还习惯单独统计正负向的高频词,这是报告里最有说服力的部分。比如正向评论里出现最多的是“好吃”“快”“新鲜”,负向评论里出现最多的是“难吃”“漏”“慢”,一眼就能看出外卖用户最关心什么。
from collections import Counter def get_top_words(comments, stopwords, top_n=30): words = [] for c in comments: words.extend([w for w in jieba.lcut(c) if w not in stopwords and len(w) > 1]) return Counter(words).most_common(top_n) pos_words = get_top_words(pos_df['comment'], stopwords, top_n=30)逻辑说明:get_top_words把每一条评论切词,过滤停用词,再用Counter统计频次,most_common(30)返回频次最高的30个词。注意这里用的是切分后的正向评论集pos_df,不是原始数据。如果你把pos_words直接写成txt,建议用别名pos_top_words.txt,避免覆盖原始候选词表pos_candi.txt。
5.2 用WordCloud生成正向/负向高频词图
生成词云时,我通常直接传入Counter对象,而不是传原文文本,因为这样能严格控制词频来源:
from wordcloud import WordCloud wc = WordCloud( font_path='C:/Windows/Fonts/simhei.ttf', background_color='white', width=800, height=600, max_words=100, colormap='Reds' ) wc.generate_from_frequencies(dict(pos_words)) wc.to_file('res/正向高频.png')逻辑说明:generate_from_frequencies接收一个字典,键是词,值是频次。width和height控制画布尺寸,max_words限制最多显示100个词,避免词云太拥挤。负向图可以把colormap换成'Blues',这样正负向的图在风格上一目了然。
我也试过用matplotlib的barh画横向条形图,把Top 20词列出来,效果比词云更精确:
import matplotlib.pyplot as plt words, counts = zip(*pos_words[:20]) plt.figure(figsize=(8, 6)) plt.barh(list(reversed(words)), list(reversed(counts))) plt.xlabel('频次') plt.tight_layout() plt.savefig('res/正向高频_bar.png', dpi=150)逻辑说明:barh画横向条形,reversed是为了让频次最高的词显示在顶部。这个图在课程设计报告里比词云更好解释,因为数字是精确的。词云适合封面展示,条形图适合正文论证。
从那以后,我每次拿到新的评论数据,都会先跑一次df.head()和value_counts(),确认编码、列名和标签分布,再决定是否按行号切分。词云也先检查字体路径,不然图再好看也是白费。希望这份拆解能帮到你,少走一点弯路。
本文还有配套的精品资源,点击获取