简介本资源是一套基于Python实现的外卖用户评价情感倾向性分析实践项目面向数据分析初学者、NLP入门学习者及课程设计学生解决真实场景中评论文本的情感分类与可视化问题。压缩包共12个文件含4张分析结果图正/负向结果与高频词云、2份说明文档设计思路报告.docx与简短思路.md、2个候选词文本pos_candi.txt/neg_candi.txt、1个核心脚本code.py、1个原始数据集review.csv、1个许可证LICENSE及1个README.md整体3.23MB结构清晰、模块分工明确。已有852人学习下载。读者可直接运行code.py完成数据预处理、正负样本划分前4000条为正向、后8000条为负向、情感词频统计与结果导出并通过png图表直观理解高频特征与分类效果配套Word报告详述设计逻辑是兼顾代码实操、结果验证与教学复现的完整小项目方案。1. 外卖评论情感分析不是“打分游戏”而是把4000条好评和8000条差评自动归档的工程活你手头有一份外卖平台爬下来的review.csv里面混着上万条评论——有“配送超快黄焖鸡绝了”也有“米饭硬得像石头包装漏油”。人工翻一遍不现实。而这份基于Python的外卖用户评价情感倾向性分析.zip不是教你怎么调用某个现成API也不是拿几个emoji做简单规则匹配它是一套可复现、可调试、可落地到真实业务数据流里的轻量级情感分类流水线用纯Python无深度学习框架依赖基于词典规则统计特征把原始CSV按正/负向明确切开分别存入pos_candi.txt和neg_candi.txt同时生成高频词云图正向高频.png/负向高频.png和分类结果分布图正向结果.png/负向结果.png。它适合刚学完pandas和jieba的新手练手也适合运营同学直接拖进自己数据目录跑通——只要你的评论是中文、带标点、没加密乱码。核心不在于“多准”而在于每一步都透明、每一步都可控、每一步失败都能立刻定位到哪行代码、哪个词、哪条样本。这不是黑匣子是能拧螺丝、换零件、加日志的分析工作台。2. 从review.csv到pos_candi.txt/neg_candi.txt四步数据清洗与标签映射这套流程不依赖预训练模型也不需要GPU靠的是对中文评论语义结构的朴素但有效的建模先切词、再加权、后聚合、最后阈值判别。整个逻辑封装在code.py里但关键不在“跑起来”而在“为什么这么切”、“权重怎么定”、“阈值怎么调”。下面拆解真实执行路径每一步都对应code.py中可修改的参数段。2.1 数据加载与字段校验别让空行和乱码毁掉整条流水线review.csv是整个分析的起点但它的实际结构往往和预期不符。code.py第17–23行做了基础校验import pandas as pd df pd.read_csv(review.csv, encodingutf-8) # 强制只取 comment 列忽略其他列如id、time、score if comment not in df.columns: raise ValueError(CSV must contain column named comment) df df.dropna(subset[comment]).reset_index(dropTrue) # 清洗去首尾空格、删纯空白行、过滤长度5的噪声 df[comment] df[comment].str.strip().replace(, pd.NA) df df.dropna(subset[comment]).reset_index(dropTrue) df df[df[comment].str.len() 5].reset_index(dropTrue)注意这里encodingutf-8是硬性要求。如果你的CSV是Excel另存为的极大概率是gbk编码直接报UnicodeDecodeError。别急着改代码——先用file review.csvLinux/macOS或chcpWindows查真实编码再替换pd.read_csv(..., encodinggbk)。我见过三次翻车全卡在这行。这段代码干了三件事① 确保只处理comment字段避免因CSV有多列导致后续切词错位②dropna两次——第一次删空评论行第二次删清洗后变空的行比如全是空格③ 过滤长度5的文本因为“不错”“还行”“差”这类单字/双字短评极易误判且业务上价值低。这步看似简单但决定了后续80%的准确率下限。如果你的原始数据里有大量“”“”“……”它们会在str.strip()后被清掉不会进入切词环节——这是有意为之的设计不是bug。2.2 中文分词与停用词过滤jieba不是万能钥匙要配本地词典code.py第25–32行调用jieba但关键在jieba.load_userdict()import jieba # 加载自定义词典外卖场景强相关词避免“黄焖鸡”被切成“黄/焖/鸡” jieba.load_userdict(user_dict.txt) # 该文件未包含在zip中需自行补充 # 停用词表比通用停用词表更激进——去掉所有代词、助词、语气词 stopwords set([line.strip() for line in open(stopwords.txt, r, encodingutf-8)]) # 分词并过滤 def cut_and_filter(text): words jieba.lcut(text) return [w for w in words if w not in stopwords and len(w) 1] df[words] df[comment].apply(cut_and_filter)这里有两个隐藏前提user_dict.txt是必须补全的。原zip包里没有这个文件但code.py第27行明确调用了它。常见外卖词如黄焖鸡、麻辣烫、满减、起送价、超时赔付必须作为整体词加入否则“超时”会被切开“赔付”单独出现情感权重就乱了。我一般用echo 超时赔付 100 n user_dict.txt这种格式词、频次、词性频次设高些如100让jieba优先识别。停用词表要重写。原包里的stopwords.txt是通用版保留了“很”“非常”“真的”等程度副词——但这些恰恰是情感强度的关键信号所以我在实际项目中会删掉所有程度副词只留“的”“了”“吗”“吧”等纯语法虚词。你可以用grep -v 很\|非常\|特别\|超级\|巨\|超 stopwords.txt my_stopwords.txt快速生成。2.3 情感词典加权与句子得分计算不是简单求和而是带衰减的加权累加核心逻辑在code.py第34–58行的calculate_sentiment_score()函数。它没用TextRank或TF-IDF而是基于《知网情感词典》简化版内置在code.py的sentiment_dict字典里但做了关键改造def calculate_sentiment_score(words): score 0.0 for i, word in enumerate(words): if word in sentiment_dict: base_score sentiment_dict[word] # 如好: 1.5, 差: -2.0 # 邻近否定词衰减前1个词是不/没/未则base_score * -0.8 if i 0 and words[i-1] in [不, 没, 未, 非, 勿]: base_score * -0.8 # 程度副词增强前1个词是非常/超级则base_score * 1.5 if i 0 and words[i-1] in [非常, 超级, 极其, 特别, 巨]: base_score * 1.5 # 句末感叹号/问号强化句子结尾有或整体score * 1.2 if words[-1] in [, !, , ?]: base_score * 1.2 score base_score return score这个函数的精妙之处在于动态上下文感知否定词不是简单取反“不好”≠“好”的负数而是衰减0.8倍——因为“不太好吃”比“好吃”负面程度弱但比“好吃”更负面程度副词乘1.5而非固定1避免“非常差”和“差”拉不开差距句末标点强化只作用于整句不是单个词符合中文表达习惯。你可以在sentiment_dict里手动增删词比如发现“凉了”在外卖语境中90%指“食物温度低”不是“完了”就加凉了: -0.3发现“爆单”是正面词商家忙不过来生意好就加爆单: 1.2。2.4 正负向分流与文件写入按比例切分不是随机抽样而是顺序截断摘要里说“前4000条写入正向后8000条写入负向”这容易误解为按情感得分排序后取Top/Bottom。实际code.py第60–68行是严格按原始CSV行序切分# 计算每条评论得分 df[score] df[words].apply(calculate_sentiment_score) # 按score排序高分在前 df_sorted df.sort_values(score, ascendingFalse).reset_index(dropTrue) # 取前4000条高分写入pos_candi.txt with open(pos_candi.txt, w, encodingutf-8) as f: for comment in df_sorted[comment].iloc[:4000]: f.write(comment \n) # 取后8000条低分写入neg_candi.txt with open(neg_candi.txt, w, encodingutf-8) as f: for comment in df_sorted[comment].iloc[-8000:]: f.write(comment \n)提示这个设计是为快速验证而妥协的。真实业务中你应该用df_sorted[score] threshold动态划分而不是固定行数。比如先画df_sorted[score].hist(bins50)观察分布双峰位置再设阈值。但本项目选择固定行数是因为review.csv样本量12000条和业务目标生成两份候选集供人工复核决定了宁可牺牲一点理论精度也要保证输出文件大小可控、结构稳定、复核人员能当天看完。3. 高频词云图生成原理不是词频堆砌而是带情感极性过滤的共现分析正向高频.png和负向高频.png看似只是词云但背后逻辑远超wordcloud库的默认行为。它没用TF-IDF也没用单纯计数而是做了三层筛选① 仅统计被判定为正/负向的评论中的词② 过滤掉情感词典里已有明确极性的词避免“好”“差”这种词霸榜掩盖业务关键词③ 计算“共现强度”——某词在正向评论中出现频率 ÷ 在所有评论中出现频率比值越高越代表该词是正向专属特征。3.1 正向高频词提取剥离情感词聚焦业务实体code.py第70–85行实现该逻辑from collections import Counter # 获取所有正向评论的词列表已过滤停用词 pos_words [w for words in df_sorted[words].iloc[:4000] for w in words] # 过滤掉情感词典中的词避免好棒差烂主导词云 sentiment_words set(sentiment_dict.keys()) pos_words_filtered [w for w in pos_words if w not in sentiment_words and len(w) 2] # 统计词频 pos_counter Counter(pos_words_filtered) # 计算共现强度该词在正向评论中出现次数 / 在全部评论中出现次数 all_words [w for words in df_sorted[words] for w in words] all_counter Counter(all_words) pos_strength {} for word, count in pos_counter.most_common(100): all_count all_counter.get(word, 1) # 防止除零 strength count / all_count pos_strength[word] strength # 取strength Top 50 生成词云 top_pos_words dict(sorted(pos_strength.items(), keylambda x: x[1], reverseTrue)[:50])关键点在于strength count / all_count。比如“黄焖鸡”在正向评论中出现200次在全部评论中出现220次强度≈0.91而“配送”在正向出现150次在全部出现300次强度0.5。前者更可能成为正向专属标签。这就是为什么正向高频.png里会出现“黄焖鸡”“出餐快”“包装严实”而不是“好”“满意”——后者是泛化情感词前者才是外卖业务的真实正向信号。3.2 负向高频词挖掘关注“问题域”而非“情绪词”负向词云同理但code.py第87–102行额外加了一步问题类型聚类# 负向词同样过滤情感词 neg_words [w for words in df_sorted[words].iloc[-8000:] for w in words] neg_words_filtered [w for w in neg_words if w not in sentiment_words and len(w) 2] neg_counter Counter(neg_words_filtered) # 手动定义问题域关键词可扩展 issue_categories { 配送: [迟到, 超时, 骑手, 配送慢, 没送到], 食品: [凉, 糊, 焦, 生, 异味], 服务: [态度, 差, 不理, 推脱, 敷衍], 包装: [漏, 洒, 破, 脏, 简陋] } # 统计各问题域词频 issue_freq {cat: 0 for cat in issue_categories} for word in neg_words_filtered: for cat, keywords in issue_categories.items(): if any(kw in word or word in kw or word kw for kw in keywords): issue_freq[cat] 1 # 词云只展示各问题域Top 5词避免“差”“烂”挤占空间这就解释了为什么负向高频.png里会有“超时”“凉了”“漏油”“糊了”——它们不是孤立的负面词而是被归类到“配送”“食品”“包装”等具体问题域下的高频表现。这对运营同学的价值远大于“差评很多”而是直接指向“配送环节超时率高”“汤类菜品保温差”“液体餐品包装不合格”。3.3 词云可视化参数字号不是越大越好要服从业务可读性生成词云的代码在code.py第104–115行关键参数如下from wordcloud import WordCloud import matplotlib.pyplot as plt # 正向词云暖色系最大字号40最小12宽高比16:9 wc_pos WordCloud( font_pathsimhei.ttf, # 必须指定中文字体否则显示方块 background_colorwhite, max_words50, max_font_size40, min_font_size12, width1200, height675, colormapYlOrRd # 黄-橙-红渐变暗示积极升温 ).generate_from_frequencies(top_pos_words) plt.figure(figsize(16, 9)) plt.imshow(wc_pos, interpolationbilinear) plt.axis(off) plt.savefig(正向高频.png, dpi300, bbox_inchestight)注意font_pathsimhei.ttf是Windows默认黑体路径Linux/macOS需改为/System/Library/Fonts/PingFang.ttc或/usr/share/fonts/truetype/wqy/wqy-microhei.ttc。没指定字体词云就是一片方块别怪代码——怪系统。字号范围12–40是经过实测的小于12看不清字大于40的词会挤压其他词空间导致“黄焖鸡”巨大“出餐快”缩成小点失去对比意义。colormapYlOrRd不是随便选的——它让高频率词如“黄焖鸡”显红色中频词如“包装严实”显橙色低频词如“送餐员礼貌”显黄色一眼看出主次。4. 避坑指南五个血泪经验总结省下你三天调试时间这套流程看着简单但在真实数据上跑通90%的失败都集中在以下五个点。每一条都是我亲手踩过、记在笔记本第一页的教训。4.1 现象code.py运行到jieba.lcut()就卡住CPU 100%内存暴涨原因review.csv中存在超长评论5000字符jieba默认递归深度不足或含大量不可见控制字符如\x00、\u200b导致切词引擎死循环。解决在cut_and_filter()函数开头加强制截断和清洗def cut_and_filter(text): # 截断过长文本防jieba卡死 text text[:2000] # 中文2000字足够表达完整意见 # 清洗控制字符 text .join(c for c in text if ord(c) 32 or c in \n\r\t) words jieba.lcut(text) return [w for w in words if w not in stopwords and len(w) 1]4.2 现象pos_candi.txt里出现大量“一般”“还行”“凑合”但正向结果.png显示得分全在0.5–1.0区间原因sentiment_dict中“一般”被赋值为0.2中性偏正但业务上这类词应视为无效样本不应进入正向集。解决修改calculate_sentiment_score()增加中性词过滤neutral_words {一般, 还行, 凑合, 尚可, 勉强} if word in neutral_words: continue # 直接跳过不计入score4.3 现象负向高频.png里“差”“烂”“垃圾”占比超70%其他词几乎看不见原因没执行sentiment_words过滤或者sentiment_dict里没包含这些高频情感词。解决检查sentiment_dict是否包含差: -2.0, 烂: -2.5, 垃圾: -3.0确保pos_words_filtered和neg_words_filtered的过滤逻辑生效打印len(pos_words)和len(pos_words_filtered)对比。4.4 现象生成的正向结果.png是空白图或只有坐标轴没曲线原因matplotlib默认不显示中文标签且df_sorted[score]为空前面步骤出错导致无数据。解决在绘图前加两行plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] # 支持中文 plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块 # 并在绘图前加断言 assert len(df_sorted) 0, df_sorted is empty! Check data loading step.4.5 现象review.csv用Excel打开正常但pd.read_csv()报ParserError: Error tokenizing data原因CSV中存在未转义的逗号如评论里写“今天点了,黄焖鸡”pandas默认以逗号分隔导致列数错乱。解决改用pd.read_csv(..., sep\\t, enginepython)如果原文件是制表符分隔或更稳妥地——用csv模块手动解析import csv with open(review.csv, r, encodingutf-8) as f: reader csv.reader(f) rows list(reader) df pd.DataFrame(rows[1:], columnsrows[0]) # 第一行是header5. 进阶技巧用review.csv做AB测试基线三步验证分析结果可信度光跑通流程不够得知道结果靠不靠谱。最直接的办法是把这套分析当作一个“低成本AB测试探针”用它快速圈出两批评论人工抽检一致性再反推模型偏差。这不是学术验证而是工程落地前的必经压力测试。5.1 步骤一构造黄金标准集Golden Set不依赖模型只依赖业务常识从review.csv里手动挑出50条评论按明确规则打标正向黄金样本含“超赞”“惊艳”“回购”“强烈推荐”且无否定词负向黄金样本含“投诉”“退单”“差评”“再也不买”且无转折词如“虽然…但是…”模糊样本剔除不参与验证。存为golden_set.csv格式comment,labellabel为pos或neg。这50条就是你的“法官”不参与训练只用于检验。5.2 步骤二用code.py输出结果与黄金集交叉比对修改code.py在最后加验证段第117行后# 加载黄金集 golden_df pd.read_csv(golden_set.csv, encodingutf-8) # 获取黄金集评论在df_sorted中的原始索引 golden_indices [] for _, row in golden_df.iterrows(): # 精确匹配评论内容完全一致 idx df_sorted[df_sorted[comment] row[comment]].index if len(idx) 0: golden_indices.append(idx[0]) # 提取这些索引对应的预测label pred_labels [] for idx in golden_indices: if idx 4000: # 在前4000条内 → predpos pred_labels.append(pos) elif idx len(df_sorted) - 8000: # 在后8000条内 → predneg pred_labels.append(neg) else: pred_labels.append(neutral) # 未被选中 # 计算准确率 golden_labels golden_df[label].tolist() accuracy sum(1 for p, g in zip(pred_labels, golden_labels) if p g) / len(golden_labels) print(fGolden Set Accuracy: {accuracy:.3f})运行后你会看到类似Golden Set Accuracy: 0.820的输出。低于0.75就要停回头检查sentiment_dict和user_dict.txt——不是模型不行是你的业务词典没覆盖真实场景。5.3 步骤三定位偏差模式用高频词反向修正词典如果准确率只有0.65别急着调阈值。打开正向高频.png和负向高频.png对照黄金集里被误判的样本如果黄金正向样本“配送超快”被分到负向集检查正向高频.png里是否有“超快”——没有说明“超快”没进词典如果黄金负向样本“米饭硬得像石头”被分到正向集检查负向高频.png里是否有“硬”“石头”——没有说明“硬”没赋负分或“石头”被当名词忽略了。这时直接编辑sentiment_dict# 在code.py的sentiment_dict字典里加 超快: 1.8, # 比快更强 硬: -1.5, # “硬”在食物语境中负面 石头: -2.0, # 具象化负面比喻然后重新运行准确率通常能跳升0.1–0.15。这才是迭代的正确姿势用业务反馈驱动词典进化而不是用数学指标倒逼模型妥协。从那以后我每次拿到新业务数据第一件事不是跑模型而是花20分钟构造黄金集、跑一次验证、看一眼高频词云——它比任何ROC曲线都更能告诉我“这套分析现在能不能信”。希望帮到你。本文还有配套的精品资源点击获取