向量化熊猫中的可变长度预见循环

时间:2019-04-12 07:14:37

标签: pandas

这是我的数据的非常简化的版本:

+----+---------+---------------------+
|    | user_id | seconds_since_start |
+----+---------+---------------------+
|  0 |       1 |                  10 |
|  1 |       1 |                  12 |
|  2 |       1 |                  15 |
|  3 |       1 |                  52 |
|  4 |       1 |                  60 |
|  5 |       1 |                  67 |
|  6 |       1 |                 120 |
|  7 |       2 |                  55 |
|  8 |       2 |                  62 |
|  9 |       2 |                 105 |
| 10 |       3 |                 200 |
| 11 |       3 |                 206 |
+----+---------+---------------------+

这是我想产生的数据:

+----+---------+---------------------+-----------------+------------------+
|    | user_id | seconds_since_start | session_ordinal | session_duration |
+----+---------+---------------------+-----------------+------------------+
|  0 |       1 |                  10 |               1 |                5 |
|  1 |       1 |                  12 |               1 |                5 |
|  2 |       1 |                  15 |               1 |                5 |
|  3 |       1 |                  52 |               2 |               15 |
|  4 |       1 |                  60 |               2 |               15 |
|  5 |       1 |                  67 |               2 |               15 |
|  6 |       1 |                 120 |               3 |                0 |
|  7 |       2 |                  55 |               1 |                7 |
|  8 |       2 |                  62 |               1 |                7 |
|  9 |       2 |                 105 |               2 |                0 |
| 10 |       3 |                 200 |               1 |                6 |
| 11 |       3 |                 206 |               1 |                6 |
+----+---------+---------------------+-----------------+------------------+

我的会话概念是一组来自单个用户的事件,它们之间的间隔不超过10秒,会话的持续时间定义为会话中第一个事件与最后一个事件之间的差(以秒为单位)

我已经编写了可以实现我想要的功能的Python。

import pandas as pd

events_data = [[1, 10], [1, 12], [1, 15], [1, 52], [1, 60], [1, 67], [1, 120], 
    [2, 55], [2, 62], [2, 105], 
    [3, 200], [3, 206]]
events = pd.DataFrame(data=events_data, columns=['user_id', 'seconds_since_start'])

def record_session(index_range, ordinal, duration):
    for i in index_range:
        events.at[i, 'session_ordinal'] = ordinal
        events.at[i, 'session_duration'] = duration

session_indexes = []
current_user = previous_time = session_start = -1
session_num = 0
for i, row in events.iterrows():
    if row['user_id'] != current_user or (row['seconds_since_start'] - previous_time) > 10:
        record_session(session_indexes, session_num, previous_time - session_start)
        session_indexes = [i]
        session_num += 1
        session_start = row['seconds_since_start'] 
    if row['user_id'] != current_user:
        current_user = row['user_id']
        session_num = 1
    previous_time = row['seconds_since_start']
    session_indexes.append(i)
record_session(session_indexes, session_num, previous_time - session_start)

我的问题是运行所需的时间。如我所说,这是我的数据的非常简化的版本,我的实际数据有70,000,000行。是否有一种方法可以矢量化(从而加快)像这样的算法,这些算法根据变长预见来制定其他列?

2 个答案:

答案 0 :(得分:1)

您可以尝试:

# Create a helper boolean Series
s = df.groupby('user_id')['seconds_since_start'].diff().gt(10)

df['session_ordinal'] = s.groupby(df['user_id']).cumsum().add(1).astype(int)

df['session_duration'] = (df.groupby(['user_id', 'session_ordinal'])['seconds_since_start']
                          .transform(lambda x: x.max() - x.min()))

[输出]

    user_id  seconds_since_start  session_ordinal  session_duration
0         1                   10                1                 5
1         1                   12                1                 5
2         1                   15                1                 5
3         1                   52                2                15
4         1                   60                2                15
5         1                   67                2                15
6         1                  120                3                 0
7         2                   55                1                 7
8         2                   62                1                 7
9         2                  105                2                 0
10        3                  200                1                 6
11        3                  206                1                 6

答案 1 :(得分:0)

Chris A的答案here很不错。它包含一些我不熟悉的技术或电话。该答案将复制他的内容并添加大量注释。

我们首先建立一个辅助布尔系列。本系列记录了哪些事件为任何用户启动了附加会话。作为布尔序列,这是可以的,因为在数字上下文中,它们的行为类似于整数0和1 (引自here)。让我们将系列一点一点地放在一起。

starts_session = events.groupby('user_id')['seconds_since_start'].diff().gt(10)

首先,我们将事件按user_id(documentation)分组,然后选择“ seconds_since_start”列,并在其上调用diff(documentation)。 events.groupby('user_id')['seconds_since_start'].diff()的结果是

+----+----------------------+
|    |  seconds_since_start |
+----+----------------------+
|  0 |                  NaN |
|  1 |                  2.0 |
|  2 |                  3.0 |
|  3 |                 37.0 |
|  4 |                  8.0 |
|  5 |                  7.0 |
|  6 |                 53.0 |
|  7 |                  NaN |
|  8 |                  7.0 |
|  9 |                 43.0 |
| 10 |                  NaN |
| 11 |                  6.0 |
+----+----------------------+

我可以看到每个组的开始都已经获得了正确的NaN差异,因为该用户没有先前的事件可以提供差值。

然后使用大于gt(10)documentation)的元素,得出

+----+----------------------+
|    |  seconds_since_start |
+----+----------------------+
|  0 |                False |
|  1 |                False |
|  2 |                False |
|  3 |                 True |
|  4 |                False |
|  5 |                False |
|  6 |                 True |
|  7 |                False |
|  8 |                False |
|  9 |                 True |
| 10 |                False |
| 11 |                False |
+----+----------------------+

(注:列标题为奇数,但未使用,因此无所谓。)

events['session_ordinal'] = starts_session.groupby(events['user_id']).cumsum().add(1).astype(int)

然后,我们根据事件中的user_id将starts_session重新分组,然后对每个组进行累积总和cumsumdocumentation)。分组在这里为我们完成了工作,确保每个用户的事件均从零重新开始。我们需要会话序数从1开始而不是零,因此我们只需添加一个add(1)documentation)并将其强制转换为int,因为它们都不是NaN astype(int)({{3} }。这给出了我想要的派生session_ordinal列。

events['session_duration'] = events.groupby(['user_id', 'session_ordinal'])['seconds_since_start'].transform(lambda x: x.max() - x.min())

为了得出每个会话的持续时间,我们首先通过user_id和新的session_ordinal对事件进行分组,即,将事件分组为会话。使用transformdocumentation),我们可以找到每个组(即每个会话)的seconds_since_start的最小值和最大值,它们之间的差是会话的持续时间。将transform应用于分组数据的这种模式在documentation流程中得到广泛使用。

谢谢克里斯。