使用 Pandas 数据框处理数百万行数据时,比较每一行与前一行最快的方法是什么?

20

我正在寻找加速一个函数的解决方案,该函数循环遍历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 笔记本中所做的非常相似):

 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都很陌生)。是否可能将一个dataframe的两行作为参数传递给该函数,然后使用Cython加速它,或者需要创建带有“diff”值的新列,以便函数一次只读取并写入一个dataframe中的一行,从而受益于使用Cython?任何其他加速技巧都将不胜感激!

(关于使用.loc,我比较了.loc、.iloc和.ix,发现.loc稍微快一些,所以目前我只使用这个)

(另外,我的“User”列实际上是unicode而不是int,这可能会对速度产生问题)


1
有了一百万行,为什么不使用Python可以轻松连接的专用数据库,如MySQL或SQLlite呢?关系型数据库可以运行复杂的SQL查询,具有逐行比较的if/then逻辑,并由索引连接。它们被设计为可扩展到数百万行。甚至可以设置触发器,以便在任何用户更改时更新特定列。 - Parfait
3个回答

20

我和Andy的想法很相似,只是加了groupby,我认为这对Andy的答案是有补充作用的。添加groupby 将会在每次执行diffshift时,在第一行放置一个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方法。


非常感谢JohnE和@Andy两位的帮助,我已经实现了两个解决方案,groupby和提取Col1的第一个元素特别有用,现在整个数据集运行大约需要3分钟 - 非常高兴! :) - AdO
重要的问题是:我们如何确保.shift()函数确切地移动排序数据?或者使用预先排序的数据框是否可行? - aram_walker
据我所知,groupby 不会改变非 groupby 行的顺序。也就是说,类似于对 groupby 变量进行稳定排序(归并排序)。但我没有任何严格的证明,如果您发现相反的证据,请注意指出。 - JohnE

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

基于“用户”自上一行以来是否更改以及“时间”值的差异是否大于1,生成“newcol1”和“newcol2”。

分别计算这些内容:

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%的情况下不需要使用,但请参见文档中提高性能部分获取技巧。

1
在你的问题中,似乎你想要逐行成对迭代。你可以先尝试这样做:
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仍然太慢,我建议这样做:
  • Create a user_id column from you unicode names using pd.unique(User) and mapping the name with a dictionary to integer ids.

  • Create a delta dataframe: to a shifted dataframe with the user_id and time column you substract the original dataframe.

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

如果user_id>0,这意味着用户在连续的两行中发生了更改。时间列可以直接使用delta [delta ['time'>1]]进行过滤。 使用此delta数据框逐行记录更改。您可以将其用作掩码,从原始数据框中更新所需的列。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接