Python - 100万行表中日期的矢量化差异

时间:2018-06-09 14:03:48

标签: python pandas date numpy

我有以下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())

对于这种情况,可能是一个矢量化解决方案,但是,我真的不知道该怎么做。

提前致谢。

2 个答案:

答案 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)

显示alt明显快于origenter image description here

除了对origalt进行基准测试外,perfplot.show还会对此进行检查 origalt返回的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_bef20T 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())

和以后的同意。