September 7, 2021

Успокоительный алгоритм для безошибочных прогнозов

Хочу поделиться алгоритмом, который позволяет выявить ошибки при правке прогнозов модели машинного обучения. Конечно, в расчет берется ситуация, когда вы не можете просмотреть все записи ввиду их большого количества.

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

pred_new, pred_ser

Для удобства сравнения я предварительно склеил два вектора прогнозов (функцией merge из библиотеки Pandas):

combo_df = pred_ser.reset_index().copy()
combo_df = combo_df.merge(pred_new.reset_index().rename(columns={'pred':'pred_new'}), how='outer')
combo_df

Затем осуществил следующую последовательность шагов:

1 Проверил, что новые предсказания не теряют значения (старые были, а новые не появились) и обратно (новые образованы, когда старых не было):

combo_df[(combo_df['pred_new'].isnull()) & (combo_df['pred'].notnull())]
combo_df[(combo_df['pred'].isnull()) & (combo_df['pred_new'].notnull())]

Следует отметить, что в зависимости от задачи данные ситуации не обязательно ошибочны. В данном случае они, прежде всего, должны привлечь ваше внимание.

2 Исключил ситуацию одновременных null значений и удостоверился, что в части данных значения совпадают, а в части - нет:

combo_df = combo_df[~((combo_df['pred_new'].isnull()) & (combo_df['pred'].isnull()))]
combo_df[combo_df['pred_new'] != combo_df['pred']]
combo_df[combo_df['pred_new'] == combo_df['pred']]

3 Дальше разбираемся в причинах именно таких значений.

В данном случае значения должны были корректироваться при наличии резервов соответствующей позиции (хранятся в столбце stock_ser):

stock_ser

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

combo_df = combo_df.merge(stock_ser.reset_index().rename(columns={'kol': 'stock', 
                                                        'level_0':0, 'level_1':1}), how='left')
combo_df

Убедимся, что изменения только в тех позициях, где есть остатки и их количество совпадает с численностью из вывода выше:

# changes only where stock not 0 and itog !=itog new
combo_df[(combo_df['stock']!=0) & (combo_df['pred'] != combo_df['pred_new'])]

Дальше проверяем логику изменений:

где остатков больше чем прогноз, новый обнуляется (если данные большие, сравниваем таблицы по количеству записей):

print(combo_df[(combo_df['stock']>=combo_df['pred']) & (combo_df['pred_new'] == 0)])
# ==
print(combo_df[(combo_df['stock'] >= combo_df['pred'])])

где остатков меньше, новый прогноз равен разности:

# stock<pred
print(combo_df[(combo_df['stock'] < combo_df['pred'])])
# ==
print(combo_df[(combo_df['stock'] < combo_df['pred']) & (combo_df['pred_new'] == combo_df['pred']-combo_df['stock'])])

Аналогичные стадии проверки осуществляются и в других ситуациях. Просто от случая к случаю логика может меняться, но в целом привлекать внимание должны присутствия где-то nan, равенства/неравенства значений.