web-dev-qa-db-ja.com

pandas何百万行のデータフレームで行と前の行を比較する最速の方法

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ノートブックで実際にやっていることとかなり似ています):

 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を使用して高速化するか、関数がデータフレームの1行のみを読み書きするように「diff」値を含む新しい列を作成する必要があります一度に、Cythonの使用から利益を得るために、他の速度のトリックは大歓迎です!

(.locの使用に関しては、.loc、.iloc、および.ixを比較しましたが、これはわずかに高速であったため、現在それを使用している唯一の理由です)

(また、実際の私のUser列はintではなくunicodeであり、迅速な比較には問題があるかもしれません)

16
AdO

私はAndyと同じ方針で、groupbyを追加しただけで考えていましたが、これはAndyの答えを補完するものだと思います。 groupbyを追加すると、diffまたはshiftを実行するたびに最初の行に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のポイントへのフォローアップとして、ここで行ったことは、リスト列の最初の要素を抽出することであったことに注意してください(そして、シフトされたバージョンも追加します)。このようにすると、高価な抽出を1回行うだけで、その後は標準のpandasメソッドに固執することができます。

13
JohnE

pandas(constructs)を使用してコードをベクトル化します。つまり、ループには使用せず、代わりに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ドキュメントのパフォーマンスセクション ヒント

8
Andy Hayden

あなたの問題では、ペアで行を反復したいようです。最初にできることは次のようなものです。

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)を使用してユニコード名からuser_id列を作成し、辞書を使用して名前を整数IDにマッピングします。

  • デルタデータフレームを作成します。user_idとtime列を使用してシフトされたデータフレームに、元のデータフレームを減算します。

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

User_id> 0の場合、ユーザーが2つの連続した行で変更されたことを意味します。時間列は、delta [delta ['time'> 1]]で直接フィルタリングできます。このデルタデータフレームを使用すると、行ごとに変更を記録できます。これをマスクとして使用して、元のデータフレームから必要な列を更新できます。

1
Kikohs