array 1D numpy: elemen mask yang mengulang lebih dari n kali


18

diberi array bilangan bulat seperti

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

Saya perlu menutupi elemen yang berulang Nkali. Untuk memperjelas: tujuan utama adalah untuk mengambil array topeng boolean, untuk menggunakannya nanti untuk perhitungan binning.

Saya datang dengan solusi yang agak rumit

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)

memberi misalnya

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

Apakah ada cara yang lebih baik untuk melakukan ini?

EDIT, # 2

Terima kasih banyak atas jawabannya! Berikut ini adalah versi ramping dari plot benchmark MSeifert. Terima kasih telah menunjuk saya simple_benchmark. Hanya menampilkan 4 opsi tercepat: masukkan deskripsi gambar di sini

Kesimpulan

Gagasan yang diajukan oleh Florian H , yang dimodifikasi oleh Paul Panzer tampaknya merupakan cara yang bagus untuk menyelesaikan masalah ini karena cukup lurus ke depan dan numpy- hanya. Jika Anda baik-baik saja dengan menggunakan numba, solusi MSeifert mengungguli yang lain.

Saya memilih untuk menerima jawaban MSeifert sebagai solusi karena ini adalah jawaban yang lebih umum: Ia dengan benar menangani array sewenang-wenang dengan blok (non-unik) elemen berulang berulang. Dalam hal numbaini adalah no-go, jawaban Divakar juga patut dilihat!


1
Apakah dijamin bahwa input akan diurutkan?
user2357112 mendukung Monica

1
dalam kasus khusus saya, ya. secara umum saya akan mengatakan, akan lebih baik untuk mempertimbangkan kasus input yang tidak disortir (dan blok unik elemen berulang).
MrFuppes

Jawaban:


4

Saya ingin menghadirkan solusi menggunakan numba yang seharusnya cukup mudah dimengerti. Saya berasumsi bahwa Anda ingin "menutupi" item berulang berulang:

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

Sebagai contoh:

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

Kinerja:

Menggunakan simple_benchmark- namun saya belum memasukkan semua pendekatan. Ini skala log-log:

masukkan deskripsi gambar di sini

Sepertinya solusi numba tidak dapat mengalahkan solusi dari Paul Panzer yang tampaknya lebih cepat untuk array besar sedikit (dan tidak memerlukan ketergantungan tambahan).

Namun keduanya tampaknya mengungguli solusi lain, tetapi mereka mengembalikan masker bukannya array yang "difilter".

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

"Sepertinya solusi numba tidak dapat mengalahkan solusi dari Paul Panzer" bisa dibilang lebih cepat untuk berbagai ukuran yang layak. Dan itu lebih kuat. Saya tidak bisa membuat milik saya (well, @ FlorianH) bekerja untuk nilai blok nonunique tanpa membuatnya lebih lambat. Menariknya, bahkan mereplikasi metode Florians dengan pythran (yang biasanya berkinerja sama dengan numba) saya tidak bisa menyamai implementasi numpy untuk array besar. pythran tidak suka outargumen (atau mungkin bentuk fungsional operator), jadi saya tidak bisa menyimpan salinan itu. Tapi saya sangat suka simple_benchmark.
Paul Panzer

petunjuk besar di sana, untuk digunakan simple_benchmark! terima kasih untuk itu dan tentu saja terima kasih atas jawabannya. Karena saya menggunakan numbauntuk hal-hal lain juga, saya juga cenderung menggunakannya di sini dan menjadikan ini solusi. antara batu dan tempat yang keras di sana ...
MrFuppes

7

Penafian: ini hanya implementasi yang lebih baik dari ide @ FlorianH:

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

Untuk array yang lebih besar ini membuat perbedaan besar:

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

Saya tidak berpikir itu berfungsi dengan benar untuk array sewenang-wenang: Misalnya dengan [1,1,1,1,2,2,1,1,2,2].
MSeifert

@MSeifert Dari contoh OP, saya berasumsi hal seperti ini tidak dapat terjadi, tetapi Anda benar karena kode aktual OP dapat menangani contoh Anda. Yah, hanya OP yang tahu, saya kira.
Paul Panzer

ketika saya membalas komentar user2357112, dalam kasus khusus saya, input diurutkan dan blok elemen berulang berturut-turut adalah unik. Namun, dari perspektif yang lebih umum, akan sangat berguna jika seseorang dapat menangani array yang sewenang-wenang.
MrFuppes

4

Pendekatan # 1: Ini cara vektorisasi -

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

Contoh dijalankan -

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

Pendekatan # 2: Versi yang sedikit lebih ringkas -

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

Pendekatan # 3: Menggunakan hitungan yang dikelompokkan dan np.repeat(tidak akan memberi kita topeng) -

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

Pendekatan # 4: Dengan view-basedmetode -

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]

Pendekatan # 5: Dengan view-basedmetode tanpa indeks dari 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

Anda bisa melakukan ini dengan pengindeksan. Untuk N apa pun kodenya adalah:

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]

keluaran:

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

benar-benar seperti itu untuk kesederhanaannya! harus cukup performan juga, akan memeriksa dengan beberapa timeitberjalan.
MrFuppes

1

Sebuah banyak cara yang lebih baik akan menggunakan numpy's unique()-fungsi. Anda akan mendapatkan entri unik dalam array Anda dan juga hitungan seberapa sering mereka muncul:

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]

keluaran:

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

1

Anda bisa menggunakan loop sementara yang memeriksa apakah elemen array posisi N kembali sama dengan yang saat ini. Catatan solusi ini mengasumsikan array dipesan.

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

Anda mungkin ingin mengubah len(question)kelen(bins)
Florian H

maaf jika pertanyaan saya tidak jelas di sana; Saya tidak ingin menghapus elemen, saya hanya perlu topeng yang bisa saya gunakan nanti (mis. Menutupi variabel dependen untuk mendapatkan jumlah sampel per bin yang sama).
MrFuppes

0

Anda bisa menggunakan grouby untuk umum unsur-unsur kelompok dan daftar filter yang lebih panjang dari 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

Larutan

Anda bisa menggunakannya numpy.unique. Variabel final_maskdapat digunakan untuk mengekstrak elemen traget dari array 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]

Keluaran :

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

itu akan membutuhkan langkah tambahan untuk mendapatkan topeng dengan bentuk yang sama bins, kan?
MrFuppes

Benar: hanya jika Anda tertarik untuk mendapatkan topeng terlebih dahulu. Jika Anda ingin final_valueslangsung, Anda bisa tanda komentar baris hanya berkomentar dalam larutan dan dalam hal ini Anda bisa membuang tiga baris: mask = ..., final_mask = ...dan bins[final_mask].
CypherX
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.