【问题标题】:Filter a list of dictionaries to remove duplicates within a key, based on another key根据另一个键过滤字典列表以删除一个键中的重复项
【发布时间】:2018-10-31 09:07:09
【问题描述】:

我有一个 Python 3.5.2 中的字典列表,我正在尝试“去重”。所有字典都是唯一的,但我想对一个特定的键进行重复数据删除,以使字典具有最多的非空值。

例如,我有以下字典列表:

d1 = {"id":"a", "foo":"bar", "baz":"bat"}
d2 = {"id":"b", "foo":"bar", "baz":None}
d3 = {"id":"a", "foo":"bar", "baz":None}
d4 = {"id":"b", "foo":"bar", "baz":"bat"}
l = [d1, d2, d3, d4]

我想将 l 过滤为具有唯一 id 键的字典,保留空值最少的字典。在这种情况下,函数应该保留d1d4

我试图为“值计数”创建一个新的键、值对,如下所示:

for d in l:
    d['val_count'] = len(set([v for v in d.values() if v]))

现在我坚持的是如何过滤我的字典列表以获得唯一的ids,其中val_count 键是更大的值。

我对其他方法持开放态度,但由于资源限制,我无法在此项目中使用 pandas

预期输出:

l = [{"id":"a", "foo":"bar", "baz":"bat"},
 {"id":"b", "foo":"bar", "baz":"bat"}]

【问题讨论】:

  • 如果你能提供一个预期输出的例子会很有帮助。您不成功尝试的描述并没有什么坏处,但在这种情况下它没有多大帮助。
  • @AGNGazer 我将进行更新以使其更加清晰,但我将其包含在帖子中:In this case the function should keep d1 and d4.
  • 当所有ds 具有相同数量的None 或所有ds 至少包含一个None 时会发生什么?
  • @AGNGazer 我愿意在这种情况下只保留第一次出现。

标签: python python-3.x list dictionary duplicates


【解决方案1】:

我会使用groupby,然后从每个组中选择第一个:

1) 首先按键(以创建组)和空值的递减计数(您声明的目标)对列表进行排序:

>>> l2=sorted(l, key=lambda d: (d['id'], -sum(1 for v in d.values() if v))) 

2) 然后按id 分组,并将每个迭代器的第一个元素作为d 在排序列表的groupby 中:

>>> from itertools import groupby
>>> [next(d) for _,d in groupby(l2, key=lambda _d: _d['id'])]
[{'id': 'a', 'foo': 'bar', 'baz': 'bat'}, {'id': 'b', 'foo': 'bar', 'baz': 'bat'}]

如果您希望 'tie break' 选择第一个 dict,否则它们具有相同的 null 计数,您可以添加一个枚举装饰器:

>>> l2=sorted(enumerate(l), key=lambda t: (t[1]['id'], t[0], -sum(1 for v in t[1].values() if v)))
>>> [next(d)[1] for _,d in groupby(l2, key=lambda t: t[1]['id'])]

我怀疑 实际上 是否需要额外的步骤,因为 Python 的排序(和 sorted)是 stable sort,并且序列只会根据键和 void 计数从列表顺序中改变。因此,除非您确定需要使用第二个版本,否则请使用第一个版本。

【讨论】:

  • 很好,我喜欢使用next。表现也不错,+1。
  • 这绝对是所有答案中最好的。谢谢!
  • @dawg 下划线的用途是什么?
  • @cdc200:下划线的用途是什么? grouby 返回一个键和一个迭代器,指向按该键分组的项目。由于在这种情况下我们没有使用密钥,因此使用_ 作为一次性占位符是一个常见的 Python 习惯用法。 _d 的另一个下划线是具有不同 _dd 的紧凑方式。某些版本的 Python 没有将 lambda 的命名空间与周围的理解分开,如果您意外更改循环变量,可能会发生不好的事情。
【解决方案2】:

如果您愿意使用 3rd 方库,您可以按 None 值的数量排序,然后输入到 toolz.unique

from toolz import unique
from operator import itemgetter

l_sorted = sorted(l, key=lambda x: sum(v is None for v in x.values()))
res = list(unique(l_sorted, key=itemgetter('id')))

[{'baz': 'bat', 'foo': 'bar', 'id': 'a'},
 {'baz': 'bat', 'foo': 'bar', 'id': 'b'}]

如果你不能使用toolzsource code 足够小,可以自己实现。


性能基准测试

我只包含了每个 id 只给出一个结果的解决方案。许多解决方案不适合重复的字典。

l = [d1, d2, d3, d4]*1000

%timeit dawg(l)  # 11.4 ms
%timeit jpp(l)   # 7.91 ms
%timeit tsw(l)   # 4.23 s

from operator import itemgetter
from itertools import groupby
from toolz import unique

def dawg(l):
    l2=sorted(enumerate(l), key=lambda t: (t[1]['id'], -sum(1 for v in t[1].values() if v), t[0]))
    return [next(d)[1] for _,d in groupby(l2, key=lambda t: t[1]['id'])]

def jpp(l):
    l_sorted = sorted(l, key=lambda x: sum(v is None for v in x.values()))
    return list(unique(l_sorted, key=itemgetter('id')))

def tsw(l):
    for d in l:
        d['val_count'] = len(set([v for v in d.values() if v]))
    new = [d for d in l if d['val_count'] == max([d_other['val_count'] for d_other in l if d_other['id'] == d['id']])]
    return [x for i, x in enumerate(new) if x['id'] not in {y['id'] for y in new[:i]}]

【讨论】:

  • 我是基准测试的傻瓜。感谢那。但是,对于苹果到苹果,您将使用我的回答中的非枚举版本,因为 tool.unique 也未枚举。这使得dawg 版本的时间更快...
【解决方案3】:

你可以使用max:

d1 = {"id":"a", "foo":"bar", "baz":"bat"}
d2 = {"id":"b", "foo":"bar", "baz":None}
d3 = {"id":"a", "foo":"bar", "baz":None}
d4 = {"id":"b", "foo":"bar", "baz":"bat"}
l = [d1, d2, d3, d4]
max_none = max(sum(c is None for c in i.values()) for i in l)
new_l = [i for i in l if sum(c is None for c in i.values()) < max_none]

输出:

[{'foo': 'bar', 'baz': 'bat', 'id': 'a'}, {'foo': 'bar', 'baz': 'bat', 'id': 'b'}]

【讨论】:

  • 澄清一下,我正在寻找一种解决方案,可以选择具有fewest null/None 值的字典,这意味着如果重复的 id 键字典有更多的 None,我可能会保留一个带有 None 的字典。在那种情况下这仍然有效吗?
  • @Ajax1234,不知道,但我讨厌看到无法解释的反对票。所以+1。
  • 仅供参考,此解决方案似乎无法处理重复的字典(即重复的同一个字典将得到两次输出)。
【解决方案4】:

这是使用列表推导的一种方法,它使用您已经计算的 'val_count' 值:

new = [d for d in l if d['val_count'] == max([d_other['val_count'] for d_other in l if d_other['id'] == d['id']])]

给予:

[{'baz': 'bat', 'foo': 'bar', 'id': 'a', 'val_count': 3},
 {'baz': 'bat', 'foo': 'bar', 'id': 'b', 'val_count': 3}]

这是通过将当前字典的'val_count' 与具有相同'id' 的所有字典中的最大值'val_count' 进行比较来实现的。请注意,在平局的情况下,将保留所有具有最大 'val_count' 的字典。

以下行应该处理关系,只保留某个'id' 的第一个实例:

final = [x for i, x in enumerate(new) if x['id'] not in {y['id'] for y in new[:i]}]

几乎肯定会有更有效的方法来解决这个问题,但这至少应该有效,并且可能适合您的需求,具体取决于数据集的大小。

【讨论】:

  • 你对如何打破平局有什么建议吗?
  • @cdc200 - 请参阅编辑以了解仅保留某个 'id' 的第一个实例以防平局的方法。
  • 此解决方案对于大型列表效率低下(请参阅benchmarking)。
  • @jpp - 是的,这并不奇怪,这是一个快速“大声思考”的解决方案,因为在我写它的时候,其他解决方案没有产生正确的结果。此后发布的解决方案显然更好。
【解决方案5】:

@cdc200,你可以试试下面的代码。这里我使用了字典的概念。

注意» 字典被定义为具有唯一键的无序数据项集合。

我使用 OrderedDict () 代替 dict() 来保留键的顺序。查看这篇不错的小文章OrderedDict in Python - GeeksforGeeks

import json
from collections import OrderedDict

d1 = {"id":"a", "foo":"bar", "baz":"bat"}
d2 = {"id":"b", "foo":"bar", "baz":None}
d3 = {"id":"a", "foo":"bar", "baz":None}
d4 = {"id":"b", "foo":"bar", "baz":"bat"}
l = [d1, d2, d3, d4]

d = OrderedDict ();

for index, item in enumerate(l):
    if item["id"] not in d:
        d[item["id"]] =item
    else:
        nones1, nones2 = 0, 0
        for k in item:
            if item[k] is None:
                 nones1 = nones1 + 1
            if d[item["id"]][k] is None:
                 nones2 = nones2 + 1

        if nones2 > nones1:
            d[item["id"]] = item

l = [dict_item for dict_item in d.values()]

print (l)

"""
{'foo': 'bar', 'id': 'a', 'baz': 'bat'}, {'foo': 'bar', 'id': 'b', 'baz': 'bat'}]
"""

# Pretty printing the above dictionary
print(json.dumps(l, indent=4))

"""
[
    {
        "foo": "bar",
        "id": "a",
        "baz": "bat"
    },
    {
        "foo": "bar",
        "id": "b",
        "baz": "bat"
    }
]
"""

谢谢。

【讨论】:

  • 如果你能用你提到的概念解释你在做什么,那就太好了。
【解决方案6】:

我会这样做:

num = [list(x.values()).count(None) for x in l]
ls = [x for _,x in sorted(zip(num, l), key=lambda z: z[0])]

然后从排序列表 (ls) 中保留任意数量的值。

例如,为了只保留那些具有最多非None 值的字典(所有字典相同 个非Nones),您可以这样做:

num = [list(x.values()).count(None) for x in l]
ls, ns = zip(*[(x, d) for d, x in sorted(zip(num, l), key=lambda z: z[0])])
top_l = ls[:list(reversed(ns)).index(ns[0])]

编辑:基于@jpp's comment,我更新了我的代码以处理重复的id 键。这是更新的代码:

def agn(l):
    num = [list(x.values()).count(None) for x in l]
    ls, ns = zip(*[(x, d) for d, x in sorted(zip(num, l), key=lambda z: z[0])])
    top_l = ls[:list(reversed(ns)).index(ns[0])]
    return list(dict((d['id'], d) for d in top_l).values())

让我们也使用与@jpp's answer 中相同的定义和设置添加时序比较:

In [113]: %timeit tsw(l)
3.9 s ± 60.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [114]: %timeit dawg(l)
7.48 ms ± 191 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [115]: %timeit jpp(l)
5.83 ms ± 104 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [116]: %timeit agn(l)
4.58 ms ± 86.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

【讨论】:

  • @jpp 我已经更新了我的解决方案以消除重复。之前没有注意到这个要求。谢谢!
猜你喜欢
  • 2017-11-06
  • 2020-09-11
  • 1970-01-01
  • 1970-01-01
  • 2022-01-19
  • 2017-04-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多