在数组列上查找常见的 friends/interactions

Find common friends/interactions on array columns

我有一个结构如下的 DF:

test = pd.DataFrame({
                    'person_1': ['Frodo', 'Frodo', 'Gandalf']
                    ,'person_2': ['Sam', 'Legolas', 'Legolas']
                    ,'relations_person_1': [
                       ['Gandalf', 'Sam', 'Legolas', 'Gollum', 'Sauron'],
                       ['Gandalf', 'Sam', 'Legolas', 'Gollum', 'Sauron'],                        
                       ['Bilbo', 'Frodo', 'Sauron', 'Sam']
                      ]
                    ,'relations_person_2': [
                       ['Gandalf', 'Frodo', 'Gimli', 'Gollum'],
                       ['Galadriel', 'Arwen', 'Gimli', 'Frodo'],
                       ['Galadriel', 'Arwen', 'Gimli', 'Frodo'],
                      ]
                    })

其中relations_person_1relations_person_2分别是person_1person_2的关系。 我需要找到“person_1”和“person_2”之间关系的通用名称。

我设法用下面的代码解决了这个问题

test['common_friends'] = test.apply(
    lambda x: np.intersect1d(x.relations_person_1 ,x.relations_person_2)
   ,axis = 1)

test.head()
#output:
    person_1    person_2    relations_person_1                      relations_person_2                  common_friends
0   Frodo       Sam         [Gandalf, Sam, Legolas, Gollum, Sauron] [Gandalf, Frodo, Gimli, Gollum]     [Gandalf, Gollum]
1   Frodo       Legolas     [Gandalf, Sam, Legolas, Gollum, Sauron] [Galadriel, Arwen, Gimli, Frodo]    []
2   Gandalf     Legolas     [Bilbo, Frodo, Sauron, Sam]             [Galadriel, Arwen, Gimli, Frodo]    [Frodo]

我的解决方案的问题是使用 apply,在整个 DF 中使用它时速度非常慢。我想知道是否有更优化的方法来获得结果,也许避免使用 apply 或在数据中使用一些图形结构。

applypandas 中本质上较慢,而不是使用 apply 我们可以 zip 列然后在 [=17 的成员资格的列表理解测试中=] 在 relations_person_2 中使用 set 交集

test['common_friends'] = [list(set(x).intersection(y)) for x, y in 
                          zip(test['relations_person_1'], test['relations_person_2'])]

结果

  person_1 person_2                       relations_person_1                relations_person_2     common_friends
0    Frodo      Sam  [Gandalf, Sam, Legolas, Gollum, Sauron]   [Gandalf, Frodo, Gimli, Gollum]  [Gollum, Gandalf]
1    Frodo  Legolas  [Gandalf, Sam, Legolas, Gollum, Sauron]  [Galadriel, Arwen, Gimli, Frodo]                 []
2  Gandalf  Legolas              [Bilbo, Frodo, Sauron, Sam]  [Galadriel, Arwen, Gimli, Frodo]            [Frodo]

时间安排供参考

# Sample dataframe with 300000 rows generated for testing purpose
test = pd.concat([test] * 100000, ignore_index=True)

%%timeit
_ = [list(set(x).intersection(y)) for x, y in zip(test['relations_person_1'], test['relations_person_2'])]
# 262 ms ± 34.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)


%%timeit
_ = test.apply(
    lambda x: np.intersect1d(x.relations_person_1 ,x.relations_person_2)
   ,axis = 1)
# 19.3 s ± 1.51 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

简单地避免 apply 并使用列表理解和 set 交集,我们可以获得大约 73x

的性能提升

我会走不同的路线,以不同的方式表示关系。 我假设这是您最初使用的数据框:

>>> df
    person                         relations_person
0    Frodo  [Gandalf, Sam, Legolas, Gollum, Sauron]
1  Gandalf              [Bilbo, Frodo, Sauron, Sam]
2      Sam          [Gandalf, Frodo, Gimli, Gollum]
3  Legolas         [Galadriel, Arwen, Gimli, Frodo]

如果没有,您可以随时使用:

>>> df = pd.concat([
...   test[['person' + suffix, 'relations_person' + suffix]]\
...   .rename(columns=lambda c: c.replace(suffix, ''))
...   for suffix in ('_1', '_2')
... ]).drop_duplicates('person').reset_index(drop=True)

然后使用 explode 创建所有交互对:

>>> relations = df.explode('relations_person')
>>> relations
     person relations_person
0     Frodo          Gandalf
1     Frodo              Sam
2     Frodo          Legolas
3     Frodo           Gollum
4     Frodo           Sauron
5   Gandalf            Bilbo
6   Gandalf            Frodo
7   Gandalf           Sauron
8   Gandalf              Sam
9       Sam          Gandalf
10      Sam            Frodo
11      Sam            Gimli
12      Sam           Gollum
13  Legolas        Galadriel
14  Legolas            Arwen
15  Legolas            Gimli
16  Legolas            Frodo

为了获得类似格式的关系以便我们可以进行简单的合并,我们 stack 列:

>>> lookup = test[['person_1', 'person_2']].stack().to_frame('person').rename_axis(['row', 'col'])
>>> lookup
               person
row col              
0   person_1    Frodo
    person_2      Sam
1   person_1    Frodo
    person_2  Legolas
2   person_1  Gandalf
    person_2  Legolas

现在很容易生成所有的关系:

>>> common = lookup.reset_index().merge(relations, how='left', on='person')
>>> common
        row       col   person relations_person
0         0  person_1    Frodo          Gandalf
1         0  person_1    Frodo              Sam
2         0  person_1    Frodo          Legolas
3         0  person_1    Frodo           Gollum
4         0  person_1    Frodo           Sauron
5         0  person_2      Sam          Gandalf
6         0  person_2      Sam            Frodo
7         0  person_2      Sam            Gimli
8         0  person_2      Sam           Gollum
9         1  person_1    Frodo          Gandalf
10        1  person_1    Frodo              Sam
11        1  person_1    Frodo          Legolas
12        1  person_1    Frodo           Gollum
13        1  person_1    Frodo           Sauron
14        1  person_2  Legolas        Galadriel
15        1  person_2  Legolas            Arwen
16        1  person_2  Legolas            Gimli
17        1  person_2  Legolas            Frodo
18        2  person_1  Gandalf            Bilbo
19        2  person_1  Gandalf            Frodo
20        2  person_1  Gandalf           Sauron
21        2  person_1  Gandalf              Sam
22        2  person_2  Legolas        Galadriel
23        2  person_2  Legolas            Arwen
24        2  person_2  Legolas            Gimli
25        2  person_2  Legolas            Frodo

从那里每个关系出现在 test 数据框的一行中的次数:

>>> relation_counts = common.groupby('row')['relations_person'].value_counts()
>>> relation_counts
row      relations_person
0        Gandalf             2
         Gollum              2
         Frodo               1
         Gimli               1
         Legolas             1
         Sam                 1
         Sauron              1
1        Arwen               1
         Frodo               1
         Galadriel           1
         Gandalf             1
         Gimli               1
         Gollum              1
         Legolas             1
         Sam                 1
         Sauron              1
2        Frodo               2
         Arwen               1
         Bilbo               1
         Galadriel           1
         Gimli               1
         Sam                 1
         Sauron              1

最后根据每行至少出现两次的条目制作列表:

>>> test.join(relation_counts[relation_counts >= 2].groupby('row').agg(list).reindex(test.index, fill_value=[]))
  person_1 person_2                       relations_person_1                relations_person_2 relations_person
0    Frodo      Sam  [Gandalf, Sam, Legolas, Gollum, Sauron]   [Gandalf, Frodo, Gimli, Gollum]           [2, 2]
1    Frodo  Legolas  [Gandalf, Sam, Legolas, Gollum, Sauron]  [Galadriel, Arwen, Gimli, Frodo]               []
2  Gandalf  Legolas              [Bilbo, Frodo, Sauron, Sam]  [Galadriel, Arwen, Gimli, Frodo]              [2]

所以在一个浓缩形式中:

relcounts = test[['person_1', 'person_2']].stack().rename('person').reset_index()\
  .merge(df.explode('relations_person'), on='person', how='left')\
  .groupby('level_0')['relations_person'].value_counts()
test.join(relcounts[relcounts > 1].groupby(level=0).agg(list))