Добавление панд DataFrame с MultiIndex с данными, содержащими новые метки, но с сохранением целочисленных позиций старого MultiIndex - PullRequest
0 голосов
/ 20 мая 2018

Базовый сценарий

Для службы рекомендаций я обучаю матричную модель факторизации (LightFM) на множестве взаимодействий пользователь-элемент.Чтобы матричная модель факторизации дала наилучшие результаты, мне нужно сопоставить мои идентификаторы пользователя и элемента с непрерывным диапазоном целочисленных идентификаторов, начиная с 0.

В процессе я использую DataFrame от pandas, и яЯ обнаружил, что MultiIndex чрезвычайно удобен для создания этого отображения, например:

ratings = [{'user_id': 1, 'item_id': 1, 'rating': 1.0},
           {'user_id': 1, 'item_id': 3, 'rating': 1.0},
           {'user_id': 3, 'item_id': 1, 'rating': 1.0},
           {'user_id': 3, 'item_id': 3, 'rating': 1.0}]

df = pd.DataFrame(ratings, columns=['user_id', 'item_id', 'rating'])
df = df.set_index(['user_id', 'item_id'])
df

Out:
                 rating
user_id item_id 
1       1        1.0
1       3        1.0
3       1        1.0
3       1        1.0

, а затем позволяет мне получать непрерывные карты примерно так:

df.index.labels[0]    # For users

Out:
FrozenNDArray([0, 0, 1, 1], dtype='int8')

df.index.labels[1]    # For items

Out:
FrozenNDArray([0, 1, 0, 1], dtype='int8')

После этого я могу отобразитьих обратно, используя метод df.index.levels[0].get_loc.Отлично!

Расширение

Но сейчас я пытаюсь упростить процесс обучения моей модели, в идеале, постепенно обучая его новым данным, сохраняя старые сопоставления идентификаторов.Примерно так:

new_ratings = [{'user_id': 2, 'item_id': 1, 'rating': 1.0},
               {'user_id': 2, 'item_id': 2, 'rating': 1.0}]

df2 = pd.DataFrame(new_ratings, columns=['user_id', 'item_id', 'rating'])
df2 = df2.set_index(['user_id', 'item_id'])
df2

Out:
                 rating
user_id item_id 
2       1        1.0
2       2        1.0

Затем, просто добавив новые рейтинги к старому DataFrame

df3 = df.append(df2)
df3

Out:
                 rating
user_id item_id 
1       1        1.0
1       3        1.0
3       1        1.0
3       3        1.0
2       1        1.0
2       2        1.0

Выглядит хорошо, но

df3.index.labels[0]    # For users

Out:
FrozenNDArray([0, 0, 2, 2, 1, 1], dtype='int8')

df3.index.labels[1]    # For items

Out:
FrozenNDArray([0, 2, 0, 2, 0, 1], dtype='int8')

Я добавил user_id = 2и item_id = 2 в последующем DataFrame специально, чтобы проиллюстрировать, где это происходит для меня.В df3 метки 3 (как для пользователя, так и для элемента) переместились из целочисленной позиции 1 в 2. Таким образом, отображение больше не совпадает.Я ищу [0, 0, 1, 1, 2, 2] и [0, 1, 0, 1, 0, 2] для сопоставления пользователя и элемента соответственно.

Это, вероятно, из-за упорядочения в индексных объектах панд, и я не уверен, что то, что я хочу, вообщевозможно использование стратегии MultiIndex.Нужна помощь в том, как наиболее эффективно решить эту проблему:)

Некоторые примечания:

  • Я считаю использование DataFrames удобным по нескольким причинам, но я использую MultiIndex исключительно для сопоставления идентификаторов,Альтернативы без MultiIndex полностью приемлемы.
  • Я не могу гарантировать, что новые записи user_id и item_id в новых рейтингах больше, чем любые значения в старом наборе данных, поэтому мой пример добавления id 2, когда присутствовали [1, 3].
  • Для моего подхода к инкрементальному обучению мне нужно где-то хранить свои идентификационные карты.Если я только частично загружаю новые рейтинги, мне придется где-то хранить старые карты данных и IDFrame.Было бы замечательно, если бы все это могло быть в одном месте, как это было бы с индексом, но столбцы тоже работают.
  • РЕДАКТИРОВАТЬ: Дополнительное требование состоит в том, чтобы разрешить переупорядочение строк исходного DataFrame, так какможет случиться, если существуют повторяющиеся рейтинги, и я хочу сохранить самый последний рейтинг.

Решение (кредиты @jpp для оригинала)

Я внес изменение в @ jpp'sответ, чтобы удовлетворить дополнительное требование, которое я добавил позже (помеченный как РЕДАКТИРОВАТЬ).Это также действительно удовлетворяет первоначальному вопросу, поставленному в заголовке, поскольку сохраняет старые целочисленные позиции индекса независимо от того, по каким причинам строки были переупорядочены.Я также обернул вещи в функции:

from itertools import chain
from toolz import unique


def expand_index(source, target, index_cols=['user_id', 'item_id']):

    # Elevate index to series, keeping source with index
    temp = source.reset_index()
    target = target.reset_index()

    # Convert columns to categorical, using the source index and target columns
    for col in index_cols:
        i = source.index.names.index(col)
        col_cats = list(unique(chain(source.index.levels[i], target[col])))

        temp[col] = pd.Categorical(temp[col], categories=col_cats)
        target[col] = pd.Categorical(target[col], categories=col_cats)

    # Convert series back to index
    source = temp.set_index(index_cols)
    target = target.set_index(index_cols)

    return source, target


def concat_expand_index(old, new):
    old, new = expand_index(old, new)
    return pd.concat([old, new])


df3 = concat_expand_index(df, df2)

Результат:

df3.index.labels[0]    # For users

Out:
FrozenNDArray([0, 0, 1, 1, 2, 2], dtype='int8')

df3.index.labels[1]    # For items

Out:
FrozenNDArray([0, 1, 0, 1, 0, 2], dtype='int8')

Ответы [ 2 ]

0 голосов
/ 15 июня 2018

Я думаю, что использование MultiIndex усложняет эту задачу:

Мне нужно сопоставить идентификаторы моего пользователя и элемента с непрерывным диапазоном целочисленных идентификаторов, начиная с 0.

Это решение относится к следующей категории:

Альтернативы без MultiIndex полностью приемлемы.


def add_mapping(df, df2, df3, column_name='user_id'):

    initial = df.loc[:, column_name].unique()
    new = df2.loc[~df2.loc[:, column_name].isin(initial), column_name].unique()
    maps = np.arange(len(initial))
    mapping = dict(zip(initial, maps))
    maps = np.append(maps, np.arange(np.max(maps)+1, np.max(maps)+1+len(new)))
    total = np.append(initial, new)
    mapping = dict(zip(total, maps))

    df3[column_name+'_map'] = df3.loc[:, column_name].map(mapping) 

    return df3

add_mapping(df, df2, df3, column_name='item_id')
add_mapping(df, df2, df3, column_name='user_id')

 user_id    item_id rating  item_id_map user_id_map
0   1          1    1.0         0           0
1   1          3    1.0         1           0
2   3          1    1.0         0           1
3   3          3    1.0         1           1
0   2          1    1.0         0           2
1   2          2    1.0         2           2

Объяснение

Это как сохранить отображение для значений user_id.То же самое относится и к значениям item_id.

Это начальные значения user_id (уникальные):

initial_users = df['user_id'].unique()
# initial_users = array([1, 3])

user_map поддерживает сопоставление для значений user_id,согласно вашему требованию:

user_id_maps = np.arange(len(initial_users))
# user_id_maps = array([0, 1])

user_map = dict(zip(initial_users, user_id_maps))
# user_map = {1: 0, 3: 1}

Это новые user_id значения, которые вы получили от df2 - те, которые вы не видели в df:

new_users = df2[~df2['user_id'].isin(initial_users)]['user_id'].unique()
# new_users = array([2])

Теперь мы обновляем user_map для всей базы пользователей с новыми пользователями:

user_id_maps = np.append(user_id_maps, np.arange(np.max(user_id_maps)+1, np.max(user_id_maps)+1+len(new_users)))
# array([0, 1, 2])
total_users = np.append(initial_users, new_users)
# array([1, 3, 2])

user_map = dict(zip(total_users, user_id_maps))
# user_map = {1: 0, 2: 2, 3: 1}

Затем просто сопоставьте значения от user_map до df['user_id']:

df3['user_map'] = df3['user_id'].map(user_map)

user_id item_id rating  user_map
0   1   1       1.0          0
1   1   3       1.0          0
2   3   1       1.0          1
3   3   3       1.0          1
0   2   1       1.0          2
1   2   2       1.0          2
0 голосов
/ 15 июня 2018

Принудительное выравнивание меток индексов после объединения не представляется простым и, если есть решение, оно плохо документировано.

Один из вариантов, который может вам понравиться, - Категориальные данные .С некоторыми осторожными манипуляциями это может достичь той же цели: каждое уникальное значение индекса в уровне имеет однозначное сопоставление с целым числом, и это сопоставление сохраняется даже после конкатенации с другими фреймами данных.

from itertools import chain
from toolz import unique

# elevate index to series
df = df.reset_index()
df2 = df2.reset_index()

# define columns for reindexing
index_cols = ['user_id', 'item_id']

# convert to categorical with merged categories
for col in index_cols:
    col_cats = list(unique(chain(df[col], df2[col])))
    df[col] = pd.Categorical(df[col], categories=col_cats)
    df2[col] = pd.Categorical(df2[col], categories=col_cats)

# convert series back to index
df = df.set_index(index_cols)
df2 = df2.set_index(index_cols)

Я использую toolz.unique для возврата упорядоченного уникального списка, но если у вас нет доступа к этой библиотеке, вы можете использовать идентичный рецепт unique_everseen из itertool документов .

Теперь давайте посмотрим на коды категорий, лежащие в основе 0-го уровня индекса:

for data in [df, df2]:
    print(data.index.get_level_values(0).codes.tolist())

[0, 0, 1, 1]
[2, 2]

Затем выполните нашу конкатенацию:

df3 = pd.concat([df, df2])

Наконец, убедитесь, что коды категорий выровнены:

print(df3.index.get_level_values(0).codes.tolist())
[0, 0, 1, 1, 2, 2]

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...