根据另一个密钥过滤字典列表以删除密钥中的重复项

时间:2018-05-21 16:57:33

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

我在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的dicts列表。

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

预期产出:

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

6 个答案:

答案 0 :(得分:4)

我会使用groupby并从每组中选择第一个:

1)首先按键排序(创建组)和减少空数(您的既定目标):

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

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

>>> 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'}]

如果你想要一个领带破坏者'要选择第一个dict,否则它们具有相同的空值,你可以添加一个枚举装饰器:

>>> 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,并且序列只会从列表顺序更改基于关键和空白计数。因此,除非您确定需要使用第二个版本,否则请使用第一个版本。

答案 1 :(得分:1)

您可以使用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'}]

答案 2 :(得分:1)

如果您愿意使用第三方库,则可以按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'}]

如果您无法使用toolz,则source 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]}]

答案 3 :(得分:0)

这是使用列表推导的一种方式,它使用您已经计算过的'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'与具有相同val_count'的所有词典的最大&{39; 'id'进行比较来实现。请注意,在tie的情况下,保留所有具有max 'val_count'的词典。

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

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

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

答案 4 :(得分:0)

我会这样做:

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值的词典(所有词典相同数量的非None s),你可以这样做:

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)

答案 5 :(得分:0)

@ 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"
    }
]
"""

感谢。