在 python 中创建 NxN similarity/distance 矩阵的有效方法

Efficient method to create NxN similarity/distance matrix in python

我需要在 python 中创建一个 NxN 相似度矩阵,其中 N = 943。 我最初使用 cosine_similarity 的 sklearns 实现,但现在我需要使用更复杂和非标准的距离度量。

下午好, 我有一个用户电影数据框(table 中的 NaN 表示用户没有对这些电影进行评分)

| movie_id | 1 | 2 | 3 | 4 | 5 |
|----------|---|---|---|---|---|
| user_id  |   |   |   |   |   |
| 1        | 1 | 1 | NaN | 4 | 5 |
| 2        | NaN | 1 | 1 | 5 | 5 |
| 3        | 4 | NaN | 4 | 1 | 2 |

我需要将 3 个单独的函数应用于用户电影数据框:接近度、影响力和流行度。
2 个用户之间的最终相似性由接近度、影响力和流行度的乘积给出。
现在棘手的部分是我只需要为每个用户的“共同评级”项目应用上述 3 个函数。所以比如在计算user1和user2的相似度时,我们应该只考虑movie_ids 2, 4 and 5.

现在我将准确定义这 3 个函数应该做什么。

  1. 首先我定义了一个名为“agreement”的辅助方法
    给定来自 2 个用户的 2 个评分,此函数 returns 为真当且仅当两个评分都在中位数的同一侧。在我们的例子中,中位数是 2.5。否则为假。
def agreement(rating1: int, rating2: int) -> bool:
    if ((rating1 > 2.5 and rating2 < 2.5) or (rating1 < 2.5 and rating2 > 2.5)):
        return False 
    else:
        True 
  1. 距离
    给定来自 2 个用户的 2 个评分,如果 2 个评分一致,此函数将简单地计算绝对差异。如果评分不一致,则会受到处罚。
def proximity(rating1: int, rating2: int) -> float: 
    if(agreement(rating1, rating2)):
        dist = np.absolute(rating1 - rating2)
    else: 
        dist = 2 * np.absolute(rating1 - rating2)
    prox = ((2*(rating_max - rating_min) + 1) - dist) ** 2
    return prox
  1. 影响
    给定来自 2 个用户的 2 个评分,如果 2 个评分一致,此函数将计算 impact_score。如果 2 个评分不一致,则 returns1/impact_score
def impact(rating1: int, rating2: int) -> float: 
    impact_score = (np.absolute(rating1 - rating_median) + 1) * (np.absolute(rating2 - rating_median) + 1)
    if(agreement(rating1, rating2)):
        return impact_score 
    else: 
        return 1/impact_score 
  1. 人气。
    给定来自 2 个用户的 2 个评分和给定 movie_id(mu_k) 的平均评分,此方法计算 pop_score 当且仅当这 2 个评分都大于(或小于)平均评分给定电影的。
def popularity(rating1: int, rating2: int, mu_k) -> float: 
    pop = 1
    if((rating1 > mu_k and rating2 > mu_k) or (rating1 < mu_k and rating2 < mu_k)):
        pop = 1 + ((rating1 + rating2)/2 - mu_k)**2
    return pop

最终的相似度矩阵应该是这样的:

#           0          1          2
#0   1.000000  60.972245  12.761905
#1  60.972245   1.000000   9.790476
#2  12.761905   9.790476   1.000000

问题是我目前的实施速度非常慢。我大约需要 1.5 小时来计算 N=943 的矩阵。

我目前遍历 NxN 矩阵的每个单元格并单独应用所有 3 个函数(当前实现代码:https://pastebin.com/zfcyBhJz)。

所以我想知道在给定要使用的 3 个函数的情况下,是否有更快更有效的方法来生成所需的相似度矩阵?

使用numpyp.ma.MaskedArray,同时充分发挥广播的作用,可以获得非常好的性能。

先获取df的values:

import numpy as np
from numpy import nan


ratings = np.array([[1., 1., nan, 4., 5.],
                    [nan, 1., 1., 5., 5.],
                    [4., nan, 4., 1., 2.]])
# ratings = df_ratings.values

转换为MaskedArray:

from numpy.ma import masked_invalid


ratings = masked_invalid(ratings)
# masked_array(
#   data=[[1.0, 1.0, --, 4.0, 5.0],
#         [--, 1.0, 1.0, 5.0, 5.0],
#         [4.0, --, 4.0, 1.0, 2.0]],
#   mask=[[False, False,  True, False, False],
#         [ True, False, False, False, False],
#         [False,  True, False, False, False]],
#   fill_value=1e+20)

计算每对用户之间所有评分的agrement的负数:

temp = ratings - 2.5
not_agreements = temp[:, None] * temp[None] < 0
# Equivalent to
# from numpy.ma import masked_array
# not_argeements = masked_array([masked_array([(i - 2.5) * (j - 2.5) < 0 for j in ratings]) for i in ratings])

同理计算所有proximityimpactpopularity,这里我假设rating_maxrating_minrating_median都是标量:

dist = np.abs(ratings[:, None] - ratings[None])
dist[not_agreements] *= 2
prox = ((2 * (rating_max - rating_min) + 1) - dist) ** 2

temp = np.abs(ratings - rating_median) + 1
impact_score = temp[:, None] * temp[None]
impact_score[not_agreements] = 1 / impact_score[not_agreements]

mu_k = ratings.mean(0)
temp = ratings - mu_k
shape = ratings.shape
pop = np.ones(shape[:1] + shape)
mask = temp[:, None] * temp[None] > 0
pop[mask] += ((temp[:, None] + temp[None]) / 2)[mask] ** 2

将它们相乘并沿最后一个轴求和,然后将对角线上的值设置为1,最后得到你想要的结果:

similarity_matrix = (prox * impact_score * pop).sum(-1)
similarity_matrix[np.diag_indices_from(similarity_matrix)] = 1

similarity_matrix_df = pd.DataFrame(similarity_matrix, index=df_ratings.index, columns=df_ratings.index)

经测试,你的遍历方法的运行时间和你例子中我的方法差不多,但是随着数组的扩大,你的方法的运行时间增加了很多快速地。当数组的形状达到(48, 50)时,最多需要10s,而我的向量化方法只需要0.06s。