将熊猫数据帧与 MultiIndex 附加包含新标签的数据,但保留旧 MultiIndex 的整数位置



基本方案

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

我在此过程中使用了熊猫数据帧,我发现 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方法将它们映射回来。伟大!

外延

但是,现在我正在尝试简化我的模型训练过程,理想情况下,方法是在新数据上增量训练它,保留旧的 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

然后,只需将新评级追加到旧数据帧

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,以说明它在哪里出错。在df3中,标签 3(用于用户和项目(已从整数位置 1 移动到 2。所以映射不再相同。我正在寻找的是分别用于用户和项目映射的[0, 0, 1, 1, 2, 2][0, 1, 0, 1, 0, 2]

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

一些注意事项:

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

解决方案(原始@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)

结果:

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')

我认为使用MultiIndex使这个目标过于复杂:

我需要将我的用户和项目 ID 映射到从 0 开始的连续整数 ID 范围。

此解决方案属于以下类别:

没有多索引的替代方案是完全可以接受的。

<小时 />
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}

这些是您从df2获得的新user_id值 - 您在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

在串联后强制对齐索引标签似乎并不简单,如果有解决方案,它的文档也很差。

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

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返回有序的唯一列表,但如果您无权访问此库,则可以使用itertool文档中相同的unique_everseen配方。

现在让我们看一下第 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,否则串联将失败。

最新更新