tableau numpy 1D: éléments de masque qui se répètent plus de n fois


18

é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 Nfois. 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. N'afficher que les 4 options les plus rapides: entrez la description de l'image ici

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 directe numpy. numbaCependant, si vous êtes satisfait de l' utilisation , 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. En cas numbaest un non- droit , la réponse de Divakar vaut également le coup d'oeil!


1
Est-il garanti que l'entrée sera triée?
user2357112 prend en charge Monica

1
dans mon cas spécifique, oui. en général, je dirais qu'il serait bon de considérer le cas d'une entrée non triée (et de blocs non uniques d'éléments répétés).
MrFuppes

Réponses:


4

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

Performance:

Utilisation simple_benchmark- mais je n'ai pas inclus toutes les approches. C'est une échelle log-log:

entrez la description de l'image ici

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

"Il semble que la solution numba ne puisse pas battre la solution de Paul Panzer" sans doute, elle est plus rapide pour une gamme de tailles décente. Et c'est plus puissant. Je ne pouvais pas faire fonctionner la mienne (enfin, celle de @ FlorianH) pour des valeurs de bloc non uniques sans la ralentir beaucoup plus. Fait intéressant, même en répliquant la méthode Florians avec pythran (qui fonctionne généralement de manière similaire à numba), je ne pouvais pas faire correspondre l'implémentation numpy pour les grands tableaux. pythran n'aime pas l' outargument (ou peut-être la forme fonctionnelle de l'opérateur), donc je n'ai pas pu enregistrer cette copie. Btw j'aime bien simple_benchmark.
Paul Panzer

super indice là-bas, à utiliser simple_benchmark! merci pour cela et merci bien sûr pour la réponse. Étant donné que j'utilise également numbad'autres choses, je suis également enclin à l'utiliser ici et à en faire la solution. entre un rocher et un endroit dur là-bas ...
MrFuppes

7

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

Je ne pense pas que cela fonctionne correctement pour les tableaux arbitraires: par exemple avec [1,1,1,1,2,2,1,1,2,2].
MSeifert

@MSeifert D'après l'exemple d'OP, j'ai supposé que ce genre de chose ne pouvait pas se produire, mais vous avez raison de dire que le code réel d'OP pourrait gérer votre exemple. Eh bien, seul OP peut le dire, je suppose.
Paul Panzer

comme je l'ai répondu au commentaire de user2357112, dans mon cas spécifique, l'entrée est triée et les blocs d'éléments répétitifs consécutifs sont uniques. Cependant, d'un point de vue plus général, il pourrait être très utile de gérer des tableaux arbitraires.
MrFuppes

4

Approche n ° 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 n ° 2: 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: utiliser les comptes 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 n ° 4: avec une view-basedmé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 une view-basedméthode sans indices 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

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]

vraiment comme celui-là pour sa simplicité! devrait également être assez performant, vérifiera avec certains timeitruns.
MrFuppes

1

Une manière beaucoup plus agréable serait d'utiliser numpyla fonction unique()-s. 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])

1

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

Vous pourriez vouloir changer len(question)pourlen(bins)
Florian H

désolé si ma question n'est pas claire là-bas; Je ne cherche pas à supprimer des éléments, j'ai juste besoin d'un masque que je pourrai utiliser plus tard (par exemple, masquer une variable dépendante pour obtenir un nombre égal d'échantillons par bin).
MrFuppes

0

Vous pouvez utiliser grouby pour regrouper les éléments communs et 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)

0

Solution

Vous pourriez utiliser numpy.unique. La variable final_maskpeut être utilisée 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])

cela nécessiterait une étape supplémentaire pour obtenir un masque de la même forme que bins, non?
MrFuppes

Vrai: uniquement si vous souhaitez obtenir le masque en premier. Si vous voulez final_valuesdirectement, vous pouvez décommenter la ligne ne commenté dans la solution et dans ce cas , vous pourriez jeter trois lignes: mask = ..., final_mask = ...et bins[final_mask].
CypherX
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.