☰
Python外卖评价情感倾向性分析:从词典法到机器学习实战
2026/10/1 19:23:39 网站建设 项目流程

简介:基于Python的外卖用户评价情感倾向性分析资料包,主要面向需要完成文本情感分类课设、毕业设计或入门自然语言处理的Python学习者。内容以外卖评论为数据对象,讲解如何将评论文本按正向与负向分为两类,并分别写入独立文件,其中读取前4000条作为正向样本、后8000条作为负向样本,实现思路与数据划分逻辑清晰。压缩包共12个文件,包含docx设计思路报告、py可执行源码、csv原始评论数据、png正负向结果及高频词可视化图、txt候选词表与md说明文档,整体仅3.23MB,便于快速下载和按需查阅。目前已有852人学习浏览,读者可依据源码和可视化图表复现完整流程,也可替换自有数据做情感分类拓展,是一份兼具课程参考与实战练习价值的数据分析资料。

1. 外卖评价情感倾向性分析:这个Python项目解决了什么实际问题

打包成“基于Python的外卖用户评价情感倾向性分析.zip”这种形式,通常是开发者把自己跑通的一整套代码、情感词典、样例数据和说明文档打包在一起,而不是一个只有几十行脚本的玩具项目。它要解决的核心任务,是把外卖用户写的一段评价文本(“味道不错,就是等了一个小时”)自动换算成情感倾向标签或分数,供运营做差评预警、店铺口碑排名、配送异常分析。这个方向对从业者很有吸引力,因为不需要GPU,不需要一开始就准备大量标注数据,一个Python环境加几个开源库就能落地。它适合刚入门自然语言处理的Python学习者,也适合本地生活行业的业务分析师,以及想给评论区做自动化洞察的产品经理。接下来的章节,我会按“先定路线,再做工程,再调参数”的顺序,把这个标题拆成一套可以照做的落地流程。

2. 情感分析的两种路线:词典打分还是机器学习分类

2.1 基于情感词典的极性打分:先能跑,再谈准

情感词典法的思路很直接:把评论文本拆成词,每命中一个情感词就累加一个情绪分数。正面词加分,负面词减分,否定词反转,程度副词加权。外卖场景里,“好吃”“分量足”“出餐快”“划算”等是高频正面词;“难吃”“慢”“凉了”“油腻”等是高频负面词。这个方法最大的优点是不需要标注数据,只需要准备词典和分词工具,一个小时就能跑通第一版。它适合作为所有更复杂模型的基线,也适合在业务方要求“先说清楚怎么算出来的”时作为解释依据。

下面是一个最简实现,使用jieba分词和微型词典:

import jieba # 微型情感词典:权重越大表示情绪越强烈 pos_dict = { "好吃": 2.0, "满意": 2.0, "推荐": 1.5, "新鲜": 1.5, "快": 1.0, "划算": 1.5, "分量足": 2.0 } neg_dict = { "难吃": -2.5, "慢": -1.5, "差": -2.0, "贵": -1.5, "凉了": -2.0, "油腻": -1.0, "不新鲜": -2.5 } negators = set(["不", "没", "没有", "别"]) def lexical_score(text): words = jieba.lcut(text.lower()) score = 0.0 for i, w in enumerate(words): weight = None if w in pos_dict: weight = pos_dict[w] elif w in neg_dict: weight = neg_dict[w] if weight is None: continue # 检查前一个词是否是否定词,如果是则反转极性 if i > 0 and words[i - 1] in negators: weight = -weight score += weight return score print(lexical_score("好吃,但是出餐太慢"))

这段代码的逻辑是:遍历分词结果,每遇到一个情感词就取出对应权重;如果这个词前一个位置是否定词,就把权重取反。这里的参数有三个地方可以调:词典权重(2.0和-2.5表示情绪强度)、否定词表大小、以及“否定词位置窗口”的长度。上面只检查前一个词,实际评价里会出现“一点都不难吃”这种两词前置的否定结构,更完整的做法是把窗口扩大到前两个位置,并区分“不”和“不怎么样”这类弱化表达。

需要提醒的是,词典法得分是连续值,但它不等同于概率。同一句话“好吃,但是出餐太慢”,词典法会给2.0 + (-1.5) = 0.5,这并不意味着一半正面一半负面,只说明两种情绪同时存在。后续做正负判断时,往往会设置阈值,比如得分大于等于1判断为正面,小于等于-1判断为负面,中间归为中性。这个阈值也需要根据店铺评价分布来调。

2.2 基于机器学习的情感分类:数据够了再上模型

当手上积累了几千条历史评价,并且有条件标注一批“正面/中性/负面”样本时,就可以把情感分析当成一个三分类文本分类任务。常见做法是TF-IDF提取文本特征 + 逻辑回归分类器。选逻辑回归而不用深度网络,原因有两个:一是外卖评论文本短,平均几十个字,深度模型在短文本上的优势有限;二是逻辑回归训练快,几分钟就能出结果,而且模型权重可以直接映射回词语,方便反查“模型到底被哪些词带偏了”。

下面的代码展示了从文本到模型的完整流程:

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report # 假设 df 已经有 cleaned_text 和 label 两列,label 取值为 0/1/2 texts = df["cleaned_text"].tolist() labels = df["label"].tolist() # 文本向量化:限制特征数量,并保留双词组合 vectorizer = TfidfVectorizer(max_features=5000, ngram_range=(1, 2), min_df=2) X = vectorizer.fit_transform(texts) # 划分训练集和测试集,随机种子固定便于复现 X_train, X_test, y_train, y_test = train_test_split( X, labels, test_size=0.2, random_state=42, stratify=labels ) model = LogisticRegression(C=1.0, class_weight="balanced", max_iter=1000) model.fit(X_train, y_train) print(classification_report(y_test, model.predict(X_test)))

这里的核心参数有四个。max_features=5000表示只保留TF-IDF值最高的前5000个词,能有效降噪;ngram_range=(1, 2)同时保留单个词和相邻双词,“不好吃”会被当成一个双词特征,从而抓住否定结构;min_df=2表示出现次数少于2次的词直接丢弃,避免某个评论的个性化表达干扰模型;class_weight="balanced"让模型根据类别数量自动调整权重,用来缓解“差评数量远少于好评”带来的偏向问题。

逻辑回归的C是正则化强度的倒数,C越小正则越强,模型越稳定;外卖评价文本噪声大,C在0.5到2.0之间通常都能工作,具体需要结合交叉验证选择。如果在本地跑模型时报ConvergenceWarning,可以调大max_iter,或者对TF-IDF特征做norm="l2"归一化。

2.3 两条路线怎么选:数据量决定起点,解释性决定终点

词典法和机器学习法并不是替代关系,而是不同阶段的产物。下面的对比表可以帮你快速做判断:

对比项情感词典法机器学习法
准备材料情感词典 + 分词器已标注评论数据
数据量需求不需要标注数据每个类别至少200条
冷启动速度1小时以内半天到一天
可解释性高,能定位到具体词中,可通过特征权重观察
维护成本新词需要持续补充新数据需要重新训练
典型局限覆盖率有限,固定表达识别弱依赖标注质量,领域迁移会掉点

我的经验是,如果只是看店铺口碑的整体涨跌,词典法已经够用;如果要做实时差评拦截,并且对每条评论的判断置信度要求高,就必须上机器学习。真正高效的工程做法是“两条腿走路”:先用词典法跑出伪标签,再基于伪标签训练逻辑回归,最后用人工抽检修正模型输出。这样既能省下从头标注几万条数据的时间,又能让模型比单一词典法覆盖更多领域表达。

3. 用Python搭建外卖评价情感分析流程:从环境到Excel结果

3.1 环境准备与数据读取:版本、编码、字段缺一不可

很多从zip包解压出来的项目,第一道坎不是算法,而是环境。我见过太多次因为Python解释器选错或包版本不对,导致jieba或sklearn一导入就报错的场景。开始之前先建一个干净的虚拟环境,不要直接在系统全局环境里慢慢补依赖。

推荐用conda创建Python 3.9环境,这个版本对旧代码和新库的兼容性都比较均衡。

conda create -n sentiment python=3.9 -y conda activate sentiment pip install pandas jieba scikit-learn openpyxl chardet

这几条命令分别做了三件事:创建名为sentiment的独立环境;激活它;安装处理数据、分词、机器学习、写Excel和识别编码所需的库。如果pip install下载很慢,可以使用国内镜像源,但注意不要把镜像源写死在项目说明里,否则他人复现时可能被误导。

环境激活后,在VSCode里按Ctrl+Shift+P选择 “Python: Select Interpreter”,指定刚才创建的sentiment环境。这一步往往比安装包本身更容易翻车,因为VSCode会默认选中全局解释器,导致命令行里pip install装到了conda环境,编辑器却用另一个Python。确认解释器路径里包含sentiment,再继续下一步。

数据读取是最容易忽略的地方。外卖平台导出的CSV文件经常是GBK或GB18030编码,直接用pd.read_csv("waimai.csv")默认用UTF-8解码,轻则乱码,重则直接抛UnicodeDecodeError。建议先用chardet自动探测编码:

import pandas as pd import chardet with open("waimai.csv", "rb") as f: encoding = chardet.detect(f.read(10000))["encoding"] df = pd.read_csv("waimai.csv", encoding=encoding) print(df.head()) print(df.columns.tolist())

读进来后先看列名。通常一份外卖评价数据会包含user_id、shop_name、rating、comment、create_time等字段。注意rating是星评级,不能直接当作情感标签,因为用户可能打3星但文本写“还可以”,也可能打4星但吐槽“份量少”。真正的情感分析应该基于评论文本,星级只能用来做后续交叉验证。

3.2 文本预处理:清洗、分词、停用词一口气做完

外卖评论文本很乱,常见问题包括emoji、重复标点、中英文混写、错别字。先写一个统一的清洗函数,把所有内容规整成“中文字母数字加空格”的形态。

import re import jieba def clean_text(text): if not isinstance(text, str): return "" # 去掉常见emoji区域 text = re.sub(r"[\U0001F300-\U0001FAFF]", "", text) # 非中文、字母、数字、空白的字符统一替换成空格 text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s]", " ", text) # 多个空格合并成单个空格 return re.sub(r"\s+", " ", text).strip() def tokenize(text): clean = clean_text(text) words = jieba.lcut(clean) stopwords = set(["的", "了", "是", "我", "你", "他", "就", "都", "还", "也", "有"]) # 去掉停用词和单字词 return [w for w in words if w not in stopwords and len(w) > 1] df["clean_text"] = df["comment"].apply(clean_text) df["tokens"] = df["clean_text"].apply(tokenize) df["token_text"] = df["tokens"].apply(lambda x: " ".join(x))

这里的关键点是洗涤顺序。先删emoji,再替换特殊符号,最后合并空格。如果把替换符号放在删除emoji之前,某些emoji会被拆成乱码字符,反而增加噪声。停用词表要精简,只去掉对情感判断没帮助的虚词;不要做“去掉所有形容词”这种错误的去噪操作,因为形容词是情感判断的主要载体。

分词参数方面,jieba.lcut返回list,默认使用精确模式。外卖场景里建议给jieba增加食材和菜品词,例如“麻辣烫”“小碗菜”“蛋炒饭”等,否则“小碗菜”会被拆成“小碗”和“菜”,影响后续情感词匹配。可以在分词前调用:

for word in ["麻辣烫", "小碗菜", "蛋炒饭", "螺蛳粉"]: jieba.add_word(word)

3.3 计算情感得分并导出Excel:让结果能直接发给业务方

把第二章的词典打分函数应用到切分好的tokens列,然后生成业务可读的标签列,最后写入Excel。

# 复用第二章的 pos_dict/neg_dict def score_row(tokens): score = 0.0 for w in tokens: if w in pos_dict: score += pos_dict[w] elif w in neg_dict: score += neg_dict[w] return score df["sentiment_score"] = df["tokens"].apply(score_row) df["sentiment_label"] = df["sentiment_score"].apply( lambda s: "正面" if s >= 1 else ("负面" if s <= -1 else "中性") ) # 按店铺聚合,输出简表 summary = df.groupby("shop_name")["sentiment_score"].agg(["mean", "count"]) summary.to_excel("waimai_sentiment_result.xlsx", index=True)

写入Excel需要openpyxl库,to_excel默认会创建新的excel文件。如果数据量大,建议先写一份全量明细,再写一份按店铺汇总的均值表。实际交付时,业务方更在意的是“哪些店铺上周差评变多了”,而不是每条的原始分数,所以可以在此基础上继续做周环比变化。

词典法跑出来的分数有一个隐藏问题:分数绝对值大小并不能直接反映情绪强度。“好吃”给2分,“超级好吃”如果“超级”不在程度副词表里,也可能只给2分,导致“超级好吃”和“好吃”被混为一谈。解决办法是在词典匹配前先处理程度副词,比如“超级/太/很/非常”对后一个情感词权重乘1.5到2.0,这样就能区分“满意”和“非常满意”。这一步可以在第五章节的否定词反转逻辑里一并实现。

4. 模型训练与阈值调参:让情感分析从“能用”到“好用”

4.1 标注样本与训练划分:不要直接在Excel里手工打标签

到这一步,你已经有了词典法产生的分数和标签。接下来要训练机器学习模型,首先需要一份相对可靠的训练标签。很多人的第一反应是打开Excel一条条打正/中/负,这样做效率低且容易前后标准不一致。更好的方式是用脚本先粗标,再人工只修正“模型可能错的地方”。

具体做法是:用词典法的分数生成一个映射标签,把分数分为0/1/2三档,然后按类别分层抽样,每类抽几十条人工复核。抽样代码里设置随机种子,保证每次复现同一个样本集。

df["weak_label"] = df["sentiment_score"].apply( lambda s: 0 if s < 0 else (2 if s > 0 else 1) ) sample = df.groupby("weak_label", group_keys=False).apply( lambda x: x.sample(min(50, len(x)), random_state=1) ) sample.to_csv("sample_for_human_check.csv", index=False)

这个代码的逻辑是,weak_label作为伪标签,先用它做分层抽样。人工只需要抽出的几百条,而不是几千条。人工修正后再用修正后的标签重新训练模型。这里有个参数值得注意:groupby(...).apply在一些pandas版本中会有性能警告,对于10万条以下的数据没有实际影响;如果数据量更大,可以改用sample参数结合StratifiedShuffleSplit。

标注时最好三个人独立标,然后取多数结果。外卖评价的“中性”判断本身很主观,比如“一般般”有人标中立,有人标负面。这类分歧样本单独拎出来,要么丢弃,要么单独设置一个“争议”类别,不要混进训练集。

4.2 必调参数:TF-IDF窗口、正则强度、得分阈值

模型从“能跑”到“好用”,差别往往不在算法,而在参数。下面这张表是我做外卖评价时最常调的几个参数:

参数作用常见范围
max_features特征词数量上限3000~8000
ngram_range保留单词/双词组合(1,2) 或 (1,3)
min_df忽略出现次数过少的词2~5
max_df忽略出现频率过高的词0.8~0.95
C逻辑回归正则强度0.5~2.0
class_weight控制类别权重balanced 或手动指定
sentiment_threshold词典得分转标签的阈值0.5~1.0 之间

以TfidfVectorizer为例,max_features=3000和min_df=2的组合能有效去掉只在一条评论里出现的“一次性词语”,避免模型学到无关噪声。ngram_range=(1, 2)很重要,因为外卖评价里“不辣”“太辣”都是双词表达,只保留单字词会丢失关键情绪信息。用网格搜索找出最优组合:

from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV pipe = Pipeline([ ("tfidf", TfidfVectorizer()), ("clf", LogisticRegression(max_iter=1000)) ]) param_grid = { "tfidf__max_features": [3000, 5000], "tfidf__ngram_range": [(1, 2), (1, 3)], "tfidf__min_df": [1, 2], "clf__C": [0.5, 1.0, 2.0] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring="f1_macro") grid.fit(df["token_text"], df["human_checked_label"]) print(grid.best_params_)

这段代码使用Pipeline把向量化和分类器串起来,GridSearchCV会遍历参数组合并输出最优参数。需要注意两个点:一是样本量超过1万条时,网格搜索的组合数要控制住,否则单机要跑很久;二是评分指标用f1_macro而不是默认的accuracy,因为外卖评价中差评比例明显偏低,只看准确率会掩盖“差评全部没抓到”的问题。如果时间紧,可以把GridSearchCV换成RandomizedSearchCV,并设置n_iter=20。

词典法的得分阈值也需要调。默认阈值是正负1,但不同业务对“负面”的定义不同。有的老板认为打4星也算不满,有的认为只有出现“难吃”“差评”才值得介入。常见做法是遍历阈值,看每个阈值下人工抽检错误率的变化,然后选择错误率最低、且与星级一致性最高的点。比如统计每个阈值下“模型判断负面但用户打了4星”的比例,这个比例越低,阈值越可靠。

4.3 评估:别只看准确率,重点看差评召回率

模型训练完后,最忌讳只打印一个accuracy。外卖场景下,差评漏检的成本远高于误检——一条没被发现的“食物变质”差评,可能导致客诉升级;而把一条好评误判成差评,顶多是运营多打一个回访电话。因此评估时至少要看混淆矩阵和分类报告。

from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test) print(classification_report(y_test, y_pred, target_names=["负面", "中性", "正面"])) print(confusion_matrix(y_test, y_pred))

输出结果中需要重点看“负面”这一行的recall召回率。如果召回率低于0.7,说明大量差评被模型放过了。改进手段有三种:把class_weight调成{"负面": 2.0, "中性": 1.0, "正面": 1.0},给差评更大的惩罚权重;或者把Tfidf特征中ngram_range加到(1, 3);还有一种非常有效的方法是增加“差评关键词表”作为额外特征,比如“变质”“拉肚子”“苍蝇”等词一旦出现,直接给样本加一个强特征维度,让模型更敏感。

除了测试集指标,还要做业务侧验证。把训练好的模型预测结果和用户打的星级交叉一下:如果模型预测负面但用户打了5星,优先查看这组样本。通常有两种解释:要么是文本里的反讽,例如“真棒,汤全洒了”;要么是模型出错。这种交叉验证不需要额外标注,是最便宜的检验方法。

5. 外卖评价情感分析避坑指南:5个典型翻车场景

5.1 现象:好评全被识别成中性,分数一直贴着零

第一版词典法跑出来,满屏都是中性,尤其是“分量很足”“出餐快”“小哥态度好”这类外卖场景好评。原因很明确:通用情感词典根本没收录这些领域表达,模型感知不到情绪。解决方法是把外卖高频描述词补进去,比如“足”“准时”“热情”“新鲜”“划算”。更系统化的做法是:先统计历史评价里高频的形容词和动宾短语,再人工标一轮。注意不要抄通用词典里所有词,像“清淡”在轻食店是正面,在重口味店铺可能代表不满意,要按品类维护。

5.2 现象:否定句“没有不好吃”被算成负面

用户写“没有不好吃”,意思是“还挺好吃”,但词典会把“不好吃”匹配成负面词,结果得分变负。原因是“不好吃”是个固化词,早期词典为了省事直接把它作为一个情感词收录,而否定逻辑没有机会介入。解决方法是把固化词拆掉,不要收录“不好吃”“不新鲜”这类组合,让否定词检查窗口去处理。具体做法是:遍历词典时,如果情感词前一个词是否定词,就把当前词权重取反;如果词典里已经存在“不好吃”,要从词典删除,否则会和反转逻辑叠加,造成“负负得正”或重复计算。

5.3 现象:“yyds”“无语子”这类网络用语让模型失灵

年轻用户爱用网络梗,“这家店yyds”表达极度认可,“味道无语子”表达不满,但词典和TF-IDF训练集里都没有这些词,模型只能判成中性。原因是外卖评价内容更新快,静态词典和定期训练都跟不上。解决方法是给项目增加一个“新词补充”步骤:把新出现的网络词用jieba.add_word加进分词器,并同时补进情感词典。机器学习路线更麻烦,需要在训练前先用词向量查相似词扩充样本,或者把这些词替换成语义一致的常规表达,比如把“yyds”替换成“非常棒”再来打分。这个替换规则要单独维护,避免每条评论都被改写导致原始表达丢失。

5.4 现象:代码换台电脑就报错,环境版本完全对不上

本地跑得好好的zip包,发到同事电脑直接抛AttributeError或ImportError。原因几乎都是依赖包版本不匹配,比如scikit-learn1.1版本把某个参数改名,或者jieba版本差异导致分词结果变了。解决方法是先在项目根目录写死requirements.txt,并明确执行安装时用==锁定版本,而不是>=。常见做法是:

pip install scikit-learn==1.0.2 jieba==0.42.1 pandas==1.3.5 openpyxl==3.0.10

另外,在VSCode里配置Python解释器时也很容易掉坑。命令行里pip list能看到包,但VSCode仍报错“找不到模块”,原因是解释器选错了。选到conda环境路径下的python.exe后,再重启终端,问题基本就解决了。如果还是不行,用python -c "import jieba"验证,不要只靠IDE提示。

5.5 现象:原始数据乱码和重复标点让分词彻底错乱

CSV文件打开能看到乱码,或者评论里“好吃!!!!”被分词器拆成“好吃”和四个感叹号,情感词匹配直接失败。原因是文件编码可能是GB18030,且导出时带了BOM;重复标点没有被归一化。解决方法是先用chardet.detect识别编码,再在清洗函数里把所有连续符号替换成单个空格。但要注意,不要把所有感叹号都删干净,因为“好吃!!!”和“好吃”的情绪强度不一样。我的做法是在清洗时保留感叹号数量作为一个辅助特征,比如新建一列exclaim_count,模型输入里加上它,差评识别精确度能提升几个点。

6. 没有标注数据也能起步:用伪标签和外场校验做增量

如果没有一份现成的标注数据,别急着花钱去人工标几千条。我推荐一个有效路径:先把词典法打分结果当作弱标签,用弱标签训练逻辑回归,再让模型输出置信度,只挑置信度中等(比如概率在0.4到0.6之间)的样本给人校验。这些样本恰恰是词典法犹豫不决、模型也拿不准的地方,修正它们对模型提升最大。三轮迭代下来,人工只需要处理几百条,就能把差评召回率从60%多提到85%以上。

另一个实用技巧是外场校验。把星级评分当作弱监督信号:1星或2星的评价,模型绝对不可以预测成正面;4星或5星的评价,基本不应该预测成负面。把冲突样本单独输出,优先排查。这个校验方法不需要额外成本,却能在模型上线前挡住最明显的错误。

我自己就曾经迷信“通用预训练模型”,直接拿一个通用情感分类器跑外卖评价,结果“米饭太硬”“汤洒了”这类具体物理描述全部被归成中性,因为预训练数据里根本没有这种场景。后来我重新调整流程,先用词典法跑通全链路,再用伪标签训练小模型,最终按业务验收口径把准确率从72%拉到89%。那之后我的习惯是:任何情感分析项目,先把词典和规则做成一道“后悔药”,再考虑要不要上复杂模型。

这个方向真正值得投入的前提,是你有持续回流的真实评价数据,并且愿意每个月做一次样本复核。如果只是一锤子分析,词典法加Excel足够;如果能形成闭环,伪标签训练才是把“情感倾向性分析”从工具做成产品的路径。希望帮到你。

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

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

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

立即咨询