【问题标题】:How to find number of rows that fall within a time period of each row, while satisfying criteria in other columns?如何在满足其他列的条件的同时找到每行的时间段内的行数?
【发布时间】:2021-02-27 15:43:15
【问题描述】:

我有一个示例数据框,其中包含一列名称和一列日期时间。

import random
np.random.seed(1)

numberList = ['Mark','James','Sarah']
df = pd.DataFrame({'Date':pd.date_range(start='1/1/2020', freq='BH', periods=20),
             'Name':[random.choice(numberList) for x in range(20)]})

    Date                Name
0   2020-01-01 09:00:00 James
1   2020-01-01 10:00:00 Sarah
2   2020-01-01 11:00:00 Sarah
3   2020-01-01 12:00:00 James
4   2020-01-01 13:00:00 Mark
5   2020-01-01 14:00:00 James
6   2020-01-01 15:00:00 Mark
7   2020-01-01 16:00:00 Sarah
8   2020-01-02 09:00:00 Mark
9   2020-01-02 10:00:00 Sarah
10  2020-01-02 11:00:00 Sarah
11  2020-01-02 12:00:00 Mark
12  2020-01-02 13:00:00 Sarah
13  2020-01-02 14:00:00 Sarah
14  2020-01-02 15:00:00 Mark
15  2020-01-02 16:00:00 Mark
16  2020-01-03 09:00:00 Sarah
17  2020-01-03 10:00:00 Sarah
18  2020-01-03 11:00:00 Mark
19  2020-01-03 12:00:00 Sarah

对于每一行,我试图找出日期时间在 10 小时内且名称匹配的总行数。

我已经设法用下面的代码做到了这一点,但是在更大的数据集上,这需要很长时间。有没有更好的办法做到这一点?

df['Total'] = 0
for i in df.Name.unique():
    df2 = df[df.Name == i]
    total = df2['Date'].apply(lambda x: len(df2[(df2.Date>=x) & (df2.Date<x + datetime.timedelta(hours = 10))]))
    df.loc[total.index,'Total'] = total.values
df

结果:

    Date                Name    Total
0   2020-01-01 09:00:00 James   3
1   2020-01-01 10:00:00 Sarah   3
2   2020-01-01 11:00:00 Sarah   2
3   2020-01-01 12:00:00 James   2
4   2020-01-01 13:00:00 Mark    2
5   2020-01-01 14:00:00 James   1
6   2020-01-01 15:00:00 Mark    1
7   2020-01-01 16:00:00 Sarah   1
8   2020-01-02 09:00:00 Mark    4
9   2020-01-02 10:00:00 Sarah   4
10  2020-01-02 11:00:00 Sarah   3
11  2020-01-02 12:00:00 Mark    3
12  2020-01-02 13:00:00 Sarah   2
13  2020-01-02 14:00:00 Sarah   1
14  2020-01-02 15:00:00 Mark    2
15  2020-01-02 16:00:00 Mark    1
16  2020-01-03 09:00:00 Sarah   3
17  2020-01-03 10:00:00 Sarah   2
18  2020-01-03 11:00:00 Mark    1
19  2020-01-03 12:00:00 Sarah   1

编辑: 实际数据至少有 80000 行,并且有 200 多个名称。 日期列具体到第二个。, Date 列包含重复的条目,其中两个不同的名称可以具有相同的日期时间,但没有一个名称将具有多个相同的日期时间条目。

编辑------------------------------ -

我已经标记了 Rik Kraan 的答案,尽管它在使用我自己的数据时确实会产生较慢的结果。 因此,我想比较两种方法的性能。下面以 1000 行为增量对最多 50000 行的样本大小进行了比较测试。对于我的具体用例,看起来 Rik 的解决方案在 48/49 千行中更快,之后原始解决方案似乎更好。

import time
import random
import datetime

Rows = []
Rik_Kraan = []
Willacya = []

for i in range(1000,50000,1000):
    
    Rows.append(i)
    
    # Creates Dataframe where number of names is 20% the length of the Dataframe.
    numberList = ["Name_"+str(j) for j in range(1,int(i*.2))]
    df_test = pd.DataFrame({'Date':pd.date_range(start='1/1/2020', freq='S', periods=i),
                 'Name':[random.choice(numberList) for x in range(i)]})
    
    # Rik_Kraan solution using masking
    start = time.time() 
    dates = df_test['Date'].values
    name = df_test['Name'].values
    df_test.assign(Total=np.sum((dates[:, None] <= (dates+pd.Timedelta(10, 'H'))) & (dates[:, None] >= dates) & (name[:, None] == name), axis=0))
    end = time.time()
    Rik_Kraan.append(end-start)

    # Original Solution
    start = time.time()
    for j in df_test.Name.unique():
        df2 = df_test[df_test.Name == j].copy()
        total = df2['Date'].apply(lambda x: len(df2[(df2.Date<=x) & (df2.Date>x - datetime.timedelta(hours = 1))]))
        df_test.loc[total.index,'Total'] = total.values
    end = time.time()
    Willacya.append(end-start)   
    
pd.DataFrame({'Num_Rows':Rows,'Rik_Kraan':Rik_Kraan,'Willacya':Willacya}).set_index('Num_Rows').plot()

【问题讨论】:

    标签: python pandas numpy datetime


    【解决方案1】:

    如果您的数据不大,请在Name 上进行自合并并查询:

    df['Total'] = (df.reset_index().merge(df, on='Name')
       .loc[lambda x: (x.Date_y-x.Date_x<thresh) & (x.Date_x <= x.Date_y)]
       .groupby('index').size()
    )
    

    输出:

                      Date   Name  Total
    0  2020-01-01 09:00:00  James      3
    1  2020-01-01 10:00:00  Sarah      3
    2  2020-01-01 11:00:00  Sarah      2
    3  2020-01-01 12:00:00  James      2
    4  2020-01-01 13:00:00   Mark      2
    5  2020-01-01 14:00:00  James      1
    6  2020-01-01 15:00:00   Mark      1
    7  2020-01-01 16:00:00  Sarah      1
    8  2020-01-02 09:00:00   Mark      4
    9  2020-01-02 10:00:00  Sarah      4
    10 2020-01-02 11:00:00  Sarah      3
    11 2020-01-02 12:00:00   Mark      3
    12 2020-01-02 13:00:00  Sarah      2
    13 2020-01-02 14:00:00  Sarah      1
    14 2020-01-02 15:00:00   Mark      2
    15 2020-01-02 16:00:00   Mark      1
    16 2020-01-03 09:00:00  Sarah      3
    17 2020-01-03 10:00:00  Sarah      2
    18 2020-01-03 11:00:00   Mark      1
    19 2020-01-03 12:00:00  Sarah      1
    

    【讨论】:

    • 很好的解决方案,但是我的真实案例数据是 80000 x 2,有 200 个唯一名称,导致我的笔记本内存不足。
    【解决方案2】:

    您可以在接下来的 10 小时内创建包含姓名的移位列。如果我们将这些列与原始的Name 进行比较,我们会得到多个布尔列,指示Name 列中的名称是否存在于以下行中。对行进行简单的 sum 然后得到预期的Total 列。

    # Make copy of the original dataframe and set the Date column as index
    df_shifted = df.set_index('Date')
    
    # Loop over the coming 10 hours and create shifted columns
    for i in range(1,10):
        df_shifted[i] = df_shifted.shift(periods=-i, freq='H')['Name']
        # Compare with the original Name column
        df_shifted[i] = df_shifted[i] == df_shifted['Name']
    
    # Set the original Name column to True (as we want to count these names as well)
    df_shifted['Name'] = True
    
    # Assign new total column to the original dataframe
    df.assign(Total=df2.sum(axis=1).values)
    

    您的解决方案的 CPU 时间为 24.9 毫秒

    CPU times: user 24.9 ms, sys: 0 ns, total: 24.9 ms
    Wall time: 21.8 ms
    

    我提出的解决方案要快一点:

    CPU times: user 9.76 ms, sys: 4.41 ms, total: 14.2 ms
    Wall time: 12.1 ms
    

    希望对你有帮助

    【讨论】:

    • 感谢这是对我提出的问题的一个很好的解决方案,但是我的真实案例数据中的日期时间列有很多重复的条目。索引必须按名称和日期分组,以使每一行都是唯一的,否则我会收到错误无法从重复轴重新索引。
    • 编辑:还应注意实际数据是特定于秒的,因此可能无法有效地移动并为时间变量创建列。
    • 啊,我明白了,你是对的。让我发布另一个可能使用 Numpy 广播的示例
    【解决方案3】:

    我们也可以使用numpy广播。本质上,对于每一行,我们想计算在 10 小时的时间间隔内有多少行具有相同的name

    首先制作numpy 感兴趣列的数组

    dates = df['Date'].values
    name = df['Name'].values
    

    第二通过相互比较行来创建掩码。这会产生一个形状数组number_of_rows * number_of_rows

    (dates[:, None] <= (dates+pd.Timedelta(10, 'H'))) & (dates[:, None] >= dates) & (name[:, None] == name)
    

    最后我们可以对每一列求和,这为我们提供了接下来 10 小时内相同名字的总数,并将其分配给一个新列。

    df.assign(Total=np.sum((dates[:, None] <= (dates+pd.Timedelta(10, 'H'))) & (dates[:, None] >= dates) & (name[:, None] == name), axis=0))
    

    【讨论】:

    • 谢谢,我喜欢这个解决方案,它在上面的数据上运行得更快,但在使用更大的数据集时会慢得多。我刚刚在大小为 80,800,8000,80000 行的数据集上进行了测试。除了最后一个数据集,您的解决方案在每个数据集上的速度都是两倍多,因为掩码的大小呈指数增长,速度会慢 4 到 6 倍。
    • 我应该注意我使用了这些大小的数据集,因为我拥有的数据集至少有 80000 行。如果没有其他解决方案,我会将您的答案标记为正确。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-05-26
    • 2017-09-15
    • 1970-01-01
    • 1970-01-01
    • 2023-01-25
    • 2022-11-15
    • 1970-01-01
    相关资源
    最近更新 更多