2020年第十届MathorCup高校数学建模
A题 无车承运人平台线路定价问题
原题再现
国内公路运输市场开放以来,逐渐形成了“小,散,乱”的发展现状。为规范运输市场,国家交通运输部办公厅于 2016 年 9 月印发《关于推进改革试点加快无车承运物流创新发展的意见》,并初步公布了 48 个无车承运人试点平台。随着我国无车承运行业的逐步兴起,承运线路的科学定价问题是众多无车承运人平台亟待解决的问题。
图 1 展示了国内无车承运人的主要运营模式,该模式下有三个主要的参与角色,分别为货主、无车承运人平台以及承运人。作为无车承运人平台,既需要面向货主的运输任务进行报价,同时也需要面向承运司机进行报价。
本研究以无车承运人的视角,暂不考虑面向货主的运输任务的报价,仅面向广大拥有运力资源(货车)的承运端司机,将需要承运的线路任务以一定价格提前发布到网络平台上供承运端司机浏览决定是否承运该运输任务。平台采用动态定价的形式保证每个任务必须被承运,若任务未被承运将带来一定损失。作为承运端的司机,会根据平台发布的线路任务和价格进行判断是否接单,司机接单则视为该线路任务交易成功,此线路任务随即从平台下架。若在给定的时间内,该任务没有司机接单,则该线路就可以进行调价。每条线路任务最多允许发布 3 次价格,即首次发布线路价格后仍可刷新两次线路价格,其中附件 1 数据文件中的线路指导价为平台首次发布的线路价格。假设上述线路任务全部为固定车型的整车任务,即一个任务需要由某种车型的 1 辆车完成,不考虑拼载任务。本无车承运人平台在当前阶段较为关注的目标是快速促进成交和较低的承运成本。
基于以上背景,请你们的团队根据附件给出的数据(可不限于此),通过数学建模的方法帮助某无车承运人平台解决以下问题:
问题 1:通过定量分析的方法,研究影响无车承运人平台进行货运线路定价的主要因素有哪些,并说明理由。
问题 2:根据附件 1 数据,通过建立数学模型,对已经成交货运线路历史交易数据中的定价进行评价。
问题 3:建立关于线路定价的数学模型,给出附件 2 的线路任务的三次报价以及总成本定价,并填充在附件 3 的表格中;给出你们的调价策略;评价你们对附件 2 的线路任务所给出的定价。其中附件 3 的表格以 Excel文件形式,连同论文答卷一起上传至参赛系统,请勿改变附件 3 中各任务ID 的原有顺序。附件 3 将用于测试报价的准确性,对于某个确定的任务,三次报价中有一次成交,则后续价格将不再考虑。
问题 4:根据你们的研究,给无车承运人平台写一封不超过一页的建议信。
附件 1:货运线路历史交易数据
附件 2:待定价的货运线路任务单
附件 3:计算结果
整体求解过程概述(摘要)
无车承运人是基于“互联网+物流”发展起来的一种货运市场运作模式,运输效率高、运营成本低、智能化水平高。但由于目前无车承运人模式的定价机制不够清晰,这将对该模式的进一步发展造成一定的阻碍。因此,本文主要采用机器学习、贝叶斯理论和对抗训练方法建立模型对无车承运人平台线路定价进行深入研究,主要解决了如下若干问题:
问题 1,利用机器学习模型,确定无车承运人平台线路定价的主要因素。本文首先进行了缺失值处理和简单的特征工程,然后基于多因素回归分析、LASSO、随机森林、GBDT 和 XGBoost 建立回归模型并得到各变量重要度。最终确定了线路定价的主要因素有:1 级运输,始发地为陕西省,车辆长度,目的地为新疆省,总里程,车辆吨位,地区 4,从发车到到达的时间,分拨月份和标的月份。
问题 2,通过对比分析附件 1 中的线路指导价(定价)和线路价格(成交价),本文假定当成交价在定价的 95%到 105%之间则认为价格一致。我们按照调价比例,将附件 1 样本分为 3 类:定价较低、定价较高和定价适中,然后基于逻辑回归和 XGBoost 建立分类模型并筛选重要变量,分别利用贝叶斯后验概率和Kruskal-Wallis 检验对定价较低或较高的原因进行了分析。
问题 3,本文给出的第一次报价为了降低平台运营成本,第二次和第三报价促使快速交易和提高成交率。对于对第一次报价,我们建立以线路指导价为标签的回归模型,给出尽可能接近平台理想的报价;第二次报价我们以成交价为标签,挖掘建模特征与成交价之间的关系,更加精准的给出能快速成交的价格;第三次报价我们采用对抗训练的思想,对成交价随机添加扰动,增强模型泛化能力,给出第三次报价。基于我们的动态定价策略,第一次报价为了降低承运成本仅有 21%的成交率,综合第二三次给出的报价能使最终成交率达到 96%以上。
问题 4,根据研究结果,由于地区的差异化会导致交通运输量的不同,因此可以结合总里程和始发地目的地两个因素,对始发地和目的地较偏远的地方由于交通不便利,可以适当提高线路价格;也可以从总里程的角度出发,结合地区交通因素制定每公里具体的线路价格。
模型假设:
为了便于模型构建与求解,在整个解题过程中,假设:
(1) 一个任务需要由某种车型的 1 辆车完成,不考虑拼载任务;
(2) 假设无车承运人平台不考虑面向货主的报价,因此本题仅考虑无车承运人平台向承运司机的报价;
(3) 假设该无车承运人不存在竞争关系;
(4) 假设供需平衡;
(5) 预测成交价在真实成交价的 95%到 105%之间时,则认为可成交;
问题分析:
针对问题一
从问题描述中可知,可以采用题目给定的附件 1 中的货运线路运作基本信息为参考提取影响该平台定价的各类别变量。对提取的类别变量,应通过定量分析的方法研究影响无车承运人平台货运线路定价的主要因素有哪些。传统的确定影响因变量的主要因素的方法有 Pearson 相关系数法、方差分析、卡方检验、多因素回归分析等方法。
在机器学习中,确定影响影响因变量的主要因素实际上是一个特征选择问题,而机器学习的特征选择方法除了上述传统方法外,还可以基于预测模型如LASSO 回归、决策树等机器学习模型进行嵌入式的特征选择。在实践中,由传统的方法筛选变量建立模型并进行预测,结果并不理想,为了提高预测精度,工程师经常采用基于预测模型的特征选择方法。
因此,本文首先利用多因素回归分析给出各个变量的重要度并进行简单的分析。然后基于 LASSO 回归、随机森林(Random Forest)、GBDT(Gradient Boosting Decison Tree,梯度提升树)、XGBoost(eXtreme Gradient Boosting)四种模型分别给出各自模型下的变量重要度,最后通过综合四个方法的变量重要度,最终确定了影响影响无车承运人平台进行货运线路定价的主要因素。问题 1 的求解思路如图 3-1。
针对问题二
根据附件 1 给出的“调价类型”信息:成交价在指导价的 95%-105%内都属于未调整。本文按调价比例将样本分成三类:线路指导价合适、线路指导价较低(调价比例大于 1.05)、线路指导价较高(调价比例小于 0.95)。
我们采用了 2 种方法对线路指导价不合适(线路指导价较低或较高)的原因进行了分析。首先我们分别利用逻辑回归(Logistics Regression, LR)和 XGBoost建立分类预测模型,得出 2 个模型下重要度排序前 10 的变量。
第一种方法,基于后验概率的分析。分别以 LR 和 XGBoost 筛选的 10 个变量为条件变量,计算线路指导价较低和较高的后验概率,从而表征每个筛选出的重要因素的特定取值对线路指导价的影响。
第二种方法,基于 Kruskal-Wallis 检验的分析。首先我们对附件一和附件二的数据进行采样,构造新的样本。具体采样方式为,分别对每个变量进行随机分层采样,构造样本𝐷1 = {(𝑥𝑖1, 𝑥𝑖2, … , 𝑥𝑖0, 𝑦𝑖)}𝑖=11000,重复一千次,得到一千个样本。该一千个样本视为对 10 个主要变量取值的随机组合。这部分样本是对给定变量的取值的组合,因此其中包含部分附件 1 中未出现的新样本。基于附件 1 建立多因素训练分类模型,预测该 1000 个样本分别对应的价格类别。基于 1000 个样本的预测标签,即线路指导价合适、线路指导价较低、线路指导价较高这三个标签,通过 Kruskal-Wallis 检验判断在三种类别下分布显著不同的因素,进而分析影响线路指导价较高和较低的原因。
针对问题三
根据题目要求可知,首次发布价格后最多还可刷新两次报价,并且平台以促进尽快成交和降低承运成本为主要目标。根据供求关系定价论可知运价取决于运输产品的供求关系,因此,在解决该问题之前,本文假设货源(货主)与车源(承运端司机)处于平衡状态。我们从以下两个角度对问题进行建模分析。首先,我们认为平台初次发布的价格,是综合考虑各项因素后能使平台获得期望利益或满足货主最高承运成本要求的期望报价,所以我们给出的第一次报价是以最低的承运成本为目标,而不关注成交率。其次,对第一次报价结束后尚未成交的任务,我们以提高成交率为目标,通过调价策略制定不同的定价,使得这些任务能快速成交。
因此,问题三的解答需要分为四步:
(1) 应用动态调价策略,基于弹性成交策略制定一个评价指标-成交率;
(2) 以附件 1 已成交数据的初次指导价为参考,延续平台的初次定价策略,制定第一次报价。
(3) 当第一次报价无法被承运端的司机认同时,则进入第二轮报价。基于附件 1 中的最终成交价建立回归模型,预测附件 2 中平台发布的初次指导价,作为第二次报价;
(4) 根据附件 1 可知,当成交价在平台报价的 95%和 105%之间时,则认为不需要调价。因此,对于附件 1 中的成交价,采用对抗训练思想的方法,建立回归模型预测第三轮报价,进一步增强模型泛化能力。
最后,对于总成本预测,采用同样建立有监督判别模型,基于附件 1 的总成本建立回归模型,预测附件二的总成本。
模型的建立与求解整体论文缩略图
全部论文请见下方“ 只会建模 QQ名片” 点击QQ名片即可
程序代码:(代码和文档not free)
The actual procedure is shown in the screenshot
import pandas as pd
import numpy as np
def fillna(alld):
for col in alld.columns:
if col == '是否续签':
alld[col] = alld[col].replace(['未知'],['N'])
if 'N' in alld[col].value_counts().index:
print(col)
vc = alld[col].value_counts().sort_values(ascending=False)
mode = vc.index[0] if vc.index[0]!='N' else vc.index[1]
alld[col] = alld[col].replace('N',mode)
return alld
def obj2float(alld):
alld['业务类型'] = alld['业务类型'].map({'速运':1,'重货':2})
alld['需求类型 1'] = alld['需求类型 1'].map({'普通':1,'区域发运':2,'二程接驳':3})
alld['需求类型 2'] = alld['需求类型 2'].map({'计划':1,'临时':2})
alld['是否续签'] = alld['是否续签'].map({'非续签':1,'续签 ECP 审批驳回或撤销':2,
'已分拨续签':3})
alld['标的展示策略'] = alld['标的展示策略'].map({'DIR':1,'BDC':2})
alld['打包类型'] = alld['打包类型'].map({'周期流向':1,'人工':2,
'单边':3,'周期往返':4,'往返':5})
alld['异常状态'] = alld['异常状态'].map({'正常':1})
alld['运输等级'] = alld['运输等级'].map({'二级运输':1,'三级运输':2,'一级运输':3})
alld['始发地省份名称'] = alld['始发地省份名称'].map({'广东省':1,'北京市':2,'河南省':3,'陕西省':4})
alld['车辆类型分类'] = alld['车辆类型分类'].map({'厢式运输车':1,'北京市':2,
'河南省':3,'陕西省':4})
alld['目的地省份名称'] = alld['目的地省份名称'].map({'广东省':1,'北京市':2,
'河南省':3,'新疆维吾尔自治区':4})
alld['交易对象'] = alld['交易对象'].map({'B':1,'BC':2,'C':3})
alld['地区类型'] = alld['地区类型'].map({'分拨区':1,'业务区':2})
# alld['异常状态'] = alld['异常状态'].map({'分拨区':1,'业务区':2})
# alld['异常状态'] = alld['异常状态'].map({'分拨区':1,'业务区':2})
alld['交易模式'] = alld['交易模式'].replace(['抢单'],[1])
alld['需求紧急程度'] = alld['需求紧急程度'].map({'常规订单':1,'特急订单':2,'紧急订单':3})
return alld
def deal_time(alld):
### 众数填充缺失值后,计算相邻时间的差值,然后计算累计多少小时
times = ['计划卸货完成时间','计划到达时间','计划发车时间','计划靠车时间',
'分拨时间','标的_创建日期']
time_diffs,hour_list,month_list,day_list = [],[],[],[]
for i in range(len(times)-1):
t1,t2 = pd.to_datetime(alld[times[i]]),pd.to_datetime(alld[times[i+1]])
hour_list+=[t1.dt.hour.values.reshape(-1,1),t2.dt.hour.values.reshape(-1,1)]
month_list+=[t1.dt.month.values.reshape(-1,1),t2.dt.month.values.reshape(-1,1)]
day_list+=[t1.dt.day.values.reshape(-1,1),t2.dt.day.values.reshape(-1,1)]
t_diff = t1-t2
hours = t_diff.astype('timedelta64[D]').astype(int)*24+\
t_diff.astype('timedelta64[h]').astype(int)
time_diffs.append(hours.values.reshape(-1,1))
time_diffs = pd.DataFrame(np.hstack(time_diffs),
columns = ['卸货_到达_时间','到达_发车_时间',
'发车_靠车_时间','靠车_分拨_时间',
'分拨_标的_时间']
### 得到当天的小时(几点)
hour_idx = [0,2,4,6,8,9]
hour_list = np.hstack(hour_list)[:,hour_idx]
hour_list = pd.DataFrame(hour_list,
columns = ['卸货_小时','到达_小时',
'发车_小时','靠车_小时',
'分拨_小时','标的_小时'])
### 得到几号
day_idx = [0,2,4,6,8,9]
day_list = np.hstack(day_list)[:,day_idx]
day_list = pd.DataFrame(day_list,
columns = ['卸货_天','到达_天',
'发车_天','靠车_天',
'分拨_天','标的_天'])
### 得到月份
month_idx = [0,2,4,6,8,9]
month_list = np.hstack(month_list)[:,month_idx]
month_list = pd.DataFrame(month_list,
columns = ['卸货_月份','到达_月份',
'发车_月份','靠车_月份',
'分拨_月份','标的_月份'])
return time_diffs,hour_list,day_list,month_list
def drop_cols(alld):
times = ['计划卸货完成时间','计划到达时间','计划发车时间','计划靠车时间',
'分拨时间','标的_创建日期','标的_创建时间','计划发车日期']
single_val = ['异常状态']
alld.drop(times+single_val,axis=1,inplace=True)
return alld
def get_alld():
train = pd.read_excel('../数据/附件 1:货运线路历史交易数据.xlsx')
test = pd.read_excel('../数据/附件 2:待定价的货运线路任务单.xlsx')
test.drop(['编号'],axis=1,inplace=True)
use_cols = test.columns
### 缺失值太多
null_cols = ['子包号','装卸的次数','装的次数','卸的次数','是否需要装卸']
use_cols = set(use_cols)-set(null_cols)
target = ['线路总成本','线路价格(不含税)','线路指导价(不含税)']
alld = pd.concat([train[list(use_cols)+target],test[use_cols]],axis=0).reset_index(drop=True)
alld = fillna(alld)
float_alld = obj2float(alld)
time_diffs,hour_list,day_list,month_list = deal_time(float_alld)
float_alld = drop_cols(float_alld)
all_data = pd.concat([float_alld,time_diffs,
hour_list,day_list,month_list],axis=1)
all_data.to_csv('../数据/all_data.csv',index=0)
get_alld()