多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

快递需求分析与预测建模:从数据清洗到ARIMA与XGBoost实战

快递需求分析与预测建模:从数据清洗到ARIMA与XGBoost实战 简介本资源为2023年五一数学建模竞赛B题「快递需求分析问题」的完整求解资料面向备战数学建模竞赛的高校学生及需要多模型融合实战案例的学习者。内容围绕熵权-TOPSIS评价、ARIMA时间序列预测、Dijkstra最短路径与最小二乘法等核心方法展开覆盖问题一到问题五的建模思路与求解过程代码采用Matlab与Python编写适合作为赛题复盘与算法迁移的参考。资源包共1个PDF文件大小约1.63MB内含完整论文正文、模型建立与求解章节、模型评价与改进等内容目录结构清晰便于按问题模块检索学习。目前已有6032人学习下载读者可从中获取完整的赛题分析框架、各问模型选型依据、公式推导与结果呈现方式并对照代码理解算法落地细节为同类预测与优化问题提供可复用的建模范式。1. 快递需求分析这道题为什么算得出来和算得准是两回事2023年五一赛B题的第一问表面上是给一张快递订单表让你算几个指标实际考的是你能不能把需求这个词拆成可量化的东西。很多人拿到数据第一反应是直接上ARIMA或者XGBoost结果发现预测出来的曲线跟实际差得离谱——不是模型不行是需求的口径没定义清楚。快递需求分析的核心矛盾在于订单量、揽收量、签收量、有效需求这四个概念在业务上不等价但在数据表里往往只给你一个订单时间字段。这一问真正要解决的是从原始运单数据里提取出按时间、按区域聚合的需求序列并给出可解释的描述性统计和趋势判断。适合正在做数学建模、物流调度优化或者供应链预测的从业者尤其是那些模型跑通了但结果没法用的人。下面按数据清洗、需求序列构建、预测建模、评价排序四条线拆开讲每一步都给可复现的操作。2. 从原始运单表到需求时间序列清洗与聚合的四个关键决策2.1 先搞清楚数据里到底有什么字段五一赛B题附录给的数据通常是运单级别的明细表常见字段包括运单号、寄件时间、揽收时间、签收时间、始发城市、目的城市、重量、件数等。第一步不是写代码是打开表看三件事时间字段有几个、缺失率多少、城市字段是编码还是中文。我一般会先用pandas做一次全字段体检把每列的缺失率、唯一值数量、最小最大值打出来。这一步的产出直接决定后面用哪个时间字段做需求序列——如果揽收时间缺失超过30%就不能用它做日粒度聚合只能退而求其次用寄件时间。import pandas as pd df pd.read_csv(express_orders.csv, parse_dates[寄件时间, 揽收时间, 签收时间]) # 字段体检缺失率 唯一值 时间范围 report pd.DataFrame({ 缺失率: df.isnull().mean().round(4), 唯一值数: df.nunique(), 最小值: df.min(numeric_onlyFalse), 最大值: df.max(numeric_onlyFalse) }) print(report)这段代码的产出是一张字段概览表。重点看三个参数缺失率超过0.3的字段直接放弃做时间轴唯一值数等于行数的字段是主键不能拿来聚合时间字段的min/max确认数据覆盖的日期范围避免后面画出一条假趋势——比如数据只覆盖了15天你硬做月度预测就是自欺欺人。2.2 需求口径定义订单量不等于需求量这是最容易翻车的地方。业务上需求指的是客户下单那一刻产生的运输诉求所以应该用寄件时间做聚合基准而不是揽收或签收时间。揽收时间反映的是运力响应速度签收时间反映的是履约完成度这两个是运营指标不是需求指标。常见做法是以寄件时间的日期为key按日、按始发城市做groupby计数得到日需求序列。如果题目要求按区域分析就把城市映射到大区华东、华南等映射表自己按常见地理划分建一个即可。# 按寄件日期 始发城市聚合需求 df[需求日期] df[寄件时间].dt.date daily_demand df.groupby([需求日期, 始发城市]).size().reset_index(name需求量) # 按大区聚合需自建城市到大区的映射 city_to_region {北京: 华北, 上海: 华东, 广州: 华南} # 按需补全 df[大区] df[始发城市].map(city_to_region) region_demand df.groupby([需求日期, 大区]).size().reset_index(name需求量) print(daily_demand.head(10)) print(region_demand.head(10))参数说明groupby的第一个键决定时间粒度改成dt.to_period(W)就是周粒度第二个键决定空间粒度去掉就是全国总量。size()和count()的区别在于size会计入NaN行这里用size更稳妥因为运单号一般不会缺。聚合完一定要检查有没有日期断档——如果某天没有订单groupby结果里那天会直接消失画出来的曲线会跳变需要用reindex补零。2.3 异常值处理别把促销峰值当噪声删掉快递需求序列里天然存在两类异常一类是数据错误比如寄件时间在系统上线之前一类是真实峰值大促、节假日。血泪经验是前者必须删后者绝对不能删。判断方法很简单看那个峰值日期是不是已知的电商大促日。如果是保留并在特征里加一个是否大促的哑变量如果不是且偏离均值超过5倍标准差再考虑是录入错误。我一般会先画箱线图和按日折线图各一张肉眼确认后再动手。import matplotlib.pyplot as plt ts daily_demand.groupby(需求日期)[需求量].sum() fig, axes plt.subplots(2, 1, figsize(12, 6)) axes[0].plot(ts.index, ts.values) axes[0].set_title(日需求序列) axes[1].boxplot(ts.values, vertFalse) axes[1].set_title(需求分布箱线图) plt.tight_layout() plt.show() # 标记超过5倍标准差的点人工确认后再决定去留 mean, std ts.mean(), ts.std() outliers ts[abs(ts - mean) 5 * std] print(疑似异常日期, outliers)这段的重点不是代码本身而是那个5 * std的阈值。3倍标准差在需求预测里太敏感会把正常周末峰值也标出来5倍以上基本只剩数据错误和超级大促。打印出疑似日期后对照日历确认是促销就保留是系统迁移导致的脏数据就整段剔除。2.4 缺失日期补全与序列对齐聚合完的序列往往不是连续的尤其是按城市分组后小城市可能好几天没订单。做时间序列建模前必须把索引补齐否则ARIMA会按有数据的行当连续时间处理周期项直接错位。用asfreq或reindex都行补出来的NaN填0因为没有订单在需求语境下就是需求量为零不是缺失。ts daily_demand.groupby(需求日期)[需求量].sum() ts.index pd.to_datetime(ts.index) ts ts.asfreq(D, fill_value0) # 补齐缺失日期填0 print(补全后序列长度, len(ts)) print(零需求天数, (ts 0).sum())asfreq(D)的D是日频改成H就是小时频。补零之后要再看一眼零需求天数占比如果超过40%说明这个城市或这个时间段的数据太稀疏不适合单独建模应该合并到上级区域。3. ARIMA和XGBoost怎么选两条预测路线的参数与落地3.1 ARIMA适合什么样的需求序列ARIMA的强项是捕捉线性自相关和趋势适合需求序列平稳、周期规律明显、样本量不大的场景。快递需求按日聚合后通常有周周期周末低、工作日高所以实际用的是SARIMA季节项设7。定阶不要靠眼睛看ACF/PACF图硬猜用pmdarima的auto_arima自动搜把搜索范围限定在合理区间就行。我一般设m7周周期seasonalTruestepwiseTrue加速。from pmdarima import auto_arima model auto_arima( ts, seasonalTrue, m7, # 周周期 dNone, DNone, # 自动差分 start_p0, max_p3, start_q0, max_q3, start_P0, max_P2, start_Q0, max_Q2, traceTrue, stepwiseTrue, information_criterionaic ) print(model.summary())参数说明m7是季节周期长度日数据按周循环就填7d和D设None让算法自己判断差分次数避免手动差分过度information_criterionaic用AIC选模型样本量小于50时建议换成aicc。stepwiseTrue会大幅缩短搜索时间代价是可能错过全局最优但对建模题来说够用。跑完看summary里的Ljung-Box检验p值大于0.05说明残差没有明显自相关模型基本可用。3.2 XGBoost做需求预测的特征工程才是胜负手XGBoost本身不处理时间结构它把预测问题当成回归问题靠特征里的滞后项和日历信息来学会时间规律。所以用XGBoost做快递需求分析80%的功夫在特征构造上。核心特征分四类滞后特征前1天、前7天、前14天需求量、滑动窗口统计近7天均值、近7天标准差、日历特征星期几、是否节假日、是否大促、区域特征城市编码或大区one-hot。滞后特征是最重要的没有它XGBoost就是个普通回归树。import numpy as np import xgboost as xgb from sklearn.metrics import mean_absolute_percentage_error def build_features(series, lags[1, 7, 14], window7): df pd.DataFrame({y: series}) for lag in lags: df[flag_{lag}] df[y].shift(lag) df[rolling_mean] df[y].shift(1).rolling(window).mean() df[rolling_std] df[y].shift(1).rolling(window).std() df[dayofweek] df.index.dayofweek df[is_weekend] (df[dayofweek] 5).astype(int) return df.dropna() feat_df build_features(ts) split int(len(feat_df) * 0.8) train, test feat_df.iloc[:split], feat_df.iloc[split:] model xgb.XGBRegressor( n_estimators500, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, early_stopping_rounds50, eval_metricmape ) model.fit(train.drop(y, axis1), train[y], eval_set[(test.drop(y, axis1), test[y])], verboseFalse) pred model.predict(test.drop(y, axis1)) print(MAPE:, mean_absolute_percentage_error(test[y], pred))参数说明n_estimators500配合early_stopping_rounds50让模型在验证集不再提升时自动停避免过拟合max_depth4是需求预测的常用深度再深容易记住训练集的噪声learning_rate0.05偏保守配合较多树数用subsample和colsample_bytree都设0.8增加随机性。注意shift(1)在构造rolling特征时的作用——如果不shift当天的y会泄露到当天的特征里验证集MAPE会假性偏低上线就翻车。3.3 两种模型的对比与融合策略ARIMA和XGBoost不是二选一的关系。ARIMA擅长外推趋势XGBoost擅长拟合非线性交互常见做法是把两者的预测值做加权平均权重用验证集上的误差倒数来定。如果题目只要求选一个我的判断标准是样本量小于60天选ARIMA大于60天且有明显外部变量促销、天气选XGBoost。五一赛B题的数据量一般在几十到几百天两条路线都值得跑一遍用MAPE和RMSE两个指标对比。对比维度ARIMA/SARIMAXGBoost最小样本量30天左右60天以上特征工程几乎不需要滞后日历区域工作量大可解释性系数有统计含义需SHAP值辅助外推能力强能延趋势弱只能内插调参难度定阶较繁琐参数多但网格搜索成熟4. 用TOPSIS给城市需求做综合评价指标体系和计算步骤4.1 评价指标怎么选才不拍脑袋快递需求分析做到后面往往要给不同城市或区域排个优先级——哪些城市需求旺盛、哪些增长快、哪些波动大需要重点保障。TOPSIS适合这种多指标排序场景。指标一般选四个日均需求量规模、需求增长率趋势、需求波动率稳定性用变异系数、峰值需求极端压力。这四个指标覆盖了大不大、涨不涨、稳不稳、扛不扛得住四个业务问题。注意正向指标和负向指标要分开——需求量和增长率越大越好波动率越小越好。4.2 TOPSIS完整计算流程TOPSIS的核心逻辑是先归一化再加权然后找正理想解和负理想解最后算每个方案到两者的距离越靠近正理想解排名越前。下面用城市需求数据走一遍完整流程。import numpy as np import pandas as pd # 假设已有城市级指标表每行一个城市四列指标 # 指标方向需求量增长率波动率-峰值 data pd.DataFrame({ 城市: [北京, 上海, 广州, 成都, 武汉], 日均需求量: [1200, 1350, 980, 870, 760], 增长率: [0.12, 0.08, 0.15, 0.20, 0.10], 波动率: [0.25, 0.30, 0.18, 0.22, 0.28], 峰值需求: [1800, 2000, 1400, 1300, 1100] }) # 1. 归一化向量归一化 matrix data.drop(城市, axis1).values.astype(float) norm matrix / np.sqrt((matrix ** 2).sum(axis0)) # 2. 加权权重按业务重要性设定和为1 weights np.array([0.35, 0.25, 0.20, 0.20]) weighted norm * weights # 3. 正负理想解波动率是负向指标需反向处理 directions np.array([1, 1, -1, 1]) # 1正向-1负向 ideal_best np.where(directions 1, weighted.max(axis0), weighted.min(axis0)) ideal_worst np.where(directions 1, weighted.min(axis0), weighted.max(axis0)) # 4. 距离计算 d_best np.sqrt(((weighted - ideal_best) ** 2).sum(axis1)) d_worst np.sqrt(((weighted - ideal_worst) ** 2).sum(axis1)) # 5. 贴近度与排序 score d_worst / (d_best d_worst) data[贴近度] score.round(4) data[排名] data[贴近度].rank(ascendingFalse).astype(int) print(data.sort_values(排名))参数说明权重weights是最需要交代清楚的地方0.35给需求量是因为规模是快递网络规划的第一考量波动率给0.20是因为它影响运力储备但不直接决定需求大小。directions数组标记每个指标的方向负向指标在找理想解时要取最小值。贴近度越接近1说明该城市越靠近需求大、增长快、波动小、峰值可控的理想状态。这套流程换成区域粒度、按季度算都通用只需替换输入矩阵。4.3 评价结果怎么和预测结果联动单独跑TOPSIS只是排个名真正有价值的是把预测结果喂进评价体系。比如用ARIMA或XGBoost预测未来30天各城市需求量把预测值作为日均需求量和峰值需求的输入再跑一次TOPSIS得到的就是未来需求优先级排名。这个排名可以直接指导运力预部署——排名前几的城市提前增加揽收点或临时线路。这一步是把第一问的分析结论往第二问、第三问衔接的关键动作很多队伍在这里断掉导致后面几问各做各的。5. 避坑与排查建模过程中最容易翻车的五个地方5.1 时间字段用错导致需求序列完全失真现象预测曲线和实际曲线趋势相反或者预测值系统性偏高。原因用了签收时间做聚合签收时间受运力影响大促期间签收延迟导致需求峰值被抹平并后移。解决统一用寄件时间做需求聚合基准揽收和签收时间只用来做履约分析不混入需求序列。5.2 滞后特征泄露导致验证集MAPE假性偏低现象XGBoost验证集MAPE只有3%上线后实际误差超过20%。原因构造rolling特征时没有shift当天的需求量被算进了当天的特征里。解决所有基于目标值的滑动统计必须.shift(1)之后再rolling确保特征只包含预测时点之前的信息。5.3 ARIMA自动定阶搜出过拟合模型现象auto_arima跑出来的模型AIC很低但预测值几乎是一条直线。原因搜索范围设太宽选出了高阶模型把噪声也拟合了。解决把max_p和max_q限制在3以内max_P和max_Q限制在2以内优先看BIC而不是AICBIC对复杂模型惩罚更重。5.4 TOPSIS权重设置没有业务依据现象排名结果和直觉完全不符某个小城市排到了第一。原因权重全设成等值或者负向指标方向搞反了。解决权重必须能说出理由比如需求量权重最高因为它是网络规划的基础负向指标在找理想解时一定要反向建议用directions数组显式标记不要靠脑子记。5.5 缺失日期补零后零值过多拉低预测精度现象模型预测出来的需求量远低于实际。原因小城市或早期数据稀疏补零后零需求占比过高模型学到了大量零模式。解决零需求占比超过40%的序列不要单独建模合并到上级区域或者改用周粒度聚合减少零值比例。6. 把第一问的产出变成后续问题的输入一个可复用的分析管线第一问做完手里应该有三样东西一份清洗后的日需求序列表、一个跑通的预测模型ARIMA或XGBoost、一张城市需求优先级排名。这三样不是交完论文就扔的它们是第二问运力调度和第三问网络优化的输入。我一般会把整个流程封装成一个函数管线换一份数据或换一个时间粒度都能直接跑。def demand_analysis_pipeline(raw_df, time_col寄件时间, group_col始发城市, freqD): 从原始运单表到需求序列 预测 评价的完整管线 df raw_df.copy() df[需求日期] pd.to_datetime(df[time_col]).dt.date ts df.groupby(需求日期).size() ts.index pd.to_datetime(ts.index) ts ts.asfreq(freq, fill_value0) # 特征构造 XGBoost预测 feat build_features(ts) split int(len(feat) * 0.8) train, test feat.iloc[:split], feat.iloc[split:] model xgb.XGBRegressor(n_estimators500, max_depth4, learning_rate0.05, early_stopping_rounds50) model.fit(train.drop(y, axis1), train[y], eval_set[(test.drop(y, axis1), test[y])], verboseFalse) pred model.predict(test.drop(y, axis1)) # 城市级指标汇总供TOPSIS使用 city_stats df.groupby(group_col).agg( 日均需求量(运单号, count), 峰值需求(需求日期, lambda x: x.value_counts().max()) ) return ts, model, pred, city_stats ts, model, pred, city_stats demand_analysis_pipeline(df)这个管线的关键设计是时间聚合和特征构造解耦换freqW就变成周度分析city_stats的输出格式直接对接TOPSIS的输入矩阵。我踩过最大的坑是早期把清洗、聚合、建模写在一个脚本里改一个参数要重跑全流程后来拆成管线函数调参效率至少翻倍。另外提醒一句XGBoost的early_stopping_rounds在sklearn接口里从1.6版本开始移到了构造函数里如果你用的版本较老需要放在fit里传跑之前先确认版本不然会报参数错误。希望帮到你。本文还有配套的精品资源点击获取
返回列表