【问题标题】:How to remove duplicates of list of lists based on element in nested list如何根据嵌套列表中的元素删除列表列表的重复项
【发布时间】:2020-01-27 11:46:35
【问题描述】:

我想从列表列表中删除重复项。对于嵌套列表中的每个第二个元素,第一个元素并不总是唯一的。第一个值对于整个列表列表是唯一的。这些数字在整个列表中仅出现一次,但没有排序。

my_list = [[4, 'C'], [1, 'A'], [3, 'B'], [2, 'A'], [5, 'C']]

删除重复项是基于嵌套列表中的第二个元素。我需要每个唯一的第二个元素的最小值,例如:

my_unique_list = [[1, 'A'], [3, 'B'], [4, 'C']]

输出的顺序无关紧要。

所以,为'A' 选择1(因为[2, 'A'] 中的1 小于2),为'B' 选择3('B' 没有其他值),为4 'C'(因为 4 小于 5,来自[5, 'C'])。

【问题讨论】:

  • 第一个值对于第二个值是否总是唯一的?

标签: python list set unique


【解决方案1】:

使用字典将唯一字母(第二个值)映射到每个字母的最小值,然后只需将该字典中的 [value, key] 对作为输出:

minimi = {}
inf = float('inf')
for val, key in my_list:
    # float('inf') as default value is always larger, so val is picked
    # if the key isn't present yet.
    minimi[key] = min(val, minimi.get(key, inf))

my_unique_list = [[v, k] for k, v in minimi.items()]

通过使用字典作为中介,您可以在线性时间内过滤输入。

演示:

>>> my_list = [[4, 'C'], [1, 'A'], [3, 'B'], [2, 'A'], [5,'C']]
>>> minimi, inf = {}, float('inf')
>>> for val, key in my_list:
...     minimi[key] = min(val, minimi.get(key, inf))
...
>>> minimi
{'C': 4, 'A': 1, 'B': 3}
>>> my_unique_list = [[v, k] for k, v in minimi.items()]
>>> my_unique_list
[[4, 'C'], [1, 'A'], [3, 'B']]

为什么要关心运行时间?因为随着您的输入增加,您的运行时间也会增加。对于需要 O(N^2)(二次)时间的方法,当您从 1000 个项目增加到 100 万个(因此是大小的 1000 倍)时,您的运行时间将增加 100 万倍!对于 O(N logN) 方法(使用排序的方法),运行时间将增加约 2000 倍,而上述线性方法将花费 1000 倍的时间,随着输入规模线性扩展。

对于大量输入,这可能会导致“需要一两个小时”与“需要数百万年”之间的差异。

这是该方法与 zamir 的排序和设置方法 (O(N logN)) 以及 TJC World 的 Pandas 方法(也是 O(N logN))之间的时间试验比较:

from string import ascii_uppercase
from functools import partial
from timeit import Timer
import random
import pandas as pd

def gen_data(N):
    return [[random.randrange(1_000_000), random.choice(ascii_uppercase)] for _ in range(N)]

def with_dict(l, _min=min, _inf=float('inf')):
    minimi = {}
    m_get = minimi.get
    for val, key in l:
        minimi[key] = _min(val, m_get(key, _inf))
    return [[v, k] for k, v in minimi.items()]

def with_set_and_sort(l):
    already_encountered = set()
    ae_add = already_encountered.add
    return [i for i in sorted(l) if i[1] not in already_encountered and not ae_add(i[1])]

def with_pandas(l):
    return (
        pd.DataFrame(l)
        .sort_values(by=0)
        .drop_duplicates(1)
        .to_numpy()
        .tolist()
    )

for n in (100, 1000, 10_000, 100_000, 1_000_000):
    testdata = gen_data(n)
    print(f"{n:,} entries:")
    for test in (with_dict, with_set_and_sort, with_pandas):
        count, total = Timer(partial(test, testdata)).autorange()
        print(f"{test.__name__:>20}: {total/count * 1000:8.3f}ms")
    print()

我已经使用了我所知道的所有性能小技巧;通过将它们缓存在循环之外的本地名称中来避免重复查找全局和属性。

这个输出:

100 entries:
           with_dict:    0.028ms
   with_set_and_sort:    0.032ms
         with_pandas:    2.070ms

1,000 entries:
           with_dict:    0.242ms
   with_set_and_sort:    0.369ms
         with_pandas:    2.312ms

10,000 entries:
           with_dict:    2.331ms
   with_set_and_sort:    5.639ms
         with_pandas:    5.476ms

100,000 entries:
           with_dict:   23.105ms
   with_set_and_sort:  127.772ms
         with_pandas:   40.330ms

1,000,000 entries:
           with_dict:  245.982ms
   with_set_and_sort: 2494.305ms
         with_pandas:  578.952ms

因此,只有 100 个输入,排序方法可能看起来一样快(两种方式都有几毫秒的差异),但随着输入的增长,这种方法会加速失地。

Pandas 在这里各方面都失败了。数据框是一个很棒的工具,但这里的工具是错误的。它们是庞大的数据结构,因此对于小输入,它们的高开销使它们进入毫秒范围,远远落后于其他两个选项。在 10k 条目时,它开始优于排序和设置方法,但即使数据帧操作得到了高度优化,具有更大输入的排序运行时间的增长仍然无法击败线性方法。

【讨论】:

    【解决方案2】:
    already_encountered = set()
    my_new_list = [i for i in sorted(my_list) if i[1] not in already_encountered and not already_encountered.add(i[1])]
    

    输出:

    [[1, 'A'], [3, 'B'], [4, 'C']]
    

    【讨论】:

    • 是的,所以现在您添加了排序,它需要 O(N logN) 时间,而不是线性时间。
    • @MartijnPieters 你的解决方案在我的机器上花费了更多时间,10.7 µs ± 1.09 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each) 而我的机器正在花费4.68 µs ± 570 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    • 多少输入?您是否使用过 1000 个项目,然后是 1000000 个项目对此进行了测试?在 small 输入上,很容易将几微秒称为胜利。
    • @MartijnPieters 实际上我也在 100 万个项目的列表上尝试过它,你的执行时间是 7.04 s 而我的是 6.76 s 这并不是一个很大的性能提升(几毫秒)
    • 我不是在谈论重复。我说的是输入数。我在我的答案中添加了计时赛,在 100 万个项目中,您的方法需要的时间几乎是 10 倍。
    【解决方案3】:

    使用熊猫;

    >>> import pandas as pd
    >>> my_list = [[1, 'A'], [2, 'A'], [3, 'B'], [4, 'C'], [5,'C']]
    >>> df = pd.DataFrame(my_list)
    >>> df.sort_values(by = 0).drop_duplicates(1).to_numpy().tolist()
    [[1, 'A'], [3, 'B'], [4, 'C']]
    

    【讨论】:

    • 排序使得这是一种 O(N logN) 方法,随着输入数量的增加,线性时间方法(例如使用字典来挑选唯一值)将胜出。
    • @MartijnPieters 所以字典使用更少的时间进行排序?
    • 字典不需要排序。添加或更新密钥需要固定的时间。你有 N 个输入,所以在 N 步之后,每一步都花费恒定的时间,我们有一个最小的字典。然后我们再采取 N 个步骤来构建输出列表。总时间:2N,但在谈到渐近运行时增长时,会删除低阶常数。
    • 我也将你的 pandas 方法添加到我的计时赛中。它表明 Pandas 是方式,方式对于这个问题来说太重了。
    • 你也可以避免在 Pandas 中排序,使用groupby(); pd.DataFrame(my_list).groupby(1).min().reset_index()[[0, 1]].to_numpy().tolist() 具有线性性能(一旦超过 1000 个条目)。然而,它的固定成本仍然比简单地使用字典高出约 50%。
    猜你喜欢
    • 2020-12-21
    • 2018-02-18
    • 1970-01-01
    • 2019-07-13
    • 2018-09-15
    • 1970-01-01
    • 1970-01-01
    • 2020-11-02
    • 1970-01-01
    相关资源
    最近更新 更多