Сдвиг элементов в массиве numpy


84

Продолжая этот вопрос много лет назад, есть ли в numpy каноническая функция «сдвига»? Я ничего не вижу в документации .

Вот простая версия того, что я ищу:

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
Zero

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_попадает ли он.
Zero

Если скорость вызывает беспокойство, размер массива играет огромную роль для лучшего алгоритма (добавлено сравнение тестов ниже). Кроме того, в настоящее время 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 shift ДЕЙСТВИТЕЛЬНО медленная. Я накатил свой, используя np.concatenate, и это было намного быстрее.
gaefan

12
numpy.roll быстрее. панды тоже его используют. github.com/pandas-dev/pandas/blob/v0.19.2/pandas/core/…
fx-

Просто протестировал 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
Это должно быть выбрано в качестве ответа
Викс,

Комментарий @avysk очень важен - обновите метод shift5. Функции, которые иногда возвращают копию, а иногда и ссылку, - это путь в ад.
David

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 вызова функций, в то время как shift214 вызовов выполняются при положительном arr и 16 при отрицательном. Я буду экспериментировать со временем, чтобы увидеть, как каждый работает с реальными данными.


1
Эй, спасибо, что взглянули на это. Я знаю о np.roll(); Я использовал технику в ссылках в моем вопросе. Что касается вашей реализации, есть ли шанс, что вы сможете заставить свою функцию работать при отрицательных значениях сдвига?
Крисэйкок

Интересно, np.concatenate()что это намного быстрее, чем np.r_[]. В np.roll()конце концов, первое - это то , что использует.
Крисайкок,

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

Отлично, многие люди используют pandas вместе с numpy, и это очень полезно!
VanDavv

6

Тесты и знакомство с Numba

1. Резюме

  • Принятый ответ ( scipy.ndimage.interpolation.shift) - самое медленное решение, указанное на этой странице.
  • Нумба (@ numba.njit) дает некоторый прирост производительности, когда размер массива меньше ~ 25000
  • «Любой метод» одинаково хорош при большом размере массива (> 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по Крисэйкоку
  • 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]берет элементы, которые соответствуют размеру исходного массива. Игра с параметрами 1and :-1as может дать вам сдвиги в разных направлениях.


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