Numpy.median.reduceat için hızlı alternatif


12

Bu cevaba ilişkin olarak , eşit sayıda öğeye sahip grupları olan bir dizi üzerinde medyanları hesaplamanın hızlı bir yolu var mı?

Örneğin:

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,    ... ]

Sonra (örneğin Medyan grubun sayısı ve grup başına medyan arasındaki farkı hesaplamak istiyoruz 0olan 1.025ilk sonuç bu yüzden 1.00 - 1.025 = -0.025). Yukarıdaki dizi için sonuçlar şu şekilde görünecektir:

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

Yana np.median.reduceat(henüz) yok, bunu başarmak için başka hızlı yol var? Dizim milyonlarca satır içerdiğinden hız çok önemlidir!

Endekslerin bitişik ve sıralı olduğu varsayılabilir (eğer değillerse dönüştürmek kolaydır).


Performans karşılaştırmaları için örnek veriler:

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]

Eğer zaman aşımına mı scipy.ndimage.medianbağlantılı yanıtında öneri? Bana öyle geliyor ki, etiket başına eşit sayıda öğeye ihtiyaç duyuyor. Yoksa bir şey mi kaçırdım?
Andras Deak

Milyonlarca satır söylediğinizde, gerçek veri kümeniz bir 2D dizisi mi ve bu işlemi bu satırların her birinde mi yapıyorsunuz?
Divakar

@Divakar Verileri test etmek için soruya bakınız
Jean-Paul

İlk verilerde zaten kıyaslama yaptınız, formatı aynı tutmak için şişirdim. Her şey şişirilmiş veri kümemle karşılaştırıldı. Şimdi değiştirmek mantıklı değil
roganjosh

Yanıtlar:


7

Bazen eğer olmayan deyimsel numpy kod yazmak gerekir gerçekten sen yerli Numpy ile yapamaz senin hesaplama hızlandırmak istiyoruz.

numbaPython kodunuzu düşük seviyeli C'ye derler. Çok sayıda numpy'nin kendisi genellikle C kadar hızlı olduğundan, probleminiz numpy ile yerel vektörleşmeye katkıda bulunmuyorsa, bu çoğunlukla yararlı olur. Bu bir örnektir (burada indekslerin bitişik ve sıralı olduğu varsayılmıştır, ayrıca örnek verilere de yansıtılır):

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'un %timeitbüyüsünü kullanan bazı zamanlamalar :

>>> %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)

Sorudaki güncellenmiş örnek verileri kullanarak bu sayılar (yani Python işlevinin çalışma zamanı ve JIT hızlandırmalı işlevin çalışma zamanı karşılaştırması)

>>> %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)

Bu, hızlandırılmış kodu kullanarak küçük durumda 65x hızlanma ve daha büyük durumda 26x hızlanma (elbette yavaş döngüsel kodla karşılaştırıldığında) anlamına gelir. Diğer bir tersi ise (yerel numpy ile tipik vektörleştirmenin aksine) bu hıza ulaşmak için ek belleğe ihtiyacımız olmadı, tamamen optimize edilmiş ve derlenmiş düşük seviyeli kodla ilgili.


Yukarıdaki işlev, numpy int dizilerinin varsayılan olarak olduğunu varsayar; int64bu aslında Windows için geçerli değildir. Bu yüzden bir alternatif, numba.njituygun zamanda tam zamanında derlemeyi tetikleyerek imzayı çağrıdan kaldırmaktır . Ancak bu, işlevin ilk yürütme sırasında derleneceği anlamına gelir, bu da zamanlama sonuçlarıyla karışabilir (temsili veri türlerini kullanarak işlevi bir kez manuel olarak yürütebiliriz veya sadece ilk zamanlama yürütmesinin çok daha yavaş olacağını kabul edebiliriz. yok sayılmak). Tam olarak derlemeyi tetikleyen bir imza belirterek tam olarak önlemeye çalıştım.

Her neyse, düzgün bir JIT durumunda ihtiyacımız olan dekoratör

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

Jit derleme işlevi için gösterdiğim yukarıdaki zamanlamaların yalnızca işlev derlendikten sonra geçerli olduğunu unutmayın. Bu ya tanımda (istekli derleme, açık bir imza iletildiğinde numba.njit) ya da ilk fonksiyon çağrısı sırasında (tembel derleme ile, hiçbir imza iletilmediğinde numba.njit) olur. Eğer fonksiyon sadece bir kez uygulanacaksa, bu yöntemin hızı için derleme süresi de dikkate alınmalıdır. Genellikle derleme + yürütme toplam süresi derlenmemiş çalışma zamanından daha azsa (yalnızca yerel python işlevinin çok yavaş olduğu yukarıdaki durumda doğrudur) derleme işlevlerine değer. Bu çoğunlukla derlenmiş işlevinizi birçok kez çağırdığınızda olur.

As max9111 bir yorumda kaydetti biri önemli özelliği numbaolan cacheanahtar kelime için jit. Geçme cache=Trueiçin numba.jit, diske derlenen fonksiyonu depolar böylece uzun vadede orada ziyade recompiled yine yedek hangi çalışma zamanı verilen piton modülünün sonraki yürütme sırasında işlev yüklenecek söyledi.


@Divakar, endekslerin bitişik ve sıralı olduğunu varsayar; bu OP'nin verilerinde bir varsayım gibi görünmektedir ve aynı zamanda otomatik olarak roganjosh'un indexverilerine dahil edilir . Bu konuda bir not bırakacağım, teşekkürler :)
Andras Deak

Tamam, bitişiklik otomatik olarak dahil edilmez ... ama yine de bitişik olması gerektiğinden eminim. Hmm ...
Andras Deak

1
@AndrasDeak Etiketlerin bitişik ve sıralı olduğunu varsaymak gerçekten iyi (yine de kolay değilse sabitleme)
Jean-Paul

1
@AndrasDeak Verileri test etmek için soruya yönelik düzenlemeye bakın (sorular arasında performans karşılaştırmaları tutarlı olacak şekilde)
Jean-Paul

1
cache=TrueTercümanın her yeniden başlatılmasında yeniden derlenmeyi önlemek için anahtar kelimeden bahsedebilirsiniz .
max9111

5

Bir yaklaşım Pandasburada sadece faydalanmak için kullanmak olacaktır groupby. Zamanlamaları daha iyi anlamak için giriş boyutlarını biraz şişirdim (çünkü DF oluşturmada ek yük var).

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']

Aşağıdakileri verir timeit:

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

Aynı örnek boyutu için, Aryerez'in diktecilik yaklaşımını şöyle elde ederim :

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

Bununla birlikte, girdileri 10 kat daha artırırsak, zamanlamalar şöyle olur:

%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)

Ancak, bazı dürüstlüğü etiket haline pahasına tarafından cevap Divakar saf numpy kullanarak saatinde geldiğini:

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

Yeni veri kümesinin ışığında (gerçekten başlangıçta ayarlanmış olması gerekir):

%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)

Bu cevap için teşekkürler! Diğer yanıtlarla tutarlılık için sorumu düzenlemede verilen örnek veriler üzerinde çözümlerinizi test edebilir misiniz?
Jean-Paul

@ Jean-Paul zamanlamalar zaten tutarlı, değil mi? İlk kıyaslama verilerimi kullandılar ve yapmadığı durumlarda, onlar için zamanlamaları aynı kıyaslama ile
sağladım

Divakar'ın cevabına zaten bir referans eklediğinizi de göz ardı ettim, bu yüzden cevabınız gerçekten farklı yaklaşımlar arasında güzel bir karşılaştırma yapıyor, bunun için teşekkürler!
Jean-Paul

1
@ Jean-Paul Aslında işleri oldukça büyük ölçüde değiştirdiği için en son zamanlamaları ekledim
roganjosh

1
Soruyu gönderirken test setini eklememekten dolayı özür dileriz, test sonuçlarını halen eklediğiniz için teşekkür ederiz! Teşekkürler!!!
Jean-Paul

4

Belki bunu zaten yaptınız, ancak değilse, bunun yeterince hızlı olup olmadığına bakın:

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

Çıktı:

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

Bariz olduğu kaçınılmaz, np.vectorizebir olduğunu çok bir döngü için ince sarıcı, bu yüzden bu yaklaşım özellikle hızlı olmasını beklemek değildir.
Andras Deak

1
@AndrasDeak Katılmıyorum :) Takip etmeye devam edeceğim ve birisi daha iyi bir çözüm gönderirse silerim.
Aryerez

1
Daha hızlı yaklaşımlar açılsa bile silmeniz gerekeceğini sanmıyorum :)
Andras Deak

Tanımlamak çünkü @roganjosh Bu muhtemelen datave indexolarak np.arrays söz konusu olduğu gibi.
Aryerez

1
@ Jean-Paul roganjosh benimki ve yöntemleri arasında bir zaman karşılaştırması yaptı ve diğerleri buradakileri karşılaştırdı. Bilgisayar donanımına bağlıdır, bu yüzden herkesin kendi yöntemlerini kontrol etmesinin bir anlamı yoktur, ancak burada en yavaş çözümü bulduğum anlaşılıyor.
Aryerez

4

İşte pozitif bidonlar / indeks değerleri için binned medyan almak için NumPy tabanlı bir yaklaşım -

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

Çıkarılan özel durumumuzu çözmek için -

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

Çok güzel bir cevap! Örneğin hız artışıyla ilgili bir belirtiniz var df.groupby('index').transform('median')mı?
Jean-Paul

@ Jean-Paul Milyonlarca gerçek veri setinizi test edebilir misiniz?
Divakar

Test verileri için soruya bakınız
Jean-Paul

@ Jean-Paul Çözümümü daha basit bir çözüm için düzenledi. Bu işlemi test etmek için kullandığınızdan emin olun.
Divakar
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.