【问题标题】:Python - Vectorized difference of dates in 1 million row tablePython - 100 万行表中日期的矢量化差异
【发布时间】:2018-11-19 08:48:51
【问题描述】:

我有以下熊猫数据框:

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 循环来遍历所有行,问题是整个系列有超过一百万行,因此我正在寻找更有效的解决方案。我目前的方法是使用熊猫函数:

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())

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

提前致谢。

【问题讨论】:

标签: python pandas date numpy


【解决方案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())

之后同上。

【讨论】:

    【解决方案2】:

    好消息是它可以向量化。 坏消息是……这并不简单。

    这是基准测试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)
    

    这表明altorig 快得多:

    除了基准测试origaltperfplot.show 还检查 origalt 返回的 DataFrames 是相等的。鉴于alt 的复杂性,这至少让我们确信它的行为与orig 相同。

    自从orig 开始后,为大 N 制作 perfplot 有点困难 花费相当长的时间,每个基准测试重复数百次。所以 这里有一些%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 来执行滚动求和。当。。。的时候 Series 有一个 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
    

    维奥拉,那是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 的全部内容。

    【讨论】:

    • 感谢您的回答!如果在其他列中验证了特定条件,您是否还知道仅计算日期的解决方案?那么,如果在其他列中验证了特定字符串,则计算前后 20 分钟的日期数?因此,我们不仅会有 nr_20_min_bef 和 nr_20_min_after 列,而且还有 nr_20_min_bef_variablex 和 nr_20_min_after_variablex(表示 variablex 在不到 30 分钟前出现在行上的次数)。
    • 我不确定我是否完全理解了这个新问题。但是,如果您发布一个包含所有详细信息的新问题(非常感谢您在此处提供的玩具示例),我很乐意看一看。
    • 就这么做了。非常感谢您的帮助!给你:stackoverflow.com/questions/50786484/…
    • 我假设该解决方案可能与此处介绍的解决方案极为相似。但是,我并没有真正到达那里。
    • 当应用于两个日期时间值系列时,是否可以调整此解决方案?我的意思是:我们不是计算同一系列中 20 分钟前的日期数量,而是计算附加系列前后 20 分钟的值(而不是像我们在这种情况下所做的那样)。一个简单的解决方案是迭代第一个系列的每个日期,并检查第二个系列前后 20 分钟的日期有多少。但是,由于它是一个 100 万行的表,这太耗时了。提前致谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-06
    • 1970-01-01
    • 2020-11-15
    • 1970-01-01
    相关资源
    最近更新 更多