问题描述
名词定义
- 库存水位:在仓库存数量,用来满足需求。
- 补货时长(交货时间,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的整体优化方案,实现该目标。库存量视角的变化过程如下图所示。
我们会给一个训练集,供参赛选手训练模型并验证模型效果使用。同时,初赛时我们会提供一个测试集,选手需要为按时间读取20210302-20210607的需求数据,并根据历史需求数据以及补货在途,决定补货量,并把决策结果CSV文件输出到指定位置。(由于赛题特殊与评价机制的问题,初赛时给选手透出了未来数据,选手在初赛时将其作为未知数据调整算法,不允许在决策时使用当前日之后的未来数据。)
由于库存控制是一个前后相关的决策过程,两次补货决策并不独立,因此我们会提供一段时间的数据,由选手在时间轴上进行多次补货决策,最后在较长的时间段内评价选手方案的好坏。在复赛时,选手需要提交一个docker镜像,镜像中需要包含用来进行库存管理所需的所有内容,如模型、脚本等,未来的需求数据以及到货时间将通过流评测的方式给出。镜像中的脚本需要能根据所得的需求量,根据历史需求数据以及先前所做的决策,决定补货量,并把决策结果CSV文件输出到指定位置;初赛时测试集文件将线下交给选手。
相关参考
https://web.mit.eu/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文件。文件中的每一行表示在未来的时间段内每个决策日每个库存单元需要补充的量,具体包含:补货单元、日期和补货量(用逗号隔开),形式如下:
unit,ts,qty
"unit1","20210308",12
"unit1","20210315",11
"unit1","20210322",9
注:在复赛中,参赛选手需要提交docker镜像,具体的提交方式及规范请参见到时发布的容器镜像提交说明。
评价指标
综合指标
另外
评测时按时间顺序更新上述变量,完毕后用上述公式计算指标。
#初赛评测程序伪代码如下
#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)
整体指标平衡了库存率和服务水平,达到了一定服务水平的情况下,库存率就会更重要。
赛题Baseline
数据准备
import os
import pandas as pd
demand_train_A = 'data/demand_train_A.csv'
geo_topo = 'data/geo_topo.csv'
inventory_info_A = 'data/inventory_info_A.csv'
product_topo = 'data/product_topo.csv'
weight_A = 'data/weight_A.csv'
demand_train_A = pd.read_csv(demand_train_A)
geo_topo = pd.read_csv(geo_topo)
inventory_info_A = pd.read_csv(inventory_info_A)
product_topo = pd.read_csv(product_topo)
weight_A = pd.read_csv(weight_A)
demand_test_A = 'data/demand_test_A.csv'
demand_test_A = pd.read_csv(demand_test_A)
dfs = [demand_train_A,geo_topo,inventory_info_A,product_topo,weight_A,demand_test_A]
for df in dfs:
if 'Unnamed: 0' in df.columns:
df.drop(columns='Unnamed: 0',inplace=True)
if 'ts' in df.columns:
df = df.sort_values(by='ts')
数据拼接
all_data = pd.concat([demand_train_A,demand_test_A])
all_data = all_data.sort_values(by='ts')
all_data = all_data.reset_index().drop(columns='index')
预测未来销量
## 真实值串14天
submission = demand_test_A
submission['yesterday_qty'] = submission.groupby('unit')['qty'].shift(1).fillna(method='ffill').reset_index().sort_index().set_index('index')
submission['diff_1'] = submission['qty'] - submission['yesterday_qty']
submission['qty'] = submission['diff_1']
submission['shift_14']=submission.groupby('unit')['qty'].shift(-14).fillna(0).reset_index().sort_index().set_index('index')
submission = submission[['unit','ts','shift_14']].rename(columns={'shift_14':'qty'})
# 按照7天聚合
submission['dt'] = pd.to_datetime(submission['ts'])
submission['weekofyear'] = submission['dt'].dt.weekofyear
submission['year'] = submission['dt'].dt.year
submission_week = submission.copy()
submission_week = submission_week.groupby(['weekofyear','year','unit'],as_index=False).sum()
submission_week['sum_qty'] = submission_week['qty']
submission = pd.merge(submission_week,submission,on = ['weekofyear','year','unit'])
submission['dayofweek'] = submission['dt'].dt.dayofweek
submission = submission[submission['dayofweek']==0]
submission = submission[['unit','ts','sum_qty']].rename(columns={'sum_qty':'qty'})
根据未来需求消耗掉初始库存
init_inventory = inventory_info_A.set_index(['unit'])['qty'].to_dict()
def consume_init_inventory(arr,init_val):
remain = init_val
i = 0
while remain>0 and i<len(arr):
arr[i] = max(0,arr[i]-remain)
remain -= arr[i]
i+=1
return arr
r = []
for i,group in submission.groupby('unit'):
unit = group['unit'].values[0]
init_val = init_inventory[unit]
group = group.sort_values(by='ts')
qty_list = group['qty'].values
qty_list = consume_init_inventory(qty_list,init_val)
group['qty'] = qty_list
r.append(group)
submission = pd.concat(r)