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

时间:2019-04-12 12:35:07

标签: python pandas time-series vectorization series

假设我有两个时间戳记,分别是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_seriesstart时间范围内从end获得值。可以在循环内天真地完成

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

我们可以看到,这种幼稚的方法在startend这对日期中循环,从数据中获取值。

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

我感觉几乎就像我想将.loc应用于矢量或pd.Series而不是单个pd.Timestamp一样?

EDIT

使用.apply不会比使用简单的for循环有效/略微更高。我希望能指出纯向量解的方向

3 个答案:

答案 0 :(得分:4)

基本思想

和往常一样,大熊猫会花时间在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_stridedscikit-image's view_as_windows来获取滑动窗口。 More info on use of as_strided based 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]

如果您无权访问scikit-image,请使用this post


基准化

让我们在给定的样本数据上按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 :(得分:1)

如果将系列移到数据框,则可以利用apply函数:

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

答案 2 :(得分:1)

您可以使用start找到enddata_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_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)