简介:机器学习在餐饮企业数据分析与预测中的完整实践资料包,面向数据分析学习者、算法工程师以及需要做经营决策的餐饮从业者,解决销售趋势预测、菜品推荐和用户流失分析等实际问题。资源文件共43个,压缩包约1.19MB,包含27个CSV数据集、6个Python脚本、7个PNG可视化图片、2个XLSX字段说明以及1个npz数据文件,覆盖原始数据、清洗结果、特征工程与模型输出等完整链路。已有242人学习下载。内容提供顾客信息、菜品订单、营业额等多维度数据及详细字段说明,代码模块涵盖数据获取、预处理、探索性分析、模型构建与评价、Apriori关联规则分析等,可辅助读者从零复现餐饮数据分析全流程。可视化图表展示了时序特征和预测效果,便于快速把握业务规律并迁移至其他行业应用。
1. 餐饮企业综合数据分析及预测:一份数据集能挖出多少经营决策
餐饮老板最头疼的不是菜品难吃,而是每天打烊后看着订单流水发愣:明天该备多少货?下个星期哪个菜该下架?会员流失到什么程度该干预?这些问题靠直觉拍板,旺季断货淡季积压是常态。机器学习餐饮企业综合数据分析及预测(含数据集及说明)要解决的,正是把流水、菜单、会员、评价这些散乱数据变成可执行的经营建议。严格说这不是一个现成的软件包,而是一条完整的数据分析流水线:从数据清洗、特征工程到营收预测、销量预测和会员流失预警,每一步都有成熟的实现路径。适合手里有一家或多家门店、已经积累了几个月以上营业数据的从业者,也适合想拿真实业务练手的数据分析初学者。这套方法跑通之后,备货量、促销时机、会员召回这些决定毛利的事,终于不再靠猜。
2. 先把数据看懂再做模型:餐饮数据集的结构、清洗与特征工程
2.1 数据集常见的字段与业务含义
餐饮企业的数据通常来自三个系统:收银机(POS)产生的订单流水、会员系统产生的注册与消费记录、第三方外卖平台导出的配送订单。把这三大块合并成一张宽表,是后续所有建模的基础。网上能找到的"餐饮企业综合数据集"字段大同小异,常见的包括这几类:
| 字段类别 | 典型字段 | 业务含义 | 建模价值 |
|---|---|---|---|
| 订单维度 | 订单号、下单时间、门店编号 | 记录每一笔交易的时间与地点 | 时间序列的粒度来源 |
| 菜品维度 | 菜品ID、菜品名称、品类、单价 | 描述卖的是什么 | 销量预测的Y变量 |
| 数量金额 | 数量、实收金额、折扣金额 | 反映交易规模 | 营收预测的直接目标 |
| 会员维度 | 会员ID、开卡时间、性别、生日 | 描述消费者画像 | RFM分层与流失预警 |
| 评价/质量 | 评分、评价内容、投诉标签 | 反映满意度和食安风险 | 文本分类与情感分析 |
需要注意,数据集说明里的"说明"文档通常标注了字段含义和单位,但真实业务数据里没有这么干净。我一般拿到数据的第一件事不是跑模型,而是先按订单号和时间排序,肉眼扫一遍数据的前几百行,确认这个数据到底是日汇总还是订单明细。如果是明细,日期字段精确到秒,做时间序列预测时要先按天或按小时聚合;如果是汇总,还要确认汇总口径是按门店还是按品类。
2.2 数据清洗的三个高频坑
数据清洗看起来是体力活,但餐饮数据有几个特殊的地方不处理干净,后面模型再怎么调参都是白搭。
第一个坑是重复订单。POS系统在网络抖动时会重传订单,导致同一条记录出现两次。常见做法是按订单号加菜品ID去重,但如果订单号本身就有重复,就需要用下单时间加金额作为联合去重键:
import pandas as pd df = pd.read_csv("restaurant_orders.csv", parse_dates=["order_time"]) # 1. 删除字段全完全相同的行 df = df.drop_duplicates() # 2. 用业务键二次去重:订单号+菜品+下单时间+金额 df = df.drop_duplicates( subset=["order_id", "dish_id", "order_time", "amount"], keep="first" ) # 3. 检查金额是否为负(退款单通常在同一个表里) refund_mask = df["amount"] < 0 print(f"退款记录数: {refund_mask.sum()}")逻辑说明:drop_duplicates是pandas最基础的去重手段,第一步去掉完全一样的行,第二步用业务键兜底。refund_mask找出退款记录,这类记录后续要么单独分析,要么从正常营收中剔除,不能直接删掉,因为退款本身就是一个值得预测的经营信号。
第二个坑是营业时间不一致。餐饮店不是全年无休的,春节、台风、装修都会导致某些天没有订单。如果直接把缺失日期填成0,模型会把"休息日"和"营业但没生意"混为一谈。我的习惯是单独维护一张营业日历表,标记每天是否营业、是否有促销活动,然后把它和订单表做左连接。预测时,模型需要知道目标日期是不是特殊日,而不是靠历史数据瞎猜。
第三个坑是菜品改名和停售。餐饮菜单经常调整,同一个菜可能从"酸辣土豆丝"改成"土豆丝(辣)",菜品ID也会变。如果按菜品ID建特征,改一次名就断了一次连续性。这个没有完美的自动解法,我通常会把菜品ID映射到"菜品家族ID",把口味微调、摆盘变化归到同一个家族下,保证时间序列连续。
2.3 特征工程的几个必做变换
餐饮数据的特征工程,核心是回答"哪些因素在影响明天的营收/销量",常见特征可以分成三类:时间特征、滞后特征、外部特征。
时间特征是把日期拆出星期几、是否周末、是否节假日、是否促销日、月份。餐饮消费有强烈的星期效应:工作日的午市和晚市走势完全不同,周五晚和周六晚是两个峰值。这类特征用一个简单的函数就能生成:
def build_time_features(df): df = df.copy() df["weekday"] = df["order_time"].dt.weekday df["is_weekend"] = df["weekday"].isin([5, 6]).astype(int) df["hour"] = df["order_time"].dt.hour df["month"] = df["order_time"].dt.month # 简单节假日标注:周一至周日中,法定节假日需要外部数据辅助 df["is_holiday"] = 0 return df逻辑说明:weekday取值范围0到6,其中5、6对应周六周日。hour字段对门店型餐饮很重要,午市(11-13点)和晚市(17-20点)的销量结构完全不同,如果做小时级预测,hour是必加特征。is_holiday这里先置0,真实场景中建议手动整理一份近三年的法定节假日表并合并进来,这是餐饮预测里最值得花时间的特征之一。
滞后特征是把历史销量搬到当前行。比如预测明天某菜品的销量,可以取过去7天同菜品的平均销量、昨天销量、上周同星期几的销量。这类特征对模型的提升往往最大,但也是数据泄漏的重灾区,后面避坑章节会展开讲。外部特征包括天气(温度、降雨量)、附近是否有大型活动,这些通常需要额外接接口,数据集里没有的话可以留空,不强求。特征工程的原则是:先从时间特征和滞后特征入手,把这两块的增益吃透,再去碰外部数据,否则很容易陷入特征越来越多、模型效果却原地踏步的窘境。
3. 营收预测与菜品销量预测:两个最实用模型的落地
3.1 营收预测:从线性回归起步的基准线
营收预测是最能给老板交出成绩单的模型。它回答的是"明天/下周大概能卖多少钱",直接影响备货和排班。很多教程一上来就上XGBoost、LSTM,但实际业务里,线性回归和随机森林往往已经能拿到80%的效果,而且可解释性好得多——老板问"为什么预测这个数"时,你能指着特征说是因为上周同期下雨。
先把订单数据按天聚合,得到每天的营收序列:
daily_revenue = ( df[df["amount"] > 0] # 剔除退款 .groupby(df["order_time"].dt.date)["amount"] .sum() .reset_index() ) daily_revenue.columns = ["date", "revenue"] daily_revenue = build_time_features(daily_revenue) # 构造滞后特征:昨天营收、上周同星期营收、7日均值 daily_revenue["rev_lag1"] = daily_revenue["revenue"].shift(1) daily_revenue["rev_lag7"] = daily_revenue["revenue"].shift(7) daily_revenue["rev_ma7"] = daily_revenue["revenue"].rolling(7).mean() daily_revenue = daily_revenue.dropna()逻辑说明:shift(1)表示取前一天的值,shift(7)表示取七天前的值,rolling(7).mean()是过去七天的滚动均值。这三个滞后特征捕捉了短期惯性、周期性、趋势性。注意dropna会把前7行丢掉,这是滞后特征必须付出的代价,样本量不够时可以缩小滞后窗口。
然后划分训练集和测试集。时间序列的划分不能用随机切分,要按时间顺序,否则就是作弊:
from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_percentage_error train = daily_revenue.iloc[:-30] test = daily_revenue.iloc[-30:] features = ["weekday", "is_weekend", "rev_lag1", "rev_lag7", "rev_ma7", "month"] model = LinearRegression() model.fit(train[features], train["revenue"]) pred = model.predict(test[features]) mape = mean_absolute_percentage_error(test["revenue"], pred) print(f"未来30天MAPE: {mape:.2%}")参数说明:iloc[:-30]表示拿最后30天做测试集,因为餐饮数据的季节性周期是7天,30天差不多能覆盖4个完整周期,能看出模型在周维度上的稳定性。features列表里weekday是类别型特征,线性回归直接吃数值也可以,但更严谨的做法是进行one-hot编码,否则周一和周二会被强行赋予线性关系。MAPE(平均绝对百分比误差)在营收预测里比RMSE更直观,老板能直接听懂"预测偏差几个点"。
线性回归跑完之后不要急着下结论,先看两个东西:一是MAPE的数值,餐饮日营收预测能做到10%以内的MAPE已经不错,超过20%说明特征或数据有问题;二是看预测残差图,如果某几天残差特别大,去查那天是不是有促销或者附近修路,这类事件往往是模型提升的下一个突破口。
3.2 销量预测:随机森林与时间特征的组合
营收预测是总量预测,销量预测则是单品预测,它的业务价值在于精确备货——每天每种菜品该准备多少份。单品销量预测比营收预测难得多,因为单个菜品的波动极大:促销日可能翻倍,天气热时麻辣菜品销量骤降。我一般不会上来就预测所有菜品,而是先挑出销量排名前10的菜品单独建模型,剩下的按品类聚合。
单品销量预测的特征组和营收预测类似,但要加上菜品自身的历史销量、同品类销量、折扣力度,这里用随机森林而不是线性回归,是因为单品销量和特征之间往往不是线性关系:折扣打到7折可能只带来10%的增量,打到5折就暴增50%,这个拐点线性模型抓不住,树模型天然能切出分段关系。
from sklearn.ensemble import RandomForestRegressor dish = df[df["dish_family_id"] == "D001"].copy() dish_daily = ( dish.groupby(dish["order_time"].dt.date)["quantity"] .sum() .reset_index() ) dish_daily.columns = ["date", "qty"] dish_daily = build_time_features(dish_daily) # 单品特有特征 dish_daily["qty_lag1"] = dish_daily["qty"].shift(1) dish_daily["qty_lag7"] = dish_daily["qty"].shift(7) dish_daily["dish_price"] = dish["price"].iloc[0] # 价格变动时需按日期取 train = dish_daily.iloc[:-14] test = dish_daily.iloc[-14:] rf = RandomForestRegressor( n_estimators=300, max_depth=8, min_samples_leaf=3, random_state=42 ) rf.fit(train[features + ["qty_lag1", "qty_lag7"]], train["qty"]) pred = rf.predict(test[features + ["qty_lag1", "qty_lag7"]])参数说明:n_estimators=300表示300棵树,餐饮数据量级通常不大,300棵是一个接近收敛的量,再往上增加对精度提升微乎其微但推理变慢。max_depth=8限制树的深度防止过拟合,因为单品销量数据量可能只有几百行,树太深会把训练集的噪声学进去。min_samples_leaf=3强制每片叶子至少3个样本,也是配合小样本的防过拟合手段。random_state=42固定随机种子,保证复现。qty_lag1和qty_lag7的构造逻辑同营收预测,区别在于这里是菜品维度,滞后特征吃的是同一道菜的历史销量。
3.3 模型评估指标怎么选
餐饮预测的评估指标选择和业务场景强相关,不能一把尺子量到底。营收预测我推荐MAPE加MAE组合看:MAPE看相对偏差,MAE看绝对偏差——一天预测差2000元和一天差200元,老板的感受完全不一样。单品销量预测更推荐用加权MAE,把销量高的菜品权重放大,因为畅销品少备一份和冷门品少备一份的损失完全不同。
还要注意一个容易忽略的问题:数据量对指标稳定性的影响。餐饮数据通常没有那么大,几百条样本里test集只有几十条,一次预测的MAPE波动会很大。我习惯的做法是跑3到5次不同切点的评估,取均值和中位数,而不是只看一次结果。如果每次的结果忽好忽坏,说明模型本身不稳,优先去补滞后特征和营业日信息,而不是调参。
4. 会员分层与流失预警:分类任务的完整闭环
4.1 会员RFM分层
营收和销量预测解决的是"货"的问题,会员分析解决的是"人"的问题。餐饮行业的会员价值差异极大:一个每周来三次的熟客和一个注册后再也没来的沉睡会员,运营策略完全不同。RFM(最近消费时间、消费频率、消费金额)是最经典的分层框架,实现不复杂。
先按会员ID聚合出三个指标:
member = df[df["member_id"].notna()].groupby("member_id").agg( last_order_date=("order_time", "max"), order_count=("order_id", "nunique"), total_spend=("amount", "sum"), first_order_date=("order_time", "min") ).reset_index() # 计算R:距今天数 ref_date = df["order_time"].max() member["recency_days"] = (ref_date - member["last_order_date"]).dt.days member["frequency"] = member["order_count"] member["monetary"] = member["total_spend"] # 按分位数打标 member["R_score"] = pd.qcut(member["recency_days"], 4, labels=[4, 3, 2, 1]) member["F_score"] = pd.qcut(member["frequency"], 4, labels=[1, 2, 3, 4]) member["M_score"] = pd.qcut(member["monetary"], 4, labels=[1, 2, 3, 4])逻辑说明:R表示最近一次消费距今几天,R_score越高代表越活跃,所以recency_days分位数越低打分越高。F对应消费频次,M对应总金额,这两个都是越大越好。四等分打1到4分,最后把三个分数拼接成三段式的分群标签,比如"441"就是高活跃高频次但低金额的会员,对应的是常来但客单价低的用户,运营策略应该是推荐高客单价套餐。注意pd.qcut的分位数会随数据集变化,新数据加入后分层边界会漂移,这是RFM的固有限制,习惯上定期重算即可。
4.2 流失预警的标签定义与模型训练
RFM分层是描述性的,流失预警则是预测性的目标:未来30天内,哪些会员有较大概率不再来消费。流失预警最关键的一步不是选模型,而是定义"流失"这个标签。
标签定义有两种思路。第一种是"未复购预警":把数据切成两个时间段,用前一段时间(特征期)的会员行为做特征,看后一段时间(观察期)内该会员有没有再来消费,没有就标记为流失。这种思路最简单直观,切分示意如下:
import datetime as dt feature_end = ref_date - dt.timedelta(days=30) label_end = ref_date df["is_consumed"] = 1 # 在特征期内聚合会员行为 member_features = ( df[df["order_time"] <= feature_end] .groupby("member_id") .agg( freq_feat=("order_id", "nunique"), spend_feat=("amount", "sum"), recency_feat=("order_time", "max") ) ) # 在观察期内判断是否流失 member_labels = ( df[df["order_time"] > feature_end] .groupby("member_id")["order_id"] .nunique() .reset_index() ) member_labels["churned"] = (member_labels["order_id"] == 0).astype(int)逻辑说明:feature_end是特征期的截止日,label_end是数据末尾。df["order_time"] <= feature_end用来筛出特征期的行为,df["order_time"] > feature_end用来筛出观察期的复购情况。观察期内没有任何订单的会员标记为churned=1,有订单的标记为0。这里有个隐含设定:能计算标签的会员必须至少有一次消费在特征期之前,否则无法区分"新注册未消费"和"老会员流失",这个过滤条件在做merge时要用inner保证。
模型训练阶段选择不做特殊化,用逻辑回归和梯度提升树各跑一遍对比。逻辑回归的优势是系数可解释,比如"距离上次消费每多一天,流失概率上升X个点",这个结果老板听得懂;梯度提升树(如XGBoost或LightGBM)的精度通常更高,但解释性差。我在业务落地时通常双轨并行:先用逻辑回归做解释性分析,定格运营策略,再用树模型做批量打分,把高流失概率会员名单导给运营做定向召回。
5. 餐饮数据分析避坑:数据泄漏、节假日偏差与季节性误导
数据泄漏是餐饮数据分析里最隐蔽、也最危险的错误,轻则模型评估虚高,重则上线后预测值完全偏离真实情况。我见过最典型的场景是:把当天的促销活动信息放进特征里预测当天营收,模型训练时的表现接近完美,但真到了预测未来时根本没有当天的活动信息,模型直接作废。更隐蔽的泄漏来自滞后特征的构造,比如用未来7天的平均销量预测今天的销量,这在代码上不容易发现,需要逐个特征检查它是否在预测时刻一定可知。检查方法很简单:假设现在是某一天晚上10点,关掉电脑,看看你手上的特征表里每一个字段是不是都能拿到,拿不到的就有泄漏嫌疑。
5.1 现象:训练集MAPE为3%,测试集MAPE变成30%
有一次我在某个模拟项目X上做营收预测,训练集的MAPE低到3%,我心里觉得不对劲——餐饮行业的噪声很大,通常在10%左右才合理。后来查了特征发现,我无意中把当天的实际销量也当作特征放进去了。原因是做数据合并时,把原始订单表和聚合特征表做了内连接,导致未来信息被带进了训练集。解决方法是重新设计特征构造流程,把特征表按时间点快照生成,每个特征只依赖该时间点之前的数据,测试集再用同样的流程离线重放。
5.2 现象:模型在节假日后一天大幅高估销量
餐饮数据的季节性不只是春夏秋冬,还包括每周、每天的节律,以及法定节假日带来的脉冲波动。最折磨人的是节假日效应:国庆假期前几天销量飙升,假期后一天断崖式下跌,用一个简单的星期特征完全抓不住。曾经有个模型在假期后的日期预测值比实际值高了接近两倍,原因是模型把假期的高销量当成了趋势延续。解决方法是专门为节假日创建一个二值特征,并限制滞后特征的窗口,让模型知道节假日后的第二天不应该参照节假日的销量,而应该参照正常周同期的销量。这个问题的本质是特征和业务日历没有对齐,靠模型自己学习这种模式,数据量不够时学不出来。
5.3 现象:预测新店时模型失效
如果手里有多家门店的数据,训练集里包含新店的早期数据,模型预测新店未来时往往表现极差。原因很简单:新店没有任何历史滞后特征,shift生成的lag特征的值为空,模型不知道该怎么预测。解决方法是直接用"同商圈老店"的数据做迁移,或者给新店单独建一个简化模型——只用时间特征和商圈特征,不做lag特征。某些数据集说明里提到跨店预测的场景,但实际落地时我非常建议每家店单独训练模型,除非门店数量实在太多且每店数据量不足。
5.4 现象:滚动均值特征导致预测严重滞后
滚动均值特征(rolling mean)在时间序列里很有用,但有个天然缺陷:它对突变不敏感。当营收突然上涨时,7日均值要等好几天才能跟上实际值,导致预测值系统性偏低。我一般的做法是同时保留滚动均值和滞后值两个特征,让模型自己学习在什么情况下更信任哪个。也可以用指数加权移动平均(EWMA)代替简单移动平均,让近期的数据权重更大,衰减速度可以用alpha参数控制,alpha越大对近期数据越敏感,顺便也缓解了滞后失真。
5.5 现象:拆分数据集时随机切分导致指标虚高
时间序列数据的训练集和测试集必须按时间切,有些初学者会习惯性地用train_test_split,这个函数默认是随机切分的。一旦随机切分,训练集里包含未来的数据,测试集里包含过去的数据,模型等于提前看到了答案。这在餐饮数据里极其危险,因为菜品销量有强烈的趋势和周期,随机切分会让模型"偷看"到周期规律。解决方式是强制使用按时间索引的切分,比如按日期排序后取前80%为训练集、后20%为测试集。另外,评估时也不能只用确定的那20%作为测试集,最好是按时间顺序滚动多次评估,这个技巧在最后一章展开。
6. 用滚动时间窗口验证模型:一套通用的验证技巧
6.1 滚动时间窗口验证
单次切分测试集有一个致命的问题:测试集只覆盖了某个时间段(比如11月),但餐饮消费行为在11月和1月(春节前)的表现可能完全不同,一次测试通过不代表模型在全年都稳定。我习惯用滚动时间窗口做验证,代码框架如下:
def rolling_evaluate(df, features, target, min_train=180, step=30): results = [] dates = sorted(df["date"].unique()) for test_end in range(min_train, len(dates), step): train_dates = dates[:test_end] test_dates = dates[test_end:test_end+step] if len(test_dates) < 7: continue train = df[df["date"].isin(train_dates)] test = df[df["date"].isin(test_dates)] model = LinearRegression() model.fit(train[features], train[target]) pred = model.predict(test[features]) mape = mean_absolute_percentage_error(test[target], pred) results.append({"window_end": test_dates[-1], "mape": mape}) return pd.DataFrame(results)逻辑说明:这个函数从第min_train+1个日期开始,每次往后推step天做一次训练和预测,预测完把窗口加大30天继续。返回的结果能画出MAE随时间的曲线,如果某个时间段的误差特别大,就去查当时发生了什么业务事件。min_train=180表示至少需要半年的训练数据,太短模型学不出季节性。step=30表示每月验证一次,这个频率能覆盖足够的周期又不至于计算量太大。这个技巧最大价值不是调参,而是帮你看清模型在一年时间里的稳定性,能做到这一点,模型上线后的心理预期才踏实。
6.2 营业日对齐
滚动验证还有一个锦上添花的做法:把测试集只留"正常营业日",剔除非营业日和促销日,单独评估模型在常规日的精度。同时把异常日子的误差也单独统计——这能告诉你模型在非常规日到底偏了多少,方便提前准备人工预案。这个细节看起来小,但你真正向老板汇报模型效果时,一句"常规营业日误差5%,节假日误差12%,需要人工干预"比一个笼统的平均数有说服力得多。
6.3 把自己当成值班经理验证特征
最后一个习惯我坚持了很久:每构造完一个特征,我就问自己一句"今天晚上10点我站在收银台后面,明天的数据里这个特征我能不能提前知道"。lag1、lag7、星期几、是不是节假日,这些都能知道;"未来三天天气""当天的实际客流量"这些一律不能。这个习惯帮我拦下了好多看似涨点、实则泄漏的特征。餐饮数据的分析说到底不是竞赛刷分,是把预测函数稳稳地跑在业务上,少一些自欺欺人,多一些按时间线重放验证,模型自然经得起真实经营的检验。
餐饮数据分析及预测这个方向,从数据清洗、特征工程到营收和菜品销量预测、再到会员流失预警,是一套可以逐步落地的完整方案。很多从业者拿着某份数据集跑了一次线性回归就说"我做完了",但真正能投入业务的模型,至少要在离线滚动验证中证明自己在不同季节、不同节假日下都稳定。我自己吃过数据泄漏的亏,也经历过节假日预测翻车的尴尬,这些坑总结下来就是一句话:尊重时间顺序,做到每步特征都可以在预测时刻真实获取。希望你从这套方法论里拿到的不只是一堆代码,更是一套能应对真实餐饮业务波动的分析习惯,这样你的模型才真正有用。希望帮到你。
本文还有配套的精品资源,点击获取