如何将循环向量化,当值用于另一个系列的切片时,使用pandas系列?

7
假设我有两个时间戳系列,它们是各种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)))

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

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

正如我们所看到的,这种朴素方法循环遍历每对“start”和“end”日期,从数据中获取值。
然而,如果“len(start)”很大,则此实现速度较慢。有没有一种方法可以利用pandas向量函数执行这种逻辑?
我觉得几乎像是想要使用向量或pd.Series来应用.loc,而不是单个pd.Timestamp?
编辑
使用.apply比使用朴素的for循环更有效率/略微更有效率。我希望能指点我一个纯向量解决方案的方向。

实际上这两个数据框有多大? - ALollz
len(data_series) 大约在 10^5 到 10^6 之间,而 len(start) 大约在 10^4 到 10^5 之间。循环执行该操作大约需要 5 秒钟,然而,对于各种 start 向量可能需要执行该操作 10^5 次,这使得该方法不可行。 - mch56
所有的 end 条目是否与相应的 start 条目在固定时间间隔内?就像给定样本中的5H一样。 - Divakar
1
他们是@Divakar。 - mch56
3个回答

4

基本思路

通常情况下,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

对于特定情况,当所有条目之间的时间段相同且所有切片都被该长度覆盖时,即无任何越界情况,我们可以使用NumPy的滑动窗口技巧

我们可以利用np.lib.stride_tricks.as_stridedscikit-image的view_as_windows获取滑动窗口。更多关于基于as_stridedview_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]

如果您无法访问 scikit-image,请使用此帖子


基准测试

我们将在给定的示例数据上将所有内容按比例放大 100 倍 并进行测试。

设置 -

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

使用这些技术可提高140倍及以上170倍的速度!

1
非常好的回答!我没有想到要使用 np.searchsorted - dubbbdan
优秀的答案 - 我以前从未遇到过np.searchsorted,但它完全符合我的需求。 - mch56

1

.apply在底层只是一个循环,应该避免使用行循环。当“len(pdf)”很大时,它仍然非常慢。我希望能指出正确向量化解决方案的方向。 - mch56
我不确定如何将其向量化,但另一个值得考虑的方向是IntervalIndex,它专门设计用于处理这种类型的索引:https://dev59.com/eFYO5IYBdhLWcg3wF93K - thomas

1

您可以使用index.get_locdata_series中查找startend的元素索引。

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

这个不起作用 - 对于未量化的数据,start 的值可能不在 grouped 的键中。 - mch56
也许可以通过在问题前加上“开始”来显示该情况。 - dubbbdan
@ojlm 编辑了我的答案 - dubbbdan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接