使用包含新标签的数据附加 pandas 带有 MultiIndex 的 DataFrame,但保留旧 MultiIndex 的整数位置
Appending pandas DataFrame with MultiIndex with data containing new labels, but preserving the integer positions of the old MultiIndex
基本场景
对于推荐服务,我在一组 user-item 交互上训练矩阵分解模型 (LightFM)。为了使矩阵分解模型产生最佳结果,我需要将我的用户 ID 和商品 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
然后允许我像这样得到连续的地图
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
然后,只需将新评分附加到旧 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')
后面的DataFrame中我特意加了user_id=2和item_id=2,说明我哪里出错了。在 df3
中,标签 3(对于用户和项目)已从整数位置 1 移动到 2。因此映射不再相同。我正在寻找的是分别用于用户和项目映射的 [0, 0, 1, 1, 2, 2]
和 [0, 1, 0, 1, 0, 2]
。
这可能是因为在 pandas 索引 objects 中排序,我不确定使用 MultiIndex 策略是否完全有可能实现我想要的。寻求有关如何最有效地解决此问题的帮助:)
一些注意事项:
- 出于多种原因,我发现使用 DataFrames 很方便,但我使用 MultiIndex 纯粹是为了 ID 映射。没有 MultiIndex 的替代品是完全可以接受的。
- 我无法保证新评级中的新 user_id 和 item_id 条目大于旧数据集中的任何值,因此我的示例是在存在 [1, 3] 时添加 id 2 .
- 对于我的增量训练方法,我需要将我的 ID 地图存储在某个地方。如果我只加载部分新评级,我将不得不在某处存储旧的 DataFrame 和 ID 映射。如果它可以全部放在一个地方,就像使用索引一样,那就太好了,但列也可以。
- 编辑:另一个要求是允许原始 DataFrame 的行 re-ordering,当存在重复评级时可能会发生这种情况,我想保留最新的一个。
解决方案(原创归功于@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')
在连接后强制对齐索引标签并不简单,即使有解决方案,也没有很好的记录。
一个可能对您有吸引力的选项是 Categorical Data。通过一些小心的操作,这可以达到相同的目的:一个级别中的每个唯一索引值都具有到整数的一对一映射,并且即使在与其他数据帧连接后此映射仍然存在。
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
到 return 一个有序的唯一列表,但如果您无权访问此库,则可以使用 itertool
docs.
现在让我们看一下第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
,否则连接将失败。
我认为使用 MultiIndex 会使这个变得过于复杂 objective:
I need to map my user and item IDs to a continuous range of integer IDs starting at 0.
此解决方案属于以下类别:
Alternatives without MultiIndex are completely acceptable.
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
基本场景
对于推荐服务,我在一组 user-item 交互上训练矩阵分解模型 (LightFM)。为了使矩阵分解模型产生最佳结果,我需要将我的用户 ID 和商品 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
然后允许我像这样得到连续的地图
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
然后,只需将新评分附加到旧 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')
后面的DataFrame中我特意加了user_id=2和item_id=2,说明我哪里出错了。在 df3
中,标签 3(对于用户和项目)已从整数位置 1 移动到 2。因此映射不再相同。我正在寻找的是分别用于用户和项目映射的 [0, 0, 1, 1, 2, 2]
和 [0, 1, 0, 1, 0, 2]
。
这可能是因为在 pandas 索引 objects 中排序,我不确定使用 MultiIndex 策略是否完全有可能实现我想要的。寻求有关如何最有效地解决此问题的帮助:)
一些注意事项:
- 出于多种原因,我发现使用 DataFrames 很方便,但我使用 MultiIndex 纯粹是为了 ID 映射。没有 MultiIndex 的替代品是完全可以接受的。
- 我无法保证新评级中的新 user_id 和 item_id 条目大于旧数据集中的任何值,因此我的示例是在存在 [1, 3] 时添加 id 2 .
- 对于我的增量训练方法,我需要将我的 ID 地图存储在某个地方。如果我只加载部分新评级,我将不得不在某处存储旧的 DataFrame 和 ID 映射。如果它可以全部放在一个地方,就像使用索引一样,那就太好了,但列也可以。
- 编辑:另一个要求是允许原始 DataFrame 的行 re-ordering,当存在重复评级时可能会发生这种情况,我想保留最新的一个。
解决方案(原创归功于@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')
在连接后强制对齐索引标签并不简单,即使有解决方案,也没有很好的记录。
一个可能对您有吸引力的选项是 Categorical Data。通过一些小心的操作,这可以达到相同的目的:一个级别中的每个唯一索引值都具有到整数的一对一映射,并且即使在与其他数据帧连接后此映射仍然存在。
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
到 return 一个有序的唯一列表,但如果您无权访问此库,则可以使用 itertool
docs.
现在让我们看一下第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
,否则连接将失败。
我认为使用 MultiIndex 会使这个变得过于复杂 objective:
I need to map my user and item IDs to a continuous range of integer IDs starting at 0.
此解决方案属于以下类别:
Alternatives without MultiIndex are completely acceptable.
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