【问题标题】:Group unique users with two changing IDs使用两个不断变化的 ID 对唯一用户进行分组
【发布时间】:2021-10-01 11:20:51
【问题描述】:

你能想出一个更快的算法来解决这个问题吗?或者改进代码?

问题:

我有两个客户 ID:

  • ID1(例如电话号码)
  • ID2(例如电子邮件地址)

用户有时会更改其 ID1,有时会更改 ID2。怎么能 我找到了唯一用户?

示例:

ID1 = [7, 7, 8, 9]

ID2 = [a, b, b, c]

期望的结果:

ID3 = [Anna, Anna, Anna, Paul]

现实世界的场景大约有。每个列表 600 000 个项目。

这里已经有SQL思路了:How can I match Employee IDs from 2 columns and group them into an array?

我从一个对 TypeScript 有这个想法的朋友那里得到了帮助:https://stackblitz.com/edit/typescript-leet-rewkmh?file=index.ts

我的第二个朋友帮助我编写了一些伪代码,我能够创建这个:

迄今为止最快(不再工作)的代码:

ID1 = [7, 7, 8, 9]
ID2 = ["a", "b", "b", "c"]

def timeit_function(ID1, ID2):
    
    def find_user_addresses():
        phone_i = []
        email_i = []
        
        tmp1 = [ID1[0]]
        tmp2 = []
        tmp_index = []

        while len(tmp1) != 0 or len(tmp2) != 0:
            while len(tmp1) != 0:
                tmp_index = []  
                for index, value in enumerate(ID1):
                    if value == tmp1[0]:
                        tmp2.append(ID2[index])
                        tmp_index.insert(-1, index)

                for i in tmp_index: 
                    del ID1[i]
                    del ID2[i]
                tmp1 = list(dict.fromkeys(tmp1))
                phone_i.append(tmp1.pop(0))

            while len(tmp2) != 0:
                tmp_index = [] 
                for index, value in enumerate(ID2):
                    if value == tmp2[0]:
                        tmp1.append(ID1[index])
                        tmp_index.insert(0, index)

                for i in tmp_index: 
                    del ID1[i]
                    del ID2[i]
                tmp2 = list(dict.fromkeys(tmp2))
                email_i.append(tmp2.pop(0))

        return phone_i, email_i
    
    users = {}
    i = 0
    while len(ID1) != 0:
        phone_i, email_i = find_user_addresses()
        users[i] = [phone_i, email_i]
        i += 1
    return users

输出:

{0: [[7, 8], ['a', 'b']], 1: [[9], ['c']]}

含义:{User_0: [[phone1, phone2], [email1, email2]], User_1: [phone3, email3]}

排名

rank Username %timeit Unique users Correct output?
1. Zachary Vance 32 ms ± 1.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) 1408 yes
2. igrinis 5.54 s ± 81.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 1408 yes
(3.) dkapitan 8 s ± 106 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 3606 no
(4.) thenarfer 2.34 µs ± 3.25 µs per loop (mean ± std. dev. of 7 runs, 1 loop each) 1494 no

代码使用两个列表here 运行(需要一些时间来加载)。

【问题讨论】:

  • 我添加了排名,因为它们在我的计算机上显示。如果没有人反对,我会将第一个赏金奖励给@Zachary Vance。感谢您的时间和贡献!将来我可能会带着另一个赏金回来!

标签: python common-table-expression recursive-cte


【解决方案1】:

思路很简单:

  • 对于每个条目
    • 扫描所有现有用户,

      • 如果条目的任何维度与之前的用户匹配,则扩展他的属性集,并将他添加到合并列表中
    • 如果与现有的任何人都不匹配,则创建一个新用户,

    • 否则将不同的用户组合在一起

这应该很快,因为它只扫描列表一次并且不需要递归。

ID1 = [7, 7, 8, 9]
ID2 = ["a", "b", "b", "c"]
user = {}
new_user_idx = 0
for i in range(len(ID1)):
    merge = []   # this is a list of users that should be merged
    for k in user:
        # find if any feature is already found in previous user  
        if ID1[i] in user[k][0]:
            user[k][1].add(ID2[i])
            merge.append(k)
        if ID2[i] in user[k][1]:
            user[k][0].add(ID1[i])
            merge.append(k)
    if not merge:
        # we have to create a new user
        user[new_user_idx] = (set([ID1[i]]), set([ID2[i]]))
        new_user_idx += 1
    elif len(merge)>1:
        # merging existing users
        for el in set(merge[1:]):
            if el==merge[0]: continue  # skip unnecessary merge
            user[merge[0]][0].update(user[el][0]) # copy attributes
            user[merge[0]][1].update(user[el][1])
            user.pop(el) # delete merged user
print(user)   


{0: ({7, 8}, {'a', 'b'}), 1: ({9}, {'c'})}
 

【讨论】:

  • 非常感谢您的代码@igrinis!如果这是最快提交的答案,我会奖励你赏金。我得到了6.28 µs ± 81 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
  • 我用ID1 = ['ccedf', 'dabdd', ..., n=1000] ID2 = [53412, 52266, ..., n=1000] 尝试了我们的两个代码。当您的代码找到 1408 个用户时,我的代码找到 1494 。 gist.github.com/thenarfer/e4488e03965f61fc5bb139f8b7995186(加载需要一些时间)
【解决方案2】:

用套装试试:

%%timeit
ID1 = [7, 7, 8, 9]
ID2 = ["a", "b", "b", "c"]

users = {0: {'phone': set([ID1[0]]), 'email': set([ID2[0]])}}

for index, id1 in enumerate(ID1):
    id2 = ID2[index]
    
    # iterate over found list of users
    i = 0
    n_users = max(users.keys()) 
    while i <= n_users:
        if any([(id1 in users[i]['phone']), (id2 in users[i]['email'])]):
            users[i]['phone'].add(id1)
            users[i]['email'].add(id2)
            break
        i += 1

    # add new user if not found
    if i > n_users:
        users[i] = {'phone': set([id1]), 'email': set([id2])}

【讨论】:

  • 刚刚更新了比较集合中的成员@YaakovBressler。仍然获得类似的性能。这篇文章比较了集合和列表,并指出使用集合检查成员资格应该更快:stackoverflow.com/questions/2831212/python-sets-vs-lists
  • 非常感谢您的回答!我使用 gist.github.com/thenarfer/e4488e03965f61fc5bb139f8b7995186 中的列表运行代码(需要一些时间来加载),出现了 3606 个唯一用户(我的代码找到 1494,@igrinis 找到 1408)。我不知道谁更接近答案。
  • 我想数据结构和操作之间的性能会因用例而异。但我同意你的看法——这种操作的集合应该稍微快一些。顺便说一句,聪明的解决方案。
  • @thenarfer 我想我已经找到了差异的原因。如果有一条新记录,其中 ID1 和 ID2 已经存在于不同的用户中,这与您所做的逻辑有关。例如,在索引 8909 处,我们有 ("abdeb", 44361)。至此,我已经找到了用户(1835, {'email': {'abdeb'}, 'phone': {53135, 55234, 64522}})(2839, {'email': {'fafac'}, 'phone': {44361}})。我没有像@igniris 那样合并这些。
  • 如果我添加这个逻辑,我的解决方案基本上变成了@igniris 解决方案(也使用集合),所以我希望我们的解决方案之间没有任何显着的性能差异。
【解决方案3】:

不要使用 5 元素列表运行 timeit。这不是评估竞争者的有效方法。使用更大的列表 (1000+) 来测试性能,否则你实际上不会得到你想要的快速程序。

我会用 O(N) 最坏情况算法(不取决于用户数量)。

我要指出,其他算法的性能很大程度上取决于典型用户拥有的 ID 数量。这个专门用于每个用户的大量 ID。

ID1 = [7, 7, 8, 9]
ID2 = ["a", "b", "b", "c"]

from collections import defaultdict

id1_to_id2 = defaultdict(set)
id2_to_id1 = defaultdict(set)
for id1, id2 in zip(ID1, ID2):
    id1_to_id2[id1].add(id2)
    id2_to_id1[id2].add(id1)

id1_to_user = {}
users = {}
for id1 in id1_to_id2:
    if id1 in id1_to_user:
        continue # Already processed

    # Find all id1 and id2 for this user, using 'floodfill'
    id1s = {id1}
    id2s = set()
    id1_queue = [id1]
    while len(id1_queue) > 0:
        old_id2s = id2s.copy()
        for id1 in id1_queue:
            id2s.update(id1_to_id2[id1]) # try using id2_queue = set().union(id1_to_id2[id1] for id1 in id1_queue)-id2s; id2s |= id2_queue as well in case it's faster
        id2_queue = id2s - old_id2s
        id1_queue = []
        old_id1s = id1s.copy()
        for id2 in id2_queue:
            id1s.update(id2_to_id1[id2])
        id2_queue = []
        id1_queue = id1s - old_id1s
    user = len(users)
    users[user] = [id1s, id2s]
    for id1 in id1s:
        id1_to_user[id1] = user

print(users)

【讨论】:

  • 在 gist.github.com/thenarfer/e4488e03965f61fc5bb139f8b7995186 中运行要点,我有 1408 个用户(与 igrinis 相同),这对我来说运行时间约为 0.1 秒(比给定的运行速度快 50 倍,我认为是 igrinis,我认为比 dkapitan 慢 500 倍?)。
  • 太棒了!非常感谢您的时间和贡献!
  • 看来1408个用户才是正确答案。在 github 列表上运行它 (len=10 000) 我得到了 32.6 ms ± 1.2 ms per loop (mean ± std. dev. of 7 runs, 10 loops each) 的时间。
猜你喜欢
  • 2016-12-04
  • 2016-10-07
  • 1970-01-01
  • 2022-08-09
  • 2015-11-23
  • 2020-01-22
  • 2017-09-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多