news 2026/9/29 18:17:44

披萨订单数据集实战:从数据清洗到特征工程与销量预测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
披萨订单数据集实战:从数据清洗到特征工程与销量预测

简介:一份围绕披萨订单数据集的机器学习实战包,面向有一定Python基础、想系统训练数据分析与建模能力的读者。案例覆盖从数据预处理、EDA可视化到决策树回归、网格搜索、交叉验证、聚类和时间序列分解等完整流程,适合作为课堂作业、竞赛入门或简历项目的参考。压缩包共21个文件,含19个可运行的.py源代码、1个约7.96MB的pizza_sales.csv原始数据集和1个readme.txt说明,压缩后仅714KB;脚本按分析主题拆分,便于按需查阅与复现。目前已有43人学习。代码手工整理、无语法错误,综合运用Pandas、Scikit-learn、Seaborn/Matplotlib、Plotly及SciPy/Statsmodels等工具,融入统计检验和交互式图表。读者可从数据清洗起步,逐步理解回归模型在销售预测中的应用,并掌握评估、调参与业务洞察的完整方法,省去数据获取和环境配置成本。

1. 披萨订单数据集:一份自带业务逻辑的机器学习入门资源

先说结论:这份披萨订单数据集,不是那种扫一眼就懂的“玩具数据”。它包含订单表、明细表、披萨类型表之间的关联关系,有日期、时间、数量、单价、总价、类别、尺寸等多个字段。用 Python 做机器学习分析时,你能同时练到数据清洗、特征工程、时间序列处理和回归预测一整条链路。项目里附带的 19 个源代码文件,正好对应从读数据到出预测结果的完整流程,7.96 MB 的数据集也足够撑起多次实验,不用自己去网上东拼西凑。适合两类人:一类是刚学完 Python 基础、想找个真实业务数据集练手的入门者;另一类是准备面试、需要拿一个完整分析项目做作品集的同学。它最大的价值在于——订单数据里天然带着“星期几、几点钟、什么品类卖得好”这些可挖掘的业务特征,模型能不能预测准,拼的就是特征做得好不好,而不是模型有多高级。

2. 数据长什么样:从 CSV 读取到字段含义梳理

2.1 打开压缩包后先建立文件清单

拿到 zip 包后,第一步不是急着跑代码,而是把文件结构理清楚。通常这类资源解压后的目录大概是这样的:

pizza_order_project/ ├── data/ │ ├── orders.csv │ ├── order_details.csv │ └── pizzas.csv ├── notebooks/ │ ├── 01_数据加载与探索.ipynb │ └── 02_可视化分析.ipynb ├── scripts/ │ ├── data_preprocess.py │ ├── feature_engineering.py │ ├── train_model.py │ └── predict.py └── README.md

我一般会先打开 README 看它声明的字段说明,再手动用 Excel 或者 DataFrame 快速瞄一眼每个 CSV 的表头和前几行。这样做的好处是避免后面写代码时对字段名想当然——比如有的文件里叫pizza_id,有的叫pizza_type_id,拼写和含义差一点,join 的时候就会出问题。

实际读取时,我习惯先把三个表都读进来,分别打印 shape 和 dtypes,确认数据类型是否符合预期。日期列在 CSV 里通常是字符串,必须显式转成 datetime 类型,否则后续做时间序列特征时会直接报错或者得到错误结果。

2.2 关联表结构与主键逻辑

披萨订单这类数据集通常是星型结构:orders是事实表,记录每个订单的日期、时间和总价;order_details是明细表,记录每个订单里具体点了什么披萨、数量多少;pizzas和pizza_types是维度表,记录披萨的尺寸、类别、配料信息。

import pandas as pd orders = pd.read_csv('data/orders.csv') order_details = pd.read_csv('data/order_details.csv') pizzas = pd.read_csv('data/pizzas.csv') # 查看表结构和字段类型 print(orders.shape, order_details.shape, pizzas.shape) print(orders.dtypes) print(order_details.dtypes) print(pizzas.dtypes) # 确认关联字段是否有空值 print(orders.isnull().sum()) print(order_details.isnull().sum())

这段代码做的事很简单,但很重要:先确认三个表的行数是否在一个数量级内,再检查关键字段类型,最后看空值分布。实际数据里,pizza_types表可能包含配料字符串字段,这个字段在特征工程阶段会变成文本长度、配料数量等衍生特征。注意主键关联的时候,先确认order_id在两个表里的数据类型一致——一个读成了 int,一个读成了字符串,join 时匹配不上是常事。

做了这一步之后,你会在脑子里建立起一张图:订单和明细是一对多关系,披萨和披萨类型是一对一或一对多关系。后面做特征聚合时,该在哪个粒度上 groupby,就取决于这张关系图。

3. 数据清洗与预处理:把脏数据变成可喂给模型的样子

3.1 缺失值与异常值处理策略

真实数据集从来不会干干净净。我打开订单数据后,首先会看几件事:日期范围是否完整、单价和数量有没有零值或负数、订单明细表里有没有重复行。披萨订单场景里最常见的坑是:部分订单可能包含“取消”状态的行,或者同一种披萨在同一订单中出现多行(本该合并成一行但没合并)。

# 去除重复行 order_details = order_details.drop_duplicates() # 过滤掉数量为0或负数的记录 order_details = order_details[order_details['quantity'] > 0] # 检查订单日期范围 orders['order_date'] = pd.to_datetime(orders['order_date']) print(orders['order_date'].min(), orders['order_date'].max()) # 计算订单总价与明细总价是否一致 detail_total = order_details.groupby('order_id')['total_price'].sum().reset_index() detail_total.columns = ['order_id', 'calc_total'] merged = orders.merge(detail_total, on='order_id', how='left') inconsistency = merged[abs(merged['calc_total'] - merged['total_price']) > 0.01] print(f'价格不一致的订单数: {len(inconsistency)}')

这段代码末尾的“订单总价 vs 明细总价核对”是很多人会跳过的步骤,但我觉得它是清洗阶段最有价值的一步。如果发现大量订单的明细汇总和订单表里的总价对不上,说明原始数据有录入误差或者订单表本身还包含额外费用(比如配送费、折扣)。此时就要决定:是删掉这些不一致的行,还是以明细汇总为准重建总价。常见做法是以明细表为准,因为明细是流水记录,错误率更低。

缺失值处理上,披萨数据量的缺失情况通常不严重。个别pizza_type或者ingredients字段缺失时,我一般用众数填充,或者直接删掉占比很小的缺失行。不建议用均值填充文本类字段,这对后续特征工程反而有害。

3.2 时间字段拆解与业务分组

订单数据的核心价值在时间。原始的order_date和order_time字段如果不做拆分,对模型来说只是一个时间戳,提取不出任何周期性规律。所以特征工程的第一个动作就是把时间拆成年、月、日、星期几、小时、是否为周末等。

orders['order_date'] = pd.to_datetime(orders['order_date']) orders['order_time'] = pd.to_datetime(orders['order_time'], format='%H:%M:%S') orders['hour'] = orders['order_time'].dt.hour orders['weekday'] = orders['order_date'].dt.weekday # 周一=0 orders['is_weekend'] = orders['weekday'].apply(lambda x: 1 if x >= 5 else 0) orders['month'] = orders['order_date'].dt.month orders['day_of_month'] = orders['order_date'].dt.day # 将时间窗口划分成业务时段 def time_period(hour): if 6 <= hour < 11: return 'morning' elif 11 <= hour < 14: return 'lunch_rush' elif 14 <= hour < 17: return 'afternoon' elif 17 <= hour < 21: return 'dinner_rush' else: return 'night' orders['time_period'] = orders['hour'].apply(time_period)

weekday是 0 到 6 的整数,比直接用字符串“星期一”更适合作为模型输入。time_period的划分不是固定的,你可以根据数据分布调整阈值——如果发现某个门店的午餐高峰在 10:30 开始,就把 11 改成 10。这种业务分组特征对树模型特别友好,它把连续变量转成了有业务含义的离散变量,模型很容易学出“晚餐高峰时段销量高”这类规则。

我一般在做这一步的同时,顺手做一次探索性可视化:按小时画销量柱状图、按星期画平均订单量折线图。这能验证字段拆分得对不对——如果画出来的图没有明显的双峰或者周期性,说明时间字段可能有解析问题,需要回头检查。

4. 特征工程到模型训练:从聚合特征到预测披萨销量

4.1 聚合特征构建:把明细表变成训练样本

建模之前先想清楚一个问题:预测目标是什么?如果是预测“未来某天某个时段的总销量”,那训练样本就应该按小时或按天聚合;如果是预测“某类披萨的销量”,那就要在明细表上按类别聚合。特征工程的粒度决定了模型能回答什么问题。

以“按小时预测总订单量”为例,训练样本的构造逻辑是:把明细表按订单日期+小时聚合,得到每个时段的总订单数和总披萨数量,再关联当天是星期几、是否周末、是否节假日等外部特征。

# 按小时聚合订单量 order_details = order_details.merge( orders[['order_id', 'order_date', 'hour', 'weekday', 'is_weekend']], on='order_id', how='left' ) hourly_sales = order_details.groupby(['order_date', 'hour']).agg( total_orders=('order_id', 'nunique'), total_pizzas=('quantity', 'sum'), total_revenue=('total_price', 'sum') ).reset_index() # 添加时间特征 hourly_sales['weekday'] = pd.to_datetime(hourly_sales['order_date']).dt.weekday hourly_sales['is_weekend'] = hourly_sales['weekday'].apply(lambda x: 1 if x >= 5 else 0) hourly_sales['hour'] = hourly_sales['hour'].astype(int) # 添加滞后特征:前一天同时段的销量 hourly_sales = hourly_sales.sort_values(['order_date', 'hour']) hourly_sales['lag_1_day_sales'] = hourly_sales.groupby('hour')['total_orders'].shift(1) print(hourly_sales.head(10))

shift(1)在这里是精髓。按小时分组后 shift,拿的是每个小时“前一天同一小时”的销量,而不是上一行的销量。这两者区别很大,用错了滞后特征直接让模型学到错误的时间依赖。groupby('hour')保证 shift 只在同一小时窗口内移动,跨日期序列的连续性就靠这个操作维持。

聚合后的数据集,行数会比明细表少很多。这时候可以做几次简单的可视化,确认聚合结果没有异常断层——比如某一天的数据缺失导致滞后特征全是 NaN,这种要提前处理。

4.2 训练集划分与模型选型

披萨销量预测本质上是回归问题。常用的候选模型有:线性回归(作为基线)、随机森林、XGBoost、LightGBM。我一般先用线性回归搭一个基线,看 RMSE 大概是什么水平,然后直接上 LightGBM 对比提升幅度。

import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error # 丢弃NaN的滞后特征行 model_data = hourly_sales.dropna(subset=['lag_1_day_sales']).copy() features = ['hour', 'weekday', 'is_weekend', 'lag_1_day_sales'] X = model_data[features] y = model_data['total_orders'] # 按时间顺序划分,防止数据泄露 split_idx = int(len(X) * 0.8) X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:] y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:] model = lgb.LGBMRegressor( n_estimators=200, learning_rate=0.05, max_depth=5, num_leaves=15, random_state=42 ) model.fit(X_train, y_train) y_pred = model.predict(X_val) rmse = mean_squared_error(y_val, y_pred, squared=False) print(f'验证集 RMSE: {rmse:.2f}')

注意训练集划分用了“按时间顺序切分”而不是随机切分。时间序列数据如果用train_test_split默认的随机切分,未来数据会混进训练集,模型评估结果会虚高,这个坑在第 5 章会重点讲。squared=False参数把 MSE 转成 RMSE,单位是“订单数”,业务上更好解释。

num_leaves=15对应max_depth=5,这个配套关系是 LightGBM 的典型设置。叶子数超过2^max_depth时模型容易过拟合,新手常犯的错是只调max_depth不改num_leaves,导致模型能力超出预期。在披萨订单这种样本量不算大的数据集上,小树配合低学习率效果更稳。

4.3 特征重要性读取与业务验证

模型跑通后,一定要看特征重要性。这能验证之前做的特征工程有没有生效,也能反向帮我们理解业务规律。

# 获取特征重要性 importance = pd.DataFrame({ 'feature': features, 'importance': model.feature_importances_ }).sort_values('importance', ascending=False) print(importance) # 查看模型对时间的理解是否合理 import matplotlib.pyplot as plt hour_range = pd.DataFrame({ 'hour': list(range(10, 22)), 'weekday': [2] * 12, 'is_weekend': [0] * 12, 'lag_1_day_sales': [20] * 12 }) pred_sales = model.predict(hour_range) plt.plot(hour_range['hour'], pred_sales) plt.xlabel('Hour') plt.ylabel('Predicted Orders') plt.show()

如果lag_1_day_sales的特征重要性排在第一位,说明销量确实有周期性,昨天的同一时段销量对今天有很强的参考价值。如果hour排第一,说明时间段的区分度比历史数据更强。这个输出结果,写进分析报告里是最有说服力的素材——它证明你不是把数据喂给黑匣子就完事,而是真的理解了每个特征的作用。用pandas.DataFrame包装特征重要性输出而不是直接打印数组,后面做可视化会方便很多。

5. 避坑与常见问题:数据泄露、时间格式与采样偏差

5.1 数据泄露:随机划分训练集导致评估虚高

现象:用train_test_split(X, y, test_size=0.2, random_state=42)随机划分后,模型在验证集上表现非常好,RMSE 很低,但一旦拿最新的真实订单去预测,误差立刻变大。

原因:时间序列数据里,相邻日期的样本具有高度相似性。随机划分时,同一天或相邻日期的样本分别出现在训练集和验证集中,模型相当于“见过”未来数据的相似版本,评估结果自然虚高。披萨订单这种按天记录的数据,这个效应尤其明显。

解决:强制执行时间顺序划分——先按日期排序,取前 80% 做训练,后 20% 做验证。如果要做交叉验证,用TimeSeriesSplit而不是KFold。我习惯把划分逻辑封装成一个带日期参数的小工具函数,每次建模前强制走一遍,避免临时用错。

5.2 日期时间解析静默失败

现象:代码不报错,但画图时发现订单量的时间分布完全不对——比如所有订单都集中在一个小时,或者日期排序错乱。

原因:CSV 里的时间字段统一被 pandas 推断为字符串,某些行的时间格式不标准(比如15:00:00变成了15:0:00),直接pd.to_datetime解析时这些行变成了NaT,后续聚合统计默认忽略NaT,导致数据量减少且分布异常。

解决:解析时间时加上errors='coerce'再显式检查isna的比例。不要相信 pandas 的自动推断,手动指定format='%H:%M:%S'能提前暴露格式问题。

5.3 小时聚合时跨天数据错位

现象:滞后特征算出来后,发现凌晨 0 点到 2 点的预测值异常偏高,甚至比午餐高峰还高。

原因:披萨订单数据里,凌晨时段本身单量极少,某些天可能完全没有订单。groupby('hour').shift(1)遇到缺失的前一天数据时会返回 NaN,默认删除后,模型没见过这些“稀疏时段”的真实分布,预测值就倾向于使用滞后值的扩散结果。

解决:凌晨时段单独处理,或者直接删除凌晨 0-6 点的数据再建模。对这个场景,删除比填充更好——凌晨单量低到对业务决策没有参考价值,留着只会干扰模型学习正常时段规律。

5.4 价格一致性校验被忽略

现象:模型加进total_price相关特征后,准确率反而下降,且特征重要性里total_price排名很低。

原因:明细表里某几行total_price和quantity * unit_price不一致——可能是原始录入时单价变了但总价没更新,导致模型学到的是错误的相关性。

解决:在预处理阶段加入价格一致性校验,对不一致的样本统一按quantity * unit_price重算总价,并记录修正了多少行。这些修正记录在写报告时还能作为数据质量的佐证。

6. 进阶:给模型加一处“后悔药”验证,用 SHAP 解释预测逻辑

模型训练完、评估指标也不错,但离“能说服别人”还差一步。很多人拿到 RMSE 就结束了,但其实用一个稍微进阶的手段——SHAP 值分析,能看出模型到底靠什么特征做决策,这对于业务方理解预测结果至关重要。

import shap # 用训练好的模型构建 SHAP explainer explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_val) # 输出每个特征对预测的贡献方向 shap.summary_plot(shap_values, X_val, feature_names=features)

shap.TreeExplainer对 LightGBM 这类树模型是零成本集成的,不需要额外训练模型,直接拿现有模型做解释。输出图里每个点代表一个样本,横轴是 SHAP 值,表示该特征对这个样本预测结果的贡献方向和大小。如果hour的 SHAP 值在 11-13 点区间内集中为正值,说明午餐高峰对预测的拉动作用被模型明确学到了——这个结论可以直接写进报告里。

使用 SHAP 前要确认版本兼容性。新版本的shap.Explainer(model, X_val)和旧版TreeExplainer接口有差异,如果你的环境报AttributeError,退回用shap.TreeExplainer大概率能跑通。这个库和 pandas 的版本冲突偶尔会出现,建议在虚拟环境里单独安装。

除了 SHAP,另一个值得做的小实验是验证滞后特征的时间窗敏感性。把lag_1_day_sales换成“过去 7 天同一小时的平均销量”,再和原模型对比 RMSE。这个对比能提供一个有价值的结论:披萨订单的周期性到底以一周为周期还是以一天为周期。如果 7 天均值特征效果更好,说明周周期性更明显;如果 1 天滞后效果更好,说明日周期性占主导。两种结果写进报告里都是亮点,因为它展示了你对时间特征的理解不是停留在表面。

这几步走完之后,你会收获一份完整的训练脚本(加载 → 清洗 → 聚合 → 建模 → 解释)、一组可复现的实验结果,以及一个拿得出手的 SHAP 可视化。每次跑完模型我都强制自己走一遍 SHAP,哪怕只是扫一眼图——我已经不止一次靠这个发现了特征之间的隐藏交互,比如“周末晚上”这个组合的预测贡献值明显大于“周末”和“晚上”分别的价值之和。这种发现,光看 RMSE 永远看不到。希望这些经验能帮你在同样的数据上少走几步弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 18:17:22

PowerShell禁止运行脚本?四步解决npm run dev报错

在 Windows 上做前端开发&#xff0c;几乎每个人都有一道躲不开的坎&#xff1a;代码写完了&#xff0c;忐忐忑忑打开终端&#xff0c;输入 npm run dev &#xff0c;结果回车之后没有等来 Vite 或者 Webpack 的启动页&#xff0c;反而等来一串红字—— npm : 无法加载文件 …

作者头像 李华
网站建设 2026/9/29 18:17:22

16GB显卡跑27B大模型256K上下文:llama.cpp分层卸载与KV Cache量化实战

1. 为什么要在16GB显卡上跑27B大模型1.1 一个看似不可能的任务16GB显存&#xff0c;27B参数&#xff0c;256K上下文。把这三个数字放在一起&#xff0c;任何一个有本地部署经验的人第一反应都是"不可能"。按照常规认知&#xff0c;27B模型即使做4-bit量化&#xff0c…

作者头像 李华
网站建设 2026/9/29 18:17:20

从零实战AI智能体:架构设计、工作流搭建与踩坑复盘

最近后台收到不少朋友的私信&#xff0c;都在问同一个问题&#xff1a;网上铺天盖地讲AI智能体&#xff0c;到底怎么从零开始把一个Agent做出来&#xff0c;而不是只跑通一个Demo&#xff1f;说实话&#xff0c;我从去年开始用大模型API做自动化工具&#xff0c;到今年正式把Ag…

作者头像 李华
网站建设 2026/9/29 18:15:44

4D高斯溅射:动态三维重建的时空建模范式

1. 什么是4DGS&#xff1a;不是“升级版3D”&#xff0c;而是动态世界的建模范式革命你最近刷技术社区&#xff0c;大概率已经看到这个词被反复提起&#xff1a;4DGS。它不像“元宇宙”那样空泛&#xff0c;也不像“AIGC”那样宽泛到失去焦点——它精准地戳中了一个长期卡在图形…

作者头像 李华