numpy 1D Array: Maskenelemente, die sich mehr als n Mal wiederholen

18

gegeben ein Array von ganzen Zahlen wie

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

Ich muss Elemente maskieren, die sich Nmehrmals wiederholen . Zur Verdeutlichung: Das Hauptziel besteht darin, das boolesche Maskenarray abzurufen und später für Binning-Berechnungen zu verwenden.

Ich habe eine ziemlich komplizierte Lösung gefunden

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)

zB geben

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

Gibt es einen schöneren Weg, dies zu tun?

EDIT, # 2

Vielen Dank für die Antworten! Hier ist eine schlanke Version von MSeiferts Benchmark-Plot. Danke, dass du mich darauf hingewiesen hast simple_benchmark. Zeigt nur die 4 schnellsten Optionen an: Geben Sie hier die Bildbeschreibung ein

Fazit

Die von Florian H vorgeschlagene , von Paul Panzer modifizierte Idee scheint eine großartige Möglichkeit zu sein, dieses Problem zu lösen, da sie ziemlich einfach und numpynur ist. Wenn Sie numbajedoch gut damit umgehen können , übertrifft die Lösung von MSeifert die andere.

Ich habe mich entschieden, die Antwort von MSeifert als Lösung zu akzeptieren, da dies die allgemeinere Antwort ist: Sie behandelt beliebige Arrays mit (nicht eindeutigen) Blöcken aufeinanderfolgender sich wiederholender Elemente korrekt. Falls numbaes ein No-Go ist, ist Divakars Antwort auch einen Blick wert!

MrFuppes
quelle
1
Ist garantiert, dass die Eingabe sortiert wird?
user2357112 unterstützt Monica
1
in meinem speziellen Fall ja. Im Allgemeinen würde ich sagen, es wäre gut, den Fall einer unsortierten Eingabe (und nicht eindeutiger Blöcke wiederholter Elemente) zu betrachten.
MrFuppes

Antworten:

4

Ich möchte eine Lösung mit numba vorstellen, die ziemlich einfach zu verstehen sein sollte. Ich gehe davon aus, dass Sie aufeinanderfolgende sich wiederholende Elemente "maskieren" möchten:

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

Zum Beispiel:

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

Performance:

Verwenden simple_benchmark- allerdings habe ich nicht alle Ansätze berücksichtigt. Es ist eine Log-Log-Skala:

Geben Sie hier die Bildbeschreibung ein

Es scheint, dass die Numba-Lösung die Lösung von Paul Panzer nicht übertreffen kann, die für große Arrays etwas schneller zu sein scheint (und keine zusätzliche Abhängigkeit erfordert).

Beide scheinen jedoch die anderen Lösungen zu übertreffen, geben jedoch anstelle des "gefilterten" Arrays eine Maske zurück.

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()
MSeifert
quelle
"Es scheint, als ob die Numba-Lösung die Lösung von Paul Panzer nicht übertreffen kann", wohl ist sie für einen anständigen Größenbereich schneller. Und es ist mächtiger. Ich konnte meine (naja, @ FlorianHs) nicht für nicht eindeutige Blockwerte arbeiten lassen, ohne sie viel langsamer zu machen. Interessanterweise konnte ich selbst bei der Replikation der Florians-Methode mit Pythran (das normalerweise ähnlich wie Numba funktioniert) die Numpy-Implementierung für große Arrays nicht erreichen. pythran mag das outArgument (oder vielleicht die funktionale Form des Operators) nicht, daher konnte ich diese Kopie nicht speichern. Übrigens mag ich ganz simple_benchmark.
Paul Panzer
toller Hinweis da, um zu benutzen simple_benchmark! danke dafür und danke natürlich für die antwort. Da ich auch numbafür andere Dinge verwende, neige ich dazu, es auch hier zu verwenden und dies zur Lösung zu machen. zwischen einem Felsen und einem harten Ort dort ...
MrFuppes
7

Haftungsausschluss: Dies ist nur eine fundiertere Umsetzung der Idee von @ 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

Bei größeren Arrays macht dies einen großen Unterschied:

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
Paul Panzer
quelle
Ich denke nicht, dass es für beliebige Arrays richtig funktioniert: Zum Beispiel mit [1,1,1,1,2,2,1,1,2,2].
MSeifert
@MSeifert Aus dem Beispiel von OP ging ich davon aus, dass so etwas nicht passieren kann, aber Sie haben Recht, dass der tatsächliche Code des OP Ihr Beispiel verarbeiten könnte. Nun, ich nehme an, nur OP kann es sagen.
Paul Panzer
Wie ich auf den Kommentar von user2357112 geantwortet habe, wird in meinem speziellen Fall die Eingabe sortiert und Blöcke aufeinanderfolgender sich wiederholender Elemente sind eindeutig. Aus einer allgemeineren Perspektive könnte es jedoch sehr nützlich sein, wenn man mit beliebigen Arrays umgehen könnte.
MrFuppes
4

Ansatz 1: Hier ist ein vektorisierter Weg -

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

Probelauf -

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

Ansatz 2: Eine etwas kompaktere Version -

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

Ansatz 3: Verwenden der gruppierten Zählungen und np.repeat(gibt uns jedoch keine Maske) -

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

Ansatz 4: Mit einer view-basedMethode -

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]

Ansatz 5: Mit einer view-basedMethode ohne Indizes von 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]
Divakar
quelle
2

Sie können dies mit der Indizierung tun. Für jedes N wäre der Code:

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]

Ausgabe:

array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6]
Florian H.
quelle
mag das wirklich wegen seiner Einfachheit! sollte auch ziemlich performant sein, wird mit einigen timeitLäufen nachsehen.
MrFuppes
1

Ein viel schönerer Weg wäre, die Funktion von numpy's zu verwenden unique(). Sie erhalten eindeutige Einträge in Ihrem Array und die Anzahl, wie oft sie angezeigt werden:

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]

Ausgabe:

array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])
Simon Fink
quelle
1

Sie können eine while-Schleife verwenden, die prüft, ob die Position N des Array-Elements N gleich der aktuellen ist. Beachten Sie, dass diese Lösung davon ausgeht, dass das Array geordnet ist.

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
zwielichtiges Dreirad
quelle
Vielleicht möchten Sie len(question)zulen(bins)
Florian H
Entschuldigung, wenn meine Frage dort unklar ist; Ich möchte keine Elemente entfernen, sondern nur eine Maske, die ich später verwenden kann (z. B. Maskieren einer abhängigen Variablen, um die gleiche Anzahl von Samples pro Bin zu erhalten).
MrFuppes
0

Sie können grouby verwenden , um allgemeine Elemente und Filterlisten zu gruppieren, die länger als N sind .

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)
Youngseok Jeon
quelle
0

Lösung

Sie könnten verwenden numpy.unique. Die Variable final_maskkann verwendet werden, um die Traget-Elemente aus dem Array zu extrahieren 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]

Ausgabe :

array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])
CypherX
quelle
das würde einen zusätzlichen Schritt erfordern, um eine Maske mit der gleichen Form wie zu erhalten bins, oder?
MrFuppes
Richtig: Nur wenn Sie daran interessiert sind, zuerst die Maske zu erhalten. Wenn Sie das wollen , final_valuesdirekt, könnten Sie Kommentar- die einzigen kommentierte Linie in der Lösung und in diesem Fall könnten Sie drei Zeilen verwerfen: mask = ..., final_mask = ...und bins[final_mask].
CypherX