阿里天池供应链需求预测比赛小结

阿里天池供应链需求预测比赛小结

一、赛题的思路回顾

1.1赛题描述

  • 使用历史平均来预测未来的需求

  • 使用测试集真实数据进行过拟合的结果

名词定义

库存水位

在仓库存数量,用来满足需求。

补货时长(交货时间,lead_time

从下达补货指令到货物到仓可用的时长。

本赛题初赛时的补货时长为14天,即假设1号A货物的库存水位为0,此时下达A货物补货指令,补货量为10,则1号至14号A货物的库存水位均为0,15号时A货物的库存水位为10。

补货在途

下达补货指令后还未到仓的货物量总和。

上例中1号至14号A货物的补货在途为10,其他时段为0。

若在8号再次下达补货量为10的补货指令,那么1号至7号的补货在途为10,8号至14号的补货在途为20,15号至21号的补货在途为10,其他时段为0。

补货策略

本赛题场景使用周期性盘点的补货策略,每周一为补货决策日,决定货物的补货量。

历史需求

货物历史需求量的时间序列,值得注意的是,因为云产品有购买与释放的概念,所以本赛题场景下需求量会为负数,即云产品被用户释放。

补货单元

货物的唯一标识。

赛题场景

基于给定过去一段时间的历史需求数据,同时结合当前的库存数据、补货时长、补货在途以及补货单元的相关信息(产品维度与地理纬度),参赛者需要自己提出方案,在补货决策日确定每一补货单元的补货量。最直接的方案,可通过历史需求数据,对未来的需求进行预测,结合当前库存水位以及补货在途的货物判断14天后的库存水位能否满足14天后的一系列需求(因为当日补货14天后才能到货),考虑对应的补货量,达到在保障一定服务水平的情况下,实现最低库存成本的效果;当然也可采用end to end的整体优化方案,实现该目标。库存量视角的变化过程如下图所示。
阿里天池供应链需求预测比赛小结_第1张图片

我们会给一个训练集,供参赛选手训练模型并验证模型效果使用。同时,初赛时我们会提供一个测试集,选手需要为按时间读取20210302-20210607的需求数据,并根据历史需求数据以及补货在途,决定补货量,并把决策结果CSV文件输出到指定位置。(由于赛题特殊与评价机制的问题,初赛时给选手透出了未来数据,选手在初赛时将其作为未知数据调整算法,不允许在决策时使用当前日之后的未来数据。)

由于库存控制是一个前后相关的决策过程,两次补货决策并不独立,因此我们会提供一段时间的数据,由选手在时间轴上进行多次补货决策,最后在较长的时间段内评价选手方案的好坏。在复赛时,选手需要提交一个docker镜像,镜像中需要包含用来进行库存管理所需的所有内容,如模型、脚本等,未来的需求数据以及到货时间将通过流评测的方式给出。镜像中的脚本需要能根据所得的需求量,根据历史需求数据以及先前所做的决策,决定补货量,并把决策结果CSV文件输出到指定位置;初赛时测试集文件将线下交给选手。

相关参考

https://web.mit.edu/2.810/www/files/readings/King_SafetyStock.pdf

https://abcsupplychain.com/safety-stock-formula-calculation/

数据描述

训练集包含如下信息:

虚拟资源使用量历史数据

demand_train.csv为虚拟资源使用量历史数据,包含7列。每列的含义如下:

字段 说明
unit 单元
ts 日期
qty 资源使用量
geography 地理信息
geography_level 地理聚合维度
product 产品信息
product_level 产品聚合维度

inventory_info.csv为虚拟资源库存数据,包含7列。每列的含义如下:

字段 说明
unit 单元
ts 日期
qty 库存量
geography 地理信息
geography_level 地理聚合维度
product 产品信息
product_level 产品聚合维度

geography_tuopu.csv为地理拓扑数据,包含3列。每列的含义如下:

字段 说明
geography_level_1 地理层级1
geography_level_2 地理层级2
geography_level_3 地理层级3

product_tuopu.csv为产品层级信息,包含2列。每列的含义如下:

字段 说明
product_level_1 产品层级1
product_level_2 产品层级2

unit_weight.csv为库存单元的库存信息,包含2列。每列的含义如下:

字段 说明
unit 单元
weight 权重

复赛阶段,测试集的数据格式和初赛阶段相同,但是测试集的内容不会提供给参赛选手。选手需要在docker代码中从指定的数据集目录中读取测试集内容,进行特征工程和模型预测,最后输出的格式和初赛一致,输出后续一段时间内决策日需要补货的集合(如果有预训练权重则docker代码中需包含本地训练好的模型)。

提交格式

在初赛中,预测结果的提交格式为CSV压缩的ZIP文件。文件中的每一行表示在未来的时间段内每个决策日每个库存单元需要补充的量,具体包含:补货单元、日期和补货量(用逗号隔开),形式如下:

"unit1","20210308",12

"unit1","20210315",11

"unit1","20210322",9

**注:在复赛中,参赛选手需

要提交docker镜像,具体的提交方式及规范请参见到时发布的容器镜像提交说明。**

评价指标

综合指标

阿里天池供应链需求预测比赛小结_第2张图片

阿里天池供应链需求预测比赛小结_第3张图片

另外
阿里天池供应链需求预测比赛小结_第4张图片

评测时按时间顺序更新上述变量,完毕后用上述公式计算指标。

#初赛评测程序伪代码如下
#intransit 是用来记录预计到达的资源的中间变量
init intransit[][] = 0
for i in I:
    for d in D:
        #初始库存值已给出
        qty_inventory[i,d] = qty_inventory[i,d - 1]

        #qty_replenish即为选手提交的补货决策
        if qty_replenish[i,d] > 0:
            #lead_time 为交货时间,在交货时间之后供应会到达
            intransit[i,d + lead_time] += qty_replenish[i,d]
            
        # 可用库存加上到达的库存
        qty_inventory[i,d] = max(qty_inventory[i,d] + intransit[i,d], 0)
        
        # 需求没有被满足的量,当天需求量和可用库存量进行比较
        stockout[i,d] = max(demand[i,d] - qty_inventory[i,d], 0)
        if demand[i,d] < 0:
            # 净需求为负时候,表示释放,可用库存会增加,释放的量和已使用的量取小
            qty_inventory[i,d] = max(qty_inventory[i,d] + min(abs(demand[i,d]),qty_using[i,d - 1]), 0)
        else:
            # 净需求为正的时候,可用库存进行扣减,>=0
            qty_inventory[i,d] = max(qty_inventory[i,d] - demand[i,d], 0)
        # 前一天的保有量+今天被满足的量
        qty_using[i,d] = max(qty_using[i,d - 1] + demand[i,d] - stockout[i,d], 0)

1.2赛事总结

这次比赛作为首次参加的机器学习竞赛的时间序列预测比赛,由于之前几乎没有相关的基础,这次还是一点点的根据baseline和官方的讲解开展的。很遗憾在初赛就止步了,没有能够进入复赛,可能这次参赛的主要目的还是学习,通过比赛了解最基础的一些时间序列建模方法以及特征工程的构建方式。同时这次的比赛针对云电商的这样一个库存补货策略还是有很高的一个要求的。

本场比赛最后使用的方法是多模的一个融合包括xgb+lgb和经典的arima。特征工程的构建呢会比较多的参考auto-X这个第四范式提供的项目。

其实还有很多没有尝试的过程比如:

  • 特征工程的过滤

阿里天池供应链需求预测比赛小结_第5张图片

  • 构建新的特征工程:

阿里天池供应链需求预测比赛小结_第6张图片

  • 以及目前kaggle竞赛和时间序列的论文研究也不够

人工智能顶会最新时序模型汇总(附原文源码)

90%冠亚军采用的时间序列建模策略

  • 最后就是天池比赛的吐槽了,目前国内的赛事其实真的没有kaggle上面那么开放,没有很多jupyter notebook和大佬的分享,大家都是各卷各的,相对的整体水平也是没办法滚雪球的。这一点考虑后期还是参考借鉴学习的话还是多看看kaggle的赛事。

二、库存需求预测模型auto-arima

2.1数据准备与预处理

Train_dt = read_csv('train.csv')[['unit','ts','qty']]
Test_dt = read_csv('test.csv')[['unit','ts','qty']]
Train_dt["ts"] = Train_dt["ts"].apply(lambda x: pd.to_datetime(x))  # 日期对应数据标准化为规范时间  这一步比较耗时间!!!!!
Test_dt["ts"] = Test_dt["ts"].apply(lambda x: pd.to_datetime(x))  # 日期对应数据标准化为规范时间
last_dt = pd.to_datetime("20210301")  # 用来限定使用的是历史数据而不是未来数据
start_dt = pd.to_datetime("20210301")  # 用来划定预测的针对test的起始时间
end_dt = pd.to_datetime("20210607")  # 预测需求的截止时间
qty_using = pd.concat([Train_dt, Test_dt])
for num,chunk in enumerate(qty_using.groupby("unit")):
unit = chunk[0]
demand = chunk[1]
eval = demand.copy()
demand["log qty"] = np.log(demand['qty'])  #对数处理源数据

2.2arima的时间序列平稳性检验

2.2.1平稳性和非白噪声

由于ARMA和ARIMA需要时间序列满足平稳性和非白噪声的要求,所以要用查分法和平滑法(滚动平均和滚动标准差)来实现序列的平稳性操作。一般情况下,对时间序列进行一阶差分法就可以实现序列的平稳性,有时需要二阶查分。

  • 取对数处理
demand["log qty"] = np.log(demand['qty'])
  • 差分法实现
demand["diff"] = demand["qty"].diff().values
del demand["diff"]
demand = demand[1:]
  • 平滑处理
#滚动平均(平滑法不平稳处理)
demand_log_moving_avg = demand['log qty'].rolling(12).mean()
#滚动标准差
demand_log_std = demand['log qty'].rolling(12).std()
  • ADF检验+非白噪声检验+时间序列定阶

这里采用的是auto_arima这些过程会有自主实现的过程不需要逐个的去实现,这里也为模型的运行效率做了提升

if __name__ == '__main__':
    # 加载数据

    Train_dt = read_csv('train.csv')[['unit','ts','qty']]
    Test_dt = read_csv('test.csv')[['unit','ts','qty']]
    Train_dt["ts"] = Train_dt["ts"].apply(lambda x: pd.to_datetime(x))  # 日期对应数据标准化为规范时间  这一步比较耗时间!!!!!
    Test_dt["ts"] = Test_dt["ts"].apply(lambda x: pd.to_datetime(x))  # 日期对应数据标准化为规范时间
    last_dt = pd.to_datetime("20210301")  # 用来限定使用的是历史数据而不是未来数据
    start_dt = pd.to_datetime("20210301")  # 用来划定预测的针对test的起始时间
    end_dt = pd.to_datetime("20210607")  # 预测需求的截止时间

    qty_using = pd.concat([Train_dt, Test_dt])
    for num,chunk in enumerate(qty_using.groupby("unit")):
        unit = chunk[0]
        demand = chunk[1]
        eval = demand.copy()
        demand["diff"] = demand["qty"].diff().values
        del demand["diff"]
        demand = demand[1:]
        demand["log qty"] = np.log(demand['qty'])
        demand_log_moving_avg = demand['log qty'].rolling(12).mean()  # 平滑处理也是针对非平稳性的处理方式
        demand_log_std = demand['log qty'].rolling(12).std()

2.3迭代预测

#迭代预测
        date_list = pd.date_range(start=start_dt, end=end_dt - datetime.timedelta(days=1))
        for date in date_list:
            if date.dayofweek != 0:
                # 周一为补货决策日,非周一不做决策
                pass
            else:
                demand_his = demand[(demand["ts"] >= date - datetime.timedelta(days=42)) & (demand["ts"] < date)]['log qty'].values.astype('float32')
                try:
                    model = auto_arima(demand_his, trace=True, error_action='ignore', suppress_warnings=True)
                    model.fit(demand_his)
                    forecast = model.predict(n_periods=7)
                    forecast = np.exp(np.array(forecast))
                    # forecast = diff1_reduction(forecast[1], demand_his)
                    demand.loc[(demand["ts"] > date) & (demand["ts"] <= date + datetime.timedelta(days=7)), 'qty'] = forecast
                except:
                    print('ARIMA检验存在问题的unit:{}'.format(unit))
                    #出了问题的用前面42天的均值代替
                    demand_future = np.mean(demand_his)    #这里是针对特殊的unit也就是arima无法预测的不能通过平稳性检验的
                    if demand_future == -np.inf:
                        demand_future = 0
                    demand.loc[(demand["ts"] > date) & (demand["ts"] <= date + datetime.timedelta(days=7)), 'qty'] = np.array([demand_future] * 7)

这里使用try和except是因为有部分unit的历史数据不能满足arima预测的平稳性要求,采用多个历史的前42天也就是我们arima依赖的历史数据周期的均值来代替。

三、库存预测特征工程构建auto_X自适应的时间序列预测

3.1模型主体代码及流程

参考github项目:auto-x

完整colab代码

# -*- coding: utf-8 -*-
# 安装autox
!git clone https://github.com/4paradigm/AutoX.git
!pip install pytorch_tabnet
!pip install ./AutoX

import os
import pandas as pd

"""## 数据预处理"""

data_name = '../../data/'
path = f'./{data_name}'

# 赛题数据demand_test_A中给了标签,我们需要将它删掉。同时我们顺便删掉无用的'Unnamed: 0'列

demand_train_A = pd.read_csv(f'{path}/demand_train_A.csv')
demand_test_A = pd.read_csv(f'{path}/demand_test_A.csv')

demand_train_A.drop('Unnamed: 0', axis=1, inplace=True)
demand_test_A.drop(['Unnamed: 0', 'qty'], axis=1, inplace=True)

# 将 demand_train_A, demand_test_A 保存为train.csv, test.csv
demand_train_A.to_csv(path + '/train.csv', index = False)
demand_test_A.to_csv(path + '/test.csv', index = False)

"""## 导入所需的包"""

from autox import AutoX

"""## 初始化AutoX类"""

# 数据集是多表数据集,需要配置表关系
relations = [
    {
            "related_to_main_table": "true", # 是否为和主表的关系
            "left_entity": "train.csv",  # 左表名字
            "left_on": ["product"],  # 左表拼表键
            "right_entity": "product_topo.csv",  # 右表名字
            "right_on": ["product_level_2"], # 右表拼表键
            "type": "1-1" # 左表与右表的连接关系
        },  # train.csv和product_topo.csv两张表是1对1的关系,拼接键为train.csv中的product列 和 product_topo.csv中的product_level_2列
    {
            "related_to_main_table": "true", # 是否为和主表的关系
            "left_entity": "test.csv",  # 左表名字
            "left_on": ["product"],  # 左表拼表键
            "right_entity": "product_topo.csv",  # 右表名字
            "right_on": ["product_level_2"], # 右表拼表键
            "type": "1-1" # 左表与右表的连接关系
        },  # test.csv和product_topo.csv两张表是1对1的关系,拼接键为test.csv中的product列 和 product_topo.csv中的product_level_2列
    {
            "related_to_main_table": "true", # 是否为和主表的关系
            "left_entity": "train.csv",  # 左表名字
            "left_on": ["geography"],  # 左表拼表键
            "right_entity": "geo_topo.csv",  # 右表名字
            "right_on": ["geography_level_3"], # 右表拼表键
            "type": "1-1" # 左表与右表的连接关系
        },  # train.csv和geo_topo.csv两张表是1对1的关系,拼接键为train.csv中的geography列 和 geo_topo.csv中的geography_level_3列
    {
            "related_to_main_table": "true", # 是否为和主表的关系
            "left_entity": "test.csv",  # 左表名字
            "left_on": ["geography"],  # 左表拼表键
            "right_entity": "geo_topo.csv",  # 右表名字
            "right_on": ["geography_level_3"], # 右表拼表键
            "type": "1-1" # 左表与右表的连接关系
        } # test.csv和geo_topo.csv两张表是1对1的关系,拼接键为test.csv中的geography列 和 geo_topo.csv中的geography_level_3列
]

autox = AutoX(target = 'qty', train_name = 'train.csv', test_name = 'test.csv', 
               id = ['unit'], path = path, time_series=True, ts_unit='D',time_col = 'ts',
               relations = relations
              )  #feature_type = feature_type,

sub = autox.get_submit_ts()

# 检查预测结果和真实结果的差距
sub.rename({'qty': 'qty_pre'}, axis=1, inplace=True)
demand_test_A = pd.read_csv(f'{path}/demand_test_A.csv', usecols = ['unit','ts','qty'])

analyze = demand_test_A.merge(sub, on = ['unit', 'ts'], how = 'left')
# 查看mae
from sklearn.metrics import mean_absolute_error
y_true = analyze['qty']
y_pred = analyze['qty_pre']

print(mean_absolute_error(y_true, y_pred))


阿里天池供应链需求预测比赛小结_第7张图片

### 3.2特征工程构建部分

def get_submit_ts(self):

    id_ = self.info_['id']
    target = self.info_['target']

    # 特征工程
    log("start feature engineer")
    df = self.dfs_['train_test']
    feature_type = self.info_['feature_type']['train_test']

    # 1-M拼表特征
    # one2M拼表特征
    log("feature engineer: one2M")
    featureOne2M = FeatureOne2M()
    featureOne2M.fit(self.info_['relations'], self.info_['train_name'], self.info_['feature_type'])
    log(f"featureOne2M ops: {featureOne2M.get_ops()}")
    if len(featureOne2M.get_ops()) != 0:
        self.dfs_['FE_One2M'] = featureOne2M.transform(df, self.dfs_)
        else:
            self.dfs_['FE_One2M'] = None
            log("ignore featureOne2M")

            # 时间特征
            log("feature engineer: time")
            featureTime = FeatureTime()
            featureTime.fit(df, df_feature_type=feature_type, silence_cols=id_ + [target])
            log(f"featureTime ops: {featureTime.get_ops()}")
            self.dfs_['FE_time'] = featureTime.transform(df)

            # lag_ts特征
            log("feature engineer: ShiftTS")
            featureShiftTS = FeatureShiftTS()
            featureShiftTS.fit(df, id_, target, feature_type, self.info_['time_col'], self.info_['ts_unit'])
            log(f"featureShiftTS ops: {featureShiftTS.get_ops()}")
            log(f"featureShiftTS lags: {featureShiftTS.get_lags()}")
            self.dfs_['FE_shift_ts'] = featureShiftTS.transform(df)

            # rolling_stat_ts特征
            log("feature engineer: RollingStatTS")
            featureRollingStatTS = FeatureRollingStatTS()
            featureRollingStatTS.fit(df, id_, target, feature_type, self.info_['time_col'], self.info_['ts_unit'])
            log(f"featureRollingStatTS ops: {featureRollingStatTS.get_ops()}")
            log(f"featureRollingStatTS windows: {featureRollingStatTS.get_windows()}")
            self.dfs_['FE_rollingStat_ts'] = featureRollingStatTS.transform(df)

            # exp_weighted_mean_ts特征
            log("feature engineer: ExpWeightedMean")
            featureExpWeightedMean = FeatureExpWeightedMean()
            featureExpWeightedMean.fit(df, id_, target, feature_type, self.info_['time_col'], self.info_['ts_unit'])
            log(f"featureExpWeightedMean ops: {featureExpWeightedMean.get_ops()}")
            log(f"featureExpWeightedMean lags: {featureExpWeightedMean.get_lags()}")
            self.dfs_['FE_ewm'] = featureExpWeightedMean.transform(df)

            # label_encoder
            df = auto_encoder(df, feature_type, id_)

            # 特征合并
            log("feature combination")
            df_list = [df, self.dfs_['FE_One2M'], self.dfs_['FE_time'], self.dfs_['FE_shift_ts'], self.dfs_['FE_rollingStat_ts'], self.dfs_['FE_ewm']]
            self.dfs_['FE_all'] = feature_combination(df_list)

            # # 内存优化
            # self.dfs_['FE_all'] = reduce_mem_usage(self.dfs_['FE_all'])


3.2特征过滤

def feature_filter(train, test, id_, target):
    not_used = id_ + [target]

    used_features = test.describe().columns
    # 过滤掉test中全为nan的特征
    for col in tqdm(used_features):
        # test中全为Nan的特征
        if test.loc[test[col].isnull()].shape[0] == test.shape[0]:
            if col not in not_used:
                not_used += [col]

        # nunique为1的特征
        if train[col].nunique() == 1:
            if col not in not_used:
                not_used += [col]

        # test中的值都比train中的值要大(或小)的特征
        if test[col].min() > train[col].max() or test[col].max() < train[col].min():
            if col not in not_used:
                not_used += [col]
    log(f"filtered features: {not_used}")
    used_features = [x for x in used_features if x not in not_used]
    return used_features

3.3数据集切分及模型训练

# train和test数据切分
train_length = self.info_['shape_of_train']
train, test = train_test_divide(self.dfs_['FE_all'], train_length)
log(f"shape of FE_all: {self.dfs_['FE_all'].shape}, shape of train: {train.shape}, shape of test: {test.shape}")

# 特征过滤
log("feature filter")
used_features = feature_filter(train, test, id_, target)
log(f"used_features: {used_features}")

# 模型训练
log("start training model")
if self.info_['task_type'] == 'regression':

    # model_cat = CatboostRegressionTs()
    # model_cat.fit(train, test, used_features, target, self.info_['time_col'], self.info_['ts_unit'])

    model_lgb = LgbRegressionTs()
    model_lgb.fit(train, test, used_features, target, self.info_['time_col'], self.info_['ts_unit'])

    model_xgb = XgbRegressionTs()
    model_xgb.fit(train, test, used_features, target, self.info_['time_col'], self.info_['ts_unit'])

    # 特征重要性
    fimp = model_lgb.feature_importances_
    log("feature importance")
    log(fimp)

3.4模型预测及后处理

# 模型预测
predict_lgb = model_lgb.predict(test, used_features)
predict_xgb = model_xgb.predict(test, used_features)
# predict_cat = model_cat.predict(test, used_features)
# predict_tabnet = model_tabnet.predict(test[used_features])
# predict = (predict_xgb + predict_lgb + predict_cat) / 3
predict = (predict_xgb * 0.5 + predict_lgb * 0.5)

predict_train = model_lgb.predict(train, used_features) * 0.5 + model_xgb.predict(train, used_features) * 0.5

# 预测结果后处理
min_ = self.info_['min_target']
max_ = self.info_['max_target']
predict = clip_label(predict, min_, max_)

predict_train = clip_label(predict_train, min_, max_)

# 获得结果
sub = test[id_ + [self.info_['time_col']]]
sub[target] = predict
sub.index = range(len(sub))

sub_train = train[id_ + [self.info_['time_col']]]
sub_train[target] = predict_train
sub_train.index = range(len(sub_train))

return sub,sub_train,self.dfs_['FE_all']

你可能感兴趣的:(竞赛,机器学习,深度学习,阿里天池,数据挖掘竞赛,机器学习竞赛,时间序列预测)