将 pandas 数据框中的行和上一行与数百万行进行比较的最快方法

Fastest way to compare row and previous row in pandas dataframe with millions of rows

我正在寻找解决方案来加速我编写的用于遍历 pandas 数据帧并比较当前行和上一行之间的列值的函数。

例如,这是我的问题的简化版本:

   User  Time                 Col1  newcol1  newcol2  newcol3  newcol4
0     1     6     [cat, dog, goat]        0        0        0        0
1     1     6         [cat, sheep]        0        0        0        0
2     1    12        [sheep, goat]        0        0        0        0
3     2     3          [cat, lion]        0        0        0        0
4     2     5  [fish, goat, lemur]        0        0        0        0
5     3     9           [cat, dog]        0        0        0        0
6     4     4          [dog, goat]        0        0        0        0
7     4    11                [cat]        0        0        0        0

目前,我有一个函数可以循环计算“newcol1”和“newcol2”的值,这取决于“User”自上次以来是否发生了变化行以及“Time”值的差异是否大于 1。它还会查看存储在“Col1”和“Col2”中的数组中的第一个值并更新'newcol3' 和 'newcol4' 如果这些值自上一行以来发生了变化。

这是我目前正在做的伪代码(因为我已经简化了问题,所以我还没有测试过这个,但它与我在 ipython notebook 中实际做的非常相似) :

 def myJFunc(df):
...     #initialize jnum counter
...     jnum = 0;
...     #loop through each row of dataframe (not including the first/zeroeth)
...     for i in range(1,len(df)):
...             #has user changed?
...             if df.User.loc[i] == df.User.loc[i-1]:
...                     #has time increased by more than 1 (hour)?
...                     if abs(df.Time.loc[i]-df.Time.loc[i-1])>1:
...                             #update new columns
...                             df['newcol2'].loc[i-1] = 1;
...                             df['newcol1'].loc[i] = 1;
...                             #increase jnum
...                             jnum += 1;
...                     #has content changed?
...                     if df.Col1.loc[i][0] != df.Col1.loc[i-1][0]:
...                             #record this change
...                             df['newcol4'].loc[i-1] = [df.Col1.loc[i-1][0], df.Col2.loc[i][0]];
...             #different user?
...             elif df.User.loc[i] != df.User.loc[i-1]:
...                     #update new columns
...                     df['newcol1'].loc[i] = 1; 
...                     df['newcol2'].loc[i-1] = 1;
...                     #store jnum elsewhere (code not included here) and reset jnum
...                     jnum = 1;

我现在需要将这个函数应用到几百万行,它的速度慢得不可思议,所以我想找出加速它的最佳方法。我听说 Cython 可以提高函数的速度,但我没有使用它的经验(而且我对 pandas 和 python 都是新手)。是否可以将数据帧的两行作为参数传递给函数,然后使用 Cython 来加速它,或者是否有必要创建其中包含“diff”值的新列,以便函数仅从中读取并一次写入一行数据帧,以便从使用 Cython 中获益?任何其他速度技巧将不胜感激!

(关于使用 .loc,我比较了 .loc、.iloc 和 .ix,这个稍微快一点,所以这是我目前使用它的唯一原因)

(此外,我的 User 列实际上是 unicode 而不是 int,这对于快速比较可能会有问题)

在您的问题中,您似乎想要成对地遍历行。你可以做的第一件事是这样的:

from itertools import tee, izip
def pairwise(iterable):
    "s -> (s0,s1), (s1,s2), (s2, s3), ..."
    a, b = tee(iterable)
    next(b, None)
    return izip(a, b)

for (idx1, row1), (idx2, row2) in pairwise(df.iterrows()):
    # you stuff

但是您不能直接修改 row1 和 row2,您仍然需要对索引使用 .loc 或 .iloc。

如果 iterrows 仍然太慢,我建议这样做:

  • 使用 pd.unique(用户)从你的 unicode 名称创建一个 user_id 列,并将名称与字典映射到整数 ID。

  • 创建一个增量数据帧:将原始数据帧减去 user_id 和时间列的移位数据帧。

    df[[col1, ..]].shift() - df[[col1, ..]])
    

如果user_id > 0,表示用户在连续两行发生变化。时间列可以直接用delta[delta['time' > 1]]过滤 使用此增量数据框,您可以按行记录更改。您可以使用它作为掩码来更新原始数据框中所需的列。

使用 pandas(构造)并矢量化您的代码,即不要使用 for 循环,而是使用 pandas/numpy 函数。

'newcol1' and 'newcol2' based on whether the 'User' has changed since the previous row and also whether the difference in the 'Time' values is greater than 1.

分别计算这些:

df['newcol1'] = df['User'].shift() == df['User']
df.ix[0, 'newcol1'] = True # possibly tweak the first row??

df['newcol1'] = (df['Time'].shift() - df['Time']).abs() > 1

我不清楚 Col1 的用途,但列中的一般 python 对象不能很好地缩放(您不能使用快速路径,内容分散在内存中)。大多数时候你可以使用其他东西来逃避......


Cython 是最后一个选项,在 99% 的用例中不需要,但请参阅 enhancing performance section of the docs 获取提示。

我的想法与安迪相同,只是添加了 groupby,我认为这是对安迪回答的补充。每当您执行 diffshift 时,添加 groupby 只会产生将 NaN 放在第一行的效果。 (请注意,这并不是试图给出确切的答案,只是勾勒出一些基本的技术。)

df['time_diff'] = df.groupby('User')['Time'].diff()

df['Col1_0'] = df['Col1'].apply( lambda x: x[0] )

df['Col1_0_prev'] = df.groupby('User')['Col1_0'].shift()

   User  Time                 Col1  time_diff Col1_0 Col1_0_prev
0     1     6     [cat, dog, goat]        NaN    cat         NaN
1     1     6         [cat, sheep]          0    cat         cat
2     1    12        [sheep, goat]          6  sheep         cat
3     2     3          [cat, lion]        NaN    cat         NaN
4     2     5  [fish, goat, lemur]          2   fish         cat
5     3     9           [cat, dog]        NaN    cat         NaN
6     4     4          [dog, goat]        NaN    dog         NaN
7     4    11                [cat]          7    cat         dog

作为 Andy 关于存储对象的观点的后续,请注意我在这里所做的是提取列表列的第一个元素(并且还添加了一个移位版本)。这样做你只需要做一次昂贵的提取,然后就可以坚持标准 pandas 方法。