如何从Pandas数据框计算Jaccard相似度

30
我有一个数据框,形状如下:(1510, 1399)。列代表产品,行代表用户为给定产品分配的值(0或1)。我该如何计算jaccard_similarity_score?

enter image description here

我创建了一个占位符数据框,列出了产品与产品之间的关系

data_ibs = pd.DataFrame(index=data_g.columns,columns=data_g.columns)

我不确定如何迭代 data_ibs 来计算相似度。

for i in range(0,len(data_ibs.columns)) :
    # Loop through the columns for each column
    for j in range(0,len(data_ibs.columns)) :
        .........
2个回答

87
使用pairwise_distances计算距离,然后从1中减去该距离以找到相似度得分:
from sklearn.metrics.pairwise import pairwise_distances
1 - pairwise_distances(df.T.to_numpy(), metric='jaccard')

解释:

在较新版本的scikit learn中,jaccard_score的定义类似于Wikipedia中的Jaccard相似系数定义:

其中

  • M11表示A和B都为1的属性总数。
  • M01表示A的属性为0且B的属性为1的属性总数。
  • M10表示A的属性为1且B的属性为0的属性总数。
  • M00表示A和B都为0的属性总数。

让我们创建一个样本数据集来查看结果是否匹配:

from pandas import DataFrame, crosstab
from numpy.random import default_rng
rng = default_rng(0)

# Create a dataframe of 40 rows and 5 columns (named A, B, C, D, E)
# Each cell in the DataFrame is either 0 or 1 with 50% probability
df = DataFrame(rng.binomial(1, 0.5, size=(40, 5)), columns=list('ABCDE'))

这将为列A和B生成以下交叉表:

A/B 0 1
0 10 7
1 14 9

根据定义,Jaccard相似度得分为:

M00 = (df['A'].eq(0) & df['B'].eq(0)).sum()  # 10
M01 = (df['A'].eq(0) & df['B'].eq(1)).sum()  # 7
M10 = (df['A'].eq(1) & df['B'].eq(0)).sum()  # 14
M11 = (df['A'].eq(1) & df['B'].eq(1)).sum()  # 9


print(M11 / (M01 + M10 + M11))  # 0.3

使用 jaccard_score,您将得到以下结果:

from sklearn.metrics import jaccard_score
print(jaccard_score(df['A'], df['B']))  # 0.3

jaccard_score 函数的问题在于它不是向量化的。您需要循环遍历所有列,以计算每个对应列的相似度分数。为了避免这种情况,您可以使用向量化的距离版本。但是,由于它是“距离”而不是“相似度”,因此您需要从1中减去该值:

from sklearn.metrics.pairwise import pairwise_distances
print(1 - pairwise_distances(df.T.to_numpy(), metric='jaccard'))

# [[1.         0.3        0.45714286 0.34285714 0.46666667]
#  [0.3        1.         0.29411765 0.33333333 0.23333333]
#  [0.45714286 0.29411765 1.         0.40540541 0.44117647]
#  [0.34285714 0.33333333 0.40540541 1.         0.36363636]
#  [0.46666667 0.23333333 0.44117647 0.36363636 1.        ]]

你可以选择将其转换回 DataFrame:

jac_sim = 1 - pairwise_distances(df.T.to_numpy(), metric='jaccard')
jac_sim_df = DataFrame(
    1 - pairwise_distances(df.T.to_numpy(), metric='jaccard'), 
    index=df.columns, columns=df.columns,
)

#           A         B         C         D         E
#  A  1.000000  0.300000  0.457143  0.342857  0.466667
#  B  0.300000  1.000000  0.294118  0.333333  0.233333
#  C  0.457143  0.294118  1.000000  0.405405  0.441176
#  D  0.342857  0.333333  0.405405  1.000000  0.363636
#  E  0.466667  0.233333  0.441176  0.363636  1.000000

注意:在此答案的先前版本中,计算使用了汉明度量和pairwise_distances,因为在scikit-learn的早期版本中,jaccard_score的计算类似于准确性得分(即(M00 + M11) / (M00 + M01 + M10 + M11))。现在不再是这种情况,因此更新了答案,改用jaccard度量而不是hamming

1
其实我认为可以通过1减去Jaccard相似度来得到Jaccard距离。 - kitchenprinzessin
1
当然,基于定义,它们可能会发生变化。我的意思是sklearn的jaccard_similarity_score不等于1-sklearn的jaccard距离。但它等于1-sklearn的汉明距离。例如,维基百科的定义与sklearn的定义不同。 - ayhan
14
我很惊讶这个帖子没有更多的赞。工作得很出色,谢谢。 - Private
1
嗨@ayhan,是否有可能对结果进行对角线切割以去除重复值?谢谢。 - user46543
1
在我的情况下,我没有得到相同的结果... - Carlos Vega
显示剩余5条评论

0

可以使用scipy.spatial.distance.pdist计算Jaccard相似度分数。其中一个指标是'jaccard',它计算Jaccard不相似度(因此必须从1中减去得分以获得Jaccard相似度)。它返回一个1D数组,其中每个值对应于两列之间的Jaccard相似度。

可以通过构建MultiIndex来从分数构建Series。

from scipy.spatial.distance import pdist
jaccard_similarity = pd.Series(1 - pdist(df.values.T, metric='jaccard'), index=pd.MultiIndex.from_tuples([(c1, c2) for i, c1 in enumerate(df) for c2 in df.columns[i+1:]]))

使用ayhan的设置,它会产生以下结果:

A  B    0.300000
   C    0.457143
   D    0.342857
   E    0.466667
B  C    0.294118
   D    0.333333
   E    0.233333
C  D    0.405405
   E    0.441176
D  E    0.363636
dtype: float64

如果需要一个矩阵,也可以从pdist构建。只需构建一个空矩阵,并用这些值填充非对角线(对角线填1)。
from scipy.spatial.distance import pdist

def jaccard_similarity_matrix(df):
    
    n = df.shape[1]
    scores = 1 - pdist(np.array(df).T, metric='jaccard')
    result = np.zeros((n,n))
    result[np.triu_indices(n, k=1)] = scores
    result += result.T
    np.fill_diagonal(result, 1)
    return pd.DataFrame(result, index=df.columns, columns=df.columns)

jaccard_similarity = jaccard_similarity_matrix(df)

result


事实上,通过使用pdist源代码,也可以编写完全仅使用numpy和基本python的自定义函数。
def jaccard_matrix(df):

    def jaccard(x, y):
        nonzero = (x != 0) | (y != 0)
        a = ((x != y) & nonzero).sum()
        b = nonzero.sum()
        return 1 - a / b if b != 0 else 1
    
    arr = df.values
    n = arr.shape[1]
    scores = [jaccard(arr[:, i], arr[:, j]) for i in range(n-1) for j in range(i+1, n)]
    result = np.zeros((n, n))
    result[np.triu_indices(n, k=1)] = scores
    result += result.T
    np.fill_diagonal(result, 1)
    return pd.DataFrame(result, index=df.columns, columns=df.columns)

所有这些函数返回相同的输出,可以通过以下方式进行验证:

df = pd.DataFrame(np.random.default_rng().binomial(1, 0.5, size=(100, 10))).add_prefix('col')
x = pd.DataFrame(1 - pairwise_distances(df.values.T.astype(bool), metric='jaccard'), index=df.columns, columns=df.columns)
y = jaccard_similarity_matrix(df)
z = jaccard_matrix(df)

np.allclose(x, y) and np.allclose(y, z)    # True

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接