Cambiar elementos en una matriz numpy

83

Siguiendo con esta pregunta hace años, ¿existe una función canónica de "cambio" en numpy? No veo nada en la documentación .

Aquí hay una versión simple de lo que estoy buscando:

def shift(xs, n):
    if n >= 0:
        return np.r_[np.full(n, np.nan), xs[:-n]]
    else:
        return np.r_[xs[-n:], np.full(-n, np.nan)]

Usar esto es como:

In [76]: xs
Out[76]: array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9.])

In [77]: shift(xs, 3)
Out[77]: array([ nan,  nan,  nan,   0.,   1.,   2.,   3.,   4.,   5.,   6.])

In [78]: shift(xs, -3)
Out[78]: array([  3.,   4.,   5.,   6.,   7.,   8.,   9.,  nan,  nan,  nan])

Esta pregunta vino de mi intento de escribir un producto rodante rápido ayer. Necesitaba una forma de "cambiar" un producto acumulativo y todo lo que podía pensar era replicar la lógica np.roll().


Entonces np.concatenate()es mucho más rápido que np.r_[]. Esta versión de la función funciona mucho mejor:

def shift(xs, n):
    if n >= 0:
        return np.concatenate((np.full(n, np.nan), xs[:-n]))
    else:
        return np.concatenate((xs[-n:], np.full(-n, np.nan)))

Una versión aún más rápida simplemente preasigna la matriz:

def shift(xs, n):
    e = np.empty_like(xs)
    if n >= 0:
        e[:n] = np.nan
        e[n:] = xs[:-n]
    else:
        e[n:] = np.nan
        e[:n] = xs[-n:]
    return e
Chrisaycock
fuente
preguntándose si np.r_[np.full(n, np.nan), xs[:-n]]podría reemplazarse con lo np.r_[[np.nan]*n, xs[:-n]]mismo para otra condición, sin la necesidad denp.full
Cero
2
@JohnGalt [np.nan]*nes python simple y, por lo tanto, será más lento que np.full(n, np.nan). No para pequeños n, pero np.r_ lo transformará en una matriz numpy, lo que le quita la ventaja.
swenzel
@swenzel Solo lo cronometró y [np.nan]*nes más rápido que np.full(n, np.nan)para n=[10,1000,10000]. Necesito comprobar si np.r_recibe un golpe.
Cero
Si le preocupa la velocidad, el tamaño de la matriz juega un papel muy importante para el mejor algoritmo (se agregó una comparación de referencia a continuación). Además, hoy en día numba.njit se puede usar para hacer el cambio más rápido si se llama repetidamente.
np8

Respuestas:

100

No numpy pero scipy proporciona exactamente la funcionalidad de cambio que desea,

import numpy as np
from scipy.ndimage.interpolation import shift

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

shift(xs, 3, cval=np.NaN)

donde el valor predeterminado es traer un valor constante desde fuera de la matriz con valor cval, establecido aquí en nan. Esto da la salida deseada,

array([ nan, nan, nan, 0., 1., 2., 3., 4., 5., 6.])

y el cambio negativo funciona de manera similar,

shift(xs, -3, cval=np.NaN)

Proporciona salida

array([  3.,   4.,   5.,   6.,   7.,   8.,   9.,  nan,  nan,  nan])
Ed Smith
fuente
23
La función de cambio scipy es REALMENTE lenta. Hice la mía usando np.concatenate y fue mucho más rápido.
gaefan
12
numpy.roll es más rápido. los pandas también lo usan. github.com/pandas-dev/pandas/blob/v0.19.2/pandas/core/…
fx-kirin
Acabo de probar scipy.ndimage.interpolation.shift (scipy 1.4.1) contra todas las otras alternativas enumeradas en esta página (vea mi respuesta a continuación), y esta es la solución más lenta posible. Úselo solo si la velocidad no es de importancia en su aplicación.
np8
71

Para aquellos que solo quieren copiar y pegar la implementación más rápida de shift, hay un punto de referencia y una conclusión (ver el final). Además, introduzco el parámetro fill_value y soluciono algunos errores.

Punto de referencia

import numpy as np
import timeit

# enhanced from IronManMark20 version
def shift1(arr, num, fill_value=np.nan):
    arr = np.roll(arr,num)
    if num < 0:
        arr[num:] = fill_value
    elif num > 0:
        arr[:num] = fill_value
    return arr

# use np.roll and np.put by IronManMark20
def shift2(arr,num):
    arr=np.roll(arr,num)
    if num<0:
         np.put(arr,range(len(arr)+num,len(arr)),np.nan)
    elif num > 0:
         np.put(arr,range(num),np.nan)
    return arr

# use np.pad and slice by me.
def shift3(arr, num, fill_value=np.nan):
    l = len(arr)
    if num < 0:
        arr = np.pad(arr, (0, abs(num)), mode='constant', constant_values=(fill_value,))[:-num]
    elif num > 0:
        arr = np.pad(arr, (num, 0), mode='constant', constant_values=(fill_value,))[:-num]

    return arr

# use np.concatenate and np.full by chrisaycock
def shift4(arr, num, fill_value=np.nan):
    if num >= 0:
        return np.concatenate((np.full(num, fill_value), arr[:-num]))
    else:
        return np.concatenate((arr[-num:], np.full(-num, fill_value)))

# preallocate empty array and assign slice by chrisaycock
def shift5(arr, num, fill_value=np.nan):
    result = np.empty_like(arr)
    if num > 0:
        result[:num] = fill_value
        result[num:] = arr[:-num]
    elif num < 0:
        result[num:] = fill_value
        result[:num] = arr[-num:]
    else:
        result[:] = arr
    return result

arr = np.arange(2000).astype(float)

def benchmark_shift1():
    shift1(arr, 3)

def benchmark_shift2():
    shift2(arr, 3)

def benchmark_shift3():
    shift3(arr, 3)

def benchmark_shift4():
    shift4(arr, 3)

def benchmark_shift5():
    shift5(arr, 3)

benchmark_set = ['benchmark_shift1', 'benchmark_shift2', 'benchmark_shift3', 'benchmark_shift4', 'benchmark_shift5']

for x in benchmark_set:
    number = 10000
    t = timeit.timeit('%s()' % x, 'from __main__ import %s' % x, number=number)
    print '%s time: %f' % (x, t)

resultado de referencia:

benchmark_shift1 time: 0.265238
benchmark_shift2 time: 0.285175
benchmark_shift3 time: 0.473890
benchmark_shift4 time: 0.099049
benchmark_shift5 time: 0.052836

Conclusión

shift5 es el ganador! Es la tercera solución de OP.

gzc
fuente
Gracias por las comparaciones. ¿Alguna idea de cuál es la forma más rápida de hacerlo sin usar una nueva matriz?
FiReTiTi
2
En la última cláusula de shift5es mejor escribir en result[:] = arrlugar de result = arr, para mantener el comportamiento de la función consistente.
avysk
2
Esto debería elegirse como una respuesta
wyx
El comentario de @avysk es bastante importante; actualice el método shift5. Las funciones que a veces devuelven una copia y a veces devuelven una referencia son el camino al infierno.
David
2
@ Josmoor98 Eso es porque type(np.NAN) is float. Si cambia la matriz de números enteros con estas funciones, debe especificar un valor de relleno entero.
gzc
9

No existe una función única que haga lo que quieres. Su definición de cambio es ligeramente diferente de lo que hace la mayoría de la gente. Las formas de cambiar una matriz son más comúnmente en bucle:

>>>xs=np.array([1,2,3,4,5])
>>>shift(xs,3)
array([3,4,5,1,2])

Sin embargo, puede hacer lo que quiera con dos funciones.
Considere a=np.array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.]):

def shift2(arr,num):
    arr=np.roll(arr,num)
    if num<0:
         np.put(arr,range(len(arr)+num,len(arr)),np.nan)
    elif num > 0:
         np.put(arr,range(num),np.nan)
    return arr
>>>shift2(a,3)
[ nan  nan  nan   0.   1.   2.   3.   4.   5.   6.]
>>>shift2(a,-3)
[  3.   4.   5.   6.   7.   8.   9.  nan  nan  nan]

Después de ejecutar cProfile en su función dada y el código anterior que proporcionó, descubrí que el código que proporcionó hace 42 llamadas a funciones mientras shift2realiza 14 llamadas cuando arr es positivo y 16 cuando es negativo. Experimentaré con el tiempo para ver cómo funciona cada uno con datos reales.

IronManMark20
fuente
1
Oye, gracias por echarle un vistazo a esto. Yo conozco np.roll(); Usé la técnica en los enlaces de mi pregunta. En cuanto a su implementación, ¿hay alguna posibilidad de que pueda hacer que su función funcione para valores de cambio negativos?
chrisaycock
Curiosamente, np.concatenate()es mucho más rápido que np.r_[]. Lo primero es lo que np.roll()usa, después de todo.
Chrisaycock
6

Benchmarks e introducción a Numba

1. Resumen

  • La respuesta aceptada ( scipy.ndimage.interpolation.shift) es la solución más lenta enumerada en esta página.
  • Numba (@ numba.njit) aumenta el rendimiento cuando el tamaño de la matriz es inferior a ~ 25.000
  • "Cualquier método" es igualmente bueno cuando el tamaño de la matriz es grande (> 250.000).
  • La opción más rápida realmente depende de
        (1) la longitud de sus matrices
        (2) la cantidad de turno que necesita hacer.
  • A continuación se muestra la imagen de los tiempos de todos los métodos diferentes enumerados en esta página (2020-07-11), usando un cambio constante = 10. Como se puede ver, con tamaños de matriz pequeños, algunos métodos se usan más de + 2000% de tiempo que el el mejor método.

Tiempos relativos, cambio constante (10), todos los métodos

2. Comparativas detalladas con las mejores opciones

  • Elija shift4_numba(definido a continuación) si quiere un buen todoterreno

Tiempos relativos, mejores métodos (puntos de referencia)

3. Código

3.1 shift4_numba

  • Buen todo terreno; max 20% wrt. al mejor método con cualquier tamaño de matriz
  • El mejor método con tamaños de matriz medianos: ~ 500 <N <20.000.
  • Advertencia: Numba jit (compilador justo a tiempo) aumentará el rendimiento solo si llama a la función decorada más de una vez. La primera llamada suele tardar entre 3 y 4 veces más que las siguientes.
import numba

@numba.njit
def shift4_numba(arr, num, fill_value=np.nan):
    if num >= 0:
        return np.concatenate((np.full(num, fill_value), arr[:-num]))
    else:
        return np.concatenate((arr[-num:], np.full(-num, fill_value)))

3.2. shift5_numba

  • La mejor opción con tamaños de matriz pequeños (N <= 300 .. 1500). El umbral depende de la cantidad de turno necesaria.
  • Buen rendimiento en cualquier tamaño de matriz; max + 50% en comparación con la solución más rápida.
  • Advertencia: Numba jit (compilador justo a tiempo) aumentará el rendimiento solo si llama a la función decorada más de una vez. La primera llamada suele tardar entre 3 y 4 veces más que las siguientes.
import numba

@numba.njit
def shift5_numba(arr, num, fill_value=np.nan):
    result = np.empty_like(arr)
    if num > 0:
        result[:num] = fill_value
        result[num:] = arr[:-num]
    elif num < 0:
        result[num:] = fill_value
        result[:num] = arr[-num:]
    else:
        result[:] = arr
    return result

3.3. shift5

  • Mejor método con tamaños de matriz ~ 20.000 <N <250.000
  • Igual que shift5_numba, simplemente elimine el decorador @ numba.njit.

4 Apéndice

4.1 Detalles sobre los métodos utilizados

  • shift_scipy: scipy.ndimage.interpolation.shift(scipy 1.4.1) - La opción de respuesta aceptada, que es claramente la alternativa más lenta .
  • shift1: np.rolly out[:num] xnp.nanpor IronManMark20 & gzc
  • shift2: np.rolly np.putpor IronManMark20
  • shift3: np.pady slicepor gzc
  • shift4: np.concatenatey np.fullpor chrisaycock
  • shift5: usando dos veces result[slice] = xpor chrisaycock
  • shift#_numba: @ numba .njit versiones decoradas del anterior.

El shift2y shift3funciones contenidas que no fueron apoyados por el numba actual (0.50.1).

4.2 Otros resultados de la prueba

4.2.1 Tiempos relativos, todos los métodos

4.2.2 Tiempos brutos, todos los métodos

4.2.3 Tiempos sin procesar, algunos mejores métodos

np8
fuente
5

Puede convertir ndarraya Serieso DataFramecon la pandasprimera, a continuación, puede utilizar shiftel método que desee.

Ejemplo:

In [1]: from pandas import Series

In [2]: data = np.arange(10)

In [3]: data
Out[3]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [4]: data = Series(data)

In [5]: data
Out[5]: 
0    0
1    1
2    2
3    3
4    4
5    5
6    6
7    7
8    8
9    9
dtype: int64

In [6]: data = data.shift(3)

In [7]: data
Out[7]: 
0    NaN
1    NaN
2    NaN
3    0.0
4    1.0
5    2.0
6    3.0
7    4.0
8    5.0
9    6.0
dtype: float64

In [8]: data = data.values

In [9]: data
Out[9]: array([ nan,  nan,  nan,   0.,   1.,   2.,   3.,   4.,   5.,   6.])
JsonBruce
fuente
Genial, muchas personas usan pandas junto con numpy, ¡y esto es muy útil!
VanDavv
4

También puedes hacer esto con Pandas:

Usando una matriz de 2356 de largo:

import numpy as np

xs = np.array([...])

Usando scipy:

from scipy.ndimage.interpolation import shift

%timeit shift(xs, 1, cval=np.nan)
# 956 µs ± 77.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Usando Pandas:

import pandas as pd

%timeit pd.Series(xs).shift(1).values
# 377 µs ± 9.42 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

En este ejemplo, usar Pandas fue aproximadamente 8 veces más rápido que Scipy

Ran Aroussi
fuente
2
El método más rápido es la asignación previa que publiqué al final de mi pregunta. Tu Seriestécnica nos tomó 146 en mi computadora, mientras que mi enfoque tomó menos de 4.
chrisaycock
0

Si quieres una frase única de numpy y no te preocupa demasiado el rendimiento, prueba:

np.sum(np.diag(the_array,1),0)[:-1]

Explicación: np.diag(the_array,1)crea una matriz con su matriz una sola vez en la diagonal, np.sum(...,0)suma la matriz en columnas y ...[:-1]toma los elementos que corresponderían al tamaño de la matriz original. Jugar con los parámetros 1y :-1as puede darte cambios en diferentes direcciones.

Nathan Chappell
fuente
-2

Una forma de hacerlo sin derramar el código en casos

con matriz:

def shift(arr, dx, default_value):
    result = np.empty_like(arr)
    get_neg_or_none = lambda s: s if s < 0 else None
    get_pos_or_none = lambda s: s if s > 0 else None
    result[get_neg_or_none(dx): get_pos_or_none(dx)] = default_value
    result[get_pos_or_none(dx): get_neg_or_none(dx)] = arr[get_pos_or_none(-dx): get_neg_or_none(-dx)]     
    return result

con matrix se puede hacer así:

def shift(image, dx, dy, default_value):
    res = np.full_like(image, default_value)

    get_neg_or_none = lambda s: s if s < 0 else None
    get_pos_or_none = lambda s : s if s > 0 else None

    res[get_pos_or_none(-dy): get_neg_or_none(-dy), get_pos_or_none(-dx): get_neg_or_none(-dx)] = \
        image[get_pos_or_none(dy): get_neg_or_none(dy), get_pos_or_none(dx): get_neg_or_none(dx)]
    return res
Alon Gweta
fuente
Esto no es ni limpio ni rápido.
chrisaycock