python
September 5, 2021

Несбывшиеся прогнозы

Рассмотрим кейс работы модели предсказания потребления в условиях имеющихся резервов. Такая ситуация может возникнуть, например, при разработке оптимальной закупочной стратегии обеспечения филиалов крупной организации необходимым инвентарем.

Предположим, что работающая модель машинного обучения у нас уже имеется, основной задачей является учет резервов и их корректировка после очередной поставки. Создадим демонстрационный набор прогнозов (pred_ser), определяемый парой букв (фактически идентификаторы филиалов):

import pandas as pd
import numpy as np
import string
np.random.seed(1)

letters = np.array(list(string.ascii_uppercase))
ids = pd.DataFrame(np.concatenate([np.random.choice(letters, size=10).reshape(-1,1), 
                                   np.random.choice(letters, size=10).reshape(-1,1)], axis=1))

pred_df = pd.concat([ids, pd.Series(np.random.randint(0,100, size=10), name='pred')], axis=1)
pred_df

также зададим резервы (stock_ser) из части индексов прогнозных значений и новых:

stock_ser = pd.Series(np.random.randint(100, size=5), index=pd.MultiIndex.from_tuples(pred_df[[0,1]].sample(5).apply(lambda x: tuple(x), axis=1)))
stock_ser = pd.concat([stock_ser,pd.Series([11], index=[('L','M')])])
stock_ser = pd.concat([stock_ser,pd.Series([13], index=[('A','A')])])
stock_ser

Логика работы реализована в двух функциях refresh_preds_stocks и add_stocks, которые отвечают за обновление прогнозов с учетом остатков и корректировку резервов с учетом их вычета из прогнозов:

def add_stocks(stock_ser, add_ser):
    combo_index = stock_ser.index.union(add_ser.index)
    stock_update = stock_ser.reindex(combo_index, fill_value=0) + 
                                        add_ser.reindex(combo_index, fill_value=0)
    stock_update = stock_update.map(lambda x: x if x>0 else None)
    stock_update.dropna(inplace=True)
    return stock_update
    
def refresh_preds_stocks(pred_col, stock_col):
    pred_ser = pred_col.copy()
    stock_ser = stock_col.copy()
    stock_ser.index.set_names(pred_ser.index.names, inplace=True)
    update_stock = add_stocks(stock_ser,-pred_ser)
    pred_sub_stock_ser = (pred_ser - stock_ser).reindex(pred_ser.index)
    pred_ser.mask(pred_sub_stock_ser.notnull(),pred_sub_stock_ser, inplace=True )
    pred_ser = pred_ser.map(lambda x: max(x,0))
    
    return pred_ser, update_stock

В add_stocks мы добавляем к резервам столбец и возвращаем обновленные результаты. Успешная реализация требует аккуратной переиндексации столбцов для учета отсутствующих в обоюдных списках индексов (с заменой пустых значений на 0) и удаления значений меньше или равных нулю. В refresh_preds_stocks логика схожая только обновление прогнозов происходит путем замены только тех значений, в которых присутствует остаток (путем их вычета).

Получим обновленные прогнозы и сравним с первоначальными:

pred_new, stock_new = refresh_preds_stocks(pred_ser, stock_ser)
pred_df.merge(pred_new.reset_index().rename(columns={'pred':'pred_new'}))

а вот "уцелевшие" остатки:

Функционал add_stocks намерено не включен в состав refresh_preds_stocks, так как он может понадобиться в последующем. Допустим, если поставщики товара в филиал округляют порции поставок до неких значений (например, иначе не продаются или штучно не включаются в упаковку) и остатки нужно корректировать на величину этих "добавок" (фактически разность между округленными и первоначальными прогнозами). Условно добавим к остаткам прогнозы и посмотрим сработает ли наша функция корректно:

add_stocks(stock_new, pred_ser)