【问题标题】:Identify groups of varying continuous numbers in a list识别列表中不同连续数字的组
【发布时间】:2016-09-26 18:13:20
【问题描述】:

other SO post 中,一位 Python 用户询问如何对连续数字进行分组,以便任何序列都可以仅由其开始/结束来表示,而任何落后者都将显示为单个项目。接受的答案非常适合连续序列。

我需要能够适应类似的解决方案,但对于可能(并非总是)具有变化增量的数字序列。理想情况下,我表示的方式还包括增量(这样他们就会知道是每 3、4、5、n 次)

参考原始问题,用户要求以下输入/输出

[2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]  # input
[(2,5), (12,17), 20]

我想要的是以下内容(注意:为了清楚起见,我写了一个元组作为输出,但 xrange 最好使用它的 step 变量):

[2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]  # input
[(2,5,1), (12,17,1), 20]  # note, the last element in the tuple would be the step value

它还可以处理以下输入

[2, 4, 6, 8, 12, 13, 14, 15, 16, 17, 20]  # input
[(2,8,2), (12,17,1), 20]  # note, the last element in the tuple would be the increment

我知道xrange() 支持一个步骤,因此甚至可以使用其他用户答案的​​变体。我尝试根据他们在解释中写的内容进行一些编辑,但我无法得到我想要的结果。

对于不想点击原始链接的任何人,Nadia Alramli最初发布的代码是:

ranges = []
for key, group in groupby(enumerate(data), lambda (index, item): index - item):
    group = map(itemgetter(1), group)
    if len(group) > 1:
        ranges.append(xrange(group[0], group[-1]))
    else:
        ranges.append(group[0])

【问题讨论】:

    标签: python


    【解决方案1】:

    itertoolspairwise recipe 是解决问题的一种方法。应用itertools.groupby,可以创建数学差异相等的对组。然后为多项目组选择每个组的第一项和最后一项,或者为单项组选择最后一项:

    from itertools import groupby, tee, izip
    
    
    def pairwise(iterable):
        "s -> (s0,s1), (s1,s2), (s2, s3), ..."
        a, b = tee(iterable)
        next(b, None)
        return izip(a, b)
    
    def grouper(lst):
        result = []
        for k, g in groupby(pairwise(lst), key=lambda x: x[1] - x[0]):
            g  = list(g)
            if len(g) > 1:
                try:
                    if g[0][0] == result[-1]:
                        del result[-1]
                    elif g[0][0] == result[-1][1]:
                        g = g[1:] # patch for duplicate start and/or end
                except (IndexError, TypeError):
                    pass
                result.append((g[0][0], g[-1][-1], k))
            else:
                result.append(g[0][-1]) if result else result.append(g[0])
        return result
    

    试用:input -> grouper(lst) -> output

    Input: [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]
    Output: [(2, 5, 1), (12, 17, 1), 20]
    
    Input: [2, 4, 6, 8, 12, 13, 14, 15, 16, 17, 20]
    Output: [(2, 8, 2), (12, 17, 1), 20]
    
    Input: [2, 4, 6, 8, 12, 12.4, 12.9, 13, 14, 15, 16, 17, 20]
    Output: [(2, 8, 2), 12, 12.4, 12.9, (13, 17, 1), 20] # 12 does not appear in the second group
    

    更新:(重复开始和/或结束值的补丁

    s1 = [i + 10 for i in xrange(0, 11, 2)]; s2 = [30]; s3 = [i + 40 for i in xrange(45)]
    
    Input: s1+s2+s3
    Output: [(10, 20, 2), (30, 40, 10), (41, 84, 1)]
    
    # to make 30 appear as an entry instead of a group change main if condition to len(g) > 2
    Input: s1+s2+s3
    Output: [(10, 20, 2), 30, (41, 84, 1)]
    
    Input: [2, 4, 6, 8, 10, 12, 13, 14, 15, 16, 17, 20]
    Output: [(2, 12, 2), (13, 17, 1), 20]
    

    【讨论】:

    • 以下序列的情况下,s1 = [i + 10 for i in xrange(0, 11, 2)]; s2 = [30]; s3 = [i + 40 for i in xrange(45)]; grouper(s1 + s2 + s3) grouper() 产生重复的起始句柄 [(10, 20, 2), (20, 40, 10), (40, 84, 1)]。请注意,20 和 40 被列出了两次。有关更多信息,请参阅我对 Padraic 的评论。你有什么办法可以防止出现这样的重复吗?
    • @user3626104 你有很好的边缘案例。我已经用修复该问题的补丁更新了我的答案。
    • 我再给你一个,如果你愿意的话。从结果中排除开始序列的异常值 s1 = [4, 8, 10, 12, 14] grouper(s1) # (8, 14, 2) - 缺少 4。如果您需要另一个示例 - s1 = [3, 4, 8, 10, 12, 14] grouper(s1) # (4, (8, 14, 2)) - 缺少 3
    • 这种情况没有很好的定义。对于[4, 8, 10, 12, 14] -> [(4, 8), (10, 14, 2)],4 不会独立,因为第一组将始终有效,代码不能在分组 4 和 8 之前检测未来模式。
    • 如果您不想在第一个测试用例中将 30 个放在一个组中,请更改 if len(g) > 1 -> if len(g) > 2
    【解决方案2】:

    您可以创建一个迭代器来帮助分组,并尝试从下一个组中提取下一个元素,该组将是上一个组的结尾:

    def ranges(lst):
        it = iter(lst)
        next(it)  # move to second element for comparison
        grps = groupby(lst, key=lambda x: (x - next(it, -float("inf"))))
        for k, v in grps:
            i = next(v)
            try:
                step = next(v) - i  # catches single element v or gives us a step
                nxt = list(next(grps)[1])
                yield xrange(i, nxt.pop(0), step)
                # outliers or another group
                if nxt:
                    yield nxt[0] if len(nxt) == 1 else xrange(nxt[0], next(next(grps)[1]), nxt[1] - nxt[0])
            except StopIteration:
                yield i  # no seq
    

    给你:

    In [2]: l1 = [2, 3, 4, 5, 8, 10, 12, 14, 13, 14, 15, 16, 17, 20, 21]
    
    In [3]: l2 = [2, 4, 6, 8, 12, 13, 14, 15, 16, 17, 20]
    
    In [4]: l3 = [13, 14, 15, 16, 17, 18]
    
    In [5]: s1 = [i + 10 for i in xrange(0, 11, 2)]
    
    In [6]: s2 = [30]
    
    In [7]: s3 = [i + 40 for i in xrange(45)]
    
    In [8]: l4 = s1 + s2 + s3
    
    In [9]: l5 = [1, 2, 5, 6, 9, 10]
    
    In [10]: l6 = {1, 2, 3, 5, 6, 9, 10, 13, 19, 21, 22, 23, 24}
    
    In [11]: 
    
    In [11]: for l in (l1, l2, l3, l4, l5, l6):
       ....:         print(list(ranges(l)))
       ....:     
    [xrange(2, 5), xrange(8, 14, 2), xrange(13, 17), 20, 21]
    [xrange(2, 8, 2), xrange(12, 17), 20]
    [xrange(13, 18)]
    [xrange(10, 20, 2), 30, xrange(40, 84)]
    [1, 2, 5, 6, 9, 10]
    [xrange(1, 3), 5, 6, 9, 10, 13, 19, xrange(21, 24)]
    

    当 step 为 1 时,它不包含在 xrange 输出中。

    【讨论】:

    • 我在测试后发现了几个错误。在不连续的连续范围变化的情况下(多么拗口!)它不会产生正确的结果。 s1 = [i + 10 for i in xrange(0, 11, 2)]; s2 = [30]; s3 = [i + 40 for i in xrange(45)];范围(s1 + s2 + s3)注意到输出中完全缺少 30。我认为这是因为 30 位于第一个和第二个序列的结束和开始之间,因此 range() 不会将其识别为异常值。但是,设置为 29/31 可以正常工作。
    • 当我回到我的比赛时会看看。
    • @user3626104,您需要概述您认为的序列,当只有两个元素时,(30, 40, 10) 如何成为有效序列? [xrange(10, 20, 2), 30, xrange(40, 84)] 不是更正确的解决方案吗?
    • 不久前我一直在与我的同事讨论这个确切的观点。总而言之,它是无效的。你的表述更正确。但是,输出中不能排除任何数据,例如在变化的不连续顺序范围的情况下。 (有 1 个或多个异常值位于两个有效序列的中间)。这种类型的数据在我们的样本集中出现了很多,因此需要包含在内。谢谢你让我澄清这一点。一个序列应该包含 3 才能成为趋势
    • @user3626104,尝试编辑。也许添加一些具有预期输出的测试样本会有所帮助。
    【解决方案3】:

    这是一个快速编写(而且非常丑陋)的答案:

    def test(inArr):
        arr=inArr[:] #copy, unnecessary if we use index in a smart way
        result = []
        while len(arr)>1: #as long as there can be an arithmetic progression
            x=[arr[0],arr[1]] #take first two
            arr=arr[2:] #remove from array
            step=x[1]-x[0]
            while len(arr)>0 and x[1]+step==arr[0]: #check if the next value in array is part of progression too
                x[1]+=step #add it
                arr=arr[1:]
            result.append((x[0],x[1],step)) #append progression to result
        if len(arr)==1:
            result.append(arr[0])
        return result
    
    print test([2, 4, 6, 8, 12, 13, 14, 15, 16, 17, 20])
    

    这将返回[(2, 8, 2), (12, 17, 1), 20]

    慢,因为它会复制一个列表并从中删除元素

    它只找到完整的进程,并且只在排序的数组中。

    简而言之,它很糟糕,但应该可以;)

    还有其他(更酷,更 Python 的)方法可以做到这一点,例如,您可以将列表转换为集合,不断删除两个元素,计算它们的算术级数并与集合相交。

    您还可以重复使用您提供的答案来检查某些步长。例如:

    ranges = []
    step_size=2
    for key, group in groupby(enumerate(data), lambda (index, item): step_size*index - item):
        group = map(itemgetter(1), group)
        if len(group) > 1:
            ranges.append(xrange(group[0], group[-1]))
        else:
            ranges.append(group[0])
    

    这会找到步长2的每个组,但只有那些。

    【讨论】:

      【解决方案4】:

      我曾经遇到过这样的情况。就这样吧。

      import more_itertools as mit
      iterable = [2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 20]  # input
      x = [list(group) for group in mit.consecutive_groups(iterable)]
      output = [(i[0],i[-1]) if len(i)>1 else i[0] for i in x]
      print(output)
      

      【讨论】:

        猜你喜欢
        • 2011-01-10
        • 2016-08-26
        • 2020-11-12
        • 2021-03-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-01-02
        相关资源
        最近更新 更多