[Numpy-discussion] Implementing a "find first" style function

Phil Elson pelson.pub at gmail.com
Fri Mar 8 12:38:23 EST 2013


Interesting. I hadn't thought of those. I've implemented (very roughly
without a sound logic check) and benchmarked:

def my_any(a, predicate, chunk_size=2048):
    try:
        next(find(a, predicate, chunk_size))
        return True
    except StopIteration:
        return False

def my_all(a, predicate, chunk_size=2048):
    return not my_any(a, lambda a: ~predicate(a), chunk_size)


With the following setup:

import numpy as np
import numpy.random

np.random.seed(1)
a = np.random.randn(1e8)


For a low frequency *any*:

In [12]: %timeit (np.abs(a) > 6).any()
1 loops, best of 3: 1.29 s per loop

In [13]: %timeit my_any(a, lambda a: np.abs(a) > 6)
1 loops, best of 3: 792 ms per loop

In [14]: %timeit my_any(a, lambda a: np.abs(a) > 6, chunk_size=10000)
1 loops, best of 3: 654 ms per loop

For a False *any*:

In [16]: %timeit (np.abs(a) > 7).any()
1 loops, best of 3: 1.22 s per loop

In [17]: %timeit my_any(a, lambda a: np.abs(a) > 7)
1 loops, best of 3: 2.4 s per loop

For a high probability *any*:

In [28]: %timeit (np.abs(a) > 1).any()
1 loops, best of 3: 972 ms per loop

In [27]: %timeit my_any(a, lambda a: np.abs(a) > 1)
10000 loops, best of 3: 67 us per loop

---------------

For a low probability *all*:

In [18]: %timeit (np.abs(a) < 6).all()
1 loops, best of 3: 1.16 s per loop

In [19]: %timeit my_all(a, lambda a: np.abs(a) < 6)
1 loops, best of 3: 880 ms per loop

In [20]: %timeit my_all(a, lambda a: np.abs(a) < 6, chunk_size=10000)
1 loops, best of 3: 706 ms per loop

For a True *all*:

In [22]: %timeit (np.abs(a) < 7).all()
1 loops, best of 3: 1.47 s per loop

In [23]: %timeit my_all(a, lambda a: np.abs(a) < 7)
1 loops, best of 3: 2.65 s per loop

For a high probability *all*:

In [25]: %timeit (np.abs(a) < 1).all()
1 loops, best of 3: 978 ms per loop

In [26]: %timeit my_all(a, lambda a: np.abs(a) < 1)
10000 loops, best of 3: 73.6 us per loop







On 6 March 2013 21:16, Benjamin Root <ben.root at ou.edu> wrote:

>
>
> On Tue, Mar 5, 2013 at 9:15 AM, Phil Elson <pelson.pub at gmail.com> wrote:
>
>> The ticket https://github.com/numpy/numpy/issues/2269 discusses the
>> possibility of implementing a "find first" style function which can
>> optimise the process of finding the first value(s) which match a predicate
>> in a given 1D array. For example:
>>
>>
>> >>> a = np.sin(np.linspace(0, np.pi, 200))
>> >>> print find_first(a, lambda a: a > 0.9)
>> ((71, ), 0.900479032457)
>>
>>
>> This has been discussed in several locations:
>>
>> https://github.com/numpy/numpy/issues/2269
>> https://github.com/numpy/numpy/issues/2333
>>
>> http://stackoverflow.com/questions/7632963/numpy-array-how-to-find-index-of-first-occurrence-of-item
>>
>>
>> *Rationale*
>>
>> For small arrays there is no real reason to avoid doing:
>>
>> >>> a = np.sin(np.linspace(0, np.pi, 200))
>> >>> ind = (a > 0.9).nonzero()[0][0]
>> >>> print (ind, ), a[ind]
>> (71,) 0.900479032457
>>
>>
>> But for larger arrays, this can lead to massive amounts of work even if
>> the result is one of the first to be computed. Example:
>>
>> >>> a = np.arange(1e8)
>> >>> print (a == 5).nonzero()[0][0]
>> 5
>>
>>
>> So a function which terminates when the first matching value is found is
>> desirable.
>>
>> As mentioned in #2269, it is possible to define a consistent ordering
>> which allows this functionality for >1D arrays, but IMHO it overcomplicates
>> the problem and was not a case that I personally needed, so I've limited
>> the scope to 1D arrays only.
>>
>>
>> *Implementation*
>>
>> My initial assumption was that to get any kind of performance I would
>> need to write the *find* function in C, however after prototyping with
>> some array chunking it became apparent that a trivial python function would
>> be quick enough for my needs.
>>
>> The approach I've implemented in the code found in #2269 simply breaks
>> the array into sub-arrays of maximum length *chunk_size* (2048 by
>> default, though there is no real science to this number), applies the given
>> predicating function, and yields the results from *nonzero()*. The given
>> function should be a python function which operates on the whole of the
>> sub-array element-wise (i.e. the function should be vectorized). Returning
>> a generator also has the benefit of allowing users to get the first *n*matching values/indices.
>>
>>
>> *Results*
>>
>>
>> I timed the implementation of *find* found in my comment at
>> https://github.com/numpy/numpy/issues/2269#issuecomment-14436725 with an
>> obvious test:
>>
>>
>> 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
>>
>>
>> This shows that picking a sensible *chunk_size* can yield massive
>> speed-ups (nonzero is x3 slower in one case). A similar example with a much
>> smaller 1D array shows similar promise:
>>
>> In [41]: a = np.random.randn(1e4)
>>
>> In [42]: %timeit next(find(a, lambda a: np.abs(a) > 3))
>> 10000 loops, best of 3: 35.8 us per loop
>>
>> In [43]: %timeit (np.abs(a) > 3).nonzero()
>> 10000 loops, best of 3: 148 us per loop
>>
>>
>> As I commented on the issue tracker, if you think this function is worth
>> taking forward, I'd be happy to open up a pull request.
>>
>> Feedback greatfully received.
>>
>> Cheers,
>>
>> Phil
>>
>>
>>
> In the interest of generalizing code and such, could such approaches be
> used for functions like np.any() and np.all() for short-circuiting if True
> or False (respectively) are found?  I wonder what other sort of functions
> in NumPy might benefit from this?
>
> Ben Root
>
>
> _______________________________________________
> NumPy-Discussion mailing list
> NumPy-Discussion at scipy.org
> http://mail.scipy.org/mailman/listinfo/numpy-discussion
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/numpy-discussion/attachments/20130308/73b041e0/attachment.html>


More information about the NumPy-Discussion mailing list