Зміщення елементів у масиві numpy


84

Наслідуючи це запитання років тому, чи існує в numpy канонічна функція "shift"? Я нічого не бачу з документації .

Ось проста версія того, що я шукаю:

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

Використання цього типу:

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

Це запитання виникла внаслідок моєї вчорашньої спроби написати швидкий файл rolling_product . Мені потрібен був спосіб "змінити" кумулятивний продукт, і все, що я міг придумати, - це повторити логіку np.roll().


Так np.concatenate()набагато швидше, ніж np.r_[]. Ця версія функції працює набагато краще:

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

Ще швидша версія просто попередньо розподіляє масив:

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

розмірковуючи, чи np.r_[np.full(n, np.nan), xs[:-n]]не можна замінити таким же np.r_[[np.nan]*n, xs[:-n]]чином на інші умови, без необхідностіnp.full
Нуль

2
@JohnGalt [np.nan]*n- це звичайний пітон, і тому він буде повільнішим, ніж np.full(n, np.nan). Не для малого n, але він буде перетворений в масив numpy за допомогою np.r_, що позбавить переваги.
swenzel

@swenzel Просто приурочив це і [np.nan]*nшвидше, ніж np.full(n, np.nan)для n=[10,1000,10000]. Потрібно перевірити, чи np.r_приймає удар.
Нуль

Якщо швидкість викликає занепокоєння, розмір масиву відіграє величезну роль для найкращого алгоритму (додано порівняльне порівняння нижче). Крім того, у наш час numba.njit можна використовувати для швидшого переключення, якщо повторно викликати.
np8

Відповіді:


101

Не numpy, а scipy забезпечує саме ту функцію перемикання, яку ви хочете,

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)

де за замовчуванням вводити постійне значення поза масивом із значенням cval, встановленим тут на nan. Це дає бажаний результат,

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

і негативний зсув працює аналогічно,

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

Забезпечує вихід

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

23
Функція зсуву scipy ДІЙСНО працює повільно. Я прокатував свій власний за допомогою np.concatenate, і це було набагато швидше.
gaefan

12
numpy.roll швидший. pandas також використовує його. github.com/pandas-dev/pandas/blob/v0.19.2/pandas/core/…
fx-kirin

Щойно перевірив scipy.ndimage.interpolation.shift (scipy 1.4.1) щодо всіх інших альтернатив, перелічених на цій сторінці (див. Мою відповідь нижче), і це найвільніше можливе рішення. Використовуйте, лише якщо швидкість не має значення у вашому додатку.
np8

72

Для тих, хто хоче просто скопіювати та вставити найшвидшу реалізацію зміни, є орієнтир та висновок (див. Кінець). Крім того, я вводжу параметр fill_value та виправляю деякі помилки.

Орієнтир

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)

контрольний результат:

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

Висновок

shift5 - переможець! Це третє рішення OP.


Дякую за порівняння. Будь-яка ідея, який найшвидший спосіб це зробити, не використовуючи новий масив?
FiReTiTi

2
В останньому пункті shift5краще писати result[:] = arrзамість result = arr, щоб підтримувати послідовність поведінки функції.
avysk

2
Це слід вибрати як відповідь
wyx

Коментар @avysk досить важливий - оновіть метод shift5. Функції, які іноді повертають копію, а іноді повертають посилання, - шлях до пекла.
Девід

2
@ Josmoor98 Це тому, що type(np.NAN) is float. Якщо ви зміщуєте цілочисельний масив, використовуючи ці функції, вам потрібно вказати ціле число fill_value.
gzc

9

Не існує жодної функції, яка робить те, що ви хочете. Ваше визначення зміни трохи відрізняється від того, що робить більшість людей. Шляхи зсуву масиву частіше циклічні:

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

Однак ви можете робити те, що хочете, за допомогою двох функцій.
Розглянемо 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]

Після запуску cProfile для даної функції та наведеного вище коду, я виявив, що код, який ви надали, робить 42 виклики функції, тоді як shift2зробив 14 викликів, коли arr є позитивним, і 16, коли він є негативним. Я буду експериментувати з хронометражем, щоб побачити, як кожен працює з реальними даними.


1
Гей, дякую, що подивились на це. Я знаю про np.roll(); Я використав техніку в посиланнях у своєму питанні. Що стосується вашої реалізації, будь-який шанс, що ви можете змусити свою функцію працювати при негативних значеннях зсуву?
chrisaycock

Цікаво, що np.concatenate() це набагато швидше, ніж np.r_[]. np.roll()Зрештою, це те, що використовує.
chrisaycock

6

Ви можете конвертувати ndarrayв Seriesабо DataFrameз pandasпершим, потім ви можете використовувати shiftметод, як ви хочете.

Приклад:

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

Чудово, багато людей використовують панди разом з numpy, і це дуже корисно!
VanDavv

6

Тести та введення в Numba

1. Підсумок

  • Прийнятою відповіддю ( scipy.ndimage.interpolation.shift) є найповільніше рішення, перелічене на цій сторінці.
  • Нумба (@ numba.njit) дає певний приріст продуктивності, коли розмір масиву менше ~ 25.000
  • "Будь-який метод" однаково хороший, коли розмір масиву великий (> 250 000).
  • Найшвидший варіант дійсно залежить від
        (1) довжини ваших масивів
        (2) кількості змін, яку вам потрібно зробити.
  • Нижче наведено зображення часу всіх різних методів, перерахованих на цій сторінці (2020-07-11), з використанням постійного зсуву = 10. Як бачимо, при малих розмірах масивів деякі методи використовують більше + 2000% часу, ніж найкращий метод.

Відносні терміни, постійний зсув (10), усі методи

2. Детальні тести з найкращими варіантами

  • Виберіть shift4_numba(визначено нижче), якщо ви хочете мати хороший універсал

Відносні терміни, найкращі методи (Тести)

3. Кодекс

3.1 shift4_numba

  • Хороший універсал; макс. 20% мас. до найкращого методу з будь-яким розміром масиву
  • Найкращий метод із середніми розмірами масиву: ~ 500 <N <20.000.
  • Застереження: Numba jit (саме в час компілятор) підвищить продуктивність, лише якщо ви викликаєте декоровану функцію більше одного разу. Перший дзвінок зазвичай триває в 3-4 рази довше, ніж наступний.
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

  • Найкращий варіант з невеликими (N <= 300 .. 1500) розмірами масиву. Поріг залежить від необхідної кількості змін.
  • Хороша продуктивність на будь-якому розмірі масиву; макс. + 50% порівняно з найшвидшим рішенням.
  • Застереження: Numba jit (саме в час компілятор) підвищить продуктивність, лише якщо ви викликаєте декоровану функцію більше одного разу. Перший дзвінок зазвичай триває в 3-4 рази довше, ніж наступний.
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

  • Найкращий метод з розмірами масиву ~ 20.000 <N <250.000
  • Так само shift5_numba, як просто видаліть декоратор @ numba.njit.

4 Додаток

4.1 Детальна інформація про використовувані методи

  • shift_scipy: scipy.ndimage.interpolation.shift(scipy 1.4.1) - варіант із прийнятої відповіді, який є, очевидно, найповільнішою альтернативою .
  • shift1: np.rollІ out[:num] xnp.nanвід IronManMark20 & gzc
  • shift2: np.rollІ np.putпо IronManMark20
  • shift3: np.padІ sliceпо gzc
  • shift4: np.concatenateІ np.fullпо chrisaycock
  • shift5: Використовуючи два рази result[slice] = xпо chrisaycock
  • shift#_numba: @ numba .njit оформлені версії попереднього.

shift2іshift3 містили функції, які не підтримувалися поточною numba (0.50.1).

4.2 Інші результати випробувань

4.2.1 Відносні терміни, усі методи

4.2.2 Сировинні терміни, усі методи

4.2.3 Сировина часу, кілька найкращих методів


4

Ви також можете зробити це за допомогою Pandas:

Використання масиву довжиною 2356:

import numpy as np

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

Використання 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)

Використання 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)

У цьому прикладі використання Pandas було приблизно в 8 разів швидше, ніж Scipy


2
Найшвидший метод - це попередній розподіл, який я розмістив наприкінці свого запитання. Ваша Seriesтехніка зайняла 146 моїх комп'ютерів, тоді як мій підхід зайняв менше 4 років.
chrisaycock

0

Якщо ви хочете однокласник від numpy і вас не турбує продуктивність, спробуйте:

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

Пояснення: np.diag(the_array,1)створює матрицю з вашим масивом одноразово по діагоналі, np.sum(...,0)підсумовує матрицю по стовпцях і ...[:-1]бере елементи, які відповідали б розміру вихідного масиву. Гра з параметрами 1і :-1як може дати вам зміщення в різних напрямках.


-2

Один із способів зробити це, не розливши код на випадки

з масивом:

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

з матрицею це можна зробити так:

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

Це ні чисто, ні швидко.
chrisaycock
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.