我有以下pandas数据帧:
Date
2018-04-10 21:05:00
2018-04-10 21:05:00
2018-04-10 21:10:00
2018-04-10 21:15:00
2018-04-10 21:35:00
我的目标是计算每次之前20分钟和之后20分钟的行数(包括之前和之后具有相同时间的行)。如下所示:
Date nr_20_min_bef nr_20_min_after
2018-04-10 21:05:00 2 4
2018-04-10 21:05:00 2 4
2018-04-10 21:10:00 3 2
2018-04-10 21:15:00 4 2
2018-04-10 21:35:00 2 1
我试图执行for循环迭代所有行,问题是整个系列有超过百万行,因此我一直在寻找更有效的解决方案。我目前的方法是使用pandas函数:
import datetime
import pandas
df = pd.DataFrame(pd.to_datetime(['2018-04-10 21:05:00',
'2018-04-10 21:05:00',
'2018-04-10 21:10:00',
'2018-04-10 21:15:00',
'2018-04-10 21:35:00']),columns = ['Date'])
nr_20_min_bef = []
nr_20_min_after = []
for i in range(0, len(df)):
nr_20_min_bef.append(df.Date.between(df.Date[i] -
pd.offsets.DateOffset(minutes=20), df.Date[i], inclusive = True).sum())
nr_20_min_after.append(df.Date.between(df.Date[i], df.Date[i] +
pd.offsets.DateOffset(minutes=20), inclusive = True).sum())
对于这种情况,可能是一个矢量化解决方案,但是,我真的不知道该怎么做。
提前致谢。
答案 0 :(得分:4)
好消息是可以对此进行矢量化。 坏消息是......这并不简单。
以下是基准perfplot代码:
import numpy as np
import pandas as pd
import perfplot
def orig(df):
nr_20_min_bef = []
nr_20_min_after = []
for i in range(0, len(df)):
nr_20_min_bef.append(df.Date.between(
df.Date[i] - pd.offsets.DateOffset(minutes=20), df.Date[i], inclusive = True).sum())
nr_20_min_after.append(df.Date.between(
df.Date[i], df.Date[i] + pd.offsets.DateOffset(minutes=20), inclusive = True).sum())
df['nr_20_min_bef'] = nr_20_min_bef
df['nr_20_min_after'] = nr_20_min_after
return df
def alt(df):
df = df.copy()
df['Date'] = pd.to_datetime(df['Date'])
df['num'] = 1
df = df.set_index('Date')
dup_count = df.groupby(level=0)['num'].count()
result = dup_count.rolling('20T', closed='both').sum()
df['nr_20_min_bef'] = result.astype(int)
max_date = df.index.max()
min_date = df.index.min()
dup_count_reversed = df.groupby((max_date - df.index)[::-1] + min_date)['num'].count()
result = dup_count_reversed.rolling('20T', closed='both').sum()
result = pd.Series(result.values[::-1], dup_count.index)
df['nr_20_min_after'] = result.astype(int)
df = df.drop('num', axis=1)
df = df.reset_index()
return df
def make_df(N):
dates = (np.array(['2018-04-10'], dtype='M8[m]')
+ (np.random.randint(10, size=N).cumsum()).astype('<i8').astype('<m8[m]'))
df = pd.DataFrame({'Date': dates})
return df
def check(df1, df2):
return df1.equals(df2)
perfplot.show(
setup=make_df,
kernels=[orig, alt],
n_range=[2**k for k in range(4,10)],
logx=True,
logy=True,
xlabel='N',
equality_check=check)
除了对orig
和alt
进行基准测试外,perfplot.show
还会对此进行检查
orig
和alt
返回的DataFrame相等。鉴于alt
的复杂性,这至少可以让我们确信它的行为与orig
相同。
从orig
启动以来为大N制作灌注图有点困难
花了很长时间,每个基准重复数百次。所以
这里是较大%timeit
的一些N
点比较:
| N | orig (ms) | alt (ms) |
|-------+-----------+----------|
| 2**10 | 3040 | 9.32 |
| 2**12 | 12600 | 10.8 |
| 2**20 | ? | 909 |
In [300]: df = make_df(2**10)
In [301]: %timeit orig(df)
1 loop, best of 3: 3.04 s per loop
In [302]: %timeit alt(df)
100 loops, best of 3: 9.32 ms per loop
In [303]: df = make_df(2**12)
In [304]: %timeit orig(df)
1 loop, best of 3: 12.6 s per loop
In [305]: %timeit alt(df)
100 loops, best of 3: 10.8 ms per loop
In [306]: df = make_df(2**20)
In [307]: %timeit alt(df)
1 loop, best of 3: 909 ms per loop
现在alt
正在做什么?也许最简单的方法是使用您发布的df
来查看一个小例子:
df = pd.DataFrame(pd.to_datetime(['2018-04-10 21:05:00',
'2018-04-10 21:05:00',
'2018-04-10 21:10:00',
'2018-04-10 21:15:00',
'2018-04-10 21:35:00']),columns = ['Date'])
主要思想是使用Series.rolling
来执行滚动总和。当。。。的时候
系列有一个DatetimeIndex,Series.rolling
可以接受一个时间频率
窗口大小。因此,我们可以使用修正的可变窗口计算滚动总和
时间跨度。因此,第一步是将日期设为DatetimeIndex:
df['Date'] = pd.to_datetime(df['Date'])
df['num'] = 1
df = df.set_index('Date')
由于df
具有重复日期,因此请按DatetimeIndex值进行分组并计算重复次数:
dup_count = df.groupby(level=0)['num'].count()
# Date
# 2018-04-10 21:05:00 2
# 2018-04-10 21:10:00 1
# 2018-04-10 21:15:00 1
# 2018-04-10 21:35:00 1
# Name: num, dtype: int64
现在计算dup_count
上的滚动总和:
result = dup_count.rolling('20T', closed='both').sum()
# Date
# 2018-04-10 21:05:00 2.0
# 2018-04-10 21:10:00 3.0
# 2018-04-10 21:15:00 4.0
# 2018-04-10 21:35:00 2.0
# Name: num, dtype: float64
Viola,那是nr_20_min_bef
。 20T
specifies the window size长达20分钟。 closed='both'
指定每个窗口都包含其左端点和右端点。
现在,如果只计算nr_20_min_after
那么简单。理论上,我们需要做的就是颠倒dup_count
中行的顺序并计算另一个滚动总和。不幸的是,Series.rolling
要求DatetimeIndex单调增加:
In [275]: dup_count[::-1].rolling('20T', closed='both').sum()
ValueError: index must be monotonic
由于明显的方式被封锁,我们绕道而行:
max_date = df.index.max()
min_date = df.index.min()
dup_count_reversed = df.groupby((max_date - df.index)[::-1] + min_date)['num'].count()
# Date
# 2018-04-10 21:05:00 1
# 2018-04-10 21:25:00 1
# 2018-04-10 21:30:00 1
# 2018-04-10 21:35:00 2
# Name: num, dtype: int64
这会生成一个新的伪日期时间DatetimeIndex来分组:
In [288]: (max_date - df.index)[::-1] + min_date
Out[288]:
DatetimeIndex(['2018-04-10 21:05:00', '2018-04-10 21:25:00',
'2018-04-10 21:30:00', '2018-04-10 21:35:00',
'2018-04-10 21:35:00'],
dtype='datetime64[ns]', name='Date', freq=None)
这些值可能不在df.index
中 - 但这没关系。我们唯一需要的是值是单调增加的,并且日期时间之间的差异
对应于df.index
时的差异。
现在使用这个反转的dup_count,我们可以通过滚动总和来享受大赢(在性能方面):
result = dup_count_reversed.rolling('20T', closed='both').sum()
# Date
# 2018-04-10 21:05:00 1.0
# 2018-04-10 21:25:00 2.0
# 2018-04-10 21:30:00 2.0
# 2018-04-10 21:35:00 4.0
# Name: num, dtype: float64
result
具有nr_20_min_after
所需的值,但顺序相反,
和错误的索引。以下是我们如何纠正这一点:
result = pd.Series(result.values[::-1], dup_count.index)
# Date
# 2018-04-10 21:05:00 4.0
# 2018-04-10 21:10:00 2.0
# 2018-04-10 21:15:00 2.0
# 2018-04-10 21:35:00 1.0
# dtype: float64
这基本上只有alt
。
答案 1 :(得分:1)
我认为您可以使用apply
,即使它不是矢量化方式,也应该比使用for
循环更快,例如:
#first create the timedelta of 20 minutes
dt_20 = pd.Timedelta(minutes=20)
# then apply on the first column
df['nr_20_min_bef'] = df['Date'].apply(lambda x: df['Date'][((x - dt_20) <= df['Date'] )
& (x >=df['Date'])].count())
df['nr_20_min_after'] = df['Date'].apply(lambda x: df['Date'][(x <= df['Date'] )&
((x + dt_20) >= df['Date'])].count())
执行了一些%timeit
之后,似乎使用between
方法比使用mask
快一点,所以你可以做到
df['nr_20_min_bef'] = df['Date'].apply(lambda x: df.Date.between(x - dt_20,
x, inclusive = True).sum())
和以后的同意。