Numpy primera aparición de valor mayor que el valor existente

144

Tengo una matriz 1D en numpy y quiero encontrar la posición del índice donde un valor excede el valor en una matriz numpy.

P.ej

aa = range(-10,10)

Encuentre la posición en aadonde 5se excede el valor .

usuario308827
fuente
2
Uno debería tener claro si no podría haber una solución (ya que, por ejemplo, la respuesta argmax no funcionará en ese caso (máx. De (0,0,0,0) = 0) como comentó
ambrus

Respuestas:

199

Esto es un poco más rápido (y se ve mejor)

np.argmax(aa>5)

Dado que argmaxse detendrá en la primera True("En caso de que aparezcan múltiples valores máximos, se devuelven los índices correspondientes a la primera aparición") y no guarda otra lista.

In [2]: N = 10000

In [3]: aa = np.arange(-N,N)

In [4]: timeit np.argmax(aa>N/2)
100000 loops, best of 3: 52.3 us per loop

In [5]: timeit np.where(aa>N/2)[0][0]
10000 loops, best of 3: 141 us per loop

In [6]: timeit np.nonzero(aa>N/2)[0][0]
10000 loops, best of 3: 142 us per loop
askewchan
fuente
103
Solo una advertencia: si no hay un valor Verdadero en su matriz de entrada, np.argmax devolverá felizmente 0 (que no es lo que desea en este caso).
emboscada
8
Los resultados son correctos, pero la explicación me parece un poco sospechosa. argmaxno parece detenerse al principio True. (Esto se puede probar creando matrices booleanas con una sola Trueen diferentes posiciones). La velocidad probablemente se explica por el hecho de que argmaxno es necesario crear una lista de salida.
DrV
1
Creo que tienes razón, @DrV. Mi explicación estaba destinada a explicar por qué da el resultado correcto a pesar de que la intención original no busca realmente un máximo, no por qué es más rápido, ya que no puedo afirmar que entiendo los detalles internos argmax.
askewchan
1
@ George, me temo que no sé por qué exactamente. Solo puedo decir que es más rápido en el ejemplo particular que mostré, por lo que no lo consideraría generalmente más rápido sin (i) saber por qué es (ver el comentario de @ DrV) o (ii) probar más casos (por ejemplo, si aaestá ordenado, como en la respuesta de @ Michael).
askewchan
3
@DrV, acabo de ejecutar argmaxen matrices booleanas de 10 millones de elementos con una sola Trueen diferentes posiciones usando NumPy 1.11.2, y la posición de lo Trueimportado. Entonces, 1.11.2 argmaxparece "cortocircuitar" en matrices booleanas.
Ulrich Stern
96

Dado el contenido ordenado de su matriz, existe un método aún más rápido: ordenado por búsqueda .

import time
N = 10000
aa = np.arange(-N,N)
%timeit np.searchsorted(aa, N/2)+1
%timeit np.argmax(aa>N/2)
%timeit np.where(aa>N/2)[0][0]
%timeit np.nonzero(aa>N/2)[0][0]

# Output
100000 loops, best of 3: 5.97 µs per loop
10000 loops, best of 3: 46.3 µs per loop
10000 loops, best of 3: 154 µs per loop
10000 loops, best of 3: 154 µs per loop
MichaelKaisers
fuente
19
Esta es realmente la mejor respuesta, suponiendo que la matriz esté ordenada (que en realidad no se especifica en la pregunta). Puede evitar lo incómodo +1connp.searchsorted(..., side='right')
askewchan
3
Creo que el sideargumento solo hace una diferencia si hay valores repetidos en la matriz ordenada. No cambia el significado del índice devuelto, que siempre es el índice en el que puede insertar el valor de la consulta, desplazando todas las siguientes entradas a la derecha y mantiene una matriz ordenada.
Gus
@Gus, sidetiene un efecto cuando el mismo valor está tanto en la matriz ordenada como en la insertada, independientemente de los valores repetidos en ambas. Los valores repetidos en la matriz ordenada simplemente exageran el efecto (la diferencia entre los lados es la cantidad de veces que el valor que se inserta aparece en la matriz ordenada). side no cambiar el significado del índice de regresar, a pesar de que no cambia la matriz resultante de la inserción de los valores en la matriz ordenada en esos índices. Una distinción sutil pero importante; De hecho, esta respuesta da el índice incorrecto si N/2no está en aa.
askewchan
Como se insinuó en el comentario anterior, esta respuesta está desactivada por uno si N/2no está en aa. La forma correcta sería np.searchsorted(aa, N/2, side='right')(sin el +1). Ambas formas dan el mismo índice de lo contrario. Considere el caso de prueba de Nser impar (y N/2.0forzar la flotación si usa Python 2).
askewchan
21

También estaba interesado en esto y comparé todas las respuestas sugeridas con perfplot . (Descargo de responsabilidad: soy el autor de perfplot).

Si sabe que la matriz que está buscando ya está ordenada , entonces

numpy.searchsorted(a, alpha)

es para ti. Es una operación de tiempo constante, es decir, la velocidad no depende del tamaño de la matriz. Tú no puedes ser más rápido que eso.

Si no sabes nada sobre tu matriz, no te equivocarás con

numpy.argmax(a > alpha)

Ya ordenado:

ingrese la descripción de la imagen aquí

Sin clasificar:

ingrese la descripción de la imagen aquí

Código para reproducir la trama:

import numpy
import perfplot


alpha = 0.5

def argmax(data):
    return numpy.argmax(data > alpha)

def where(data):
    return numpy.where(data > alpha)[0][0]

def nonzero(data):
    return numpy.nonzero(data > alpha)[0][0]

def searchsorted(data):
    return numpy.searchsorted(data, alpha)

out = perfplot.show(
    # setup=numpy.random.rand,
    setup=lambda n: numpy.sort(numpy.random.rand(n)),
    kernels=[
        argmax, where,
        nonzero,
        searchsorted
        ],
    n_range=[2**k for k in range(2, 20)],
    logx=True,
    logy=True,
    xlabel='len(array)'
    )
Nico Schlömer
fuente
44
np.searchsortedNo es tiempo constante. En realidad es O(log(n)). Pero su caso de prueba realmente compara el mejor de los casos searchsorted(que es O(1)).
MSeifert
@MSeifert ¿Qué tipo de matriz de entrada / alfa necesita para ver O (log (n))?
Nico Schlömer
1
Obtener el elemento en el índice sqrt (longitud) condujo a un rendimiento muy malo. También escribí una respuesta aquí que incluye ese punto de referencia.
MSeifert
Dudo searchsorted(o cualquier algoritmo) puede superar la O(log(n))búsqueda binaria de datos ordenados distribuidos uniformemente. EDITAR: searchsorted es una búsqueda binaria.
Mateen Ulhaq
16
In [34]: a=np.arange(-10,10)

In [35]: a
Out[35]:
array([-10,  -9,  -8,  -7,  -6,  -5,  -4,  -3,  -2,  -1,   0,   1,   2,
         3,   4,   5,   6,   7,   8,   9])

In [36]: np.where(a>5)
Out[36]: (array([16, 17, 18, 19]),)

In [37]: np.where(a>5)[0][0]
Out[37]: 16
Moj
fuente
8

Matrices que tienen un paso constante entre elementos

En el caso de una rangeo cualquier otra matriz que aumente linealmente, simplemente puede calcular el índice mediante programación, sin necesidad de iterar sobre la matriz:

def first_index_calculate_range_like(val, arr):
    if len(arr) == 0:
        raise ValueError('no value greater than {}'.format(val))
    elif len(arr) == 1:
        if arr[0] > val:
            return 0
        else:
            raise ValueError('no value greater than {}'.format(val))

    first_value = arr[0]
    step = arr[1] - first_value
    # For linearly decreasing arrays or constant arrays we only need to check
    # the first element, because if that does not satisfy the condition
    # no other element will.
    if step <= 0:
        if first_value > val:
            return 0
        else:
            raise ValueError('no value greater than {}'.format(val))

    calculated_position = (val - first_value) / step

    if calculated_position < 0:
        return 0
    elif calculated_position > len(arr) - 1:
        raise ValueError('no value greater than {}'.format(val))

    return int(calculated_position) + 1

Uno probablemente podría mejorar eso un poco. Me he asegurado de que funcione correctamente para algunas matrices y valores de muestra, pero eso no significa que no pueda haber errores allí, especialmente teniendo en cuenta que usa flotantes ...

>>> import numpy as np
>>> first_index_calculate_range_like(5, np.arange(-10, 10))
16
>>> np.arange(-10, 10)[16]  # double check
6

>>> first_index_calculate_range_like(4.8, np.arange(-10, 10))
15

Dado que puede calcular la posición sin ninguna iteración, será un tiempo constante ( O(1)) y probablemente pueda vencer a todos los otros enfoques mencionados. Sin embargo, requiere un paso constante en la matriz, de lo contrario producirá resultados incorrectos.

Solución general usando numba

Un enfoque más general sería usar una función numba:

@nb.njit
def first_index_numba(val, arr):
    for idx in range(len(arr)):
        if arr[idx] > val:
            return idx
    return -1

Eso funcionará para cualquier matriz, pero tiene que iterar sobre la matriz, por lo que en el caso promedio será O(n):

>>> first_index_numba(4.8, np.arange(-10, 10))
15
>>> first_index_numba(5, np.arange(-10, 10))
16

Punto de referencia

Aunque Nico Schlömer ya proporcionó algunos puntos de referencia, pensé que podría ser útil incluir mis nuevas soluciones y probar diferentes "valores".

La configuración de prueba:

import numpy as np
import math
import numba as nb

def first_index_using_argmax(val, arr):
    return np.argmax(arr > val)

def first_index_using_where(val, arr):
    return np.where(arr > val)[0][0]

def first_index_using_nonzero(val, arr):
    return np.nonzero(arr > val)[0][0]

def first_index_using_searchsorted(val, arr):
    return np.searchsorted(arr, val) + 1

def first_index_using_min(val, arr):
    return np.min(np.where(arr > val))

def first_index_calculate_range_like(val, arr):
    if len(arr) == 0:
        raise ValueError('empty array')
    elif len(arr) == 1:
        if arr[0] > val:
            return 0
        else:
            raise ValueError('no value greater than {}'.format(val))

    first_value = arr[0]
    step = arr[1] - first_value
    if step <= 0:
        if first_value > val:
            return 0
        else:
            raise ValueError('no value greater than {}'.format(val))

    calculated_position = (val - first_value) / step

    if calculated_position < 0:
        return 0
    elif calculated_position > len(arr) - 1:
        raise ValueError('no value greater than {}'.format(val))

    return int(calculated_position) + 1

@nb.njit
def first_index_numba(val, arr):
    for idx in range(len(arr)):
        if arr[idx] > val:
            return idx
    return -1

funcs = [
    first_index_using_argmax, 
    first_index_using_min, 
    first_index_using_nonzero,
    first_index_calculate_range_like, 
    first_index_numba, 
    first_index_using_searchsorted, 
    first_index_using_where
]

from simple_benchmark import benchmark, MultiArgument

y las parcelas se generaron usando:

%matplotlib notebook
b.plot()

el artículo está al principio

b = benchmark(
    funcs,
    {2**i: MultiArgument([0, np.arange(2**i)]) for i in range(2, 20)},
    argument_name="array size")

ingrese la descripción de la imagen aquí

La función numba funciona mejor seguida de la función de cálculo y la función de clasificación de búsqueda. Las otras soluciones funcionan mucho peor.

el artículo está al final

b = benchmark(
    funcs,
    {2**i: MultiArgument([2**i-2, np.arange(2**i)]) for i in range(2, 20)},
    argument_name="array size")

ingrese la descripción de la imagen aquí

Para las matrices pequeñas, la función numba funciona increíblemente rápido, sin embargo, para las matrices más grandes, la función de cálculo y la función ordenada de búsqueda la superan.

el artículo está en sqrt (len)

b = benchmark(
    funcs,
    {2**i: MultiArgument([np.sqrt(2**i), np.arange(2**i)]) for i in range(2, 20)},
    argument_name="array size")

ingrese la descripción de la imagen aquí

Esto es mas interesante. Nuevamente, numba y la función de cálculo funcionan muy bien, sin embargo, esto en realidad está desencadenando el peor caso de búsqueda ordenada, que realmente no funciona bien en este caso.

Comparación de las funciones cuando ningún valor satisface la condición

Otro punto interesante es cómo se comportan estas funciones si no hay ningún valor cuyo índice deba devolverse:

arr = np.ones(100)
value = 2

for func in funcs:
    print(func.__name__)
    try:
        print('-->', func(value, arr))
    except Exception as e:
        print('-->', e)

Con este resultado:

first_index_using_argmax
--> 0
first_index_using_min
--> zero-size array to reduction operation minimum which has no identity
first_index_using_nonzero
--> index 0 is out of bounds for axis 0 with size 0
first_index_calculate_range_like
--> no value greater than 2
first_index_numba
--> -1
first_index_using_searchsorted
--> 101
first_index_using_where
--> index 0 is out of bounds for axis 0 with size 0

Searchsorted, argmax y numba simplemente devuelven un valor incorrecto. Sin embargo searchsortedynumba devolver un índice que no es un índice válido para la matriz.

Las funciones where, min, nonzeroy calculatelanzan una excepción. Sin embargo, solo la excepción paracalculate realmente dice algo útil.

Eso significa que uno realmente tiene que ajustar estas llamadas en una función de contenedor apropiada que capture excepciones o valores de retorno no válidos y manejar adecuadamente, al menos si no está seguro de si el valor podría estar en la matriz.


Nota: El cálculo y las searchsortedopciones solo funcionan en condiciones especiales. La función "calcular" requiere un paso constante y la búsqueda ordenada requiere que se ordene la matriz. Por lo tanto, estos podrían ser útiles en las circunstancias correctas, pero no son soluciones generales para este problema. En caso de que estés lidiando con ordenados listas de Python es posible que desee echar un vistazo a la bisect módulo en lugar de utilizar Numpys searchsorted.

MSeifert
fuente
3

Me gustaría proponer

np.min(np.append(np.where(aa>5)[0],np.inf))

Esto devolverá el índice más pequeño donde se cumple la condición, mientras que devuelve el infinito si la condición nunca se cumple (y wheredevuelve una matriz vacía).

mfeldt
fuente
1

Yo iria con

i = np.min(np.where(V >= x))

donde Ves vector (matriz 1d), xes el valor y ies el índice resultante.

sivic
fuente