Skip to main content
 首页 » 编程设计

Python NumPy : Efficiently get rows containing min value of column for each unique tuple of 3 other columns

2025年05月04日23lonelyxmas

我有一些数据存储在列表的列表中(约 200,000 行 x 6 列的列表)。

我需要获取以下数据子集: 对于 [1,2,4] 列中的每组唯一值,我需要找到具有第 0 列最小值的行并仅保留该行。

我必须在旧的 numpy 1.10 中执行此操作(不要问...),因此 np.unique() 中没有“axis=0”选项。

下面的示例运行并产生了正确的输出,但是速度非常慢。这看起来很基本,所以我觉得(缺乏)速度一定是我的错。

# S-L-O-W way to get the desired output: 
import numpy as np 
 
# Example dataset 
data = [[1, 1, 1, 'a', 1], 
        [0, 1, 1, 'b', 1], 
        [0, 3, 1, 'c', 4], 
        [3, 1, 1, 'd', 1], 
        [4, 3, 1, 'e', 4]] 
 
desired_output = [[0, 1, 1, 'b', 1], 
                  [0, 3, 1, 'c', 4]] 
 
# Currently coding on a geriatric machine with numpy pre-version 1.13 and no ability to upgrade, 
# so np.unique() won't take an axis argument. The next few hack lines of code get around this with strings... 
tuples_str = [] 
tuples_raw = [[datarow[jj] for jj in [1,2,4]]  for datarow in data ] 
for datarow in data: 
    one_tuple = [datarow[jj] for jj in [1,2,4]] 
    tuples_str.append( '_'.join([str(ww) for ww in one_tuple]) ) 
 
# Numpy unique on this data subset with just columns [1,2,4] of original data 
unq, unq_inv, unq_cnt = np.unique(tuples_str, return_inverse=True, return_counts=True) 
 
# Storage 
output = [] 
 
# Here's the painfully slow part: 
# Iterate over each subset of data where rows take the value in one unique tuple (i.e. columns [1,2,4] are identical) 
for ii, idx in enumerate(np.unique(unq_inv)): 
 
    # Get the rows that have the same values in columns [1,2,4] 
    all_matches_thistuple = [row for ii, row in enumerate(data) if unq_inv[ii]==idx] 
 
    # Find the index of the row with the minimum value for column 0 
    first_line_min_idx = np.argmin([int(row1[0]) for row1 in all_matches_thistuple]) 
 
    # Save only that row 
    output.append(all_matches_thistuple[first_line_min_idx]) 
print(output) 

请您参考如下方法:

如果您开始使用列表的列表,您可以使用纯 Python 轻松完成此操作,而且它会非常高效。事实上,你正在使用 numpy 和 object dtype,所以我怀疑你使用内置例程在性能上获得的 yield 很少,因为你失去了数据局部性(并且是基本上只剩下蹩脚的 Python list 对象)。相反,您可以在线性时间内完成此操作(不计算数据的初始排序,这将是 O(n*logN),但它将使用 Python 的 timsort,因此它实际上非常快),只对数据做几次传递:

In [1]: data = [[1, 1, 1, 'a', 1], 
   ...:         [0, 1, 1, 'b', 1], 
   ...:         [0, 3, 1, 'c', 4], 
   ...:         [3, 1, 1, 'd', 1], 
   ...:         [4, 3, 1, 'e', 4]] 
   ...: 
 
In [2]: from operator import itemgetter 
 
In [3]: group_key = itemgetter(1,2,4) 
 
In [4]: data.sort(key=group_key) 

然后简单地:

In [6]: first = itemgetter(0) 
 
In [7]: result = [] 
 
In [8]: from itertools import groupby 
   ...: for _, g in groupby(data, group_key): 
   ...:     result.append(min(g, key=first)) 
   ...: 
 
In [9]: result 
Out[9]: [[0, 1, 1, 'b', 1], [0, 3, 1, 'c', 4]] 

另一种方法是使用 defaultdict 构建辅助数据结构。这是对未排序数据进行分组的惯用方法。如果您希望能够将这些值分组,这可能很有用:

In [10]: from collections import defaultdict 
 
In [11]: grouper = defaultdict(list) 
 
In [12]: data = [[1, 1, 1, 'a', 1], 
    ...:         [0, 1, 1, 'b', 1], 
    ...:         [0, 3, 1, 'c', 4], 
    ...:         [3, 1, 1, 'd', 1], 
    ...:         [4, 3, 1, 'e', 4]] 
 
In [13]: for row in data: 
    ...:     _,x,y,_, z = row 
    ...:     grouper[(x,y,z)].append(row) 
    ...: 
 
In [14]: grouper 
Out[14]: 
defaultdict(list, 
            {(1, 1, 1): [[1, 1, 1, 'a', 1], 
              [0, 1, 1, 'b', 1], 
              [3, 1, 1, 'd', 1]], 
             (3, 1, 4): [[0, 3, 1, 'c', 4], [4, 3, 1, 'e', 4]]}) 
 
In [15]: first = itemgetter(0) 
 
In [16]: [min(group, key=first) for group in grouper.values()] 
Out[16]: [[0, 1, 1, 'b', 1], [0, 3, 1, 'c', 4]]