numpy 1D dizisi: n kereden fazla yinelenen maske öğeleri


18

gibi bir tamsayı dizisi verildi

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

NDefalarca yinelenen öğeleri maskelemem gerekiyor . Açıklığa kavuşturmak için: birincil hedef, daha sonra binning hesaplamaları için kullanmak üzere, boole maske dizisini almaktır.

Oldukça karmaşık bir çözüm buldum

import numpy as np

bins = np.array([1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5])

N = 3
splits = np.split(bins, np.where(np.diff(bins) != 0)[0]+1)
mask = []
for s in splits:
    if s.shape[0] <= N:
        mask.append(np.ones(s.shape[0]).astype(np.bool_))
    else:
        mask.append(np.append(np.ones(N), np.zeros(s.shape[0]-N)).astype(np.bool_)) 

mask = np.concatenate(mask)

vermek örneğin

bins[mask]
Out[90]: array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])

Bunu yapmanın daha güzel bir yolu var mı?

DÜZENLE, # 2

Cevaplar için çok teşekkürler! İşte MSeifert'in referans grafiğinin ince bir versiyonu. Beni işaret ettiğin için teşekkürler simple_benchmark. Yalnızca en hızlı 4 seçenek gösteriliyor: resim açıklamasını buraya girin

Sonuç

Florian H tarafından değiştirilen, Paul Panzer tarafından değiştirilen fikir , bu sorunu çözmenin harika bir yolu gibi görünüyor, çünkü bu oldukça basit ve numpysadece. numbaBununla birlikte kullanmakta sorun yaşıyorsanız , MSeifert'in çözümü diğerinden daha iyi sonuç verir .

MSeifert'in cevabını daha genel cevap olduğu için çözüm olarak kabul etmeyi seçtim: Ardışık yinelenen elemanların (benzersiz olmayan) blokları ile rastgele dizileri doğru bir şekilde işler. Hareketsiz olması durumunda numba, Divakar'ın yanıtı da bir göz atmaya değer!


1
Girişin sıralanacağı garanti ediliyor mu?
user2357112 Monica

1
benim özel durumumda, evet. genel olarak, sıralanmamış bir girdi (ve tekrarlanan elemanların benzersiz olmayan blokları) durumunu düşünmek iyi olur.
MrFuppes

Yanıtlar:


4

Anlaması oldukça kolay olan numba kullanarak bir çözüm sunmak istiyorum . Art arda yinelenen öğeleri "maske" istediğinizi varsayalım:

import numpy as np
import numba as nb

@nb.njit
def mask_more_n(arr, n):
    mask = np.ones(arr.shape, np.bool_)

    current = arr[0]
    count = 0
    for idx, item in enumerate(arr):
        if item == current:
            count += 1
        else:
            current = item
            count = 1
        mask[idx] = count <= n
    return mask

Örneğin:

>>> bins = np.array([1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5])
>>> bins[mask_more_n(bins, 3)]
array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])
>>> bins[mask_more_n(bins, 2)]
array([1, 1, 2, 2, 3, 3, 4, 4, 5, 5])

Verim:

Kullanılması simple_benchmark- ancak ben tüm yaklaşımları dahil etmedik. Bu bir günlük kaydı ölçeği:

resim açıklamasını buraya girin

Numba çözümü, büyük diziler için daha hızlı görünen (ve ek bir bağımlılık gerektirmeyen) Paul Panzer'in çözümünü yenemez gibi görünüyor.

Bununla birlikte, her ikisi de diğer çözümlerden daha iyi performans gösterir, ancak "filtrelenmiş" dizi yerine bir maske döndürürler.

import numpy as np
import numba as nb
from simple_benchmark import BenchmarkBuilder, MultiArgument

b = BenchmarkBuilder()

bins = np.array([1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5])

@nb.njit
def mask_more_n(arr, n):
    mask = np.ones(arr.shape, np.bool_)

    current = arr[0]
    count = 0
    for idx, item in enumerate(arr):
        if item == current:
            count += 1
        else:
            current = item
            count = 1
        mask[idx] = count <= n
    return mask

@b.add_function(warmups=True)
def MSeifert(arr, n):
    return mask_more_n(arr, n)

from scipy.ndimage.morphology import binary_dilation

@b.add_function()
def Divakar_1(a, N):
    k = np.ones(N,dtype=bool)
    m = np.r_[True,a[:-1]!=a[1:]]
    return a[binary_dilation(m,k,origin=-(N//2))]

@b.add_function()
def Divakar_2(a, N):
    k = np.ones(N,dtype=bool)
    return a[binary_dilation(np.ediff1d(a,to_begin=a[0])!=0,k,origin=-(N//2))]

@b.add_function()
def Divakar_3(a, N):
    m = np.r_[True,a[:-1]!=a[1:],True]
    idx = np.flatnonzero(m)
    c = np.diff(idx)
    return np.repeat(a[idx[:-1]],np.minimum(c,N))

from skimage.util import view_as_windows

@b.add_function()
def Divakar_4(a, N):
    m = np.r_[True,a[:-1]!=a[1:]]
    w = view_as_windows(m,N)
    idx = np.flatnonzero(m)
    v = idx<len(w)
    w[idx[v]] = 1
    if v.all()==0:
        m[idx[v.argmin()]:] = 1
    return a[m]

@b.add_function()
def Divakar_5(a, N):
    m = np.r_[True,a[:-1]!=a[1:]]
    w = view_as_windows(m,N)
    last_idx = len(a)-m[::-1].argmax()-1
    w[m[:-N+1]] = 1
    m[last_idx:last_idx+N] = 1
    return a[m]

@b.add_function()
def PaulPanzer(a,N):
    mask = np.empty(a.size,bool)
    mask[:N] = True
    np.not_equal(a[N:],a[:-N],out=mask[N:])
    return mask

import random

@b.add_arguments('array size')
def argument_provider():
    for exp in range(2, 20):
        size = 2**exp
        yield size, MultiArgument([np.array([random.randint(0, 5) for _ in range(size)]), 3])

r = b.run()
import matplotlib.pyplot as plt

plt.figure(figsize=[10, 8])
r.plot()

"Numba çözümü, Paul Panzer'in çözümünü yenemez gibi görünüyor" tartışmalı olarak iyi bir boyut aralığı için daha hızlı. Ve daha güçlü. Benim (iyi, @ FlorianH's) çok yavaş yapmadan benzersiz blok değerleri için çalışmasını sağlayamadı. İlginç bir şekilde, Florians yöntemini (tipik olarak numba'ya benzer şekilde çalışan) piran ile çoğaltarak bile büyük diziler için numpy uygulamasıyla eşleşemedim. pythran outargümanı (veya operatörün işlevsel formunu) sevmez , bu yüzden bu kopyayı kaydedemedim. Btw çok hoşuma gitti simple_benchmark.
Paul Panzer

orada büyük ipucu, kullanmak simple_benchmark! bunun için teşekkürler ve elbette cevap için teşekkürler. Ben kullanıyorum beri numbade başka şeyler için, ben eğilimli olduğum da burada kullanın ve bu çözüm olun. bir kaya ve orada zor bir yer arasında ...
MrFuppes

7

Feragatname: Bu, @ FlorianH'ın fikrinin daha sağlam bir uygulamasıdır:

def f(a,N):
    mask = np.empty(a.size,bool)
    mask[:N] = True
    np.not_equal(a[N:],a[:-N],out=mask[N:])
    return mask

Daha büyük diziler için bu büyük bir fark yaratır:

a = np.arange(1000).repeat(np.random.randint(0,10,1000))
N = 3

print(timeit(lambda:f(a,N),number=1000)*1000,"us")
# 5.443050000394578 us

# compare to
print(timeit(lambda:[True for _ in range(N)] + list(bins[:-N] != bins[N:]),number=1000)*1000,"us")
# 76.18969900067896 us

Rasgele diziler için doğru çalıştığını düşünmüyorum: Örneğin ile [1,1,1,1,2,2,1,1,2,2].
MSeifert

@ MSeifert OP örneğinden, bu tür bir şeyin gerçekleşemeyeceğini varsaydım, ancak OP'nin gerçek kodunun örneğinizi işleyebileceğini doğru söylüyorsunuz. Sanırım, sadece OP söyleyebilir.
Paul Panzer

user2357112'nin yorumuna cevap verdiğim gibi, özel durumumda, giriş sıralanır ve ardışık tekrar eden elemanların blokları benzersizdir. Bununla birlikte, daha genel bir perspektiften, birinin rastgele dizilerle başa çıkabilmesi çok yararlı olabilir.
MrFuppes

4

Yaklaşım # 1: İşte vectorized bir yol -

from scipy.ndimage.morphology import binary_dilation

def keep_N_per_group(a, N):
    k = np.ones(N,dtype=bool)
    m = np.r_[True,a[:-1]!=a[1:]]
    return a[binary_dilation(m,k,origin=-(N//2))]

Örnek çalışma -

In [42]: a
Out[42]: array([1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5])

In [43]: keep_N_per_group(a, N=3)
Out[43]: array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])

Yaklaşım # 2: Biraz daha kompakt bir sürüm -

def keep_N_per_group_v2(a, N):
    k = np.ones(N,dtype=bool)
    return a[binary_dilation(np.ediff1d(a,to_begin=a[0])!=0,k,origin=-(N//2))]

Yaklaşım # 3: Gruplandırılmış sayıları kullanma ve np.repeat(bize maskeyi vermeyeceğim) -

def keep_N_per_group_v3(a, N):
    m = np.r_[True,a[:-1]!=a[1:],True]
    idx = np.flatnonzero(m)
    c = np.diff(idx)
    return np.repeat(a[idx[:-1]],np.minimum(c,N))

Yaklaşım # 4: Bir view-basedyöntemle -

from skimage.util import view_as_windows

def keep_N_per_group_v4(a, N):
    m = np.r_[True,a[:-1]!=a[1:]]
    w = view_as_windows(m,N)
    idx = np.flatnonzero(m)
    v = idx<len(w)
    w[idx[v]] = 1
    if v.all()==0:
        m[idx[v.argmin()]:] = 1
    return a[m]

5. Yaklaşımı: With a view-basedgelen endeksleri olmadan yöntemiyle flatnonzero-

def keep_N_per_group_v5(a, N):
    m = np.r_[True,a[:-1]!=a[1:]]
    w = view_as_windows(m,N)
    last_idx = len(a)-m[::-1].argmax()-1
    w[m[:-N+1]] = 1
    m[last_idx:last_idx+N] = 1
    return a[m]

2

Bunu indeksleme ile yapabilirsiniz. Herhangi bir N için kod şöyle olacaktır:

N = 3
bins = np.array([1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5,6])

mask = [True for _ in range(N)] + list(bins[:-N] != bins[N:])
bins[mask]

çıktı:

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

basitliği için bunu gerçekten seviyorum! aynı zamanda oldukça performanslı olmalı, bazı timeitkoşular ile kontrol edecektir .
MrFuppes

1

numpy'S işlevini kullanmak çok daha güzel bir yol olacaktır unique(). Dizinizde benzersiz girişler ve ayrıca ne sıklıkta göründüklerini görebilirsiniz:

bins = np.array([1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5])
N = 3

unique, index,count = np.unique(bins, return_index=True, return_counts=True)
mask = np.full(bins.shape, True, dtype=bool)
for i,c in zip(index,count):
    if c>N:
        mask[i+N:i+c] = False

bins[mask]

çıktı:

array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])

1

N konumunun geri konumlandırdığı dizi öğesinin geçerli olana eşit olup olmadığını kontrol eden bir while döngüsü kullanabilirsiniz. Bu çözüm dizinin sıralandığını varsayar.

import numpy as np

bins = [1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5]
N = 3
counter = N

while counter < len(bins):
    drop_condition = (bins[counter] == bins[counter - N])
    if drop_condition:
        bins = np.delete(bins, counter)
    else:
        # move on to next element
        counter += 1

Değiştirmek len(question)isteyebilirsinizlen(bins)
Florian H

sorum orada net değilse üzgünüm; Öğeleri kaldırmak istemiyorum, sadece daha sonra kullanabileceğim bir maskeye ihtiyacım var (örneğin, bin başına eşit sayıda örnek almak için bağımlı bir değişkeni maskeleme).
MrFuppes

0

Sen kullanabilirsiniz grouby daha uzun olan grup ortak elemanları ve filtre listesine N .

import numpy as np
from itertools import groupby, chain

def ifElse(condition, exec1, exec2):

    if condition : return exec1 
    else         : return exec2


def solve(bins, N = None):

    xss = groupby(bins)
    xss = map(lambda xs : list(xs[1]), xss)
    xss = map(lambda xs : ifElse(len(xs) > N, xs[:N], xs), xss)
    xs  = chain.from_iterable(xss)
    return list(xs)

bins = np.array([1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5])
solve(bins, N = 3)

0

Çözüm

Kullanabilirsin numpy.unique. Değişken final_mask, dizi öğelerinden traget öğelerini çıkarmak için kullanılabilir bins.

import numpy as np

bins = np.array([1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5])
repeat_max = 3

unique, counts = np.unique(bins, return_counts=True)
mod_counts = np.array([x if x<=repeat_max else repeat_max for x in counts])
mask = np.arange(bins.size)
#final_values = np.hstack([bins[bins==value][:count] for value, count in zip(unique, mod_counts)])
final_mask = np.hstack([mask[bins==value][:count] for value, count in zip(unique, mod_counts)])
bins[final_mask]

Çıktı :

array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])

aynı şekle sahip bir maske almak için ek bir adım gerektirir bins, değil mi?
MrFuppes

Doğru: sadece önce maskeyi almakla ilgileniyorsanız. İsterseniz final_valuesdoğrudan, sen olabilir yorumsuz çözeltide sadece yorumladı hattı ve bu durumda üç satırını iptal olabilir: mask = ..., final_mask = ...ve bins[final_mask].
CypherX
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.