简介:基于Python构建的大众点评数据可视化和情感分析系统,是一套面向计算机相关专业毕业设计、期末大作业及课程设计的完整项目方案。资源提供系统实现源码与配套演示PPT,代码内含有详细注释,尽量降低阅读门槛,新手也能跟随注释理解数据清洗、情感分类、可视化展示等核心环节。项目聚焦大众点评评论数据,能够帮助学生在真实数据上完整体验从数据准备、特征处理到分析结果呈现的流程,并以此支撑功能演示与毕业答辩。资源以zip压缩包形式发布,整体大小约26.35MB,目前已有207人学习下载。该方案由作者个人手打完成,曾获导师认可并被评为98分高分项目,模块划分清晰,可直接参考其设计与实现思路;下载后按文档简单配置即可运行,既适合作为课程作业或毕业设计的高分样例,也便于在此基础上进行功能扩展与算法优化。
1. 这个系统解决什么问题:毕业设计里一条真正能跑通的全链路
毕业设计最容易踩的坑,是选题听起来高级,交付时却拿不出一个能现场演示的完整系统。基于Python的大众点评数据可视化和情感分析系统恰好卡在“能交付”和“有深度”之间:把评论数据经过清洗和情感打分,落到MySQL,再用Flask提供接口,前端用ECharts把情感占比、评论趋势、星级分布画成图,最后把过程装进答辩PPT。这套链路覆盖了Pandas数据处理、SnowNLP情感分析、Flask后端、ECharts可视化四个知识块,对毕设、课设和想练Python全栈的新手来说,性价比很高。下面按“数据从哪来 → 情感怎么算 → 图怎么出 → 坑在哪”往下拆。
2. 架构与技术选型:数据从哪来,框架为什么这么配
2.1 数据来源与合规:先解决“你的数据哪来的”这个答辩第一问
这个题目最容易被追问的是“数据哪来的”。答辩老师一定会问,所以第一步就要想清楚。常见三条路:自己写爬虫抓大众点评公开页面;找现成的评论数据集;手工整理一小批样例数据跑通流程再说明扩展思路。我的建议是优先用自己能控制的数据源,比如自己店铺的公开评论,做演示时数据真实、规模小、不容易出合规问题,然后写一个采集脚本说明——注意遵守robots规则、控制请求频率、不碰手机号等个人隐私字段。数据合规这一点在答辩时是加分项,说明你想过边界。
不管数据怎么来,表结构要一开始就定好。我用MySQL,两张表,店铺和评论分开:
CREATE TABLE shop ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(128) NOT NULL, category VARCHAR(64), address VARCHAR(255), avg_cost DECIMAL(10,2), star_level TINYINT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE review ( id INT PRIMARY KEY AUTO_INCREMENT, shop_id INT NOT NULL, content TEXT NOT NULL, star TINYINT COMMENT '用户打分 1-5', created_at DATE, sentiment_score DECIMAL(4,3) COMMENT '情感得分 0-1', sentiment_label VARCHAR(8) COMMENT '正面/中性/负面', KEY idx_shop_id (shop_id), KEY idx_sentiment (sentiment_label), CONSTRAINT fk_review_shop FOREIGN KEY (shop_id) REFERENCES shop(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:店铺和评论拆成两张表,是为了后面按品类、商圈做聚合统计时不用把评论全文反复加载。sentiment_score和sentiment_label留空,等第3章情感分析跑完回填。字段里star是用户打的原始星级,sentiment_label是模型判定的情感倾向,两者可以对比,这是答辩时一个很好的分析点。
参数说明:utf8mb4必须选,不要用utf8,评论里的emoji用utf8写入会失败;content用TEXT而不是VARCHAR(255),评论长度经常超;created_at用DATE,情感趋势按天聚合时避免时间精度干扰。
拿到CSV后,导入前先用Pandas做一轮清洗:
import pandas as pd df = pd.read_csv('data/reviews.csv', encoding='utf-8') df = df.drop_duplicates(subset='content') df = df[df['content'].str.len() >= 5] df = df[~df['content'].str.contains('代金券|优惠券|秒杀|广告')] df.to_csv('data/reviews_clean.csv', index=False, encoding='utf-8')逻辑说明:去重用content而不是id,因为多次采集会产生重复评论。过滤营销文本是因为广告内容会对情感统计产生明显干扰——一条“全场五折代金券”会被SnowNLP判成正面,拉高整体好评率。短评过滤是为了后续分词和情感打分有足够上下文。
2.2 技术栈对比:Flask、SnowNLP、ECharts为什么是合适组合
这套系统常见实现方案有三层:数据处理用Pandas,情感分析用SnowNLP,展示用Flask加ECharts。不是没有更重的选择,而是这个组合在“能跑”和“能讲清楚”之间最平衡。
| 环节 | 轻量方案 | 重型方案 | 选谁的理由 |
|---|---|---|---|
| Web框架 | Flask | Django | Flask路由简单,几个JSON接口几十行写完,课设不需要Django的Admin和ORM全家桶 |
| 情感分析 | SnowNLP | 百度AI / BERT微调 | SnowNLP离线可用、免费、代码短;百度AI要申请Key,BERT要GPU和标注数据 |
| 可视化 | ECharts | Plotly / 自绘 | ECharts中文文档全、图表类型多,饼图折线图词云伸手就来 |
这里要重点说情感分析的选型。SnowNLP的准确率不是90%以上,它内置语料是购物评论训练的,用在餐饮点评上会有偏差——这一点不是缺陷,而是你要在答辩里展示“你怎么修正它”的机会。诚实的做法是:先用默认模型跑一遍,人工抽检50条统计准确率,再用自己的标注语料微调,对比提升幅度。这个路径本身就是系统设计的一部分,比单纯吹“准确率95%”可信得多。
2.3 工程目录结构:让代码从一开始就是可交付状态
一个规整的工程目录比代码本身更容易让答辩老师信任。我一般这样组织:
dianping_project/ ├── app.py # Flask 主程序与全部接口 ├── config.py # 数据库连接、阈值等配置 ├── data/ │ ├── reviews_clean.csv # 清洗后的评论数据 │ ├── pos.txt # 微调用正情感语料 │ └── neg.txt # 微调用负情感语料 ├── modules/ │ ├── emotion.py # 情感分析封装 │ └── stats.py # 聚合统计 ├── static/ │ └── echarts/ # 前端JS与图表页面 └── sql/ └── schema.sql # 建表脚本把情感分析封装成独立模块而不是全塞进app.py,原因很简单:答辩时你说“情感分析模块复用度高”比“代码全在一个文件里”更有说服力。运行流程是导入数据、跑情感标注、再起Flask,这条主线在第3、4章展开。config.py里集中放数据库连接串、情感阈值、语料路径,换机器只需要改一处。
3. 情感分析模块:从“不知道说什么”到分得出好负面
3.1 用SnowNLP跑第一版批量打分:代码短,坑也不少
先写最小可用版本:
import pandas as pd from snownlp import SnowNLP def infer_sentiment(text): if not isinstance(text, str) or len(text.strip()) < 2: return 0.5 try: return SnowNLP(text).sentiments except Exception: return 0.5 df = pd.read_csv('data/reviews_clean.csv') df['score'] = df['content'].apply(infer_sentiment) df['label'] = pd.cut(df['score'], bins=[0, 0.4, 0.6, 1.0], labels=['neg', 'neutral', 'pos'], include_lowest=True) df.to_csv('data/reviews_scored.csv', index=False)逻辑说明:SnowNLP的sentiments属性返回0到1之间的概率,越接近1越正面。给长度小于2的文本返回0.5,是因为“赞”“呸”这类单字文本没有有效上下文;捕获异常是为了避免个别特殊字符导致整个批次中断——这一行是血泪经验,批量处理时一个异常字符能让后面全部停摆。
参数说明:0.4和0.6是常用的经验阈值,0.4以下算负面、0.6以上算正面、中间算中性。如果希望系统偏保守,只把非常明确的判成正面,把正面阈值提到0.65;如果希望召回更多正面,降到0.55。这个调整后面要结合人工抽检标定,不能拍脑袋。
跑完这一步打开reviews_scored.csv看分布:正常餐饮评论负面占比大约10%到25%。如果负面占比低于5%,先别高兴,大概率是广告评论混进来了,或者SnowNLP把“不太好吃”“实在一般”这类表达均值化了。均值化是关键词,下面细说。
3.2 自定义语料微调:把“一般般”从正面池里拎出来
SnowNLP的情感分类本质是朴素贝叶斯分类器,内置词典来自购物评论。餐饮场景里“分量足”“环境好”“服务差”“等位太久”是高频表达,默认模型对它们的权重并不准。我第一次跑这个项目,把“等位一小时东西还行”判成了正面,因为“东西还行”的权重盖过了“等位一小时”的不爽。
微调方法是用SnowNLP自带的train接口:
from snownlp import sentiment # 训练新模型,pos.txt 和 neg.txt 每行一条语料 sentiment.train('data/pos.txt', 'data/neg.txt') sentiment.save('data/my_sentiment.marshal') # 关键:替换当前进程使用的模型 sentiment.data_path = 'data/my_sentiment.marshal' sentiment.classifier = None # 强制下次调用时重载 s = SnowNLP('等位一小时,菜品也就一般般') print(s.sentiments)逻辑说明:train接收两个参数,分别是正样本和负样本文件路径,内部用朴素贝叶斯重新训练并保存模型。保存后必须把data_path指到新文件并置空classifier,否则同一进程里SnowNLP读到的还是默认模型——这个坑不自己跑一遍很难意识到。
语料怎么准备:正负各200条起步,越多越好。不要从网上复制大段新闻当情感语料,新闻句子没有情感极性,纯属干扰。最可靠的做法是从手头这批评论里挑:正面挑“味道很好”“服务热情”“环境干净”,负面挑“上菜慢”“口味咸了”“吃完拉肚子”。每条一行。训练时长在秒级,不需要GPU。
3.3 回填数据库与阈值标定:准确率是标出来的,不是猜出来的
微调完成后把分数写回MySQL。这里有个设计细节:不要把整个DataFrame一次性覆盖全表,否则改一个阈值就要全表重算。保留score原始值,让label在查询时计算:
from sqlalchemy import create_engine engine = create_engine('mysql+pymysql://user:pass@localhost/dianping?charset=utf8mb4') df[['id', 'score']].to_sql('review', engine, if_exists='append', index=False, chunksize=2000)逻辑说明:chunksize=2000按批写入,避免大表一次性提交把数据库事务日志写爆;字符集utf8mb4必须带上,否则emoji写入时又是一轮报错。
阈值定多少,给一个可复现的校准方法:抽200条评论人工标正负,用不同阈值各跑一遍,对比准确率。下表是我一组数据的例子:
| 阈值 | 准确率 | 召回率 |
|---|---|---|
| 0.55 | 0.81 | 0.76 |
| 0.60 | 0.84 | 0.72 |
| 0.65 | 0.86 | 0.63 |
不同数据集最优阈值差异很大。有一组数据0.55最优,因为那家店差评写得克制,评分普遍压在“还行”区间。所以阈值不是玄学,是你自己标出来的。答辩时能掏出这张表,比任何技术名词都管用。
4. 可视化模块:用ECharts把情感结论变成一张答辩图
4.1 Flask侧提供JSON接口:前后端分离的思路从这里开始
可视化部分不要在Jinja2模板里拼HTML,尽量做成前后端分离:Flask只负责提供JSON,前端用fetch拉取数据、ECharts渲染。好处是后续换图表不用改后端,而且答辩时可以说“按企业级前后端分离的思路设计”。
from flask import Flask, jsonify import pandas as pd app = Flask(__name__) df = pd.read_csv('data/reviews_scored.csv', parse_dates=['created_at']) @app.route('/api/overview') def overview(): label_counts = df['label'].value_counts() label_counts.index = label_counts.index.map({'pos': '正面', 'neg': '负面', 'neutral': '中性'}) return jsonify(label_counts.to_dict()) @app.route('/api/trend') def trend(): g = df.groupby([df['created_at'].dt.date, 'label']).size().reset_index(name='count') return jsonify(g.to_dict(orient='records')) @app.route('/api/star_sentiment') def star_sentiment(): cross = df.groupby(['star', 'label']).size().unstack(fill_value=0) return jsonify(cross.to_dict())逻辑说明:overview接口返回饼图数据,trend返回每日情感数量序列,star_sentiment返回星级与情感倾向的交叉表。Pandas的groupby加to_dict是核心,to_dict(orient='records')把DataFrame转成字典列表,正好对应ECharts的data格式。
参数说明:parse_dates在读取时就把时间列转好,避免后面用datetime.strptime逐个转;如果数据量不大,启动时读CSV完全够,但数据量增长后要改成接口内实时查MySQL,我这段代码是课设规模的写法。
4.2 前端图表配置:饼图、折线图、词云的三个核心参数
前端用单页演示,静态文件放static目录。先看饼图:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="/static/echarts.min.js"></script> </head> <body> <div id="pie" style="width:50%;height:400px"></div> <script> fetch('/api/overview') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('pie')); chart.setOption({ title: { text: '评论情感分布' }, tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: ['40%', '70%'], data: Object.keys(data).map(k => ({ name: k, value: data[k] })) }] }); }); </script> </body> </html>逻辑说明:环形图用两个半径,中间留白可以放总数,视觉上比实心饼图信息密度高。tooltip的trigger: 'item'表示鼠标悬停时按数据项展示,是ECharts最常用的交互配置。
折线图的配置要点是x轴用日期、y轴用数量、series按情感标签拆分:
fetch('/api/trend') .then(res => res.json()) .then(rows => { const dates = [...new Set(rows.map(r => r.created_at))].sort(); const posData = rows.filter(r => r.label === 'pos').map(r => r.count); chart2.setOption({ xAxis: { type: 'category', data: dates }, yAxis: { type: 'value' }, series: [{ name: '正面', type: 'line', data: posData }] }); });逻辑说明:前端拿到后端返回的平坦记录列表后,用Set去重拿日期序列,再按label过滤出对应数据列。注意后端返回的字段名要和前端保持一致,否则整张图渲染出来是空的,这个问题出得特别多。
词云的type是wordCloud,需要额外引入echarts-wordcloud插件,基础包不含它。数据格式是[{name: '好吃', value: 120}],value来自分词计数,通常先jieba分词再统计。词云有个坑在第5章细说。
4.3 星级与情感交叉:一张图说清“嘴上说好但打三星”
star_sentiment接口返回的交叉数据常出现一个有意思的现象:三星评论里“中性”占比高,四星里“负面”也有一小撮。前者是“还行吧”,后者是“服务扣分所以打四星但内容跟朋友吐槽”。这种不一致恰好是情感分析与结构化评分的互补证据。可视化只给结论——图上两条线不重合,分析就可以写:用户打分行为比文本情感更保守或更激进。答辩时老师看到这张图一定会追问原因,提前准备好这段分析就能接住。
5. 避坑记录:从开发到答辩的五个真实翻车现场
这些问题不是语法错误,而是环境和数据层面的坑。前三个影响系统效果,后两个影响演示体验,都值得记下来。
5.1 Pandas读大文件没爆,但to_sql写库卡到怀疑人生
现象是导入5万条评论时Pandas没炸,MySQL写入却卡了十几分钟没完成。原因是to_sql默认走SQLAlchemy的逐行insert,每行一次事务提交,写入时间和条数成正比。解决方法是加chunksize参数按批次提交,批量写和逐行写性能至少差一个数量级。这一点和机器配置无关,纯属API默认行为,第一次写项目的人都容易撞上。
5.2 “不太好吃”被判成正面:SnowNLP的否定结构缺陷
现象是一批带“不太”“一般”“还行吧”的评论全程正面,人工抽检完直接想换模型。原因是SnowNLP的朴素贝叶斯把“好吃”这个词给了极高正向权重,而“不”字在训练语料里的频率优势抵消不了它。解决分两步:第一,把带否定结构的正负句重写进训练语料,让贝叶斯把“不”的条件概率学进去;第二,在预处理层做规则补充——出现“不|没|难|差”且上下文有明显负面词时直接降权。规则不用写特别精确,跑完对比一下统计分布就知道有没有效。
5.3 地图可视化翻车:坐标偏移到几公里外
现象是商户经纬度画到底图上全偏了,像撒了一把芝麻。原因是大众点评页面里的坐标不是标准WGS84,而是经过GCJ-02偏移过的火星坐标,底图坐标系不匹配就完全对不上。解决有两种:先用坐标系纠偏工具把坐标转换到目标坐标系再给ECharts;或者干脆放弃地图,用柱状图按商圈聚合替代,比如“各区商家评分分布”,信息完全不损失。我建议初版直接不要做地图,答辩没人要求你一定画地图,但画歪了会被当场指出来。
5.4 前端单独打开页面,控制台报跨域错
现象是Flask跑在5000端口,前端页面用file://打开,浏览器F12一片红色CORS报错。原因是同源策略,file协议和localhost:5000不是同一个源。解决是开发期在Flask里加flask-cors:
from flask_cors import CORS CORS(app)一行代码就放行。但我更推荐让Flask直接render_template返回页面,彻底绕开跨域。这条坑对新手很常见,根本原因是对前后端分离理解不透——分离是逻辑分离,不是物理上非得开两个服务两个端口。
5.5 词云全是单字,“好吃”永远比不过“好”
现象是词云里最大号的是“好”和“吃”,整张图没有信息量。原因是jieba默认词典在餐饮场景切分不理想,单字天然高频。解决方法是给jieba加载用户词典,把“好吃”“服务”“分量足”这些词固定成整词,统计完过滤长度小于2的token。我再加一步,用TF-IDF权重替代纯词频,词云会更有区分度。这块处理完,词云才有展示价值,否则老师看一眼就过去了。
6. 从“能跑”到“能答辩”:准确率验证与三个进阶方向
6.1 用人工标注做一次可信的准确率验证
答辩必被问“你的准确率多少”。直接用SnowNLP默认值回答是不负责任的。我一般抽300条评论,人工标正负类,然后算混淆矩阵:
import pandas as pd from sklearn.metrics import classification_report gold = pd.read_csv('data/manual_labeled.csv') # 字段:content, human_label pred = gold['content'].apply(lambda x: 1 if infer_sentiment(x) >= 0.6 else 0) print(classification_report(gold['human_label'], pred, target_names=['负面', '正面']))逻辑说明:manual_labeled.csv是自己的人工标注集。classification_report会输出精确率、召回率、F1值。我见过的数据是默认模型F1大约0.7,微调后能到0.85。这个对比放PPT里比任何技术名词都有说服力。抽300条而不是30条,样本太少波动大,老师一问就露馅。
6.2 三个容易出效果的进阶方向
第一个是多模态情感分析,把评论配图的视觉信息也纳入分析,但课设工作量偏大。第二个是LDA主题建模,把负面评论里的“服务”“出餐速度”“口味”这些维度拆出来,比单纯的情感比例更有业务价值。第三个是BERT序列分类微调,准确率会再涨一截,但需要GPU或至少云环境,看手里资源再选。三个方向里我推荐LDA,因为小样本下输出稳定,代码量不大,能和现有情感分析形成互补。注意贪多嚼不烂,做深一个就好。
6.3 部署的最后一公里
课设到答辩基本在本地跑,但如果要在现场给老师演示,最稳的部署方式是用Gunicorn起Flask:
pip install gunicorn gunicorn -w 2 -b 0.0.0.0:8000 app:app参数说明:-w 2开两个worker,课设机器扛得住;0.0.0.0:8000让局域网内的设备也能访问,答辩现场不用拖着笔记本到处插线。
我个人的习惯是把所有配置收敛到config.py,数据库连接串、阈值、语料路径都在那,换机器只需要改一处。另外强烈建议把数据采集脚本单独放一个文件夹,答辩PPT里不要过多强调采集过程,重点放情感分析准确率对比和可视化效果。这两个点是我带过的学生里最容易在答辩台上被追问并且答不出来的地方。希望帮到你。
本文还有配套的精品资源,点击获取