matriz 1D numpy: elementos de máscara que se repiten más de n veces


18

dado un conjunto de enteros como

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

Necesito enmascarar elementos que se repiten más de Nveces. Para aclarar: el objetivo principal es recuperar la matriz de máscara booleana, para usarla más adelante para los cálculos de agrupamiento.

Se me ocurrió una solución 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 ejemplo

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

¿Hay una mejor manera de hacer esto?

EDITAR, # 2

¡Muchas gracias por las respuestas! Aquí hay una versión delgada de la trama de referencia de MSeifert. Gracias por señalarme simple_benchmark. Mostrando solo las 4 opciones más rápidas: ingrese la descripción de la imagen aquí

Conclusión

La idea propuesta por Florian H , modificada por Paul Panzer parece ser una excelente manera de resolver este problema, ya que es bastante simple y directo numpy. numbaSin embargo, si está de acuerdo con el uso , la solución de MSeifert supera a la otra.

Elegí aceptar la respuesta de MSeifert como solución, ya que es la respuesta más general: maneja correctamente matrices arbitrarias con bloques (no únicos) de elementos repetidos consecutivos. En caso de numbaque no se pueda , ¡ la respuesta de Divakar también vale la pena!


1
¿Se garantiza que la entrada se ordenará?
user2357112 es compatible con Monica el

1
En mi caso específico, sí. En general, diría que sería bueno considerar el caso de una entrada no ordenada (y bloques no únicos de elementos repetidos).
MrFuppes

Respuestas:


4

Quiero presentar una solución usando numba que debería ser bastante fácil de entender. Supongo que desea "enmascarar" elementos 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 ejemplo:

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

Actuación:

Utilizando simple_benchmark, sin embargo, no he incluido todos los enfoques. Es una escala log-log:

ingrese la descripción de la imagen aquí

Parece que la solución de numba no puede vencer a la solución de Paul Panzer, que parece ser un poco más rápida para arreglos grandes (y no requiere una dependencia adicional).

Sin embargo, ambos parecen superar a las otras soluciones, pero devuelven una máscara en lugar de la 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()

"Parece que la solución de numba no puede superar la solución de Paul Panzer" podría decirse que es más rápido para una gama de tamaños decente. Y es más poderoso. No pude hacer que el mío (bueno, @ FlorianH's) funcione para valores de bloque no únicos sin hacerlo mucho más lento. Curiosamente, incluso replicando el método de Florians con pythran (que generalmente funciona de manera similar a numba) no pude igualar la implementación numpy para matrices grandes. Pythran no le gusta el outargumento (o tal vez la forma funcional del operador), por lo que no pude guardar esa copia. Por cierto, me gusta bastante simple_benchmark.
Paul Panzer

gran sugerencia allí, para usar simple_benchmark! gracias por eso y gracias por supuesto por la respuesta. Como también lo estoy usando numbapara otras cosas, soy propenso a usarlo aquí y hacer que esta sea la solución. entre una roca y un lugar duro allí ...
MrFuppes

7

Descargo de responsabilidad: esta es solo una implementación más sólida de la idea 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 matrices más grandes, esto hace una gran diferencia:

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

No creo que funcione correctamente para matrices arbitrarias: por ejemplo con [1,1,1,1,2,2,1,1,2,2].
MSeifert

@MSeifert Del ejemplo de OP asumí que este tipo de cosas no pueden suceder, pero tienes razón en que el código real de OP podría manejar tu ejemplo. Bueno, solo OP puede decirlo, supongo.
Paul Panzer

como respondí al comentario del usuario2357112, en mi caso específico, la entrada se ordena y los bloques de elementos repetidos consecutivos son únicos. Sin embargo, desde una perspectiva más general, podría ser muy útil si uno pudiera manejar matrices arbitrarias.
MrFuppes

4

Enfoque n. ° 1: aquí hay una forma vectorizada:

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

Ejecución de muestra:

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

Enfoque # 2: versión un poco más 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))]

Enfoque n. ° 3: Usar los recuentos agrupados y np.repeat(aunque no nos dará la 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))

Enfoque n. ° 4: con un 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]

Enfoque # 5: con un view-basedmétodo sin í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]

2

Podrías hacer esto con la indexación. Para cualquier N el código sería:

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]

salida:

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

realmente me gusta ese por su simplicidad! también debería ser bastante eficiente, lo verificaremos con algunas timeitejecuciones.
MrFuppes

1

Una forma mucho más agradable sería usar la función numpy's unique(). Obtendrá entradas únicas en su matriz y también el recuento de la frecuencia con la que aparecen:

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]

salida:

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

1

Podría usar un ciclo while que verifique si el elemento de matriz N posiciona de nuevo es igual al actual. Tenga en cuenta que esta solución supone que la 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

Es posible que desee cambiar len(question)alen(bins)
Florian H

perdón si mi pregunta no está clara allí; No estoy buscando eliminar elementos, solo necesito una máscara que pueda usar más adelante (por ejemplo, enmascarar una variable dependiente para obtener el mismo número de muestras por contenedor).
MrFuppes

0

Se podría utilizar grouby a elementos comunes del grupo y lista de filtros que son más 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)

0

Solución

Podrías usar numpy.unique. La variable final_maskse puede usar para extraer los elementos traget de la 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]

Salida :

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

eso requeriría un paso adicional para obtener una máscara de la misma forma que bins, ¿verdad?
MrFuppes

Cierto: solo si está interesado en obtener la máscara primero. Si desea que la final_valuesdirecta, se puede eliminar el comentario de la línea sólo se ha comentado en la solución y en ese caso se podría descartar tres líneas: mask = ..., final_mask = ...y bins[final_mask].
CypherX
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.