Сдвиг элементов в массиве 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
Chrisaycock
источник
интересно, 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.

GZC
источник
Спасибо за сравнения. Есть идеи, какой самый быстрый способ сделать это без использования нового массива?
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 при отрицательном. Я буду экспериментировать со временем, чтобы увидеть, как каждый работает с реальными данными.

IronManMark20
источник
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.])
JsonBruce
источник
Отлично, многие люди используют 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 Необработанные тайминги, несколько лучших методов

np8
источник
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