python arrays numpy binning

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



arrays binning (8)

Solución

Podrías usar numpy.unique . La variable final_mask se puede utilizar para extraer los elementos traget de los bins matriz.

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

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 N veces. 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 a simple_benchmark . Mostrando solo las 4 opciones más rápidas:

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 directa y numpy . numba embargo, si está de acuerdo con usar numba , 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 que numba se numba , ¡ la respuesta de Divakar también vale la pena!


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


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


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]


Puede usar grouby para agrupar elementos comunes y filtrar listas que son más largas que 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)


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:

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

Parece que la solución 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()


Una forma mucho mejor sería utilizar la numpy unique() numpy. 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])


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 ( np.repeat 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 método view-based :

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 n. ° 5: con un método view-based 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]