web-dev-qa-db-ja.com

Pandas DataFrameの行ごとのネストされた(double)反復

こんにちは私は反復問題のベクトル化された(またはより効率的な)解決策を見つけようとしています。私が見つけた唯一の解決策は、複数のループを持つDataFrameの行ごとの反復を必要とします。実際のデータファイルは巨大であるため、私の現在のソリューションは実質的に実行不可能です。ご覧になりたい場合は、最後にラインプロファイラー出力を含めました。実際の問題は非常に複雑なので、簡単な例でこれを説明しようと思います(簡単にするためにかなりの時間がかかりました:)):

2つの着陸帯が並んでいる空港があるとします。各飛行機は着陸し(到着時間)、しばらくの間着陸帯の1つにタクシーを乗せ、その後離陸します(出発時間)。すべてがPandas DataFrameに格納され、次のように到着時間でソートされます(テスト用のより大きなデータセットについてはEDIT2を参照):

PLANE   STRIP   ARRIVAL   DEPARTURE
0       1       85.00     86.00
1       1       87.87     92.76
2       2       88.34     89.72
3       1       88.92     90.88
4       2       90.03     92.77
5       2       90.27     91.95
6       2       92.42     93.58
7       2       94.42     95.58

2つのケースの解決策を探しています:

1。一度に1つのストリップに複数の平面が存在するイベントのリストを作成します。イベントのサブセットを含めないでください(たとえば、有効な[3,4,5]ケースがある場合は[3,4]を表示しません)。リストには、実際のDataFrame行のインデックスを格納する必要があります。この場合の解決策については、関数findSingleEvents()を参照してください(約5ミリ秒実行)。

2。各ストリップに一度に少なくとも1つの平面があるイベントのリストを作成します。イベントのサブセットをカウントせず、最大数のプレーンでイベントを記録するだけです。 (たとえば、[3,4,5]の場合は、[3,4]を表示しません)。単一のストリップで完全に発生するイベントをカウントしないでください。リストには、実際のDataFrame行のインデックスを格納する必要があります。この場合の解決策については、関数findMultiEvents()を参照してください(約15ミリ秒実行)。

作業コード:

import numpy as np
import pandas as pd
import itertools
from __future__ import division

data =  [{'PLANE':0, 'STRIP':1, 'ARRIVAL':85.00, 'DEPARTURE':86.00},
         {'PLANE':1, 'STRIP':1, 'ARRIVAL':87.87, 'DEPARTURE':92.76},
         {'PLANE':2, 'STRIP':2, 'ARRIVAL':88.34, 'DEPARTURE':89.72},
         {'PLANE':3, 'STRIP':1, 'ARRIVAL':88.92, 'DEPARTURE':90.88},
         {'PLANE':4, 'STRIP':2, 'ARRIVAL':90.03, 'DEPARTURE':92.77},
         {'PLANE':5, 'STRIP':2, 'ARRIVAL':90.27, 'DEPARTURE':91.95},
         {'PLANE':6, 'STRIP':2, 'ARRIVAL':92.42, 'DEPARTURE':93.58},
         {'PLANE':7, 'STRIP':2, 'ARRIVAL':94.42, 'DEPARTURE':95.58}]

df = pd.DataFrame(data, columns = ['PLANE','STRIP','ARRIVAL','DEPARTURE'])

def findSingleEvents(df):
    events = []
    for row in df.itertuples():
        #Create temporary dataframe for each main iteration
        dfTemp = df[(row.DEPARTURE>df.ARRIVAL) & (row.ARRIVAL<df.DEPARTURE)]
        if len(dfTemp)>1:
            #convert index values to integers from long
            current_event = [int(v) for v in dfTemp.index.tolist()]
            #loop backwards to remove elements that do not comply
            for i in reversed(current_event):
                if (dfTemp.loc[i].ARRIVAL > dfTemp.DEPARTURE).any():
                    current_event.remove(i)
            events.append(current_event)
    #remove duplicate events
    events = map(list, set(map(Tuple, events)))
    return events

def findMultiEvents(df):
    events = []
    for row in df.itertuples():
        #Create temporary dataframe for each main iteration
        dfTemp = df[(row.DEPARTURE>df.ARRIVAL) & (row.ARRIVAL<df.DEPARTURE)]
        if len(dfTemp)>1:
            #convert index values to integers from long
            current_event = [int(v) for v in dfTemp.index.tolist()]
            #loop backwards to remove elements that do not comply
            for i in reversed(current_event):
                if (dfTemp.loc[i].ARRIVAL > dfTemp.DEPARTURE).any():
                    current_event.remove(i)
            #remove elements only on 1 strip
            if len(df.iloc[current_event].STRIP.unique()) > 1:
                events.append(current_event)
    #remove duplicate events
    events = map(list, set(map(Tuple, events)))
    return events

print findSingleEvents(df[df.STRIP==1])
print findSingleEvents(df[df.STRIP==2])
print findMultiEvents(df)

検証済み出力:

[[1, 3]]
[[4, 5], [4, 6]]
[[1, 3, 4, 5], [1, 4, 6], [1, 2, 3]]

明らかに、これらは効率的でもエレガントなソリューションでもありません。私が持っている巨大なDataFrameでは、これを実行するにはおそらく数時間かかります。かなり前からベクトル化されたアプローチを考えていましたが、しっかりしたものは何も思いつきませんでした。どんなポインタ/ヘルプも大歓迎です! Numpy/Cython/Numbaベースのアプローチも受け付けています。

ありがとう!

PS:リストをどうするか疑問に思ったら:各EVENTEVENT番号を割り当て、上記のデータをマージして個別のデータベースを構築します、およびEVENT番号は、別の列として使用され、他の目的で使用されます。ケース1の場合、次のようになります。

EVENT    PLANE   STRIP   ARRIVAL   DEPARTURE
0        4       2       90.03     92.77
0        5       2       90.27     91.95
1        5       2       90.27     91.95
1        6       2       92.42     95.58

編集:コードとテストデータセットを改訂しました。

EDIT2:以下のコードを使用して、テスト目的で1000行(またはそれ以上)の長さのDataFrameを生成します。 (@ImportanceOfBeingErnestの推奨による)

import random
import pandas as pd
import numpy as np

data =  []
for i in range(1000):
    arrival = random.uniform(0,1000)
    departure = arrival + random.uniform(2.0, 10.0)
    data.append({'PLANE':i, 'STRIP':random.randint(1, 2),'ARRIVAL':arrival,'DEPARTURE':departure})

df = pd.DataFrame(data, columns = ['PLANE','STRIP','ARRIVAL','DEPARTURE'])
df = df.sort_values(by=['ARRIVAL'])
df = df.reset_index(drop=True)
df.PLANE  = df.index

EDIT3:

受け入れられた回答の修正版。受け入れられた回答は、イベントのサブセットを削除できませんでした。変更されたバージョンは、「有効な[3,4,5]の場合がある場合、[3,4]を表示しない)」というルールを満たしています。

def maximal_subsets_modified(sets):
    sets.sort()
    maximal_sets = []
    s0 = frozenset()
    for s in sets:
        if not (s > s0) and len(s0) > 1:
            not_in_list = True
            for x in maximal_sets:
                if set(x).issubset(set(s0)):
                    maximal_sets.remove(x)
                if set(s0).issubset(set(x)):
                    not_in_list = False
            if not_in_list:
                maximal_sets.append(list(s0))
        s0 = s
    if len(s0) > 1:
        not_in_list = True
        for x in maximal_sets:
            if set(x).issubset(set(s0)):
                maximal_sets.remove(x)
            if set(s0).issubset(set(x)):
                not_in_list = False
        if not_in_list:
            maximal_sets.append(list(s0))
    return maximal_sets

def maximal_subsets_2_modified(sets, d):
    sets.sort()
    maximal_sets = []
    s0 = frozenset()
    for s in sets:
        if not (s > s0) and len(s0) > 1 and d.loc[list(s0), 'STRIP'].nunique() == 2:
            not_in_list = True
            for x in maximal_sets:
                if set(x).issubset(set(s0)):
                    maximal_sets.remove(x)
                if set(s0).issubset(set(x)):
                    not_in_list = False
            if not_in_list:
                maximal_sets.append(list(s0))
        s0 = s
    if len(s0) > 1 and d.loc[list(s), 'STRIP'].nunique() == 2:
        not_in_list = True
        for x in maximal_sets:
            if set(x).issubset(set(s0)):
                maximal_sets.remove(x)
            if set(s0).issubset(set(x)):
                not_in_list = False
        if not_in_list:
            maximal_sets.append(list(s0))
    return maximal_sets

# single

def hal_3_modified(d):
    sets = np.apply_along_axis(
        lambda x: frozenset(d.PLANE.values[(d.PLANE.values <= x[0]) & (d.DEPARTURE.values > x[2])]), 
        1, d.values
    )
    return maximal_subsets_modified(sets)

# multi

def hal_5_modified(d):
    sets = np.apply_along_axis(
        lambda x: frozenset(d.PLANE.values[(d.PLANE.values <= x[0]) & (d.DEPARTURE.values > x[2])]), 
        1, d.values
    )
    return maximal_subsets_2_modified(sets, d)
11
marillion

反復する代わりに_DataFrame.apply_を使用してソリューションを書き直し、最適化として可能な限りnumpy配列を使用しました。 frozensetを使用したのは、それらが不変でハッシュ可能であり、したがって_Series.unique_が正しく機能するためです。 _Series.unique_は、タイプsetの要素で失敗します。

また、d.loc[list(x), 'STRIP'].nunique()d.loc[list(x)].STRIP.nunique()よりもわずかに速いことがわかりました。理由はわかりませんが、以下のソリューションでより高速なステートメントを使用しました。

平易な英語のアルゴリズム:

各行について、出発が現在の到着よりも大きい現在のインデックスのインデックスよりも低い(または等しい)インデックスのセットを作成します。これにより、セットのリストが作成されます。

他のセットのサブセットではない一意のセットを返します(2番目のアルゴリズムでは、両方のSTRIPsがセットによって参照されるようにさらにフィルター処理します)

(更新)2番目の改善:

numpyレイヤーにドロップダウンし、df.applyを使用する代わりに_np.apply_along_axis_を使用することで、1つの小さな改善が行われました。 PLANEは常にデータフレームインデックスと等しく、_df.values_を使用して基になる行列にアクセスできるため、これが可能です。

最大のサブセットを返すリスト内包表記で大きな改善が見られました

_[list(x) for x in sets if ~np.any(sets > x)]
_

上記はO(n ^ 2)の順序演算です。小さなデータセットでは、これは非常に高速です。ただし、より大きなデータセットでは、このステートメントがボトルネックになります。これを最適化するには、最初にsetsを並べ替え、要素をもう一度ループして最大のサブセットを見つけます。ソートしたら、elem [n]がelem [n + 1]のサブセットではないことを確認して、elem [n]が最大のサブセットであるかどうかを判断するだけで十分です。ソート手順は、2つの要素を_<_操作と比較します

タイミング:

私の元の実装では、OPの試みと比較してパフォーマンスが大幅に向上しましたが、次のグラフに示すように、アルゴリズムは指数関数的に順序付けられていました。

findMultiEvents、_hal_2_、および_hal_5_のタイミングのみを示します。 findSinglEvents、_hal_1_、および_hal_3_の相対的なパフォーマンスも同様に同等です。

algorithm execution time ~ input sizes

下にスクロールして、ベンチマークコードを確認してください。

findMumtiEvents&_hal_2_のベンチマークを、指数因子によって効率が低いことが明らかになったときに停止したことに注意してください

実装


改善された実装:

_def maximal_subsets(sets):
    sets.sort()
    maximal_sets = []
    s0 = frozenset()
    for s in sets[::-1]:
        if s0 > s or len(s) < 2:
            continue
        maximal_sets.append(list(s))
        s0 = s
    return maximal_sets

def maximal_subsets_2(sets, d):
    sets.sort()
    maximal_sets = []
    s0 = frozenset()
    for s in sets[::-1]:
        if s0 > s or len(s) < 2 or d.loc[list(s), 'STRIP'].nunique() < 2:
            continue
        maximal_sets.append(list(s))
        s0 = s
    return maximal_sets

# single
def hal_3(d):
    sets = np.apply_along_axis(
        lambda x: frozenset(d.PLANE.values[(d.PLANE.values <= x[0]) & (d.DEPARTURE.values > x[2])]), 
        1, d.values
    )
    return maximal_subsets(sets)
# multi
def hal_5(d):
    sets = np.apply_along_axis(
        lambda x: frozenset(d.PLANE.values[(d.PLANE.values <= x[0]) & (d.DEPARTURE.values > x[2])]), 
        1, d.values
    )
    return maximal_subsets_2(sets, d)
_

元の実装:

_# findSingleEvents
def hal_1(d):
    sets = d.apply(
       lambda x: frozenset(
           d.index.values[(d.index.values <= x.name) & (d.DEPARTURE.values > x.ARRIVAL)]
       ),
       axis=1
    ).unique()
    return [list(x) for x in sets if ~np.any(sets > x) and len(x) > 1]

# findMultiEvents
def hal_2(d):
    sets = d.apply(
        lambda x: frozenset(
            d.index.values[(d.index.values <= x.name) & (d.DEPARTURE.values > x.ARRIVAL)]
        ),
        axis=1
    ).unique()
    return [list(x) for x in sets 
            if ~np.any(sets > x) and
               len(x) > 1 and 
               d.loc[list(x), 'STRIP'].nunique() == 2]
_

出力:

出力はOPの実装と同じです。

_hal_1(df[df.STRIP==1])
[[1, 3]]
hal_1(df[df.STRIP==2])
[[4, 5], [4, 6]]
hal_2(df)
[[1, 2, 3], [1, 3, 4, 5], [1, 4, 6]]
hal_3(df[df.STRIP==1])
[[1, 3]]
hal_3(df[df.STRIP==2])
[[4, 5], [4, 6]]
hal_5(df)
[[1, 2, 3], [1, 3, 4, 5], [1, 4, 6]]
_

テストシステムの詳細:

_os: windows 10
python: 3.6 (Anaconda)
pandas: 0.22.0
numpy: 1.14.3
_

ベンチマークコード:


_import random

def mk_random_df(n):
    data =  []
    for i in range(n):
        arrival = random.uniform(0,1000)
        departure = arrival + random.uniform(2.0, 10.0)
        data.append({'PLANE':i, 'STRIP':random.randint(1, 2),'ARRIVAL':arrival,'DEPARTURE':departure})

    df = pd.DataFrame(data, columns = ['PLANE','STRIP','ARRIVAL','DEPARTURE'])
    df = df.sort_values(by=['ARRIVAL'])
    df = df.reset_index(drop=True)
    df.PLANE = df.index
    return df

dfs = {i: mk_random_df(100*(2**i)) for i in range(0, 10)}
times, times_2, times_5 = [], [], []

for i, v in dfs.items():
    if i < 5:
        t = %timeit -o -n 3 -r 3 findMultiEvents(v)
        times.append({'size(pow. of 2)': i, 'timings': t})

for i, v in dfs.items():
    t = %timeit -o -n 3 -r 3 hal_5(v)
    times_5.append({'size(pow. of 2)': i, 'timings': t})

for i, v in dfs.items():
    if i < 9:
        t = %timeit -o -n 3 -r 3 hal_2(v)
        times_2.append({'size(pow. of 2)': i, 'timings': t})
_
1
Haleemur Ali