外卖评价情感倾向性分析:词典打分与Python实现
2026/9/16 3:19:17 网站建设 项目流程

简介:针对外卖平台用户评价的情感倾向性分析需求,这份打包资料提供了一套基于Python的完整实践方案,适合自然语言处理初学者或数据分析入门者参考学习。资源共12个文件,压缩包仅3.23MB,包含可运行的源码、评论文本数据集、正负向候选词字典、处理结果可视化图片,以及详细的设计思路报告和简要说明文档,便于从数据到结果逐步复盘。目前已有852人进入学习,热度适中。读者可从中掌握将原始评论按情感倾向拆分为正向与负向文件的具体做法,例如读取前4000条归入正向、后8000条归入负向,并结合词频统计与可视化观察两类评分的语言特征。设计思路报告对需求背景、数据划分和实现流程做了完整梳理,有助于快速迁移到其他情感分析场景,进一步提升文本处理与结果呈现能力。

1. 外卖评价情感倾向性分析的词典打分与文件落盘

“骑手在楼下等了十分钟,态度很好,包装也严实”和“超时四十分钟,汤全洒了,打电话还不接”,这两条评论给到同一个商家,后者几乎必然被平台标记为差评。外卖评价情感倾向性分析要做的,就是从这类短文本里判定评论人的情绪极性,并把它转成可落盘、可统计分析的数据。这个资源不依赖BERT微调,也不训练贝叶斯分类器,而是走基于情感词典的极性匹配路线:把评论切词后与正负向候选词表比对,按命中情况计算得分,再按阈值把每条评论写入正向或负向文件。配套文件覆盖了完整闭环:review.csv评论数据、code.py主程序、pos_candi与neg_candi候选情感词、设计思路报告,以及正负向高频词可视化图。适合文本挖掘课程设计、想快速验证情感分类效果的Python开发者,以及需要向业务方解释“这条评论为什么是差评”的可解释性分析场景。

2. 评论语料读取与候选情感词加载策略

情感词典方案的第一道工序不是打分,而是把原始语料和词表都变成程序可直接操作的数据结构。外卖评论和新闻语料不一样,句式短、口语词多、还混着“叮咚”“骑手”这类场景词,任何一步清洗不到位,都会在后面的匹配阶段放大误差。

2.1 数据形态与CSV读取细节

review.csv是典型的平台导出数据,字段里至少包含评论正文和评分。直接用Python内置的csv模块也能读,但在列名不稳定、有空值时处理麻烦,我一般用pandas先把全表拉进来,做一次形态确认:

import pandas as pd # 平台导出的csv常见编码是utf-8或gbk,utf-8-sig能兼容带BOM的文件 df = pd.read_csv("review.csv", encoding="utf-8-sig") print(df.shape) print(df.columns.tolist()) print(df.head(3).to_string())

这段代码先把csv读成DataFrame,随后打印行列数、字段名和前3条记录。encoding是这里最容易踩坑的参数:Windows下导出的文件常是gbk编码,直接读会抛UnicodeDecodeError,改成encoding="gbk"即可;utf-8-sig则能自动剥离Excel生成的BOM头,避免第一列字段名出现“\ufeff”前缀。对外卖评论这种高噪声文本,列名确认比数据条数更重要,comment_text字段名在不同版本脚本里可能叫commentcontent,程序里要统一引用实际列名。

字段形态大致如下表,label这一列在原始导出里不一定存在,需要在预处理阶段根据业务口径生成。

字段类型说明
order_idstr订单号,可能大量重复
ratingint1~5星评分
comment_textstr评论文本,可能有空值
labelint1为正向,-1为负向,预处理阶段生成

2.2 中文分词与停用词过滤

词典匹配的前提是分词。英文按空格切就可以,中文必须用分词工具把句子切成“骑手 / 态度 / 很好 / 包装 / 严实”这样的词序列。这个资源里选择jieba做精确模式分词,是中文情感分析场景里最常见也最不折腾的配置:

import jieba def cut_words(text: str) -> list[str]: """把单条评论文本切成词列表,去掉纯空白后返回""" if not isinstance(text, str): return [] words = jieba.lcut(text.strip()) # 这里不按长度过滤,单字情感词如“好”“慢”要保留给词典匹配 return [w for w in words if w.strip()]

jieba.lcut返回的是精确模式分词结果,适合外卖评论这种几十字的短文本;全模式会切出大量冗余词组合,搜索引擎场景才用得上。函数里做了两件小事:非字符串输入直接返回空列表,避免空值评论让下游报错;只过滤纯空白,不在这里砍单字词,原因是“好”“慢”“差”本身就是强情感信号,砍掉它们会让召回率明显下降。停用词过滤在这个方案里可以推迟到高频词统计阶段做,因为词典匹配本身只会命中候选词表里的词,无关词对得分没有贡献。

2.3 候选情感词文件的标准化

pos_candi.txt和neg_candi.txt是从语料里统计出来的候选情感词,格式是每行一个词。直接读文件时最容易遇到三类问题:行尾换行符残留、空行混入、同一个词大小写或空格不一致。加载逻辑里统一做一次标准化:

def load_lexicon(path: str) -> set[str]: with open(path, "r", encoding="utf-8") as f: words = {line.strip() for line in f if line.strip()} # 过滤长度大于4的词,保留单字情感词和常见双字词 return {w for w in words if 1 <= len(w) <= 4} pos_words = load_lexicon("pos_candi.txt") neg_words = load_lexicon("neg_candi.txt")

这里用集合推导式一次性完成去重、去空行和strip,长度控制在1到4个字符是为了过滤掉“味道真的非常好吃”这类包含多个词的污染行。候选词表是外部生成的结果,导入后可以先打印词表大小:如果pos文件只有几十个词,说明统计阶段的频次阈值设高了,匹配阶段会出现大量评论被判为中性。

提示:候选词文件必须和code.py放在同一级目录,运行前用os.path.exists检查一下,避免路径硬编码导致在IDE里能跑、命令行跑就报FileNotFoundError

3. 词典打分模型与正负样本分流写入

词表和分词器就绪后,进入核心环节:对每条评论计算情感得分,再按标签写入不同文件。这一步的工程要点是打分规则要可解释、参数要能调,而不是追求一个黑盒准确率。

3.1 情感得分算法:极性累计与否定词取反

最朴素的情感词典打分是统计命中正负词的数量做差。外卖评论里的转折和否定表达很多,单纯做差会把“味道不错但送得太慢”打成正分。因此代码里处理了否定词取反,否定词命中后,相邻的极性词翻转符号:

negation_words = {"不", "没", "没有", "不太", "不怎么"} def feel_score(words: list[str]) -> float: score = 0 negation = False for w in words: if w in negation_words: negation = True continue if w in pos_words: if negation: score -= 1 # “不好”视为负向 else: score += 1 negation = False elif w in neg_words: if negation: score += 1 # “不慢”视为正向 else: score -= 1 negation = False # 归一化,避免长文本天然占优 return score / max(1, len(words))

逻辑说明:遍历分词结果,遇到否定词只改状态不计分;命中正向词时,若前面是否定态则记为负向分,否则正向分;命中负向词相反。每处理完一个极性词就把否定态复位,避免“不太满意”这种连续否定被二次翻转。分母用max(1, len(words))做归一化,把分数压到[-1, 1],长评论不会因为词多而得分更高。下面是参数的含义:

参数含义建议值
negation_words否定词表,命中后翻转下一个极性词符号覆盖“不”“没”“不太”“不怎么”即可
score > 0判为正向正负样本不平衡时可上调到0.05
score < 0判为负向同上
score == 0判为中性需单独落盘观察

3.2 分类写入正向与负向文件

得分算出来后,把每条评论原样写入对应文件。写入时采用utf-8-sig编码,这是决定Excel能否直接打开不乱码的关键参数:

pos_f = open("reviews_pos.txt", "w", encoding="utf-8-sig") neg_f = open("reviews_neg.txt", "w", encoding="utf-8-sig") neutral_f = open("reviews_neutral.txt", "w", encoding="utf-8-sig") for comment in df["comment_text"]: words = cut_words(comment) s = feel_score(words) if s > 0: pos_f.write(comment + "\n") elif s < 0: neg_f.write(comment + "\n") else: neutral_f.write(comment + "\n") pos_f.close() neg_f.close() neutral_f.close()

这段代码的意图是把模型判断结果固化到磁盘,后续高频词统计、可视化全部基于这三个文件而不是在内存里重算。中性评论单独落盘是个好习惯,外卖场景里“s>0才正向”的阈值会把纯事实描述(“送达时间19:30”)推到中性文件,这些样本是后续调阈值的重要依据。注意写入时逐条加\n,防止最后一条评论没有换行符。

3.3 前4000与后8000:种子样本切分与评测隔离

摘要里提到的“读取前4000条写入正向文件,后8000条写入负向文件”,本质是种子样本切分,不是模型预测结果。review.csv的实际排列往往按业务规则排过序,前4000条对应评分4星以上的好评,后8000条对应2星以下的差评。这个切分动作有两个用途:一是从这两批种子样本里统计高频候选词,生成pos_candi与neg_candi;二是作为后续词典匹配结果的对照集,用来算召回率。代码里的实现通常是这样:

# 种子样本切分:前4000条作为正向语料,后8000条作为负向语料 positive_seed = df.iloc[:4000]["comment_text"] negative_seed = df.iloc[4000:12000]["comment_text"] positive_seed.to_csv("positive_seed.txt", index=False, header=False, encoding="utf-8-sig") negative_seed.to_csv("negative_seed.txt", index=False, header=False, encoding="utf-8-sig")

这里的关键是评测隔离:种子语料只用于生成候选词表和观察词频分布,模型打分的对象是切分后的全量数据。如果拿种子样本同时去统计词表和评估准确率,属于数据泄漏,得到的指标会虚高。原代码把这两个阶段分开,先切分种子样本,再基于词典对全量评论做推断并写入文件,流程上是干净的。下表总结了两个阶段的边界:

阶段输入输出用途
样本切分review.csv前12000条positive_seed.txt / negative_seed.txt生成候选情感词、验证词典覆盖率
词典打分全量评论切词结果reviews_pos.txt / reviews_neg.txt / reviews_neutral.txt业务侧的情感分类落盘

4. 高频词统计与matplotlib可视化图像

打分落盘之后,评价分析还不能算结束。外卖评论的情感分析最终要给业务方一个直觉结论:正向评论里出现最多的词是什么?负向评论里又在抱怨什么?这一步用词频统计加可视化完成,输出就是资源里的正向高频.png与负向高频.png。

4.1 用Counter统计正负向评论的高频词

Python标准库collections.Counter做词频统计足够高效,12000条评论处理时间在秒级。统计时要把候选词典本身剔除,否则“好吃”“差评”这些词必然霸榜,看不到业务侧的真实表达:

from collections import Counter def top_words(file_path: str, top_n: int = 15) -> list[tuple[str, int]]: counter = Counter() with open(file_path, "r", encoding="utf-8") as f: for line in f: counter.update(cut_words(line)) # 去掉情感词典命中的词,剩余词反映业务侧关注点 stop_words = pos_words | neg_words return [item for item in counter.items() if item[0] not in stop_words][:top_n] pos_common = top_words("reviews_pos.txt") neg_common = top_words("reviews_neg.txt")

counter.update(cut_words(line))是对每条评论先切词,再把词列表合并进Counter实例,等价于遍历所有词逐个加1。返回结果用[:top_n]截断前15项,注意要先把词典词过滤掉再截断,否则高频位置被候选词占满。这一步的细节是:停用词过滤放在这里比放在分词阶段更合理,因为“这”“了”“的”等高频虚词在这个环节被剔除不会影响词典匹配。

4.2 中文字体配置与水平条形图绘制

matplotlib画图有两个经典坑:中文显示成方块、负号显示成乱码。根源是默认字体不支持中文,需要在画图前全局指定字体:

import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "Noto Sans CJK SC"] plt.rcParams["axes.unicode_minus"] = False def draw_bar(data: list[tuple[str, int]], title: str, save_path: str) -> None: words, counts = zip(*data) plt.figure(figsize=(8, 6)) plt.barh(range(len(words)), counts, color="#4C72B0") plt.yticks(range(len(words)), words) plt.title(title) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(save_path, dpi=150) plt.close() draw_bar(pos_common, "正向高频词 Top15", "正向高频.png") draw_bar(neg_common, "负向高频词 Top15", "负向高频.png")

font.sans-serif列表里按OS顺序填了Windows的SimHei、微软雅黑和Linux的Noto Sans CJK,程序换机器不会立刻崩。axes.unicode_minus=False用来解决坐标轴负号渲染问题。条形图用barh横向画,词在y轴,频次在x轴,比纵向柱状图更易读,词长和频次关系一目了然。invert_yaxis让频次最高的词在顶部,更符合阅读习惯。

4.3 从高频图里读业务结论

负向高频.png里通常会出现“慢”“漏”“凉”“差”“等”这类词,正向高频.png则集中在“快”“好吃”“干净”“态度”上。这不是巧合,而是外卖场景的真实痛点分布:时效、配送完整性、餐品温度构成差评主因,口味、卫生、服务构成好评主因。观察两张图的差异还能反过来检查词典质量——如果负向高频图里出现“不错”“划算”,说明有大量正极性评论被误判进了负向文件,这时应回头查否定词表和阈值,而不是先调分词器。

5. 调优词典与验证分类质量的三个动作

5.1 用中性样本率校准阈值

reviews_neutral.txt的大小是调整阈值的第一信号。全量评论里中性占比超过30%,说明阈值0的判定太保守,很多带弱情感倾向的评论没被捕获。常见做法是把阈值从s > 0改到s >= 0.05,同时负向阈值改到s <= -0.05,中间的样本全部归入中性。调整后在控制台打印三个文件的行数,对比偏移量,一般中性占比降到20%以内是健康状态。

5.2 转折词切分消除双极性抵消

“味道不错但送得慢”这种评论在3.1节的朴素模型里正负互相抵消,得分接近0,被丢进中性文件。这类评论恰好是差评的高潜样本——用户对核心体验不满,只是先肯定了次要维度。处理方式是遇到“但”“不过”“就是”时把句子切成两段,只对后段打分:

def split_by_turn(words: list[str]) -> list[str]: for i, w in enumerate(words): if w in {"但", "但是", "不过", "就是"}: return words[i + 1:] return words

feel_score开头调用split_by_turn,后段保留转折之后的真实评价意图。注意“就是”在口语里也用于强调正面,如“就是快”,所以“就是”放到转折词表后要注意看高频图中的负面词变化。

5.3 把误判样本的实义词回流到词典

从正向高频.png里挑选与业务相关但没被pos_candi覆盖的词,比如“新鲜”“实惠”“准时”,手动追加到pos_candi.txt;负向文件里的“漏洒”“串味”“不接”等追加到neg_candi.txt。每次追加后重跑全量流程,对比两轮高频图的词频变化区间,就能判断词典迭代是提升召回还是引入噪声。

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

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

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

立即咨询