
简介面向计算机相关专业正在完成课程设计、期末大作业以及希望借助真实项目进行数据挖掘实战练习的学生这是一套米其林餐厅数据挖掘管理系统源码及使用文档。项目经导师指导并获评98分覆盖米其林一星、二星、三星餐厅数据集包含数据清洗、餐厅信息管理、分类统计与后台展示等环节是完整度较高的教学型管理系统。资源包共46个文件大小292KB含18个Java源码、10个FreeMarker页面模板、5个CSS样式、4个CSV原始数据及SQL数据库脚本另附Maven配置、环境配置、README说明与示例图片可直接导入常见IDE运行便于查看前后端配合逻辑。目前已有336人学习适合用于课程设计参考、答辩展示或项目复现。结合源码与文档可完整走通从原始星级餐厅数据清洗、入库到后台查询统计、页面模板渲染的全流程其模块划分和代码结构也可作为撰写课程设计报告的参照帮助快速搭建属于自己的数据挖掘管理系统。1. 什么课程设计把米其林餐厅做成了挖掘系统这个标题里最值钱的两个词不是“米其林”而是组合出来的“挖掘管理系统”。一个课程设计如果把米其林餐厅数据做成增删改查的 CRUD 页面期末成绩大概率只有中规中矩的及格分。但如果在中间加上了“挖掘”——先采集一批餐厅数据做数据清洗、建模、分类、聚类、关联规则再把结果通过一个带界面的系统展示出来——这就能成一份从“数据 → 知识 → 决策”完整闭环的作品答辩时讲起来也有底气。我是怎么理解这个题目的它要求你构建一个能够对米其林餐厅相关数据进行采集、存储、分析并展示的系统。注意“课程设计”的定位它不需要我们做出生产级的大规模推荐系统但要求每个环节都能落地、能看到原始数据到结论的完整链路需要有具体场景比如预测某家餐厅可能获得几颗星聚类出不同风格的餐厅群或者用关联规则发现菜品搭配。对新手来说这个项目适合选做数据挖掘课设因为它数据维度丰富、算法适度、展示效果强。下面我就按“数据怎么来 → 模型怎么做 → 系统怎么搭 → 坑在哪”这条完整路线把这个方向讲透。2. 构建米其林餐厅数据集先解决“没有现成数据”的问题2.1 米其林餐厅数据的常见获取方式做数据挖掘项目的第一个难题永远是数据。严谨地说米其林官方不开放完整的数据集所以课程设计里常见的数据获取方案有四种按可靠度排序分别是公开比赛数据集、开放平台接口、自己爬取公开信息、人工手工整理。我的建议是混合使用而不是只依赖一种。先明确一个问题本系统需要哪些数据我们做挖掘至少需要餐厅名称、星级、菜系、人均消费、所在地区、评分、评价数量、招牌菜品等信息。这些字段分布在多个来源星级和地区可以从米其林指南的新闻稿或第三方聚合页面获取菜品和人均价格可以从点评类平台获取评分和评价数量则可以从各类美食榜单获取。有一种做法是只从一个渠道拿数据比如某平台但往往数据量不足或字段缺失挖掘效果不好。更常见、更稳妥的做法是自己爬虫采集 网络公开资料整理最终合成一个统一格式的 CSV 作为系统的基础数据。核心要点是建立统一的数据字典把所有字段规范成固定类型因为后续的数据挖掘阶段输入数据的质量直接决定输出效果。2.2 爬虫采集哪些页面目标字段与页面结构我一般会把爬虫部分拆成三层列表页、详情页、补充页。考虑版权问题爬虫代码仅供个人课程作业注意遵守网站 robots 协议控制访问频率。下面是一段核心的采集逻辑目标是从某餐厅聚合平台的搜索列表页提取餐厅基本信息和详情页URL然后再进入详情页取菜品和价格数据。import requests from bs4 import BeautifulSoup import time, random import csv def fetch_list_page(city, page): url fhttps://example.com/search?city{city}page{page} headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)} resp requests.get(url, headersheaders, timeout10) soup BeautifulSoup(resp.text, html.parser) items [] # 每条记录卡片通常包含餐厅名、标签、详情页链接 for card in soup.select(.rest-item): name card.select_one(.name).text.strip() stars card.select_one(.star).text.strip() if card.select_one(.star) else 0 cuisine card.select_one(.cuisine).text.strip() if card.select_one(.cuisine) else 未知 link card.select_one(a)[href] items.append({ name: name, stars: stars, cuisine: cuisine, detail_url: link }) return items # 采集多个城市的第一页控制请求频率 all_items [] for city in [上海, 北京, 广州]: for page in range(1, 5): # 每城市只取前4页避免给目标站点压力 try: all_items.extend(fetch_list_page(city, page)) except Exception as err: print(f[警告] {city} 第{page}页失败: {err}) time.sleep(random.uniform(1, 2)) print(f共采集到 {len(all_items)} 条基础记录)这段代码有几个参数值得说明timeout10是请求超时上限防止某次网络异常导致整个程序挂掉random.uniform(1, 2)是随机延迟避免固定频率被认为是大规模抓取range(1, 5)限制页数范围课程设计有 500~800 条数据就足够了不需要全量数据。.rest-item这类选择器是假设性的需要按实际页面结构调整最好在写完整爬虫前先用开发者工具确认一下。2.3 清洗与预处理从脏数据到建模可用的干净表爬下来的数据很脏最常见的问题星级字段是“米其林一星”“二星”“三星”这样的中文字符串难以直接用于建模菜品价格里有“¥128/人”混合字符同一家餐厅在不同来源出现重复记录。清洗这一步直接决定后续算法的有效性。import pandas as pd import re df pd.read_csv(raw_restaurants.csv) # 星级转为有序数值未上榜0必比登1一星2二星3三星4 star_map {未上榜: 0, 必比登: 1, 一星: 2, 二星: 3, 三星: 4} df[star_label] df[star_text].map(star_map) # 人均消费提取数字 def parse_price(text): nums re.findall(r\d, str(text)) return int(nums[0]) if nums else None df[avg_price] df[price_text].apply(parse_price) # 重复值处理按名称地区去重保留最近一条 df df.drop_duplicates(subset[name, region], keeplast) # 缺失值处理人均价格缺失用该菜系的中位数填充 df[avg_price] df.groupby(cuisine)[avg_price].transform( lambda s: s.fillna(s.median()) ) df df.dropna(subset[star_label, avg_price]) df.to_csv(clean_restaurants.csv, indexFalse, encodingutf-8-sig) print(df[[name, star_label, avg_price, cuisine]].head())这里需要注意star_map的映射设计把“必比登”放在一星之前是因为它是推荐等级按课程设计常规处理方式归为一个过渡档位这也符合业务常识。transform(lambda s: s.fillna(s.median()))的做法比直接填全局中位数更合理因为不同菜系的价格区间差异很大法餐和快餐的中位数完全不同。encodingutf-8-sig导出 Excel 时可避免中文乱码这是容易被忽略的细节。清洗之后的字段格式要统一成数值型、类别型、文本型三类后续建模时才能方便区分特征类型。3. 数据挖掘建模星级预测、商圈聚类与菜品关联3.1 挖掘方案设计一个系统里做三个算法米其林餐厅系统归根到底要回答三类问题某家餐厅会不会被评为高星级哪些地区的餐厅更值得收藏顾客常点的菜品组合是什么对应到数据挖掘算法上是三类经典任务分类预测、聚类分析和关联规则挖掘。一份能拿高分的课设在方案设计上一定是有逻辑层次的分类模型需要标签列星级聚类模型不需要标签关联规则只需要事务型数据。三个算法各用一套参数展示时展示三种不同类型的挖掘结果评委一看就知道你对数据挖掘课程的整体脉络是清楚的。特征选择上星级预测的输入特征我通常选人均消费数值、所在城市类别编码、菜系类型类别编码、评分数值、评论数量数值输出标签是二分类“是否为一星及以上”也可以做多分类但对课设数据量来说分类过多容易翻车二分类更稳健。聚类输入特征是人均消费评分评论数量地区编码做 K-Means 聚类期望得到几个语义明确的组高档西餐组、平价小吃组、热门连锁组。关联规则输入是餐厅的招牌菜品名做 Apriori 挖掘看哪些菜经常一起出现。3.2 用随机森林做星级预测代码与参数from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import LabelEncoder from sklearn.metrics import classification_report import pandas as pd df pd.read_csv(clean_restaurants.csv) # 类别列编码城市、菜系 le_city LabelEncoder() le_cuisine LabelEncoder() df[city_enc] le_city.fit_transform(df[city]) df[cuisine_enc] le_cuisine.fit_transform(df[cuisine]) # 二分类标签1表示至少一星0表示未上榜或必比登 df[target] (df[star_label] 2).astype(int) features [avg_price, score, comment_cnt, city_enc, cuisine_enc] X df[features] y df[target] # 分层划分保证训练集和测试集中正例比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model RandomForestClassifier( n_estimators100, max_depth6, min_samples_leaf4, random_state42 ) model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred, target_names[非星级, 星级])) # 特征重要性排序 import numpy as np importance pd.DataFrame({ feature: features, importance: model.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance)这段代码里最值得琢磨的是几个参数max_depth6是树的最大深度浅树能防止过拟合毕竟我们的训练数据只有几百条min_samples_leaf4控制叶子结点的最少样本数这个值对不平衡数据尤其友好避免出现某个叶子结点只有一个样本的极端情况stratifyy分层抽样非常重要如果星级餐厅只占 20%不做分层划分很可能测试集里一条星级餐厅都没有。特征重要性输出是答辩时的亮点——你可以直接说明“人均消费和评论数是最主要的区分特征”这比笼统的“我们用了随机森林”有说服力得多。3.3 用 K-Means 聚类分析餐厅商圈特征K-Means 适合对数值连续型特征做聚类。我一般先做标准化再聚类然后统计每个簇的平均特征给簇打业务标签。from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans import pandas as pd df pd.read_csv(clean_restaurants.csv) cluster_features [avg_price, score, comment_cnt] X StandardScaler().fit_transform(df[cluster_features]) # 先跑一次带 k4 的聚类然后按簇统计特征均值 kmeans KMeans(n_clusters4, random_state42, n_init10) df[cluster] kmeans.fit_predict(X) cluster_summary df.groupby(cluster)[cluster_features].mean().round(2) print(cluster_summary) # 业务标签映射 label_map {} for cid in range(4): row cluster_summary.loc[cid] if row[avg_price] 300 and row[score] 4.5: label_map[cid] 高端精品餐厅群 elif row[avg_price] 100 and row[comment_cnt] 500: label_map[cid] 热门平价餐厅群 else: label_map[cid] 常规餐厅群 df[cluster_label] df[cluster].map(label_map)StandardScaler是必须的因为avg_price是几百上千的量级、score是 4~5 的量级不标准化时欧氏距离完全被价格主导。n_init10表示用 10 个不同的初始质心各跑一次取最优结果因为 K-Means 对初始值敏感这是调参里性价比最高的一步。聚类结果不只是可视化时画个散点图更重要的是给每个簇起个业务名字这在答辩时能让老师感觉到你不是在瞎调库。3.4 用 Apriori 挖掘菜品搭配规律关联规则适合处理“每行是一个餐厅列出该餐厅的招牌菜”这种事务数据。Apriori 的两个核心参数是min_support和min_threshold置信度。from mlxtend.frequent_patterns import apriori from mlxtend.frequent_patterns import association_rules import pandas as pd # 构造事务独热编码表行餐厅列菜品单元格1/0 # 先读取长表每行一个餐厅-菜品对 trans_df pd.read_csv(restaurant_dishes.csv) basket trans_df.groupby([restaurant_id, dish])[cnt].sum().unstack().fillna(0) basket basket.applymap(lambda x: 1 if x 0 else 0) freq_items apriori( basket, min_support0.05, use_colnamesTrue, max_len3 ) rules association_rules( freq_items, metriclift, min_threshold1.2, num_itemsetslen(basket) ) # 按提升度排序只保留前20条组合 rules rules.sort_values(lift, ascendingFalse).head(20) print(rules[[antecedents, consequents, support, confidence, lift]])min_support0.05意味着一个菜品组合至少在 5% 的餐厅里出现才被纳入候选集这个值要根据数据量调整如果只有 300 家餐厅0.05就是 15 家已经很低了如果数据达到 800 家可以试着提高到0.08减少规则数量。max_len3限制组合长度避免生成“ABCD”这种难以解读的超长组合。用lift提升度来过滤而不只信置信度是因为“某菜出现时另一菜也出现”的概率高可能是两道菜本来就是爆款提升度才更能反映真实的关联强度。挖掘出来的“松露鹅肝”“龙虾香槟”这类组合直接展示成一个“推荐搭配”页面视觉效果和业务意义都很好。4. 系统实现从挖掘模型到带界面的管理系统4.1 技术选型与整体架构一个课程设计级别的管理系统不需要微服务不要上分布式最简单稳妥的方案是前后端轻量分离后端用 FlaskPython提供接口前端用 Vue 或原生 HTML 模板数据库用 SQLite。选择这套组合的核心原因是这三个技术都能在答辩演示时快速改代码、快速重启SQLite 是一个单文件数据库不需要额外安装数据库服务数据挖掘部分的代码直接用 Python 写可以无缝嵌入到 Flask 后端里而不必用 Java 重写一遍模型。系统模块划分建议这样设计模块功能关联文件数据管理模块上传CSV、查看原始数据、编辑单条记录data_api.py挖掘分析模块触发模型训练、展示分类结果model_api.py可视化模块生成图表、展示聚类散点图chart_api.py页面展示模块前端页面路由、表格渲染views.py每条查询请求走“前端 → Flask 接口 → SQLite → 返回 JSON”的路线挖掘任务则是在请求触发时实时调用 Python 脚本运行结束后把结果写回数据库。这种架构下课上演示时只需运行python app.py浏览器打开对应地址即可不需要额外配置。4.2 数据库表结构设计三个核心表系统至少需要三张表餐厅主表、菜品事务表、挖掘结果表。餐厅主表存基本信息菜品表存每个餐厅对应的菜品挖掘结果表用于保存模型输出参数和结果。CREATE TABLE restaurants ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, city TEXT, region TEXT, cuisine TEXT, star_label INTEGER, avg_price REAL, score REAL, comment_cnt INTEGER, address TEXT ); CREATE TABLE dishes ( id INTEGER PRIMARY KEY AUTOINCREMENT, restaurant_id INTEGER NOT NULL, dish_name TEXT NOT NULL, FOREIGN KEY (restaurant_id) REFERENCES restaurants(id) ); CREATE TABLE mining_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, result_type TEXT NOT NULL, -- classification, cluster, association, stats result_json TEXT NOT NULL, -- JSON 字符串存模型输出结果 created_at TEXT DEFAULT (datetime(now, localtime)) );这里要说明一个设计巧思mining_results表用result_type区分结果类型用result_json存 JSON 字符串。把挖掘结果序列化成 JSON 存入数据库比单独建三张结果表高效得多因为展示页面只需要读出来渲染。JSON 字段的代价是不能在 SQL 里直接针对结果做复杂查询但对课设体量完全够用也让系统的扩展性大大提升。4.3 Flask 后端把训练好的模型包成接口from flask import Flask, jsonify, request import pandas as pd import json import sqlite3 from joblib import load app Flask(__name__) def query_db(sql, args()): conn sqlite3.connect(michelin.db) cur conn.cursor() cur.execute(sql, args) rows cur.fetchall() conn.close() return rows app.route(/api/restaurants, methods[GET]) def list_restaurants(): city request.args.get(city, ) sql SELECT * FROM restaurants WHERE (? OR city?) rows query_db(sql, (city, city)) return jsonify({count: len(rows), data: rows}) app.route(/api/predict, methods[POST]) def predict_star(): body request.get_json() # 接收前端传来的特征构建单条样本 DataFrame sample { avg_price: body[avg_price], score: body[score], comment_cnt: body[comment_cnt], city_enc: body[city_enc], cuisine_enc: body[cuisine_enc] } sample_df pd.DataFrame([sample]) model load(models/rf_model.joblib) prob model.predict_proba(sample_df)[0].tolist() pred int(model.predict(sample_df)[0]) # 将预测结果同样存入挖掘结果表 conn sqlite3.connect(michelin.db) conn.execute( INSERT INTO mining_results (result_type, result_json) VALUES (?, ?), (classification, json.dumps({pred: pred, prob: prob}, ensure_asciiFalse)) ) conn.commit() conn.close() return jsonify({prediction: pred, probability: prob}) app.route(/api/chart/data, methods[GET]) def chart_data(): rows query_db(SELECT avg_price, score, cluster_label FROM restaurants) return jsonify(rows)这里有两个实操要点。第一用joblib.load每次请求时加载模型会有一点性能损耗但好处是模型一旦更新接口立即生效不需要重启服务适合课程设计的现场演示。第二INSERT INTO mining_results这个动作值得保留它让每次预测都留存记录答辩时可以展示“我调用了多少次预测接口系统里查到了哪些日志记录”这种细节很加分。ensure_asciiFalse是为了让 JSON 里的中文在数据库里可读。4.4 前端可视化展示ECharts 渲染挖掘结果可视化我只推荐 ECharts因为它的文档和示例库非常完整答辩现场现改参数也来得及。核心页面由三个部分组成全量数据表格、统计分析图、挖掘结果图。// 基于 ECharts 绘制餐厅价格 vs 评分散点图按聚类标签着色 const chartDom document.getElementById(clusterChart); const chart echarts.init(chartDom); fetch(/api/chart/data) .then(res res.json()) .then(rows { const colors { 高端精品餐厅群: #9a2a2a, 热门平价餐厅群: #1e7a3a, 常规餐厅群: #3a5a9a }; const seriesData rows.map(r ({ value: [r[0], r[1]], name: r[2] || 未分类 })); chart.setOption({ xAxis: { name: 人均消费(元), type: value }, yAxis: { name: 评分, type: value }, series: [{ type: scatter, data: seriesData, symbolSize: 10 }], tooltip: { trigger: item } }); });散点图是课程设计里最能直观展示聚类效果的可视化手段。颜色按cluster_label映射让不同簇一眼分开观众如果看到红色点集中在右上角、绿色点集中在左下角分析结论“高价高分餐厅是独立一簇”就一目了然。除了散点图还需要一个柱状图展示各菜系餐厅数量、一个饼图展示星级占比这几个图足够支撑一次 15 分钟的演示了。5. 课设避坑米其林数据挖掘系统最常见的5个翻车现场5.1 数据量太小导致模型全部失效现象训练出来的随机森林准确率只有 50%几乎等于随机猜测Apriori 挖掘不出任何规则。原因我只采集了 60 条餐厅数据其中星级餐厅只有 12 条类别严重不平衡模型根本学不到有效模式。Apriori 的min_support如果设成 0.160 条数据里至少要 6 条同时出现才有候选集多数菜品组合根本达不到。解决把数据量扩充到至少 300 条且星级餐厅比例不低于 20%。如果实在采集不到采用 SMOTE 过采样扩充少数类样本虽然有一定失真但课程设计的规模下完全能接受。我的建议是“先保证数据规模再谈算法调优”数据不足时调任何参数都是玄学。5.2 收集数据时未保存原始快照现象清洗完成后发现某个字段理解错了比如把“价格区间”当成了“人均价格”却找不到原始数据在哪里。原因爬虫直接在原 DataFrame 上做drop_duplicates和fillna清洗结束后的数据覆盖了读取的 CSV 文件没有保留 raw 副本。解决在任何清洗步骤之前先执行df.to_csv(raw_backup.csv)。所有原始数据文件加_原始后缀单独保存。整个项目目录下永远保留三层数据raw/原始采集数据、clean/清洗后数据、final/建模用最终数据。这不是流程洁癖是防止中间步骤出错后无法回退的保命手段。5.3 编码问题让爬虫拿到乱码文本现象从某个聚合平台用 Requests 拿到的页面内容中所有中文都变成\u开头或乱码导致解析出的餐厅名全是垃圾值。原因部分页面的编码是 GBK 或 GB2312而resp.text使用了 Requests 默认的编码解析没有正确识别。这类情形很隐蔽页面在浏览器里看起来正常但脚本拿到的字符串就是乱码。解决统一在请求后手动指定编码例如resp.encoding gbk或从响应头获取resp.apparent_encoding再做解码。经验法则先打印resp.encoding看是什么值然后用resp.content.decode(gbk, errorsignore)做一次兜底提取确认中文正常后再批量跑全量采集。5.4 训练集和测试集数据泄漏现象随机森林在训练集上准确率 95%但测试集上暴跌到 62%答辩时被问“为什么差这么多”。原因在做LabelEncoder编码时先对整个 DataFrame 的“城市”和“菜系”列做了编码然后才切分训练集和测试集。测试集的信息混入了编码器模型在训练时其实“看”到了测试集的数据分布这就是典型的数据泄漏。此外fillna用全局中位数也是同样的风险。解决所有编码器、填充值、标准化器的fit都必须在训练集上完成然后用转换器只对测试集做transform。把数据预处理流程封装成一个build_features(fit_df, target_df)函数严格区分拟合和转换两个阶段从流程上杜绝泄漏。5.5 前端页面数据接口字段不一致现象页面表格显示正常但图表区域空白浏览器控制台报错Cannot read properties of undefined。原因后端接口返回的字段名是avg_price但前端读取的是avgPrice或者 SQL 查询返回的是元组列表而前端按对象属性访问导致r[0]和r.name混用。解决前后端换数据格式时固定用一份接口文档最重要的做法是后端统一把字段名映射成 JSON 对象里确定的格式用dict(zip(columns, row))再返回。前端在fetch后先打印一次数据确认结构再写渲染逻辑。这个坑占了调试时间的一半根源就是没有约定好“接口返回什么”。6. 进阶技巧把课程设计从“能跑”做到“能答辩”6 章的正文需要有最后一章这一章不是总结而是把重点转向进阶技巧和值得投入的优化方向。我建议从三方面做强化。第一模型可解释性。随机森林除了给出预测结果还可以输出单个样本的 SHAP 解释值。用shap.TreeExplainer(model)计算每个特征对某条预测的贡献以“人均价格 120 元使评分上升 0.3”这种可读句子展示在页面上答辩时说服力完全不一样。这一步代码量很少但视觉差距极大。第二系统扩展性设计。现在的数据采集还是一次性脚本稍微改动后就能定时增量采集例如用schedule库每周自动抓取一次页面新增数据自动进入待清洗队列。让“管理系统”拥有动态更新的数据而不是一个静态存档这是从课程设计到真实系统分水岭。第三部署透明化。不用部署到真正的服务器但要在 README 里写清环境要求Python 版本、依赖库列表、启动命令。我自己的做法是提交一个requirements.txt并且让主程序入口只剩一行python app.py能跑起来。很多代码写得好的人挂在“依赖不齐、环境起不来”这点反而是最容易提前准备好的。最后想说一个血泪经验这个项目我最早接触时把精力耗在了爬虫上花了两周差不过把数据弄齐了结果建模阶段发现数据格式设计不好回炉重造又花了一周。后来改成“先定结果表结构再反向设计数据采集字段”效率明显高了。另一个翻车的地方是加载训练好的模型时忘记检查特征顺序导致接口预测结果全错排查了一整晚才发现是特征列表顺序问题。希望这篇能从“这个题目是什么”到“怎么一步步落地”帮到你。你现在唯一要做的是先把数据采集和清洗这一步走通建模和系统只是顺理成章的事。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。