ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python机器学习实战:外卖送餐时间预测模型全流程

Python机器学习实战:外卖送餐时间预测模型全流程 简介这份资源面向具备一定Python基础、希望入门机器学习实战的开发者与数据爱好者围绕外卖送餐时间预测这一典型回归问题展开。它基于Kaggle公开数据集涵盖地点、外卖员ID、评分等字段通过计算经纬度距离挖掘特征间关系并搭建LSTM神经网络完成送餐时长建模帮助读者理解时间预估背后的机理。压缩包共2个文件包含1个txt数据集与1个py代码脚本整体约942KB体量轻便便于快速上手复现。目前已有1055人学习下载说明该案例在实战练习中具有一定参考价值。读者可借此获得一套完整的特征工程与LSTM建模流程从数据清洗、距离计算到模型训练与评估逐步实践理解神经网络在时空预测任务中的应用思路适合作为课程设计、竞赛练手或自学项目的参考范例。1. 外卖送餐时间预测从「预计 38 分钟」到可复现的机器学习流水线点外卖时那个「预计 38 分钟送达」的数字背后其实是一条完整的机器学习推理链路。骑手接单、商家出餐、路况拥堵、天气变化、楼层高低每一个环节都在往最终时间上叠加噪声。用 Python 机器学习预测外卖送餐时间本质是把这些噪声拆成可量化的特征再训练一个回归模型去逼近真实送达时长。这件事适合谁做想入门机器学习但苦于没有真实场景的 Python 学习者、做本地生活调度的后端工程师、以及需要给配送系统加一层预估能力的产品技术团队。它不需要 GPU 集群一台普通笔记本加一份历史订单数据就能跑通全流程。下面我按自己实际搭过的方案把数据构造、特征工程、模型训练、调参和踩坑一条线讲清楚让你看完能直接复现。2. 先搞清楚预测目标送餐时间到底怎么定义2.1 标签定义决定了模型上限很多人一上来就抓数据、套模型结果 RMSE 怎么调都下不去问题往往出在标签本身。外卖送餐时间不是一个单一变量它至少可以拆成三种口径下单到送达的总时长、骑手接单到送达的配送时长、商家出餐到送达的履约时长。你选哪个当标签直接决定了特征该怎么设计。我一般用「下单时间戳到送达时间戳」的总时长作为预测目标因为这是用户真正感知到的数字。但这个口径里混入了商家出餐时间而商家出餐时间在历史数据里往往缺失或不准。常见做法是如果数据里有「骑手取餐时间」字段就用取餐到送达作为主标签把出餐时长单独建一个子模型去估。两个模型串联比硬用一个端到端模型去拟合全部噪声要稳。标签还要做异常值清洗。超过 120 分钟的订单大概率是异常单取消、地址错误、极端天气直接截断或剔除。低于 10 分钟的订单也要检查可能是测试单或自提单。这一步不做模型会被长尾拉偏。2.2 特征体系从订单、骑手、商家、环境四个维度展开特征工程是这类任务里最花时间也最值钱的部分。我习惯按四个维度组织维度典型特征获取难度订单菜品数量、订单金额、是否预订单、备注长度低商家出餐速度历史均值、当前待处理单量、品类中骑手同时接单数、历史准时率、当前已行驶距离高环境时段、星期、天气、距离、路况等级中其中「距离」是最强特征但注意直线距离和骑行距离差别很大。有条件就用路径规划 API 拿骑行距离没条件就用经纬度算 Haversine 距离再乘一个 1.3 到 1.5 的绕路系数。这个系数因城市而异北京上海老城区偏高新区偏低最好按区域分组统计。时段特征不要只给一个「小时」整数要构造「是否午高峰 11:00-13:00」「是否晚高峰 17:30-19:30」这样的布尔特征再叠加一个「距最近高峰的时间差」。模型对周期性特征的捕捉能力有限人工拆出来效果立竿见影。2.3 数据切分时间序列切分而不是随机切分这是新手最容易翻车的地方。外卖数据天然带时间顺序如果你用 train_test_split 随机切分未来数据会泄漏到训练集里离线指标好看得离谱上线就崩。正确做法是按时间切前 80% 时间段的订单做训练后 20% 做验证。如果要做交叉验证用 TimeSeriesSplit。import pandas as pd from sklearn.model_selection import TimeSeriesSplit # df 已按订单时间升序排列 df df.sort_values(order_time).reset_index(dropTrue) # 按时间切分前 80% 训练后 20% 验证 split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx] valid_df df.iloc[split_idx:] # 时间序列交叉验证 tscv TimeSeriesSplit(n_splits5) for train_index, val_index in tscv.split(train_df): X_tr, X_val train_df.iloc[train_index], train_df.iloc[val_index] # 后续训练逻辑这段代码的关键点是sort_values(order_time)和TimeSeriesSplit。前者保证数据严格按时间排列后者在训练集内部再做滚动切分每一折的验证集都在训练集之后。参数n_splits5表示切 5 折数据量小于 1 万条时建议降到 3 折否则每折验证集太小指标波动大。3. 用 Python 搭一条能跑通的训练流水线3.1 环境准备与依赖安装我一般用 conda 建一个干净环境避免和系统 Python 打架。Python 版本选 3.9 或 3.10太新的版本有些库轮子还没跟上。conda create -n delivery-eta python3.10 -y conda activate delivery-eta pip install pandas numpy scikit-learn lightgbm xgboost matplotlib joblib这里选 LightGBM 作为主力模型原因是它在表格数据上训练快、内存占用低、对缺失值和类别特征友好调参难度也比 XGBoost 低一档。scikit-learn 用来做基线模型和特征预处理joblib 用来持久化模型。3.2 特征工程代码把原始字段变成模型能吃的数字假设你手上有这样一张订单表order_time、pickup_time、delivery_time、shop_lat、shop_lng、user_lat、user_lng、item_count、order_amount、weather、rider_id。下面这段代码把原始字段加工成特征矩阵。import numpy as np import pandas as pd def haversine(lat1, lng1, lat2, lng2): 计算两点球面距离单位公里 R 6371.0 lat1, lng1, lat2, lng2 map(np.radians, [lat1, lng1, lat2, lng2]) dlat lat2 - lat1 dlng lng2 - lng1 a np.sin(dlat/2)**2 np.cos(lat1)*np.cos(lat2)*np.sin(dlng/2)**2 return 2 * R * np.arcsin(np.sqrt(a)) def build_features(df): df df.copy() # 时间特征 df[order_time] pd.to_datetime(df[order_time]) df[hour] df[order_time].dt.hour df[weekday] df[order_time].dt.weekday df[is_lunch_peak] df[hour].between(11, 13).astype(int) df[is_dinner_peak] df[hour].between(17, 19).astype(int) # 距离特征 df[distance_km] haversine( df[shop_lat], df[shop_lng], df[user_lat], df[user_lng] ) df[distance_detour] df[distance_km] * 1.4 # 绕路系数 # 订单特征 df[amount_per_item] df[order_amount] / (df[item_count] 1) # 天气编码 weather_map {晴: 0, 阴: 1, 小雨: 2, 大雨: 3, 雪: 4} df[weather_code] df[weather].map(weather_map).fillna(0) # 标签取餐到送达的分钟数 df[pickup_time] pd.to_datetime(df[pickup_time]) df[delivery_time] pd.to_datetime(df[delivery_time]) df[eta_minutes] (df[delivery_time] - df[pickup_time]).dt.total_seconds() / 60 # 清洗异常标签 df df[(df[eta_minutes] 5) (df[eta_minutes] 120)] return df feature_cols [ hour, weekday, is_lunch_peak, is_dinner_peak, distance_km, distance_detour, item_count, order_amount, amount_per_item, weather_code ]逻辑说明haversine函数用球面公式算直线距离比欧氏距离准确。distance_detour乘 1.4 是经验绕路系数你可以按自己城市的数据回归出一个更准的值。amount_per_item是构造的比率特征比单独用金额或数量更能反映订单复杂度。标签用取餐到送达的时长避开了商家出餐这个不可控环节。清洗规则 5 到 120 分钟是经验边界你可以根据数据分布调整。参数说明绕路系数 1.4 不是拍脑袋是我在几个城市数据上试出来的中位数新区可以降到 1.25老城区可能要到 1.6。天气编码是有序编码因为大雨比小雨对配送的影响确实更大用 one-hot 反而丢失了顺序信息。3.3 训练 LightGBM 回归模型并评估特征准备好之后训练本身反而简单。关键是把评估指标选对别只看 RMSE。import lightgbm as lgb from sklearn.metrics import mean_absolute_error, mean_squared_error # 按时间切分 split_idx int(len(df) * 0.8) train_df, valid_df df.iloc[:split_idx], df.iloc[split_idx:] X_train, y_train train_df[feature_cols], train_df[eta_minutes] X_valid, y_valid valid_df[feature_cols], valid_df[eta_minutes] model lgb.LGBMRegressor( n_estimators800, learning_rate0.05, num_leaves63, max_depth-1, min_child_samples30, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda0.1, random_state42 ) model.fit( X_train, y_train, eval_set[(X_valid, y_valid)], eval_metricmae, callbacks[lgb.early_stopping(50), lgb.log_evaluation(100)] ) pred model.predict(X_valid) mae mean_absolute_error(y_valid, pred) rmse np.sqrt(mean_squared_error(y_valid, pred)) print(fMAE: {mae:.2f} 分钟, RMSE: {rmse:.2f} 分钟)逻辑说明early_stopping(50)表示验证集 MAE 连续 50 轮不下降就停防止过拟合。eval_metricmae而不是默认的 RMSE因为业务上更关心平均偏差多少分钟而不是大偏差的平方惩罚。num_leaves63是 LightGBM 最核心的复杂度参数数据量在 10 万条以下时建议 31 到 63超过 50 万条可以试 127。参数说明learning_rate0.05配合n_estimators800是稳妥组合想更快收敛可以提到 0.1 但容易过拟合。min_child_samples30控制叶子节点最小样本数防止模型记住噪声。subsample和colsample_bytree都设 0.8 是常规正则化手段。评估时除了 MAE 和 RMSE我还会看一个业务指标预测误差在 ±5 分钟以内的订单占比。这个数字比 MAE 更能反映用户体验因为用户对 5 分钟以内的偏差基本无感。3.4 特征重要性分析与模型解释模型跑通之后一定要看特征重要性否则你不知道模型到底学到了什么。import matplotlib.pyplot as plt importance pd.DataFrame({ feature: feature_cols, gain: model.booster_.feature_importance(importance_typegain) }).sort_values(gain, ascendingFalse) print(importance.head(10)) # 画图 plt.figure(figsize(8, 5)) plt.barh(importance[feature][:10], importance[gain][:10]) plt.xlabel(Gain) plt.gca().invert_yaxis() plt.tight_layout() plt.savefig(feature_importance.png, dpi150)用gain而不是split因为 gain 反映特征对损失下降的实际贡献split 只统计被用了多少次。如果发现distance_detour排第一、is_lunch_peak排前三说明特征设计方向对了。如果某个特征重要性异常高但你直觉上觉得不该那么高要警惕数据泄漏比如把pickup_time的小时数当特征用了而它和标签有直接时间重叠。4. 避坑与排查那些让我返工三次的问题4.1 离线 MAE 很低但上线偏差大现象离线验证 MAE 只有 4.2 分钟上线后用户投诉预估不准实际偏差经常超过 15 分钟。原因训练数据里混入了「已完成的订单」这些订单的送达时间是确定的但线上预测时面对的是「刚下单、骑手还没接」的订单特征分布完全不同。尤其是骑手特征训练时骑手的历史准时率是已知的线上预测时这个骑手可能还没分配。解决训练时只用「下单时刻可获得的信息」构造特征把骑手相关特征全部去掉或者用「该区域平均骑手准时率」替代个体骑手特征。重新训练后离线 MAE 会升到 6 分钟左右但上线偏差明显收窄。4.2 午高峰时段预测系统性偏短现象11:30 到 13:00 的订单模型预测值普遍比实际少 5 到 8 分钟。原因午高峰的拥堵不是线性的模型用is_lunch_peak这个布尔特征只能捕捉「是不是高峰」捕捉不到「高峰有多堵」。而且高峰时段商家出餐也变慢但标签用的是取餐到送达这部分被隐藏了。解决加一个「当前时段该区域平均配送时长」的实时特征用过去 30 分钟的滑动窗口统计。这个特征在训练时用历史数据回填线上用实时流计算。加上之后午高峰 MAE 从 9.1 降到 6.3。4.3 距离特征在跨江跨河场景失效现象直线距离 2 公里的订单实际骑行 8 公里模型预测 15 分钟实际 40 分钟。原因Haversine 距离不考虑地形障碍。城市里有江、河、铁路、封闭小区直线可达但骑行要绕大圈。解决如果拿不到路径规划 API至少加一个「是否跨行政区」的布尔特征或者用历史订单的「实际距离/直线距离」比值按网格做局部修正。更彻底的做法是接入骑行路径规划把骑行距离和骑行时间作为特征。4.4 类别特征直接 label encode 导致序关系错误现象把商家品类用 0 到 20 编码后模型认为品类 20 比品类 1「更大」预测出现莫名其妙的偏差。原因无序类别用整数编码会引入虚假的序关系。LightGBM 虽然对类别特征有原生支持但需要显式声明。解决要么用 one-hot要么在 LightGBM 里用categorical_feature参数声明。数据量大时 one-hot 会稀疏推荐用 LightGBM 原生类别支持。cat_features [shop_category, weather_code] for col in cat_features: df[col] df[col].astype(category) model lgb.LGBMRegressor(...) model.fit(X_train, y_train, categorical_featurecat_features)4.5 模型文件跨环境加载报错现象本地训练好的 joblib 模型部署到服务器上加载时报版本不兼容或属性缺失。原因LightGBM 和 scikit-learn 的版本差异会导致 pickle 反序列化失败。训练环境 Python 3.10 LightGBM 4.0部署环境 Python 3.8 LightGBM 3.3大概率翻车。解决用model.booster_.save_model(model.txt)保存为文本格式加载时用lgb.Booster(model_filemodel.txt)跨版本兼容性好得多。同时把依赖版本写进 requirements.txt 锁死。5. 把 MAE 再压 1 分钟的进阶技巧模型上线跑了一段时间后你会发现 MAE 卡在 5 到 6 分钟下不去。这时候单靠调参已经没用了得从数据和建模策略上想办法。我试过几个有效的手段按性价比排序。第一个是分时段建模。把订单按午高峰、晚高峰、平峰、夜宵切成四份每份单独训一个模型。听起来粗暴但效果立竿见影因为不同时段的特征重要性和非线性关系完全不同。午高峰里距离和待处理单量是主导夜宵时段天气和骑手接单数更关键。分模型后整体 MAE 能降 0.8 到 1.2 分钟。代价是维护四个模型推理时要先判断时段再路由。第二个是加实时特征。前面提过的「过去 30 分钟该区域平均配送时长」是最有效的实时特征。再进一步可以加「该商家过去 1 小时平均出餐时长」和「该骑手当前在手订单数」。这些特征需要一套流式计算管道工程成本不低但收益也最大。我见过一个团队靠实时特征把 MAE 从 6.5 压到 4.1。第三个是目标变换。送餐时间分布是右偏的直接回归 MAE 会被长尾拉偏。可以对标签取对数再训练预测时再指数还原。或者用分位数回归直接预测 50 分位数和 90 分位数给用户一个区间而不是单点。分位数回归在 LightGBM 里通过objectivequantile和alpha参数实现。# 预测 90 分位数给用户一个「最晚送达」估计 model_p90 lgb.LGBMRegressor( objectivequantile, alpha0.9, n_estimators800, learning_rate0.05, num_leaves63 ) model_p90.fit(X_train, y_train, eval_set[(X_valid, y_valid)]) p90_pred model_p90.predict(X_valid)这个 90 分位数预测的业务含义是90% 的订单会在这个时间内送达。用它做「最晚送达承诺」比用均值更合理能显著降低超时投诉率。最后一个技巧是后处理校准。模型预测出来后按「时段 × 距离段」分组统计每组的历史偏差中位数预测时减去这个偏差。这是一个轻量的在线校准层不需要重训模型但能修正系统性偏差。我一般用过去 7 天的数据滚动计算校准系数每天更新一次。# 后处理校准示例 calib valid_df.copy() calib[residual] calib[eta_minutes] - pred calib[dist_bin] pd.cut(calib[distance_km], bins[0,1,3,5,10,50]) calib[hour_bin] pd.cut(calib[hour], bins[0,10,14,17,21,24]) calib_factor calib.groupby([dist_bin,hour_bin])[residual].median() # 预测时按分组减去校准值这几个手段不用全上按你的工程能力选。我的习惯是先把分时段建模和后处理校准做了这两个改动小、见效快。实时特征留到有流计算资源时再上。模型这东西离线指标再好看上线后也得靠真实反馈一轮轮修。希望帮到你。本文还有配套的精品资源点击获取
返回列表