如何基于密钥合并两个元组列表?

时间:2015-08-07 22:19:10

标签: python algorithm python-2.7

我有两个需要合并的元组列表。这与数据库术语中的JOIN相当。每个列表中元组的顺序可能会改变。元组中项目的顺序不会改变。 A中的项目数应该等于B中的数量,但可能存在差异。

这是我的两个元组列表。每个列表中将有10,000多个这样的元组,因此性能是一个问题。每个元组中的第一个元素是每个列表共有的键。

listA = [(u'123', u'a1', u'a2', 123, 789), (u'124', u'b1', u'b2', 456, 357), (u'125', u'c1', u'c2', 156, 852)]
listB = [(u'125', u'd1', u'N', u'd2', 1), (u'123', u'f1', u'Y', u'f2', 2)]

所需的输出是:

listC = [(u'123', u'a1', u'a2', 123, 789, u'f1', u'Y', u'f2', 2), (u'125', u'c1', u'c2', 156, 852, u'd1', u'N', u'd2', 1)]

这是我为测试这个概念而拼凑的代码。它可以工作,但正如你所看到的,性能是一个问题。使用真实数据运行时此代码的性能(每个列表中有10K项)是不可接受的,因为它可能需要数小时才能完成。

以下是代码:

for row in listA:
    for item in listB:
        if item[0] == row[0]:
            item = list(item)
            del item[0]
            row = list(row)
            merged.append(tuple(row + item))

如何合并/加入这两个列表并获得更好的性能?

3 个答案:

答案 0 :(得分:7)

使用itertools.groupby() suggested by @CoryKramer in the comments在第一个(每个列表中唯一的)列中连接两个元组列表:

from itertools import groupby
from operator import itemgetter

def inner_join(a, b):
    L = a + b
    L.sort(key=itemgetter(0)) # sort by the first column
    for _, group in groupby(L, itemgetter(0)):
        row_a, row_b = next(group), next(group, None)
        if row_b is not None: # join
            yield row_a + row_b[1:] # cut 1st column from 2nd row

示例:

result = list(inner_join(listA, listB))
assert result == listC

此解决方案的时间复杂度O(n*log n)(您的解决方案(问题中)为O(n*n)n ~ 10000}的情况要差得多。

对于问题中的n这样的小10**4并不重要,但在Python 3.5+中,您可以使用带有heapq.merge()参数的key来避免分配新列表,即O(1)常量内存解决方案:

from heapq import merge # merge has key parameter in Python 3.5

def inner_join(a, b):
    key = itemgetter(0)
    a.sort(key=key) 
    b.sort(key=key)
    for _, group in groupby(merge(a, b, key=key), key):
        row_a, row_b = next(group), next(group, None)
        if row_b is not None: # join
            yield row_a + row_b[1:] # cut 1st column from 2nd row

这是一个基于字典的解决方案。时间和空间算法是O(n)线性的:

def inner_join(a, b):
    d = {}
    for row in b:
        d[row[0]] = row
    for row_a in a:
        row_b = d.get(row_a[0])
        if row_b is not None: # join
            yield row_a + row_b[1:]

此处基于collections.defaultdict的解决方案mentioned by @Padraic Cunningham

from collections import defaultdict
from itertools import chain

def inner_join(a, b):
    d = defaultdict(list)
    for row in chain(a, b):
        d[row[0]].append(row[1:])
    for id, rows in d.iteritems():
        if len(rows) > 1:
            assert len(rows) == 2
            yield (id,) + rows[0] + rows[1]

答案 1 :(得分:2)

你以前用过熊猫吗?这似乎给出了你想要的输出:

n [41]:
import pandas as pd
listA = [(u'123', u'a1', u'a2', 123, 789), (u'124', u'b1', u'b2', 456, 357), (u'125', u'c1', u'c2', 156, 852)]
listB = [(u'125', u'd1', u'N', u'd2', 1), (u'123', u'f1', u'Y', u'f2', 2)]

A = pd.DataFrame(listA)
B = pd.DataFrame(listB)

A.merge(B, on=0)
Out[41]:
    0   1_x     2_x     3_x     4_x     1_y     2_y     3_y     4_y
0   123     a1  a2  123     789     f1  Y   f2  2
1   125     c1  c2  156     852     d1  N   d2  1

`甲'和' B'是pandas数据帧,它们内置了一些类似SQL的功能,例如merge。如果您还没有使用过熊猫,请告诉我您是否需要进一步解释。

请参阅Database-style DataFrame joining/merging

答案 2 :(得分:0)

您可以使用OrderedDict按第一个元素进行分组,附加每个元组然后只保留并加入值列表长度为>的元组。 1:

from itertools import chain
from collections import OrderedDict

od = OrderedDict()

for ele in chain(listA,listB):
    od.setdefault(ele[0], []).append(ele[1:])

print([(k,) + tuple(chain.from_iterable(v)) for k,v in od.iteritems() if len(v) > 1])

输出:

[('123', 'a1', 'a2', 123, 789, 'f1', 'Y', 'f2', 2), ('125', 'c1', 'c2', 156, 852, 'd1', 'N', 'd2', 1)]

如果订单无关紧要,collections.defaultdict会更快,无论哪种方式,这都会比你自己的方法快得多。

使用标志存储itertools.islice个对象以查找匹配的键:

from itertools import chain, islice
from collections import OrderedDict

od = OrderedDict()

for ele in chain(listA, listB):
    k = ele[0]
    if k in od:
        od[k]["v"].append(islice(ele, 1, None))
        od[k]["flag"] = True
    else:
        od.setdefault(k, {"flag": False, "v": []})["v"].append(islice(ele, 1, None))

print([(k,) + tuple(chain.from_iterable(v["v"])) for k, v in od.items() if v["flag"]])

输出:

[('123', 'a1', 'a2', 123, 789, 'f1', 'Y', 'f2', 2), ('125', 'c1', 'c2', 156, 852, 'd1', 'N', 'd2', 1)]