Przesuwaj elementy w tablicy numpy


84

W następstwie tego pytania sprzed lat, czy istnieje kanoniczna funkcja „przesunięcia” w numpy? Nie widzę nic z dokumentacji .

Oto prosta wersja tego, czego szukam:

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)]

Używanie tego jest jak:

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])

To pytanie zrodziło się z mojej wczorajszej próby napisania produktu typu fast rolling_product . Potrzebowałem sposobu na „przesunięcie” skumulowanego produktu i jedyne, o czym mogłem myśleć, to powielenie logiki np.roll().


Więc np.concatenate()jest znacznie szybszy niż np.r_[]. Ta wersja funkcji działa znacznie lepiej:

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)))

Jeszcze szybsza wersja po prostu wstępnie alokuje tablicę:

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

zastanawiam się, czy np.r_[np.full(n, np.nan), xs[:-n]]można by go wymienić na np.r_[[np.nan]*n, xs[:-n]]podobnie w innym stanie, bez potrzebynp.full
Zero

2
@JohnGalt [np.nan]*nto zwykły Python i dlatego będzie wolniejszy niż np.full(n, np.nan). Nie dla małych n, ale zostanie przekształcony w tablicę numpy przez np.r_, co odbiera przewagę.
swenzel

@swenzel Po prostu zmierzył czas i [np.nan]*njest szybszy niż w np.full(n, np.nan)przypadku n=[10,1000,10000]. Muszę sprawdzić, czy np.r_trafia.
Zero

Jeśli chodzi o szybkość, rozmiar tablicy odgrywa ogromną rolę dla najlepszego algorytmu (dodano porównanie wzorcowe poniżej). Ponadto w dzisiejszych czasach numba.njit może być używany do przyspieszenia zmiany, jeśli jest wywoływany wielokrotnie.
np8

Odpowiedzi:


101

Nie numpy, ale scipy zapewnia dokładnie taką funkcjonalność zmiany, jaką chcesz,

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)

gdzie domyślnie jest wprowadzana stała wartość spoza tablicy z wartością cval, ustawioną tutaj na nan. Daje to pożądaną wydajność,

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

a ujemne przesunięcie działa podobnie,

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

Zapewnia wyjście

array([  3.,   4.,   5.,   6.,   7.,   8.,   9.,  nan,  nan,  nan])

23
Funkcja Scipy Shift jest NAPRAWDĘ powolna. Sam wywaliłem używając np.concatenate i było znacznie szybciej.
gaefan

12
numpy.roll jest szybszy. pandy też go używają. github.com/pandas-dev/pandas/blob/v0.19.2/pandas/core/…
fx-kirin

Właśnie przetestowałem scipy.ndimage.interpolation.shift (scipy 1.4.1) ze wszystkimi innymi alternatywami wymienionymi na tej stronie (zobacz moją odpowiedź poniżej) i jest to najwolniejsze możliwe rozwiązanie. Używaj tylko wtedy, gdy prędkość nie ma znaczenia w twojej aplikacji.
np8

72

Dla tych, którzy chcą po prostu skopiować i wkleić najszybszą implementację zmiany, jest punkt odniesienia i wniosek (patrz koniec). Dodatkowo wprowadzam parametr fill_value i poprawiam kilka błędów.

Reper

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)

wynik testu porównawczego:

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

Wniosek

shift5 jest zwycięzcą! To trzecie rozwiązanie OP.


Dzięki za porównania. Masz pomysł, jaki jest najszybszy sposób na zrobienie tego bez użycia nowej tablicy?
FiReTiTi

2
W ostatniej klauzuli shift5lepiej jest pisać result[:] = arrzamiast pisać result = arr, aby zachować spójność zachowania funkcji.
avysk

2
To powinno być wybrane jako odpowiedź
wyx

Komentarz @avysk jest dość ważny - zaktualizuj metodę shift5. Funkcje, które czasami zwracają kopię, a czasami zwracają odniesienie, są ścieżką do piekła.
David

2
@ Josmoor98 To dlatego type(np.NAN) is float. Jeśli przesuniesz tablicę liczb całkowitych za pomocą tych funkcji, musisz określić liczbę całkowitą wartość_pełnienia.
gzc

9

Nie ma jednej funkcji, która robi to, co chcesz. Twoja definicja zmiany jest nieco inna niż to, co robi większość ludzi. Sposoby przesuwania tablicy są częściej zapętlone:

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

Możesz jednak robić, co chcesz, dzięki dwóm funkcjom.
Rozważ 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]

Po uruchomieniu cProfile na podanej funkcji i powyższym kodzie, który podałeś, stwierdziłem, że podany kod wykonuje 42 wywołania funkcji podczas shift2wykonywania 14 wywołań, gdy arr jest dodatni i 16, gdy jest ujemny. Będę eksperymentować z synchronizacją, aby zobaczyć, jak każdy radzi sobie z rzeczywistymi danymi.


1
Hej, dzięki za przyjrzenie się temu. Wiem o np.roll(); Użyłem tej techniki w linkach w moim pytaniu. Jeśli chodzi o Twoją implementację, czy jest szansa, że ​​Twoja funkcja będzie działać dla ujemnych wartości przesunięcia?
chrisaycock

Co ciekawe, np.concatenate()jest dużo szybszy niż np.r_[]. W np.roll()końcu to pierwsze jest tym , czego używa.
chrisaycock

6

Możesz najpierw przekonwertować ndarrayna Serieslub DataFramez pandas, a następnie możesz użyć shiftmetody, jak chcesz.

Przykład:

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.])

Świetnie, wiele osób używa pand razem z numpy, a to jest bardzo pomocne!
VanDavv

6

Benchmarki i wprowadzenie Numba

1. Podsumowanie

  • Zaakceptowana odpowiedź ( scipy.ndimage.interpolation.shift) jest najwolniejszym rozwiązaniem wymienionym na tej stronie.
  • Numba (@ numba.njit) daje pewien wzrost wydajności, gdy rozmiar tablicy jest mniejszy niż ~ 25.000
  • „Dowolna metoda” jest równie dobra, gdy rozmiar tablicy jest duży (> 250 000).
  • Najszybsza opcja naprawdę zależy od
        (1) długości twoich tablic
        (2) ilości zmiany, którą musisz zrobić.
  • Poniżej znajduje się obraz czasów wszystkich różnych metod wymienionych na tej stronie (2020-07-11), przy użyciu stałego przesunięcia = 10. Jak widać, przy małych rozmiarach tablic niektóre metody używają więcej niż + 2000% czasu niż najlepsza metoda.

Względne czasy, stałe przesunięcie (10), wszystkie metody

2. Szczegółowe testy porównawcze z najlepszymi opcjami

  • Wybierz shift4_numba(zdefiniowane poniżej), jeśli chcesz mieć dobry, wszechstronny

Względne czasy, najlepsze metody (testy porównawcze)

3. Kod

3.1 shift4_numba

  • Dobry wszechstronny; max 20% wrt. do najlepszej metody z dowolnym rozmiarem tablicy
  • Najlepsza metoda dla średnich rozmiarów macierzy: ~ 500 <N <20 000.
  • Uwaga: Numba jit (kompilator just in time) zwiększy wydajność tylko wtedy, gdy wywołujesz dekorowaną funkcję więcej niż raz. Pierwsza rozmowa trwa zwykle 3-4 razy dłużej niż kolejne.
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

  • Najlepsza opcja w przypadku małych (N <= 300… 1500) rozmiarów tablic. Próg zależy od potrzebnej ilości zmiany.
  • Dobra wydajność na dowolnej wielkości tablicy; max + 50% w porównaniu do najszybszego rozwiązania.
  • Uwaga: Numba jit (kompilator just in time) zwiększy wydajność tylko wtedy, gdy wywołujesz dekorowaną funkcję więcej niż raz. Pierwsza rozmowa trwa zwykle 3-4 razy dłużej niż kolejne.
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

  • Najlepsza metoda z rozmiarami tablic ~ 20 000 <N <250 000
  • To samo co shift5_numba, po prostu usuń dekorator @ numba.njit.

4 Dodatek

4.1 Szczegóły dotyczące zastosowanych metod

  • shift_scipy: scipy.ndimage.interpolation.shift(scipy 1.4.1) - Opcja z zaakceptowanej odpowiedzi, która jest zdecydowanie najwolniejszą alternatywą .
  • shift1: np.rolloraz out[:num] xnp.nanprzez IronManMark20 i gzc
  • shift2: np.rolli np.putprzez IronManMark20
  • shift3: np.padi sliceprzez gzc
  • shift4: np.concatenatei np.fullprzez chrisaycock
  • shift5: używając dwa razy result[slice] = xprzez chrisaycock
  • shift#_numba: @ numba .njit zdobione wersje poprzedniego.

shift2I shift3zawarte funkcje, które nie były obsługiwane przez obecną Numba (0.50.1).

4.2 Inne wyniki testów

4.2.1 Względne czasy, wszystkie metody

4.2.2 Surowe czasy, wszystkie metody

4.2.3 Surowe czasy, kilka najlepszych metod


4

Możesz to również zrobić z Pandami:

Korzystanie z tablicy o długości 2356:

import numpy as np

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

Korzystanie z 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)

Korzystanie z Pand:

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)

W tym przykładzie użycie Pand było około ~ 8 razy szybsze niż Scipy


2
Najszybszą metodą jest wstępna alokacja, którą zamieściłem na końcu mojego pytania. Twoja Seriestechnika zajęła 146 nas na moim komputerze, podczas gdy moje podejście zajęło mniej niż 4 nas.
chrisaycock

0

Jeśli chcesz mieć jedną linijkę od numpy i nie przejmujesz się zbytnio wydajnością, spróbuj:

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

Wyjaśnienie: np.diag(the_array,1)tworzy macierz z tablicą równą przekątnej, np.sum(...,0)sumuje macierz według kolumn i ...[:-1]pobiera elementy, które odpowiadałyby rozmiarowi oryginalnej tablicy. Zabawa z parametrami 1i :-1as może spowodować przesunięcia w różnych kierunkach.


-2

Jednym ze sposobów na zrobienie tego bez rozlewania kodu na skrzynki

z tablicą:

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

z matrycą można to zrobić w następujący sposób:

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

To nie jest ani czyste, ani szybkie.
chrisaycock
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.