numpy.median.reduceatの高速代替


12

この回答に関連し、要素数が等しくないグループを持つ配列の中央値を計算する高速な方法はありますか?

例えば:

data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67, ... ]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3,    ... ]

そして私は、(例えば、中央値グループの数やグループごとの中央値との差異を計算したい0です1.025最初の結果があるので1.00 - 1.025 = -0.025)。したがって、上記の配列の場合、結果は次のようになります。

result = [-0.025, 0.025, 0.05, -0.05, -0.19, 0.29, 0.00, 0.10, -0.10, ...]

np.median.reduceat(まだ)存在しないので、これを達成する別の高速な方法はありますか?私のアレイには数百万の行が含まれるため、速度は非常に重要です。

インデックスは連続していて順序付けられていると見なすことができます(そうでない場合は簡単に変換できます)。


パフォーマンス比較のデータ例:

import numpy as np

np.random.seed(0)
rows = 10000
cols = 500
ngroup = 100

# Create random data and groups (unique per column)
data = np.random.rand(rows,cols)
groups = np.random.randint(ngroup, size=(rows,cols)) + 10*np.tile(np.arange(cols),(rows,1))

# Flatten
data = data.ravel()
groups = groups.ravel()

# Sort by group
idx_sort = groups.argsort()
data = data[idx_sort]
groups = groups[idx_sort]

scipy.ndimage.medianリンクされた回答で提案を時間調整しましたか?ラベルごとに同じ数の要素が必要だとは思えません。または私は何かを逃したのですか?
Andras Deak

では、数百万行と言ったとき、実際のデータセットは2D配列であり、これらの各行でこの操作を実行していますか?
Divakar

@Divakarテストデータについては、質問の編集を参照してください
Jean-Paul

あなたはすでに初期データでベンチマークを与えました、私はフォーマットを同じに保つためにそれを膨らませました。すべては私の膨らんだデータセットに対してベンチマークされています。今すぐ変更するのは妥当ではありません
roganjosh '10 / 11/19

回答:


7

ネイティブnumpyでは実行できない計算を本当に高速化したい場合は、非慣用的なnumpyコードを記述する必要がある場合があります。

numbaPythonコードを低レベルのCにコンパイルします。多くのnumpy自体は通常Cと同じくらい高速であるため、問題がnumpyを使用したネイティブのベクトル化に向いていない場合、これはほとんど役に立ちます。これは1つの例です(ここでは、インデックスが隣接してソートされていると想定しましたが、これもサンプルデータに反映されています)。

import numpy as np
import numba

# use the inflated example of roganjosh https://stackoverflow.com/a/58788534
data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3] 

data = np.array(data * 500) # using arrays is important for numba!
index = np.sort(np.random.randint(0, 30, 4500))               

# jit-decorate; original is available as .py_func attribute
@numba.njit('f8[:](f8[:], i8[:])') # explicit signature implies ahead-of-time compile
def diffmedian_jit(data, index): 
    res = np.empty_like(data) 
    i_start = 0 
    for i in range(1, index.size): 
        if index[i] == index[i_start]: 
            continue 

        # here: i is the first _next_ index 
        inds = slice(i_start, i)  # i_start:i slice 
        res[inds] = data[inds] - np.median(data[inds]) 

        i_start = i 

    # also fix last label 
    res[i_start:] = data[i_start:] - np.median(data[i_start:])

    return res

そして、IPythonの%timeit魔法を使用したいくつかのタイミングを次に示します。

>>> %timeit diffmedian_jit.py_func(data, index)  # non-jitted function
... %timeit diffmedian_jit(data, index)  # jitted function
...
4.27 ms ± 109 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
65.2 µs ± 1.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

問題の更新されたサンプルデータを使用すると、これらの数値(つまり、Python関数のランタイムとJIT加速機能のランタイムの比較)は次のようになります。

>>> %timeit diffmedian_jit.py_func(data, groups) 
... %timeit diffmedian_jit(data, groups)
2.45 s ± 34.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
93.6 ms ± 518 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

これは、加速されたコードを使用すると、小さなケースでは65倍のスピードアップ、大きなケースでは26倍のスピードアップになります(もちろん、ルーピーな低速コードと比較して)。もう1つの利点は、(ネイティブnumpyを使用した通常のベクトル化とは異なり)この速度を実現するために追加のメモリを必要としないことです。すべてが最適化され、コンパイルされた低レベルコードが実行されることになります。


上記の関数は、numpy int配列がint64デフォルトであると想定していますが、これは実際にはWindowsでは当てはまりません。したがって、代替手段は、への呼び出しから署名を削除してnumba.njit、適切なジャストインタイムコンパイルをトリガーすることです。しかし、これは最初の実行中に関数がコンパイルされ、タイミング結果に干渉する可能性があることを意味します(関数を手動で1回実行して、代表的なデータ型を使用するか、最初のタイミング実行がはるかに遅くなることを受け入れるだけで、無視されます)。これは、シグネチャを指定して事前にコンパイルをトリガーすることで防止しようとしたものとまったく同じです。

とにかく、適切なJITの場合、必要なデコレーターはただ

@numba.njit
def diffmedian_jit(...):

上記のjitコンパイル済み関数のタイミングは、関数がコンパイルされた後にのみ適用されることに注意してください。これは、定義時に(明示的なシグネチャがに渡されるときの熱心なコンパイルでnumba.njit)、または最初の関数呼び出し中に行われます(遅延コンパイルで、シグネチャが渡されないときnumba.njit)。関数が1回だけ実行される場合は、コンパイル時間もこのメソッドの速度を考慮する必要があります。通常、コンパイル+実行の合計時間がコンパイルされていないランタイムよりも短い場合にのみ、関数をコンパイルする価値があります(これは、ネイティブpython関数が非常に遅い上記の場合に実際に当てはまります)。これは主に、コンパイルした関数を何度も呼び出すときに発生します。

MAX9111はコメントで述べたように、のいずれかの重要な特徴は、numbaあるcacheキーワードjitcache=Trueto numba.jitを渡すと、コンパイルされた関数がディスクに保存されるため、指定されたpythonモジュールの次回の実行時に、関数は再コンパイルされるのではなく、そこから読み込まれます。これにより、長期的にランタイムを節約できます。


@Divakarは確かに、OPのデータでは仮定のように見え、また自動的にroganjoshのindexデータにも含まれるように、インデックスが連続してソートされていることを前提としています。私はこれについてメモを残します、ありがとう:)
Andras Deak '10

OK、連続性は自動的に含まれません...しかし、とにかくそれは連続している必要があると確信しています。うーん...
Andras Deak

1
@AndrasDeakラベルが隣接し、並べ替えられていると仮定することは本当に問題ありません(とにかく簡単でない場合は修正する)
Jean-Paul

1
@AndrasDeakテストデータの質問の編集を参照してください(質問間のパフォーマンス比較が一貫しているなど)
Jean-Paul

1
cache=Trueインタプリタを再起動するたびに再コンパイルすることを避けるために、キーワードを言及することができます。
max9111

5

1つのアプローチは、Pandasここで純粋にを利用することですgroupby。タイミングをよりよく理解するために、入力サイズを少し大きくしました(DFの作成にはオーバーヘッドがあるため)。

import numpy as np
import pandas as pd

data =  [1.00, 1.05, 1.30, 1.20, 1.06, 1.54, 1.33, 1.87, 1.67]
index = [0,    0,    1,    1,    1,    1,    2,    3,    3]

data = data * 500
index = np.sort(np.random.randint(0, 30, 4500))

def df_approach(data, index):
    df = pd.DataFrame({'data': data, 'label': index})
    df['median'] = df.groupby('label')['data'].transform('median')
    df['result'] = df['data'] - df['median']

次のようになりますtimeit

%timeit df_approach(data, index)
5.38 ms ± 50.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

同じサンプルサイズの場合、Aryerezdictアプローチは次のようになります。

%timeit dict_approach(data, index)
8.12 ms ± 3.47 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

ただし、入力をさらに10倍に増やすと、タイミングは次のようになります。

%timeit df_approach(data, index)
7.72 ms ± 85 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit dict_approach(data, index)
30.2 ms ± 10.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

ただし、ある程度の信頼性を犠牲にして、純粋なnumpyを使用したDivakarの答えは次のとおりです。

%timeit bin_median_subtract(data, index)
573 µs ± 7.48 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

新しいデータセット(実際には最初に設定されているはずです)を考慮して:

%timeit df_approach(data, groups)
472 ms ± 2.52 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit bin_median_subtract(data, groups) #https://stackoverflow.com/a/58788623/4799172
3.02 s ± 31.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit dict_approach(data, groups) #https://stackoverflow.com/a/58788199/4799172
<I gave up after 1 minute>

# jitted (using @numba.njit('f8[:](f8[:], i4[:]') on Windows) from  https://stackoverflow.com/a/58788635/4799172
%timeit diffmedian_jit(data, groups)
132 ms ± 3.12 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

この回答ありがとうございます!他の回答との一貫性を保つために、私の質問の編集で提供されたサンプルデータでソリューションをテストできますか?
Jean-Paul

@ Jean-Paulタイミングはすでに一貫していますか?彼らは私の最初のベンチマークデータを使用しましたが、使用しなかった場合は、同じベンチマークで彼らにタイミングを提供しました
roganjosh

私もあなたがDivakarの回答への参照をすでに追加しているのを見逃していたので、あなたの回答は確かにすでにさまざまなアプローチをうまく比較しています。
Jean-Paul

1
@ Jean-Paul実際に物事をかなり劇的に変えたので、下部に最新のタイミングを追加しました
roganjosh

1
質問を投稿するときにテストセットを追加しなかったことをお詫びします。今でもテスト結果を追加していることを高く評価します!ありがとう!!!
Jean-Paul

4

多分あなたはすでにこれをしましたが、もしそうでなければ、それが十分に速いかどうかを見てください:

median_dict = {i: np.median(data[index == i]) for i in np.unique(index)}
def myFunc(my_dict, a): 
    return my_dict[a]
vect_func = np.vectorize(myFunc)
median_diff = data - vect_func(median_dict, index)
median_diff

出力:

array([-0.025,  0.025,  0.05 , -0.05 , -0.19 ,  0.29 ,  0.   ,  0.1  ,
   -0.1  ])

明白なことを述べるのリスクが、np.vectorizeある非常にループのために薄いラッパーので、私はこのアプローチは、特に高速であることを期待していません。
Andras Deak

1
@AndrasDeak私は同意しません:)私はフォローし続けます、そして誰かがより良い解決策を投稿するならば、私はそれを削除します。
Aryerez

1
より高速なアプローチがポップアップしても、削除する必要はないと思います:)
Andras Deak

あなたが定義されていませんでしたので、@roganjoshそれはおそらくだdataindexなどnp.arrayの質問のように。
Aryerez

1
@ Jean-Paul roganjoshは私の方法と彼の方法を時間比較し、他の方法はここで比較しました。それはコンピュータのハードウェアに依存しているので、誰もが自分の方法をチェックする意味はありませんが、私がここで最も遅い解決策を思いついたようです。
Aryerez

4

これは、正のビン/インデックス値のビン中央値を取得するNumPyベースのアプローチです-

def bin_median(a, i):
    sidx = np.lexsort((a,i))

    a = a[sidx]
    i = i[sidx]

    c = np.bincount(i)
    c = c[c!=0]

    s1 = c//2

    e = c.cumsum()
    s1[1:] += e[:-1]

    firstval = a[s1-1]
    secondval = a[s1]
    out = np.where(c%2,secondval,(firstval+secondval)/2.0)
    return out

差し引かれた特定のケースを解決するには-

def bin_median_subtract(a, i):
    sidx = np.lexsort((a,i))

    c = np.bincount(i)

    valid_mask = c!=0
    c = c[valid_mask]    

    e = c.cumsum()
    s1 = c//2
    s1[1:] += e[:-1]
    ssidx = sidx.argsort()
    starts = c%2+s1-1
    ends = s1

    starts_orgindx = sidx[np.searchsorted(sidx,starts,sorter=ssidx)]
    ends_orgindx  = sidx[np.searchsorted(sidx,ends,sorter=ssidx)]
    val = (a[starts_orgindx] + a[ends_orgindx])/2.
    out = a-np.repeat(val,c)
    return out

とてもいい答えです!速度の向上などの兆候はありdf.groupby('index').transform('median')ますか?
Jean-Paul

@ Jean-Paul数百万の実際のデータセットをテストできますか?
Divakar

テストデータについては、質問の編集を参照してください
Jean-Paul

@ Jean-Paul私のソリューションをより簡単なものに編集しました。テストする場合は、必ずこれを使用してください。
Divakar
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.