【问题标题】:Find Top N Most Frequent Sequence of Numbers in List of Lists在列表列表中查找前 N 个最频繁的数字序列
【发布时间】:2020-05-13 00:46:16
【问题描述】:

假设我有以下列表:

x = [[1, 2, 3, 4, 5, 6, 7],  # sequence 1
     [6, 5, 10, 11],  # sequence 2
     [9, 8, 2, 3, 4, 5],  # sequence 3
     [12, 12, 6, 5],  # sequence 4
     [5, 8, 3, 4, 2],  # sequence 5
     [1, 5],  # sequence 6
     [2, 8, 8, 3, 5, 9, 1, 4, 12, 5, 6],  # sequence 7
     [7, 1, 7, 3, 4, 1, 2],  # sequence 8
     [9, 4, 12, 12, 6, 5, 1],  # sequence 9
]

基本上,对于在列表中任意位置包含目标编号5(即target=5)的任何列表,在N=2 中最常观察到的长度为M=4 的子序列是什么?

所以,条件是:

  1. 如果列表中不存在target,那么我们将完全忽略该列表
  2. 如果列表长度小于M,那么我们将完全忽略该列表
  3. 如果列表的长度正好是M,但target 不在Mth 位置,那么我们将忽略它(但如果targetMth 位置,我们就计算它)
  4. 如果列表长度LM 长并且targeti=M 位置(ori=M+1position, ori=M+2position, ...,i= Lposition) then we count the subsequence of lengthMwheretarget`在子序列的最后位置

因此,使用我们的列表示例,我们将计算以下子序列:

subseqs = [[2, 3, 4, 5],  # taken from sequence 1
           [2, 3, 4, 5],  # taken from sequence 3
           [12, 12, 6, 5],  # taken from sequence 4
           [8, 8, 3, 5],  # taken from sequence 7
           [1, 4, 12, 5],  # taken from sequence 7
           [12, 12, 6, 5],  # taken from sequence 9
]

当然,我们想要的是频率最高的N=2 子序列。因此,[2, 3, 4, 5][12, 12, 6, 5] 是计数最高的两个最频繁序列。如果 N=3 则所有子序列 (subseqs) 将被返回,因为第三个存在平局。

这是超级简化的,但实际上,我的实际列表列表

  1. 由数十亿个正整数列表(1 到 10,000 之间)组成
  2. 每个列表可以短至 1 个元素,也可以长达 500 个元素
  3. NM 可以小到 1 也可以大到 100

我的问题是:

  1. 假设NM 总是小于100,是否存在允许快速查询的有效数据结构?
  2. 是否有有效的算法或相关研究领域可以对NM 的各种组合进行此类分析?

【问题讨论】:

  • 看起来您实际上有 2 个相当独立的问题:1. 找到一种有效生成子序列流的方法,以及 2. 找到一种有效选择该序列中前 N 个条目的方法。对于 2. 您需要遍历所有子序列,并且您可能可以使用某种基于前缀的树结构来保持条目数。对于 1. 我认为您找不到不涉及完全遍历每个列表的方法,其缓存长度为 M 个条目。我想说最佳效率与元素总数成线性关系。
  • 实际上,我想多了 - 因为 1. 如果空间对您来说不是问题,您可以使用字典。
  • 作为一个单独的点,如果需要,这很好地并行化。
  • 我认为你的一些编号是错误的,应该取自序列5 取自序列4,取自序列8 取自序列9?
  • 为了避免需要缓存,您可以向后扫描每个列表,当您找到目标时,您的字典键是接下来的 M 个元素 - 如果还有 M 个元素。

标签: python algorithm list numpy graph


【解决方案1】:

这是一个基于generalized suffix tree 结构的想法。您的列表列表可以看作是一个字符串列表,其中字母表由整数组成(因此,您提供的信息中大约有 10k 个字符)。
广义后缀树的构造是在线性时间 w.r.t 字符串长度内完成的,所以这应该不是问题,因为在任何情况下,您都必须在某些时候检查您的列表。

首先,将所有字符串存储在后缀树中。这需要对结构进行 2 次小的调整。
您需要保留某个后缀出现次数的计数器,因为您的目标最终是找到尊重某些属性的最常见子序列。

然后,您还希望有一个来自(i, d) 的查找表(其中i 是您要查找的整数,目标,d 是树中的深度,M)到您的后缀链接的一组节点,这些节点标有“字母”i(您的字母表不是由字符组成,而是由整数组成),位于深度d。可以通过遍历您的后缀链接(BFS 或 DFS)来构建此查找表。您甚至可以只存储对应于最高计数器值的节点。

从那里,对于某些查询(target, M),您将首先查看查找表,然后在树中找到具有最高计数器值的节点。这将对应于列表列表中最常遇到的“后缀”(或子序列)。

实现相当复杂,因为广义后缀树(根本)不是一个简单的结构,并且通过修改正确实现它并不是一件容易的事。但我认为这将允许非常有效的查询时间。

对于后缀树的实现,我建议您只阅读原始论文,直到您对它们有深刻而真实的理解(例如thisthat,sc*-h*b 可以是您的朋友)关于这个问题,而不是它的在线“解释”,其中充斥着近似值和错误(即使this post 可以帮助获得第一个想法,但如果您的目标是实施正确的版本,则会在某些时候误导您)。

【讨论】:

  • 不,我从未听说过任何实现后缀树的 python 包,我想它必须是 100% 自制的(过去做过,但代码不是我要分享的)。但即使你不去,我建议你看看后缀树结构,这是一个了不起的设计,在某些情况下可以派上用场。
  • 我在寻找解决方案时遇到了这个问题,因此您的回复令人放心。
  • 有没有办法构建这棵树来同时计算不同长度的序列?那么,长度M=1all 可能达到M=50 和任何target?或者我们是否需要为每个不同的M 和每个不同的target 重新构建树?
  • 当您说“高效查询时间”时,您认为这会是不到一秒、不到 10 秒、不到一分钟、不到一小时吗?
  • 基本上,它取决于实现(python 不是真的很快)和预计算时间....但是通过几个小时的预计算,我想说你可以简单地预计算所有可能的查询,并在几十毫秒内返回结果。
【解决方案2】:

回答您的第一个问题:您可以将所有列表放在一个数组中,通过扩展零来固定长度,这样数组就可以成为您可以使用的东西。从答案here

x = [[1, 2, 3, 4, 5, 6, 7],  # sequence 1
     [6, 5, 10, 11],  # sequence 2
     [9, 8, 2, 3, 4, 5],  # sequence 3
     [12, 12, 6, 5],  # sequence 4
     [5, 8, 3, 4, 2],  # sequence 5
     [1, 5],  # sequence 6
     [2, 8, 8, 3, 5, 9, 1, 4, 12, 5, 6],  # sequence 7
     [7, 1, 7, 3, 4, 1, 2],  # sequence 8
     [9, 4, 12, 12, 6, 5, 1],  # sequence 9
]

lens = np.fromiter(map(len, x), np.int)
n1, n2 = len(lens), lens.max()
arr = np.zeros((n1, n2), dtype=np.int)

mask = np.arange(n2) < lens[:,None]
arr[mask] = np.concatenate(x)
arr
>> [[ 1  2  3  4  5  6  7  0  0  0  0]
[ 6  5 10 11  0  0  0  0  0  0  0]
 [ 9  8  2  3  4  5  0  0  0  0  0]
 [12 12  6  5  0  0  0  0  0  0  0]
 [ 5  8  3  4  2  0  0  0  0  0  0]
 [ 1  5  0  0  0  0  0  0  0  0  0]
 [ 2  8  8  3  5  9  1  4 12  5  6]
 [ 7  1  7  3  4  1  2  0  0  0  0]
 [ 9  4 12 12  6  5  1  0  0  0  0]]

对于第二个问题:使用np.where 查找符合您条件的不同职位。然后您可以通过添加维度来广播行和列的索引以包含5s 和前面的4个元素:

M = 4
N = 5
r, c = np.where(arr[:, M-1:]==N)
arr[r[:,None], (c[:,None] + np.arange(M))]
>>array([[ 2,  3,  4,  5],
   [ 2,  3,  4,  5],
   [12, 12,  6,  5],
   [ 8,  8,  3,  5],
   [ 1,  4, 12,  5],
   [12, 12,  6,  5]])

【讨论】:

    【解决方案3】:

    您的问题分为两部分:

    要生成您想要的子序列,您可以使用生成器来帮助您:

    def gen_m(lst, m, val):
        '''
        lst = sub_list to parse
        m = length required
        val = target value
        '''
    
        found = 0                                  # starts with 0 index
        for i in range(lst[m-1:].count(val)):      # repeat by the count of val
            found = lst.index(val, found) + 1      # set and find the next index of val
            yield tuple(lst[found-m: found])       # yield the sliced sub_list of m length as a tuple
    

    然后,使用另一个生成器,您可以创建子列表的Counter

    from collections import Counter
    target = 5
    req_len = 4
    
    # the yielded sub_lists need to be tuples to be hashable for the Counter
    counter = Counter(sub_tup for lst in x for sub_tup in gen_m(lst, req_len, target))
    

    然后,创建一个生成器来检查计数器对象以返回所需的N 计数:

    req_N = 2
    
    def gen_common(counter, n):
        s = set()
        for i, (item, count) in enumerate(counter.most_common()):
            if i < n or count in s:
                yield item
            else:
                return
            s.add(count)
    
    result = list(gen_common(counter, req_N))
    

    N == 2:

    [[2, 3, 4, 5], [12, 12, 6, 5]]
    

    N == 3:

    [[2, 3, 4, 5], [12, 12, 6, 5], [8, 8, 3, 5], [1, 4, 12, 5]]
    

    使用更大的样本:

    x = [[1, 2, 3, 4, 5, 6, 7],  
         [6, 5, 10, 11],  
         [9, 8, 2, 3, 4, 5],  
         [12, 12, 6, 5],  
         [5, 8, 3, 4, 2],  
         [1, 5],  
         [2, 8, 8, 3, 5, 9, 1, 4, 12, 5, 6],  
         [7, 1, 7, 3, 4, 1, 2],  
         [9, 4, 12, 12, 6, 5, 1],  
         [9, 4, 12, 12, 6, 5, 1],  
         [9, 4, 2, 3, 4, 5, 1],  
         [9, 4, 8, 8, 3, 5, 1],  
         [9, 4, 7, 8, 9, 5, 1],     
         [9, 4, 1, 2, 2, 5, 1],  
         [9, 4, 12, 12, 6, 5, 1],  
         [9, 4, 12, 12, 6, 5, 1],  
         [9, 4, 1, 4, 12, 5],  
         [9, 1, 4, 12, 5, 1]  
    ]
    

    Counter 现在在哪里:

    Counter({(12, 12, 6, 5): 5, (2, 3, 4, 5): 3, (1, 4, 12, 5): 3, (8, 8, 3, 5): 2, (7, 8, 9, 5): 1, (1, 2, 2, 5): 1})
    

    您可以获得如下结果:

    for i in range(6):
        # testing req_N from 0 to 5
        list(gen_common(c, i))
    
    # req_N = 0: []
    # req_N = 1: [(12, 12, 6, 5)]
    # req_N = 2: [(12, 12, 6, 5), (2, 3, 4, 5), (1, 4, 12, 5)]
    # req_N = 3: [(12, 12, 6, 5), (2, 3, 4, 5), (1, 4, 12, 5)]
    # req_N = 4: [(12, 12, 6, 5), (2, 3, 4, 5), (1, 4, 12, 5), (8, 8, 3, 5)]
    # req_N = 5: [(12, 12, 6, 5), (2, 3, 4, 5), (1, 4, 12, 5), (8, 8, 3, 5), (7, 8, 9, 5), (1, 2, 2, 5)]
    

    【讨论】:

      【解决方案4】:

      由于不存在唯一的 N、M 和目标,我假设存在带有列表的列表块。这是一种 O(N + M) 时间复杂度方式的方法(其中 N 是块中的列表数,M 是元素的总数):

      def get_seq(x, M, target):
          index_for_length_m = M - 1
          for v in [l for l in x if len(l) >= M]:
              for i in [i for i, v in enumerate(v[index_for_length_m:], start=index_for_length_m) if v == target]:
                  # convert to str to be hashable
                  yield str(v[i - index_for_length_m : i + 1])
      
      def process_chunk(x, M, N, target):
          return Counter(get_seq(x, M, target)).most_common(N)
      

      用你的例子:

      process_chunk(x, M, 2, target)
      

      输出:

      [('[2, 3, 4, 5]', 2), ('[12, 12, 6, 5]', 2)]
      

      表演:

      %timeit process_chunk(x, M, 2, target)
      # 25 µs ± 713 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
      

      【讨论】:

        猜你喜欢
        • 2020-05-16
        • 1970-01-01
        • 2015-10-08
        • 1970-01-01
        • 2021-12-09
        • 1970-01-01
        • 2022-08-16
        • 2022-06-15
        • 1970-01-01
        相关资源
        最近更新 更多