当在另一个系列的切片中使用值时,如何通过 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)))
我们希望在每个start
和end
时间范围内获取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
循环更有效 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
找到 start
和 end
的元素在 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]
处搜索那个特定索引,其中 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
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
使用这些加速!
假设我有两个时间戳系列,它们是 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)))
我们希望在每个start
和end
时间范围内获取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
循环更有效 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
start
和 end
的元素在 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
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]
处搜索那个特定索引,其中 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
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
使用这些加速!