【问题标题】:Fastest way to compare row and previous row in pandas dataframe with millions of rows将熊猫数据框中的行和上一行与数百万行进行比较的最快方法
【发布时间】:2015-06-09 09:48:32
【问题描述】:

我正在寻找加速我编写的函数的解决方案,该函数用于循环遍历 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

目前我有一个函数可以循环并根据“User”自上一行以来是否发生变化以及“newcol1”和“newcol2”的值'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,这对于快速比较可能会有问题)

【问题讨论】:

  • 拥有一百万行,为什么不使用 Python 可以轻松连接的专用数据库,例如 MySQL 或 SQLlite?关系数据库可以使用 if/then 逻辑运行复杂的 SQL 查询,用于通过索引连接的行间比较。它们旨在扩展到数百万行。甚至可以设置触发器,因此可以在任何用户更改时更新特定列。

标签: python performance pandas bigdata cython


【解决方案1】:

我的想法与安迪相同,只是添加了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 方法。

【讨论】:

  • 非常感谢(JohnE 和@Andy),我实现了这两个解决方案,groupby 和提取 Col1 的第一个元素特别有用,现在在整个数据集上运行大约需要 3 分钟 - 非常高兴! :)
  • 重要的问题是:我们如何保证.shift() 函数准确地移动排序的数据?或者使用预先排序的数据框可以工作?
  • @aram_walker 据我所知,groupby 不会更改非 groupby 行的顺序。 IE。类似于 groupby 变量上的稳定排序(合并排序)。但是我没有任何严格的证据证明这一点,如果您看到相反的证据,请注意。
【解决方案2】:

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

'newcol1' 和 'newcol2' 基于 'User' 自上一行以来是否发生变化,以及 'Time' 值的差异是否大于 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 以获取提示。

【讨论】:

    【解决方案3】:

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

    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(User) 从您的 unicode 名称创建一个 user_id 列,并将名称与字典映射到整数 id。

    • 创建一个增量数据帧:使用 user_id 和 time 列减去原始数据帧。

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-12-13
      • 1970-01-01
      • 1970-01-01
      • 2014-11-07
      • 1970-01-01
      • 2017-02-28
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多