Как векторизовать цикл через ряд панд, когда значения используются в срезе другого ряда - PullRequest
7 голосов
/ 12 апреля 2019

Предположим, у меня есть две серии временных меток, которые представляют собой пары времени начала / окончания для различных 5-часовых диапазонов.Они не обязательно являются последовательными и не квантируются по часам.

import pandas as pd

start = pd.Series(pd.date_range('20190412',freq='H',periods=25))

# Drop a few indexes to make the series not sequential
start.drop([4,5,10,14]).reset_index(drop=True,inplace=True)

# Add some random minutes to the start as it's not necessarily quantized
start = start + pd.to_timedelta(np.random.randint(59,size=len(start)),unit='T')

end = start + pd.Timedelta('5H')

Теперь предположим, что у нас есть некоторые данные, которые помечены метками времени в минутах в диапазоне, охватывающем все пары начала / конца.

data_series = pd.Series(data=np.random.randint(20, size=(75*60)), 
                        index=pd.date_range('20190411',freq='T',periods=(75*60)))

Мы хотим получить значения из data_series в диапазоне каждого времени start и end.Это можно сделать наивно в цикле

frm = []
for s,e in zip(start,end):
    frm.append(data_series.loc[s:e].values)

Как мы видим, этот наивный подход зацикливается на каждую пару start и end дат, получает значения из данных.

Однако эта реализация медленная, если len(start) большое.Есть ли способ выполнить такую ​​логику, используя pandas векторные функции?

Я чувствую, что это почти то же самое, что я хочу применить .loc с вектором или pd.Series вместо одного pd.Timestamp?

РЕДАКТИРОВАТЬ

Использование .apply не более / незначительно более эффективно, чем использование наивного цикла for.Я надеялся, что меня укажут в направлении чисто векторного решения

Ответы [ 3 ]

4 голосов
/ 12 апреля 2019

Основная идея

Как обычно панды будут тратить время на поиск этого конкретного индекса в data_series.loc[s:e], где s и e - индексы даты и времени. Это дорого обходится в цикле, и это именно то, что мы могли бы улучшить Мы нашли бы все эти индексы в векторизованном виде с searchsorted Затем мы извлекаем значения из data_series в виде массива и используем те индексы, полученные из searchsorted, с простой целочисленной индексацией. Таким образом, был бы цикл с минимальной работой простого вырезания массива.

Общая мантра - Большинство работает с предварительной обработкой в ​​векторизованном виде и минимально при зацикливании.

Реализация будет выглядеть примерно так -

def select_slices_by_index(data_series, start, end):
    idx = data_series.index.values
    S = np.searchsorted(idx,start.values)
    E = np.searchsorted(idx,end.values)
    ar = data_series.values
    return [ar[i:j] for (i,j) in zip(S,E+1)]

Использование NumPy-striding

Для конкретного случая, когда период времени между starts и ends одинаков для всех записей, и все срезы охватываются этой длиной, то есть нет случаев за пределами допустимого диапазона, мы можем использовать NumPy's sliding window trick.

Мы можем использовать np.lib.stride_tricks.as_strided на основе scikit-image's view_as_windows, чтобы получить раздвижные окна. Подробнее об использовании as_strided на основе view_as_windows.

from skimage.util.shape import view_as_windows

def select_slices_by_index_strided(data_series, start, end):
    idx = data_series.index.values
    L = np.searchsorted(idx,end.values[0])-np.searchsorted(idx,start.values[0])+1
    S = np.searchsorted(idx,start.values)
    ar = data_series.values
    w = view_as_windows(ar,L)
    return w[S]

Используйте this post, если у вас нет доступа к scikit-image.


Бенчмаркинг

Давайте увеличим все на 100x на данных образца и протестируем.

Настройка -

np.random.seed(0)
start = pd.Series(pd.date_range('20190412',freq='H',periods=2500))

# Drop a few indexes to make the series not sequential
start.drop([4,5,10,14]).reset_index(drop=True,inplace=True)

# Add some random minutes to the start as it's not necessarily quantized
start = start + pd.to_timedelta(np.random.randint(59,size=len(start)),unit='T')

end = start + pd.Timedelta('5H')
data_series = pd.Series(data=np.random.randint(20, size=(750*600)), 
                        index=pd.date_range('20190411',freq='T',periods=(750*600)))

Сроки -

In [156]: %%timeit
     ...: frm = []
     ...: for s,e in zip(start,end):
     ...:     frm.append(data_series.loc[s:e].values)
1 loop, best of 3: 172 ms per loop

In [157]: %timeit select_slices_by_index(data_series, start, end)
1000 loops, best of 3: 1.23 ms per loop

In [158]: %timeit select_slices_by_index_strided(data_series, start, end)
1000 loops, best of 3: 994 µs per loop

In [161]: frm = []
     ...: for s,e in zip(start,end):
     ...:     frm.append(data_series.loc[s:e].values)

In [162]: np.allclose(select_slices_by_index(data_series, start, end),frm)
Out[162]: True

In [163]: np.allclose(select_slices_by_index_strided(data_series, start, end),frm)
Out[163]: True

140x+ и 170x ускорений с этими!

1 голос
/ 12 апреля 2019

Вы можете найти индексы, где элементы start и end находятся в data_series, используя index.get_loc

ind_start = [data_series.index.get_loc(i) for i in start]
ind_end = [data_series.index.get_loc(i) for i in end]

Затем с помощью np.take_along_axis и np.r_ для формирования среза.

frm = [np.take_along_axis(data_series.values, np.r_[s,e],axis=0) for s,e in zip(ind_start,ind_end)]

с использованием %timeit

%timeit [np.take_along_axis(data_series.values, np.r_[s,e],axis=0) for s,e in zip(ind_start,ind_end)]
425 µs ± 4.28 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Сравнение с методом for с использованием .loc

def timeme(start,end):
    frm = []
    for s,e in zip(start,end):
        frm.append(data_series.loc[s:e].values)

%timeit timeme(start,end)
2.99 ms ± 65.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1 голос
/ 12 апреля 2019

Вы можете воспользоваться функцией применения, если переместите серию в Dataframe:

pdf = pd.DataFrame({'s': start,'e':end})
pdf.apply(lambda x: data_series.loc[x['s']:x['e']].values, axis=1)

Dask может помочь вам распараллелить это вычисление для больших объемов данных.

http://docs.dask.org/en/latest/dataframe-api.html#dask.dataframe.DataFrame.apply https://github.com/dask/dask

...