使用MultiIndex附加pandas DataFrame,数据包含新标签,但保留旧MultiIndex的整数位置

Ful*_*lco 9 python numpy recommendation-engine pandas categorical-data

基本情景

对于推荐服务,我正在一组用户 - 项目交互上训练矩阵分解模型(LightFM).为了使矩阵分解模型产生最佳结果,我需要将我的用户和项目ID映射到从0开始的连续范围的整数ID.

我在这个过程中使用了一个pandas DataFrame,我发现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
Run Code Online (Sandbox Code Playgroud)

然后允许我像这样获得连续的地图

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')
Run Code Online (Sandbox Code Playgroud)

之后,我可以使用df.index.levels[0].get_loc方法将它们映射回来.大!

延期

但是,现在我正在尝试简化我的模型训练过程,理想情况是通过逐步训练新数据,保留旧的ID映射.就像是:

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
Run Code Online (Sandbox Code Playgroud)

然后,只需将新评级附加到旧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
Run Code Online (Sandbox Code Playgroud)

看起来不错,但是

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')
Run Code Online (Sandbox Code Playgroud)

我故意在后面的DataFrame中添加了user_id = 2和item_id = 2,以说明它出错的地方.在df3,标签3(用户和项目)已从整数位置1移动到2.因此映射不再相同.我正在寻找的是[0, 0, 1, 1, 2, 2][0, 1, 0, 1, 0, 2]分别为用户和项目映射.

这可能是因为pandas Index对象的排序,我不确定我想要的是使用MultiIndex策略.寻求有关如何最有效地解决这个问题的帮助:)

一些说明:

  • 我发现使用DataFrames有几个原因,但我使用MultiIndex纯粹用于ID映射.没有MultiIndex的替代方案是完全可以接受的.
  • 我不能保证新评级中的新user_id和item_id条目大于旧数据集中的任何值,因此我在[1,3]存在时添加id 2的示例.
  • 对于我的增量培训方法,我需要在某处存储我的ID地图.如果我只是部分加载新的评级,我将不得不将旧的DataFrame和ID映射存储在某处.如果它可以在一个地方,就像它与索引一样,那将是很棒的,但是列也可以工作.
  • 编辑:另一个要求是允许对原始DataFrame进行行重新排序,这可能在存在重复评级时发生,并且我想保留最新的评级.

解决方案(归功于@jpp原创)

我已经对@jpp的答案进行了修改,以满足我后来添加的额外要求(用EDIT标记).这也真正满足了标题中提出的原始问题,因为它保留了旧的索引整数位置,无论出于何种原因重新排序行.我还把东西包装成了函数:

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)
Run Code Online (Sandbox Code Playgroud)

结果:

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')
Run Code Online (Sandbox Code Playgroud)

jpp*_*jpp 2

连接后强制对齐索引标签似乎并不简单,即使有解决方案,也很少有文档记录。

可能对您有吸引力的一种选项是分类数据。通过一些仔细的操作,这可以实现相同的目的:级别内的每个唯一索引值都具有到整数的一对一映射,并且即使在与其他数据帧串联之后,该映射仍然存在。

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)
Run Code Online (Sandbox Code Playgroud)

我用来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]
Run Code Online (Sandbox Code Playgroud)

然后执行我们的串联:

df3 = pd.concat([df, df2])
Run Code Online (Sandbox Code Playgroud)

最后,检查分类代码是否对齐:

print(df3.index.get_level_values(0).codes.tolist())
[0, 0, 1, 1, 2, 2]
Run Code Online (Sandbox Code Playgroud)

对于每个索引级别,请注意,我们必须将数据帧中的所有索引值进行并集以形成col_cats,否则串联将失败。