当在另一个系列的切片中使用值时,如何通过 pandas 系列对循环进行矢量化

How to vectorize a loop through pandas series when values are used in slice of another series

假设我有两个时间戳系列,它们是 start/end 对不同 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')

现在假设我们有一些数据按分钟加时间戳,范围涵盖所有 start/end 对。

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

我们希望在每个startend时间范围内获取data_series的值。这可以在循环内天真地完成

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

正如我们所见,这种天真的方法循环遍历每对 startend 日期,从数据中获取值。

但是如果 len(start) 很大,这个实现会很慢。有没有一种方法可以利用 pandas 向量函数来执行这种逻辑?

我觉得这几乎就像我想用矢量或 pd.Series 应用 .loc 而不是单个 pd.Timestamp?

编辑

使用 .apply 并不比使用简单的 for 循环更有效 more/marginally。我希望能指出纯向量解决方案的方向

如果将系列移动到 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

您可以使用 index.get_loc

找到 startend 的元素在 data_series 中的索引
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 and 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)

与使用.loc

的for循环方法比较
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)

基本思路

像往常一样,pandas 会花时间在 data_series.loc[s:e] 处搜索那个特定索引,其中 se 是日期时间索引。这在循环时成本很高,而这正是我们要改进的地方。我们会用 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

对于所有条目的 startsends 之间的时间段相同并且所有切片都被该长度覆盖的特定情况,即没有越界情况,我们可以使用 NumPy's sliding window trick

我们可以利用 np.lib.stride_tricks.as_strided based scikit-image's view_as_windows to get sliding 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]

如果您无权访问 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 使用这些加速!