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

时间:2018-05-19 23:36:35

标签: python pandas numpy recommendation-engine 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

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

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策略。寻求有关如何最有效地解决这个问题的帮助:)

一些注意事项:

  • 我发现使用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)

结果:

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 :(得分: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,否则连接将失败。