étant donné un tableau d'entiers comme
[1, 1, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5]
J'ai besoin de masquer des éléments qui se répètent plus de N
fois. Pour clarifier: l'objectif principal est de récupérer le tableau de masques booléens, pour l'utiliser plus tard pour les calculs de binning.
J'ai trouvé une solution assez compliquée
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)
donner par exemple.
bins[mask]
Out[90]: array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])
Existe-t-il une meilleure façon de procéder?
EDIT, # 2
Merci beaucoup pour les réponses! Voici une version mince de l'intrigue de référence de MSeifert. Merci de m'avoir indiqué simple_benchmark
. Affichage uniquement des 4 options les plus rapides:
Conclusion
L'idée proposée par Florian H , modifiée par Paul Panzer semble être un excellent moyen de résoudre ce problème car elle est assez simple et numpy
- uniquement . Si vous êtes d'accord avec l'utilisation de numba
cependant, la solution de MSeifert surpasse l'autre.
J'ai choisi d'accepter la réponse de MSeifert comme solution car c'est la réponse la plus générale: elle gère correctement les tableaux arbitraires avec des blocs (non uniques) d'éléments répétitifs consécutifs. Dans le cas où numba
est un no-go, la réponse de Divakar vaut également le détour!
Je veux présenter une solution utilisant numba qui devrait être assez facile à comprendre. Je suppose que vous voulez "masquer" les éléments répétitifs consécutifs:
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
Par exemple:
>>> 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])
En utilisant simple_benchmark
- mais je n'ai pas inclus toutes les approches. C'est une échelle log-log:
Il semble que la solution numba ne puisse pas battre la solution de Paul Panzer qui semble être un peu plus rapide pour les grands tableaux (et ne nécessite pas de dépendance supplémentaire).
Cependant, les deux semblent surpasser les autres solutions, mais ils renvoient un masque au lieu du tableau "filtré".
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()
Avertissement: ceci est juste une implémentation plus solide de l'idée 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
Pour les baies plus grandes, cela fait une énorme différence:
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
Approche # 1: Voici une méthode vectorisée -
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))]
Exemple d'exécution -
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])
Approche # 2: Une version un peu plus compacte -
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))]
Approche n ° 3: Utilisation des comptages groupés et np.repeat
(ne nous donnera pas le masque) -
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))
Approche # 4: Avec un view-based
méthode -
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]
Approche # 5: Avec un view-based
méthode sans index 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]
Vous pouvez le faire avec l'indexation. Pour tout N, le code serait:
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]
production:
array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6]
Vous pouvez utiliser une boucle while qui vérifie si l'élément de tableau N positions en arrière est égal à celui en cours. Notez que cette solution suppose que la baie est ordonnée.
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
Une manière beaucoup plus agréable serait d'utiliser la fonction unique()
- de numpy
. Vous obtiendrez des entrées uniques dans votre tableau et également le nombre de fois qu'elles apparaissent:
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]
production:
array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])
Vous pouvez utiliser groupby pour regrouper les éléments communs et la liste de filtres qui sont plus longs 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)
Vous pouvez utiliser numpy.unique
. La variable final_mask
peut être utilisé pour extraire les éléments tragiques du tableau 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]
Sortie:
array([1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5])