对于推荐服务,我正在一组用户 - 项目交互上训练矩阵分解模型(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
然后允许我像这样获得连续的地图
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 Index对象的排序,我不确定我想要的是使用MultiIndex策略。寻求有关如何最有效地解决这个问题的帮助:)
一些注意事项:
我已经修改了@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')
答案 0 :(得分:4)
我认为MultiIndex的使用过度复杂化了这个目标:
我需要将用户和项目ID映射到从0开始的连续范围的整数ID。
此解决方案属于以下类别:
没有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
答案 1 :(得分:2)
连接后强制对齐索引标签并不简单,如果有解决方案,则说明文档很少。
可能对您有吸引力的一个选项是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
返回已订购的唯一列表,但如果您无权访问此库,则可以使用unique_everseen
{中相同的itertool
配方{3}}
现在让我们看看第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
,否则连接将失败。