Numpy: 第一个非零元素 (Trac #1673)

创建于 2012-10-20  ·  26评论  ·  资料来源: numpy/numpy

_原始票http://projects.scipy.org/numpy/ticket/1673于 2010-11-13 由 trac 用户 tom3118 分配给未知数。_

“matlab 用户的 numpy”建议使用
nonzero(A)[0][0]
查找数组 A 的第一个非零元素的索引。

这样做的问题是 A 可能有一百万个元素长,而第一个元素可能为零。

这是一种极为常见的操作。 一种有效的内置方法将非常有用。 它还可以让人们轻松地从find如此普遍的 Matlab 过渡。

01 - Enhancement Other

最有用的评论

我知道这已经晚了 3 年,但是现在 numpy 中包含吗? 来自 Matlab 背景,这个函数对我来说似乎真的很重要。 PR 将不胜感激(不是我是开发人员之一)。

所有26条评论

_trac 用户 tom3118 写于 2010-11-13_

一个相关的用例是:
filter(test,A)[0]
其中A很长或test很贵。

_@rgommers写于 2011-03-24_

不必只是第一个非零值,首先任何值都是有用的。

_@rgommers写于 2011-03-24_

如#2333 中所述,一维的含义是明确的。 对于 >1-D 语义有待讨论。

也许确定轴上迭代顺序的关键字有效。 或者对于 >1-D 可以简单地未定义。

_trac 用户 lcampagn 写于 2011-07-09_

我在 numpy 中看到了许多对 find_first 的请求,但其中大多数请求都有细微的不同(且不兼容)要求,例如“查找小于 x 的第一个值”或“查找第一个非零值”。 我建议以下功能规范:

  ind = array.find(x, testOp='eq', arrayOp='all', axis=0, test=None)
  arguments:
    x       -> value to search for
    testOp  -> condition to test for ('eq', 'ne', 'gt', 'lt', 'ge', 'le')
    arrayOp -> method for joining multiple comparisons ('any' or 'all')
    axis    -> the axis over which to search
    test    -> for convenience, this may specify a function to call to perform
               the test. This is not expected to be efficient.
  returns: 
    first index where condition is true (or test returns true, if given)
    or None if the condition was never met

如果数组的 ndim > 1,则使用正常的广播规则执行测试。
因此,例如,如果我有一个形状为 (2,3) 的数组,则以下内容将是有效的:

  ## find first row with all values=0
  array.find(0, testOp='eq', arrayOp='all', axis=0)
  ## equivalent to:
  for i in range(array.shape[axis]):
    if (array[i] == 0).all():
      return i

  ## find first column with any element greater than its corresponding element in col
  col = array([1,2])
  array.find(col, testOp='gt', arrayOp='any', axis=1)
  ## equivalent to:
  for i in range(array.shape[axis]):
    if (array[:,i] == col.any():
      return i

因为前几天我需要这个功能,所以我仔细研究了一下,并确信需要一个 C 解决方案才能获得适当快速的结果,但是用 python 编写的分块方法已被证明是适当快速的,而且很多就我而言,启动起来更灵活。

import numpy as np
from itertools import chain, izip


def find(a, predicate, chunk_size=1024):
    """
    Find the indices of array elements that match the predicate.

    Parameters
    ----------
    a : array_like
        Input data, must be 1D.

    predicate : function
        A function which operates on sections of the given array, returning
        element-wise True or False for each data value.

    chunk_size : integer
        The length of the chunks to use when searching for matching indices.
        For high probability predicates, a smaller number will make this
        function quicker, similarly choose a larger number for low
        probabilities.

    Returns
    -------
    index_generator : generator
        A generator of (indices, data value) tuples which make the predicate
        True.

    See Also
    --------
    where, nonzero

    Notes
    -----
    This function is best used for finding the first, or first few, data values
    which match the predicate.

    Examples
    --------
    >>> a = np.sin(np.linspace(0, np.pi, 200))
    >>> result = find(a, lambda arr: arr > 0.9)
    >>> next(result)
    ((71, ), 0.900479032457)
    >>> np.where(a > 0.9)[0][0]
    71


    """
    if a.ndim != 1:
        raise ValueError('The array must be 1D, not {}.'.format(a.ndim))

    i0 = 0
    chunk_inds = chain(xrange(chunk_size, a.size, chunk_size), 
                 [None])

    for i1 in chunk_inds:
        chunk = a[i0:i1]
        for inds in izip(*predicate(chunk).nonzero()):
            yield (inds[0] + i0, ), chunk[inds]
        i0 = i1
In [1]: from np_utils import find

In [2]: import numpy as np

In [3]: import numpy.random    

In [4]: np.random.seed(1)

In [5]: a = np.random.randn(1e8)

In [6]: a.min(), a.max()
Out[6]: (-6.1194900990552776, 5.9632246301166321)

In [7]: next(find(a, lambda a: np.abs(a) > 6))
Out[7]: ((33105441,), -6.1194900990552776)

In [8]: (np.abs(a) > 6).nonzero()
Out[8]: (array([33105441]),)

In [9]: %timeit (np.abs(a) > 6).nonzero()
1 loops, best of 3: 1.51 s per loop

In [10]: %timeit next(find(a, lambda a: np.abs(a) > 6))
1 loops, best of 3: 912 ms per loop

In [11]: %timeit next(find(a, lambda a: np.abs(a) > 6, chunk_size=100000))
1 loops, best of 3: 470 ms per loop

In [12]: %timeit next(find(a, lambda a: np.abs(a) > 6, chunk_size=1000000))
1 loops, best of 3: 483 ms per loop

我会把它放在开发者邮件列表中,但如果有足够的兴趣,我会很乐意把它变成 PR。

干杯,

我知道这已经晚了 3 年,但是现在 numpy 中包含吗? 来自 Matlab 背景,这个函数对我来说似乎真的很重要。 PR 将不胜感激(不是我是开发人员之一)。

我也会对此感兴趣。

也许这很明显,但由于没有提及: np.all()np.any()可能更容易(并且对于维度 > 1 明确)变得懒惰。 现在...

In [2]: zz = np.zeros(shape=10000000)

In [3]: zz[0] = 1

In [4]: %timeit -r 1 -n 1 any(zz)
1 loop, best of 1: 3.52 µs per loop

In [5]: %timeit -r 1 -n 1 np.any(zz)
1 loop, best of 1: 16.7 ms per loop

(对不起,我错过了对 #3446 的引用)

由于我一直在寻找这个问题的有效解决方案已经有一段时间了,而且似乎没有支持这个功能的具体计划,所以我试图提出一个解决方案,但它并不像 api 所建议的那样完全和通用上面(特别是目前仅支持一维数组),但这具有完全用 C 编写的优点,因此看起来相当有效。

您可以在此处找到来源和详细信息:

https://pypi.python.org/pypi?name=py_find_1st& :action=display

我将非常感谢您对实现的任何评论,特别是在传递布尔数组和搜索第一个真值时有点令人惊讶的性能问题,这在 PyPi 页面上有所描述。

我是从一个正在寻找此功能的stackexchange 帖子中找到的,该功能已被查看超过 70k 次。 @roebel你有没有得到任何反馈? 您可以为该功能添加一个 PR,这可能会引起更多关注吗?

不,我从来没有收到任何反馈,但显然有几个人使用该软件包没有问题。
顺便说一句,对于 anaconda linux 和 macos,我制作了 anaconda 安装程序

https://anaconda.org/roebel/py_find_1st

关于 PR,我将不得不研究它需要我调整它以便可以轻松合并到 numpy 中的努力。 我没有时间讨论 API 更改和扩展。

删除“ priority:normal ”是否意味着这个重要的特性会以某种方式得到较少的关注?

优先级仍然是“正常”,只是没有标签。 这个问题需要一个拥护者来实际进行 PR 并推动它通过批准过程,包括文档和希望的基准。

在这里指向#8528 可能很有用,它名义上约为all_equal ,但可以看作是实现它的一部分。 实际上,在https://github.com/numpy/numpy/pull/8528#issuecomment -365358119 中, @ahaldane明确建议对所有比较运算符实施first缩减方法,而不是新的 gufunc all_equal

这也意味着有相当多的实现等待适应(尽管从 gufunc 到新的归约方法并不是一个微不足道的变化,而且我们是否需要在所有 ufunc 上使用新方法,即使是那些这first没有什么意义。

自(至少)2012 年以来,此问题已为人所知。有关阻止nonzero(A)[0][0]搜索所有A的方法的任何更新?

是不是一直扫描所有元素的所谓 Pythonic 方式?

@yunyoulu :这是 ufunc 方式。 让我们退后一步,看一下numpy中多步计算的一般过程,以及它所需要的遍数:

  1. np.argwhere(x)[0] - 执行 1 次数据传递
  2. np.argwhere(f(x))[0] - 执行 2 次数据传递
  3. np.argwhere(f(g(x)))[0] - 执行 3 次数据传递

一种选择是引入np.first函数或类似函数 - 然后看起来如下所示,其中k <= 1取决于第一个元素的位置:

  1. np.first(x)[0] - 执行 0+k 次数据传递
  2. np.first(f(x))[0] - 执行 1+k 次数据传递
  3. np.first(f(g(x)))[0] - 执行 2+k 次数据传递

这里要问的问题是——这种节省真的值那么多钱吗? Numpy 本质上不是一个惰性计算平台,如果前面的所有步骤都没有,那么让计算的最后一步变得惰性并不是特别有价值。


过时的

@eric-wieser

我认为这措辞不太正确。 如果k = 10出现问题,则不是1+10=11传递np.first(f(x))[0]的数据

(为简洁起见,由@eric-wieser 编辑,这段对话已经太长了)

我最认为需要此功能的用例是A是一个带有A.shape = (n_1, n_2, ..., n_m)的大张量。 在这种情况下, np.first(A)将只需要查看 $ Ak元素,而不是n_1*n_2*...*n_m (潜在的显着节省)。

我最看到这个功能的需要是当 A 是一个大张量

大概在这种情况下,您已经完成了至少一次完整的数据传递——所以充其量您得到的代码运行速度是原来的两倍。

这里要问的问题是——这种节省真的值那么多钱吗? Numpy 本质上不是一个惰性计算平台,如果前面的所有步骤都没有,那么让计算的最后一步变得惰性并不是特别有价值。

这是一个有趣的观点,如果成立,可以用来证明几乎所有提高计算性能的努力都是合理的,“因为我们也在计算其他东西,而且速度仍然很慢”。 (这与气候变化行动的否认者所使用的论点相同——好吧,在这个其他国家采取行动之前,在我们国家采取行动对任何人都无济于事。)我一点也不相信。 如果有任何机会将计算的某些部分加快 1/k,而 k 可能非常非常小,我认为这是值得的。

此外,在交互工作时(Jupyter 等),您通常会在单独的单元格中“传递”数据,因此您最终也可以加快整个单元格的速度。

np.first(f(x))[0] - 执行 1+k 次数据传递

@eric-wieser 确实,当我在 2017 年看到这个问题时,我真的希望这将是迈向某种np.firstwhere(x, array_or_value_to_compare)的第一步,这确实是一个具体案例 - 但在我的经验中很重要 - f(x)

@toobaz :我假设你在那个例子中有f = lambda x: x == value_to_compare

这正是我对走这条路持谨慎态度的原因(cc @bersbersbers)。 如果你不小心,我们最终会得到(拼写推测):

  1. np.first(x) - 保存通行证与非零
  2. np.first_equal(x, v) - 保存通行证与first(np.equal(x, v))
  3. np.first_square_equal(x*x, v) - 保存通行证与first_equal(np.square(x), v)

很明显,这根本没有缩放,我们必须在某处画线。 我略微赞成允许 1,但允许 2 已经是 API 表面积的爆炸式增长,而 3 对我来说似乎很不明智。

支持np.first的一个论点 - 如果我们实现它, numba可以对其进行特殊处理,使得np.first(x*x == v) _within a numba context_ 实际上_does_ 执行一次传递。

无论如何,很高兴知道在 numpy 中做懒惰的事情是不可能的,这澄清了问题的当前状态。

但是,当只考虑可伸缩性的性能调整时,我会感到不舒服。

让我们问一个简单的问题:今天的个人计算机是否可以扩展? 答案肯定是否定的。 三年前,当您购买一台标准笔记本电脑时,它们配备了 8GB 内存; 现在你仍然会在市场上找到 8GB。 但是,每个软件使用的内存都是过去的 2 倍或 4 倍。 至少工作站没有像集群那样扩展。

在不改变其复杂性的情况下让函数慢 10 倍足以让一位数据科学家发疯。 更糟糕的是,即使通过 profiling 找出了瓶颈,他也无能为力。

我要详细说明的是,具有进行惰性处理的能力总是可取的,并且对于系统的响应能力以及使用该语言的人们的生产力至关重要。 库开发中的困难或工作量构成了不实现这些功能的一个很好的借口,当然是可以理解的,但不要说它们没有用。

@toobaz :我假设你在那个例子中有f = lambda x: x == value_to_compare

正确的

这正是我对走这条路持谨慎态度的原因(cc @bersbersbers)。 如果你不小心,我们最终会得到(拼写推测):

1. `np.first(x)` - save a pass vs nonzero

2. `np.first_equal(x, v)` - save a pass vs `first(np.equal(x, v))`

3. `np.first_square_equal(x*x, v)` - save a pass vs `first_equal(np.square(x), v)`

我理解您的担忧,但我永远不会像我永远不会要求(我希望没有人要求) np.square_where np.first_square_equal 是的,我确实看到这意味着如果您执行 3,则需要完整传递数据。但是v只创建一次,我可能需要在其上查找x的许多不同值. 例如(为了简单起见,回到示例 2。),我想验证我的 30 个可能的类别是否都出现在我的 10^9 项数组中 - 我强烈怀疑它们都出现在前 10^3 个元素中。

所以首先让我澄清一下我之前的评论:我希望np.firstwhere(x, array_or_value_to_compare)作为一个符合我直觉的函数,但我在 2017 年遇到的计算问题即使只使用np.first也可以解决。

其次,重点是——我认为——不仅仅是单个调用的运行时间。 确实,无论如何我都需要完整传递数据才能执行 2. 和 3... 但也许我在初始化数据时已经完成了这个传递,现在我真的在寻找一种加快速度的方法一个频繁的操作。

我明白您的观点,即np.first确实偏离了标准的 numpy 方法,我认为实现良好可能并非易事......我没有看到它会如何“感染”API 的其余部分,或发展自己的大型 API。

这就是说,如果您认为它确实超出了 numpy 范围,那么可能有一个小的独立包的范围。

你好,保罗,

我做了一个小基准,将您的解决方案与 np.flatnonzero 和我的 py_find_1st 扩展进行了比较。

您会发现附加的基准。

这里的结果

(base) m3088.roebel: (test) (g:master)514> ./benchmark.py
utf1st.find_1st(rr, limit, utf1st.cmp_equal)::
运行时间 0.131s
np.flatnonzero(rr==limit)[0]::
运行时间 2.121s
next((ii for ii, vv in enumerate(rr) if vv == limit))::
运行时间 1.612s

因此,虽然您提出的解决方案比 flatnonzero 快 25%,但它不需要
创建结果数组,它仍然比 py_find_1st.find_1st 慢 ~12。

最好的
阿克塞尔

编辑:
我通过邮件回复的消息似乎消失了,我的邮件中附加的基准也消失了。 基准在这里

https://github.com/roebel/py_find_1st/blob/master/test/benchmark.py

对不起噪音。

PK 于 2020 年 5 月 15 日 17:33 写道:

|next(i for i, v in enumerate(x) if v)| 怎么样?


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看https://github.com/numpy/numpy/issues/2269#issuecomment-629314457 ,或退订
https://github.com/notifications/unsubscribe-auth/ACAL2LS2YZALARHBHNABVILRRVOEPANCNFSM4ABV5HGA

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

inducer picture inducer  ·  3评论

dcsaba89 picture dcsaba89  ·  3评论

Kreol64 picture Kreol64  ·  3评论

dmvianna picture dmvianna  ·  4评论

qualiaa picture qualiaa  ·  3评论