【问题标题】:How to do a full outer join / merge of iterators by key?如何按键进行迭代器的完全外连接/合并?
【发布时间】:2018-08-27 11:32:58
【问题描述】:

我有多个产生键控数据的排序迭代器,可以用列表表示:

a = iter([(1, 'a'), (2, 't'), (4, 'c')])
b = iter([(1, 'a'), (3, 'g'), (4, 'g')])

我想合并它们,使用键并跟踪哪个迭代器具有键值。这应该相当于 SQL 中的完全外连接:

>>> list(full_outer_join(a, b, key=lambda x: x[0]))
[(1, 'a', 'a'), (2, 't', None), (3, None, 'g'), (4, 'c', 'g')]

我尝试使用 heapq.mergeitertools.groupby,但使用 merge 我已经丢失了有关迭代器的信息:

>>> list(heapq.merge(a, b, key=lambda x: x[0]))
[(1, 'a'), (1, 'a'), (2, 't'), (3, 'g'), (4, 'c'), (4, 'g')]

所以我可以使用标签生成器

def tagged(it, tag):
    for item in it:
        yield (tag, *x)

并合并标记的迭代器,按键分组并使用标记创建字典:

merged = merge(tagged(a, 'a'), tagged(b, 'b'), key=lambda x: x[1])
grouped = groupby(merged, key=lambda x: x[1])
[(key, {g[0]: g[2] for g in group}) for key, group in grouped]

这给了我这个可用的输出:

[(1, {'a': 'a', 'b': 'a'}),
 (2, {'a': 't'}),
 (3, {'b': 'g'}),
 (4, {'a': 'c', 'b': 'g'})]

但是,我认为为每个组创建字典在性能方面非常昂贵,所以也许有更优雅的方法?

编辑:

为了澄清,数据集太大而无法放入内存,所以我肯定需要使用生成器/迭代器。

编辑 2:

为了进一步澄清,a 和 b 应该只迭代一次,因为它们代表读取速度很慢的大文件。

【问题讨论】:

  • 您的问题解决了吗?随意发布您自己的解决方案,或者如果他们有帮助,请接受以下其中一项。
  • 我有一个解决方案但我还没有实现它,我一有它就会发布它。干杯 =)

标签: python functional-programming iterator generator


【解决方案1】:

这是通过字典的一种解决方案。我在这里提供它是因为我不清楚在这种情况下字典 效率低下。

我相信dict_of_lists 可以替换为迭代器,但我在下面的解决方案中使用它来进行演示。

a = [(1, 'a'), (2, 't'), (4, 'c')]
b = [(1, 'a'), (3, 'g'), (4, 'g')]

dict_of_lists = {'a': a, 'b': b}

def gen_results(dict_of_lists):
    keys = {num for k, v in dict_of_lists.items() \
                for num, val in v}
    for key in keys:
        d = {k: val for k, v in dict_of_lists.items() \
                    for num, val in v if num == key}
        yield (key, d)

结果

list(gen_results(dict_of_lists))

[(1, {'a': 'a', 'b': 'a'}),
 (2, {'a': 't'}),
 (3, {'b': 'g'}),
 (4, {'a': 'c', 'b': 'g'})]

【讨论】:

  • 他试图避免每个组的听写。
  • @StephenRauch,同意了。我添加了一条评论来反映这一事实。
  • 虽然这确实很方便,但它会将整个数据集加载到内存中。我正在寻找使用生成器的解决方案。
  • @membranepotential,您的“可用输出”肯定需要内存中的全新数据集吗?分组方法会增加额外的复杂性。
  • 为了显示输出,我需要在其中放入一个列表,但您可以将[(key, {g[0]: g[2] for g in group}) for key, group in grouped]((key, {g[0]: g[2] for g in group}) for key, group in grouped) 交换,这将是一个生成器表达式
【解决方案2】:

您可以通过在函数中使用 reduce 和生成器来更改您的 groupby 解决方案:

from itertools import groupby
from functools import reduce
def group_data(a, b):
   sorted_data = sorted(a+b, key=lambda x:x[0])
   data = [reduce(lambda x, y:(*x, y[-1]), list(b)) for _, b in groupby(sorted_data, key=lambda x:x[0])]
   current = iter(range(len(list(filter(lambda x:len(x) == 2, data)))))
   yield from [i if len(i) == 3 else (*i, None) if next(current)%2 == 0 else (i[0], None, i[-1]) for i in data]

print(list(group_data([(1, 'a'), (2, 't'), (4, 'c')], [(1, 'a'), (3, 'g'), (4, 'g')])))

输出:

[(1, 'a', 'a'), (2, 't', None), (3, None, 'g'), (4, 'c', 'g')]

【讨论】:

  • 这里的创意不错! +1 三个音符。 (1) 最好让group_data 接受数据作为参数进行操作,而不是硬编码数据值。 (2) 在 OP 的示例中,3 元组中的 None 是第一项,而在您的代码中,None 是第二项。我不确定这是一个问题,只是让你知道。 (3) 您的代码有点难以阅读。我建议将解决方案分解为更多步骤。
  • 虽然这看起来确实非常好,但我丢失了有关哪个迭代器提供数据的信息(正如 Christian 在 (2) 中解释的那样)。使用普通 reduce 是我以前没有考虑过的事情,也许这是解决方案的关键。
  • @membranepotential 您能否说明如何获得None 的位置?从您的代码中并不能立即清楚地看到这一点。
  • @ChristianDean 谢谢! (1) 已修复,我目前正在等待 OP 关于None 的反馈。 (3) 希望是一种改进。
  • 当然:元组应该有第一个位置的键和 None 或者在它们被提供给 group_data 函数的位置的数据:所以结果就像[ (a[0] or b[0], a[1] or None, b[1] or None) ]。另请参阅替代输出。
猜你喜欢
  • 2011-12-31
  • 1970-01-01
  • 2014-09-23
  • 2022-01-23
  • 2021-02-19
  • 2019-01-12
  • 2012-04-25
  • 2014-05-14
  • 2010-10-31
相关资源
最近更新 更多