matriz 1D numpy: mascara elementos que se repetem mais de n vezes

18

dada uma matriz de números inteiros como

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

Eu preciso mascarar elementos que se repetem mais que Nvezes. Para esclarecer: o objetivo principal é recuperar a matriz de máscaras booleanas e usá-la posteriormente para cálculos de classificação.

Eu vim com uma solução bastante complicada

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)

dando por exemplo

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

Existe uma maneira melhor de fazer isso?

EDIT, # 2

Muito obrigado pelas respostas! Aqui está uma versão reduzida do gráfico de benchmark do MSeifert. Obrigado por me indicar simple_benchmark. Mostrando apenas as 4 opções mais rápidas: insira a descrição da imagem aqui

Conclusão

A idéia proposta por Florian H , modificada por Paul Panzer, parece ser uma ótima maneira de resolver esse problema, pois é bem simples e direto numpy. numbaNo entanto, se você estiver bem em usar , a solução do MSeifert supera a outra.

Optei por aceitar a resposta do MSeifert como solução, pois é a resposta mais geral: lida corretamente com matrizes arbitrárias com blocos (não exclusivos) de elementos repetidos consecutivos. Caso numbanão seja possível, a resposta de Divakar também vale a pena dar uma olhada!

MrFuppes
fonte
11
É garantido que a entrada será classificada?
User2357112 suporta Monica
11
no meu caso específico, sim. em geral, eu diria que seria bom considerar o caso de uma entrada não classificada (e blocos não exclusivos de elementos repetidos).
MrFuppes

Respostas:

4

Quero apresentar uma solução usando o numba que deve ser bastante fácil de entender. Presumo que você deseja "mascarar" itens repetidos consecutivos:

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

Por exemplo:

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

Atuação:

Usando simple_benchmark- no entanto, não incluí todas as abordagens. É uma escala de log-log:

insira a descrição da imagem aqui

Parece que a solução numba não consegue superar a solução de Paul Panzer, que parece um pouco mais rápida para grandes matrizes (e não requer uma dependência adicional).

No entanto, ambos parecem ter um desempenho superior às outras soluções, mas retornam uma máscara em vez da matriz "filtrada".

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
fonte
"Parece que a solução numba não pode superar a solução de Paul Panzer", sem dúvida é mais rápida para uma variedade decente de tamanhos. E é mais poderoso. Eu não poderia fazer o meu (bem, no @ FlorianH) funcionar com valores de bloco não exclusivos sem torná-lo muito mais lento. Curiosamente, mesmo replicando o método Florians com pythran (que normalmente funciona de maneira semelhante ao numba) eu não conseguia combinar a implementação numpy para matrizes grandes. pythran não gosta do outargumento (ou talvez da forma funcional do operador), então não pude salvar essa cópia. Btw eu gosto bastante simple_benchmark.
Paul Panzer
ótima dica, para usar simple_benchmark! obrigado por isso e, claro, pela resposta. Como também estou usando numbapara outras coisas, estou propenso a usá-lo aqui e fazer dessa a solução. entre uma rocha e um lugar duro lá ...
MrFuppes
7

Isenção de responsabilidade: esta é apenas uma implementação mais sólida da ideia de @ 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

Para matrizes maiores, isso faz uma enorme diferença:

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
fonte
Não acho que funcione corretamente para matrizes arbitrárias: por exemplo, com [1,1,1,1,2,2,1,1,2,2].
MSDIFER #
@ MSeifert No exemplo do OP, presumi que esse tipo de coisa não pode acontecer, mas você está correto no código real do OP que pode lidar com o seu exemplo. Bem, apenas OP pode dizer, suponho.
Paul Panzer
como respondi ao comentário do usuário2357112, no meu caso específico, a entrada é classificada e os blocos de elementos repetidos consecutivos são únicos. No entanto, de uma perspectiva mais geral, poderia ser muito útil se alguém pudesse lidar com matrizes arbitrárias.
MrFuppes
4

Abordagem 1: aqui está uma maneira vetorizada -

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

Amostra de execução -

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

Abordagem 2: versão um pouco mais compacta -

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

Abordagem # 3: Usando as contagens agrupadas e np.repeat(embora não nos dê a máscara) -

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

Abordagem # 4: com um view-basedmétodo -

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]

Abordagem 5: com um view-basedmétodo sem índices de 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
fonte
2

Você poderia fazer isso com a indexação. Para qualquer N, o código seria:

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]

resultado:

array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6]
Florian H
fonte
realmente gosto desse por sua simplicidade! deve ter bom desempenho também, verificará com algumas timeitexecuções.
precisa saber é o seguinte
1

Uma maneira muito mais agradável seria usar numpyounique() função . Você receberá entradas exclusivas em sua matriz e também a contagem de quantas vezes elas aparecem:

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]

resultado:

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

Você pode usar um loop while que verifica se o elemento N da matriz está posicionado de volta é igual ao atual. Observe que esta solução assume que a matriz está ordenada.

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
dodgytricycle
fonte
Você pode querer mudar len(question)paralen(bins)
Florian H
desculpe se minha pergunta não está clara lá; Não estou procurando remover elementos, só preciso de uma máscara que possa ser usada mais tarde (por exemplo, mascarar uma variável dependente para obter o mesmo número de amostras por lixeira).
precisa saber é o seguinte
0

Você poderia usar grouby agrupar os elementos comuns e lista de filtros que são mais de 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)
youngseok jeon
fonte
0

Solução

Você poderia usar numpy.unique. A variável final_maskpode ser usada para extrair os elementos traget da matriz 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]

Saída :

array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])
CypherX
fonte
isso exigiria uma etapa adicional para obter uma máscara da mesma forma que bins, certo?
precisa saber é o seguinte
Verdadeiro: somente se você estiver interessado em obter a máscara primeiro. Se quiser que o final_valuesdiretamente, você pode descomentar a linha só comentou na solução e, nesse caso, você pode descartar três linhas: mask = ..., final_mask = ...e bins[final_mask].
CypherX 21/10/19