如何使用一个二维布尔行数组来过滤另一个二维数组?

4
我有一个形状为(3, m)的数组。
我还有一个形状为(n, 3)的掩码数组。这个掩码数组的行是布尔过滤器,需要在对数据数组执行某些函数之前应用。是否存在一种向量化的方式来应用筛选器并计算函数?
以下是一个使用循环的示例,假设函数是mean()。我想纯粹使用Numpy进行此操作(不使用列表推导)。
(显然,实际上数组的大小要大得多。)
import numpy as np

data = np.array([
       [ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]
    ])

masks = np.array([
        [True, True, False],
        [False, True, False],
        [False, True, True],
        [True, False, False],
        [True, False, True]
    ])

means = np.array([data[mask].mean(axis=0) for mask in masks])

# means
array([[ 2.,  3.,  4.,  5.],
       [ 4.,  5.,  6.,  7.],
       [ 6.,  7.,  8.,  9.],
       [ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.]])

你试过所有发布的方法了吗?有没有哪个对你有效?实际输入设置的运行时间如何? - Divakar
抱歉,是的,我尝试过它们。你的方法对于特定目的来说是最快的,并且按照我下面描述的方式进行广播可以适用于其他功能。 - capitalistcuttle
3个回答

0

这感觉有点粗糙和混乱,但它可以在没有循环的情况下工作。

有两个主要任务:

  • 扩展data,使其可以使用masks进行索引-从(5,4)到(5,3,4)
  • means应用于行组;我能找到的最接近的是np.sum.reduceat

构建reduceat索引:

In [253]: cnt = masks.sum(axis=1)
In [254]: cnt1=np.concatenate(([0],np.cumsum(cnt)[:-1]))
In [255]: cnt
Out[255]: array([2, 1, 2, 1, 2])   # True count per row
In [256]: cnt1
Out[256]: array([0, 2, 3, 5, 6])   # reduceat index positions

展开 datamask

In [257]: mdata=data[None,...].repeat(masks.shape[0],0)[masks,:]

添加行并对每个组的行数进行除法运算

In [258]: np.add.reduceat(mdata,cnt1,0)/cnt[:,None]
Out[258]: 
array([[ 2.,  3.,  4.,  5.],
       [ 4.,  5.,  6.,  7.],
       [ 6.,  7.,  8.,  9.],
       [ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.]])

如果有帮助的话:
In [263]: mdata
Out[263]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 4,  5,  6,  7],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [ 0,  1,  2,  3],
       [ 0,  1,  2,  3],
       [ 8,  9, 10, 11]])

获取这个mdata的可能更好的方法是

In [285]: data[np.where(masks)[1],:]
Out[285]: 
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 4,  5,  6,  7],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [ 0,  1,  2,  3],
       [ 0,  1,  2,  3],
       [ 8,  9, 10, 11]])

where(...)[1]masks 中 True 的列位置,这些是我们想要从 data 中选择的行。

===========================

@capitalistcuttle 也创建了一个 (5,3,4) 的数组,但通过将 False 行清零来避免了使用 reduceat。这样就可以在进入 meansum 时不影响值。这让我想起掩码数组执行此类任务的方式。它们使用像 0 或 1 这样不影响计算的值来填充掩码值。

受此启发,这里提供了一个 MaskedArray 解决方案

datamasks 扩展到 (5,3,4) 大小:

In [322]: data1=data[None,:,:].repeat(5,0)
In [323]: masks1=masks[:,:,None].repeat(4,-1)
In [324]: data1.shape, masks1.shape
Out[324]: ((5, 3, 4), (5, 3, 4))

从那个创建掩码数组:

In [325]: madata=np.ma.MaskedArray(data1,~masks1)
In [326]: madata
Out[326]: 
masked_array(data =
 [[[0 1 2 3]
  [4 5 6 7]
  [-- -- -- --]]

 [[-- -- -- --]
  [4 5 6 7]
  [-- -- -- --]]
 ...
 [[0 1 2 3]
  [-- -- -- --]
  [8 9 10 11]]],
             mask =
 [[[False False False False]
  [False False False False]
  [ True  True  True  True]]

 [[ True  True  True  True]
  [False False False False]
  [ True  True  True  True]]
  ...],
       fill_value = 999999)

现在我们可以简单地使用mean方法,让它处理0填充并调整有效行数。
In [327]: madata.mean(axis=1)
Out[327]: 
masked_array(data =
 [[2.0 3.0 4.0 5.0]
 [4.0 5.0 6.0 7.0]
 [6.0 7.0 8.0 9.0]
 [0.0 1.0 2.0 3.0]
 [4.0 5.0 6.0 7.0]],
             mask =
 [[False False False False]
 [False False False False]
 [False False False False]
 [False False False False]
 [False False False False]],
       fill_value = 1e+20)

使用.data属性将其转换回常规数组。

这种MaskedArray方法可能会更慢,因为它创建了一个较大的数组,但它可能更通用-只要在np.ma或其方法中定义了操作,就可以使用它们。


0

所以,玩了一会儿之后,似乎这种广播对于mean()函数是起作用的:

means = (masks[:, :, np.newaxis] * data).sum(axis=1) / masks.sum(axis=1)[:, np.newaxis]

# means
array([[ 2.,  3.,  4.,  5.],
       [ 4.,  5.,  6.,  7.],
       [ 6.,  7.,  8.,  9.],
       [ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.]])

对于其他函数,您可以使用以下格式(其中mean()可以替换为所需的函数):

means = (masks[:, :, np.newaxis] * data).mean(axis=1) * masks.shape[1] / masks.sum(axis=1)[:, np.newaxis]

# means
array([[ 2.,  3.,  4.,  5.],
       [ 4.,  5.,  6.,  7.],
       [ 6.,  7.,  8.,  9.],
       [ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.]])

这将复制像我的“tile”或“repeat”一样的行,但通过将“False”行清零来避免使用“reduceat”。 - hpaulj

0

使用np.dot进行矩阵乘法可以轻松解决这个问题,因此必须非常高效。以下是实现代码 -

np.true_divide(masks.dot(data),masks.sum(1)[:,None])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接