matrice 1D intorpidita: maschera gli elementi che si ripetono più di n volte


18

dato un array di numeri interi come

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

Devo mascherare elementi che si ripetono più Nvolte. Per chiarire: l'obiettivo principale è recuperare l'array di maschere booleane, per usarlo in seguito per i calcoli di binning.

Ho trovato una soluzione piuttosto complicata

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)

dare ad es

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

C'è un modo migliore per farlo?

EDIT, # 2

Grazie mille per le risposte! Ecco una versione ridotta del diagramma di riferimento di MSeifert. Grazie per avermi indicato simple_benchmark. Mostra solo le 4 opzioni più veloci: inserisci qui la descrizione dell'immagine

Conclusione

L'idea proposta da Florian H , modificata da Paul Panzer sembra essere un ottimo modo per risolvere questo problema in quanto è piuttosto semplice e diretto numpy. Se stai bene con l'utilizzo numba, tuttavia, la soluzione di MSeifert supera l'altro.

Ho scelto di accettare la risposta di MSeifert come soluzione in quanto è la risposta più generale: gestisce correttamente array arbitrari con blocchi (non unici) di elementi ripetitivi consecutivi. Nel caso in cui numbasia un no-go, vale la pena dare una risposta anche a Divakar !


1
È garantito che l'ingresso sarà ordinato?
user2357112 supporta Monica il

1
nel mio caso specifico, sì. in generale direi che sarebbe bene considerare il caso di un input non ordinato (e blocchi non unici di elementi ripetuti).
MrFuppes,

Risposte:


4

Voglio presentare una soluzione usando numba che dovrebbe essere abbastanza facile da capire. Presumo che tu voglia "mascherare" gli elementi consecutivi ricorrenti:

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

Per esempio:

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

Prestazione:

Utilizzo simple_benchmark- tuttavia non ho incluso tutti gli approcci. È una scala log-log:

inserisci qui la descrizione dell'immagine

Sembra che la soluzione numba non possa battere la soluzione di Paul Panzer, che sembra essere più veloce per array di grandi dimensioni di un po '(e non richiede una dipendenza aggiuntiva).

Tuttavia entrambi sembrano sovraperformare le altre soluzioni, ma restituiscono una maschera anziché l'array "filtrato".

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

"Sembra che la soluzione numba non possa battere la soluzione di Paul Panzer" probabilmente è più veloce per una discreta gamma di dimensioni. Ed è più potente. Non potevo far funzionare il mio (beh, @ FlorianH) per valori di blocco non unici senza renderlo molto più lento. È interessante notare che, anche replicando il metodo Florians con pythran (che in genere si comporta in modo simile al numba) non sono riuscito a eguagliare l'implementazione intorpidita per array di grandi dimensioni. pythran non mi piace l' outargomento (o forse la forma funzionale dell'operatore), quindi non ho potuto salvare quella copia. A proposito mi piace molto simple_benchmark.
Paul Panzer,

ottimo suggerimento lì, da usare simple_benchmark! grazie per quello e grazie ovviamente per la risposta. Dal momento che sto usando anche numbaper altre cose, sono incline a usarlo anche qui e ne faccio la soluzione. tra una roccia e un luogo difficile lì ...
MrFuppes,

7

Disclaimer: questa è solo un'implementazione più solida dell'idea di @ 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

Per array più grandi questo fa una differenza enorme:

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

Non penso che funzioni correttamente per array arbitrari: ad esempio con [1,1,1,1,2,2,1,1,2,2].
MSeifert,

@MSeifert Dall'esempio di OP ho ipotizzato che questo tipo di cose non possano accadere, ma hai ragione nel vero codice dell'OP in grado di gestire il tuo esempio. Bene, solo OP può dirlo, suppongo.
Paul Panzer,

come ho risposto al commento di user2357112, nel mio caso specifico, l'input è ordinato e i blocchi di elementi ripetitivi consecutivi sono unici. Tuttavia, da una prospettiva più generale, potrebbe essere molto utile se si potessero gestire array arbitrari.
MrFuppes,

4

Approccio n. 1: ecco un modo vettoriale -

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

Esecuzione campione -

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

Approccio n. 2: versione un po 'più compatta -

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

Approccio n. 3: usare i conteggi raggruppati e np.repeat(non ci darà la maschera però) -

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

Approccio n. 4: con un view-basedmetodo -

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]

Approccio n. 5: con un view-basedmetodo senza indici da 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

Potresti farlo con l'indicizzazione. Per ogni N il codice sarebbe:

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]

produzione:

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

piace davvero quello per la sua semplicità! dovrebbe anche essere abbastanza performante, verificherà con alcune timeitcorse.
MrFuppes,

1

Un modo molto più bello sarebbe usare numpyla funzione unique(). Otterrai voci uniche nel tuo array e anche il conteggio della frequenza con cui vengono visualizzate:

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]

produzione:

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

1

È possibile utilizzare un ciclo while che controlla se l'elemento dell'array N posizioni indietro è uguale a quello corrente. Nota questa soluzione presuppone che l'array sia ordinato.

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

Potresti voler passare len(question)alen(bins)
Florian H

scusate se la mia domanda non è chiara lì; Non sto cercando di rimuovere elementi, ho solo bisogno di una maschera che posso usare in seguito (ad esempio mascherando una variabile dipendente per ottenere lo stesso numero di campioni per bin).
MrFuppes,

0

Si potrebbe utilizzare grouby per elementi comuni di gruppo e lista dei filtri che sono più di 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

Soluzione

Puoi usare numpy.unique. La variabile final_maskpuò essere utilizzata per estrarre gli elementi traget dall'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]

Uscita :

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

ciò richiederebbe un ulteriore passaggio per ottenere una maschera della stessa forma di bins, giusto?
MrFuppes,

Vero: solo se sei interessato a ottenere prima la maschera. Se si desidera che la final_valuesdiretta, è possibile rimuovere il commento la linea unica commentata nella soluzione e in quel caso si potrebbe scartare tre linee: mask = ..., final_mask = ...e bins[final_mask].
CypherX,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.