September 17, 2021

Подмена в прогнозах, которой никто не рад

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

Рассмотрим вопрос на примере системы предсказаний потребления объектами товаров. Сгенерируем синтетические данные (использованные приемы описаны ранее):

import pandas as pd
import numpy as np
np.random.seed(0)

# предсказания количества item-ов для объектов
items = ['item1','item2','item3','item4','item5']
all_df = pd.MultiIndex.from_product([np.arange(5), items]).to_frame()\
            .reset_index(drop=True).rename(columns={0:'objs', 1:'items'})

preds_df = pd.concat([all_df.sample(10).reset_index(drop=True), 
                      pd.Series(np.random.randint(100, size=10), name='preds')], axis=1)
preds_df.sort_values(by='objs')

objs, items задают пары объектов и товаров, для которых мы делаем предсказания. Подмена так же осуществляется для пары, например, филиал организации использовал картриджи двух типов, а теперь только одного.

Правила замены определяются исходя из перечня имеющихся на obj item-ов (например, всех используемых в филиале товаров, зададим в obj_items_df) и словаря замены (например, вместо item1 - item2 /шариковые -> гелевые ручки, зададим в maps_d):

# что имеется на объектах
obj_items_df = preds_df.copy()[['objs', 'items']]
obj_items_df.drop([7], inplace=True)
obj_items_df.sort_values(by='objs')
maps_d = {'item1':'item2', 'item2':'item1', 'item3':'item5', 'item5':'item3'}

Отмечу, что к описанной логике замены, базирующейся на перечне всех имеющихся на объектах товаров, можно перейти, имея список отсутствующих номенклатур, следующим образом:

obj_items_minus_df = pd.MultiIndex.from_tuples([(4,'item5')]).to_frame().\
                    reset_index(drop=True).rename(columns={0:'objs',1:'items'})
obj_items_df = preds_df.copy()[['objs', 'items']]
obj_items_df = obj_items_df[~((obj_items_df['objs'].isin(obj_items_minus_df['objs']))&
                            (obj_items_df['items'].isin(obj_items_minus_df['items'])))]

obj_items_df.sort_values(by='objs')

Теперь перейдем к функции подмены - change_items. Ее логика предполагает фильтрацию и замену значений на уровне индексов Pandas, так как этот способ работает быстрее других (детальнее рассказывал ранее) . Код функции описан ниже:

def change_items(preds_df, obj_items_df, maps_d, minus=False):
    ''' input - preds_df with cols objs,items,preds
        obj_items_df - objs,items
        maps_d - vocab of item transforms
        minus - if obj_items_df is a collection of to be excluded examples
    '''
    if minus:
        obj_items_minus_df = obj_items_df.copy()
        obj_items_df = preds_df.copy()
        obj_items_df = obj_items_df[~((obj_items_df['objs'].isin(obj_items_minus_df['objs']))&
                    (obj_items_df['items'].isin(obj_items_minus_df['items'])))].drop('preds',axis=1)
    else:
        obj_items_df = obj_items_df.copy()
    cols = obj_items_df.columns
    obj_items_df['flag']=1
    obj_items_flag = obj_items_df.set_index(['objs','items'])
    preds_ser = preds_df.set_index(['objs','items'])
    obj_items_flag = obj_items_flag.reindex(preds_ser.index)
    match_data = np.where(obj_items_flag['flag'].notnull(), obj_items_flag.index.map(
                        lambda x: (x[0], (x[1], x[1]))),
                     obj_items_flag.index.map(lambda x: (x[0], (x[1], maps_d.get(x[1], x[1])))))
    match_df = pd.MultiIndex.from_tuples(match_data).to_frame().reset_index(drop=True)
    match_df[cols[1]] = match_df[1].str[0]
    match_df['items_new'] = match_df[1].str[1]
    match_df.drop([1], axis=1, inplace=True)
    return match_df.rename(columns={0:cols[0]})

Ниже представлены результаты ее работы:

# match_df = change_items(preds_df, obj_items_df, maps_d)
match_df = change_items(preds_df, obj_items_minus_df, maps_d, minus=True)

res_df = preds_df.merge(match_df, on = ['objs', 'items'], how='outer').drop('items', axis=1).rename(
                        columns = {'items_new':'item'})
res_df

Теперь может понадобиться агрегировать прогнозы с повторяющимися item-ми (например, прогнозировали картриджи двух типов, а теперь используется только один, соответственно, и количество потребуется просуммировать):

res_df.groupby(['objs','item'], as_index=False)['preds'].sum()

Не пропустите ничего интересного и подписывайтесь на страницы канала в других социальных сетях:

Instagram

Яндекс Дзен

Telegram