numpy 1D масив: елементи маски, які повторюються більше n разів


18

заданий масив на зразок цілих чисел

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

Мені потрібно замаскувати елементи, які повторюються більше, ніж Nраз. Для уточнення: головна мета - отримати масив булевих масок, щоб згодом використовувати його для обчислень бінінгу.

Я придумав досить складне рішення

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)

даючи напр

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

Чи є кращий спосіб це зробити?

РЕДАКЦІЯ, №2

Дякую за відповіді! Ось тонка версія базового сюжету MSeifert. Дякую, що вказали на мене simple_benchmark. Показано лише 4 найшвидших варіанти: введіть тут опис зображення

Висновок

Ідея, запропонована Флоріаном Н , модифікована Полом Панзером, здається, є прекрасним способом вирішення цієї проблеми, оскільки вона досить пряма вперед та лише numpy. Якщо ви все добре не використовуєте numba, рішення MSeifert перевершує інше.

Я вирішив прийняти відповідь MSeifert як рішення, оскільки це більш загальна відповідь: Він правильно обробляє довільні масиви з (не унікальними) блоками послідовних повторюваних елементів. У випадку, якщо numbaце не можна, відповідь Дівакара також варто переглянути!


1
Чи гарантується, що вхід буде відсортований?
user2357112 підтримує Моніку

1
в моєму конкретному випадку, так. загалом я б сказав, було б добре розглянути випадок несортованого введення (і не унікальних блоків повторних елементів).
MrFuppes

Відповіді:


4

Я хочу представити рішення з використанням numba, яке має бути досить легко зрозуміти. Я припускаю, що ви хочете "замаскувати" послідовно повторювані елементи:

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

Наприклад:

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

Продуктивність:

Використання simple_benchmark- проте я не включив усіх підходів. Це шкала журналу журналу:

введіть тут опис зображення

Здається, що рішення numba не може перемогти рішення від Paul Panzer, яке здається, що для великих масивів трохи швидше (і не потребує додаткової залежності).

Однак, здається, обидва переважають інші рішення, але вони повертають маску замість "відфільтрованого" масиву.

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

"Схоже, рішення numba не може перемогти рішення від Пола Панзера", мабуть, це швидше для пристойного діапазону розмірів. І вона потужніша. Я не міг змусити шахту (ну, @ FlorianH) працювати на значення ненонітного блоку, не роблячи це набагато повільніше. Цікаво, що навіть при реплікації методу Флоріана з pythran (який, як правило, працює аналогічно numba), я не міг відповідати numpy-реалізації для великих масивів. pythran не любить outаргумент (або, можливо, функціональна форма оператора), тому мені не вдалося зберегти цю копію. Дорога мені дуже подобається simple_benchmark.
Пол Панцер

чудовий натяк на використання simple_benchmark! дякую за це і звичайно дякую за відповідь. Оскільки я використовую і numbaдля інших речей, я схильний також використовувати його тут і вирішувати це рішення. між скелею і важким місцем там ...
MrFuppes

7

Відмова від відповідальності: це лише грунтовна реалізація ідеї @ 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

Для великих масивів це робить величезну різницю:

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

Я не думаю, що він працює правильно для довільних масивів: Наприклад, з [1,1,1,1,2,2,1,1,2,2].
MSeifert

@MSeifert З прикладу ОП я припустив, що подібного не може статися, але ви вірні, що фактичний код ОП може обробляти ваш приклад. Ну, гадаю, тільки ОП може сказати.
Пол Панцер

як я відповів на коментар user2357112, в моєму конкретному випадку введення сортується і блоки послідовних повторюваних елементів є унікальними. Однак з більш загальної точки зору це може бути дуже корисно, якби можна було обробляти довільні масиви.
MrFuppes

4

Підхід №1: ось векторизований спосіб -

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

Проба зразка -

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

Підхід №2: трохи більш компактна версія -

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

Підхід №3: Використання згрупованих підрахунків і np.repeat(не дасть нам маски) -

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

Підхід №4: За допомогою 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]

Підхід №5: За допомогою view-basedметоду без індексів від 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

Це можна зробити за допомогою індексації. Для будь-якого N код буде таким:

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]

вихід:

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

дуже подобається той за його простоту! також повинен бути досить ефективним, перевірятимуть з деякими timeitпробіжками.
MrFuppes

1

Набагато приємніше було б використовувати функцію numpy's unique(). Ви отримаєте унікальні записи у своєму масиві, а також підрахунок того, як часто вони з'являються:

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]

вихід:

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

1

Ви можете використовувати цикл while, який перевіряє, чи відповідає позиція елемента масиву N назад поточній. Зверніть увагу, що це рішення передбачає, що масив впорядкований.

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

Ви можете захотіти змінити len(question)наlen(bins)
Florian H

вибачте, якщо моє запитання там незрозуміле; Я не хочу видаляти елементи, мені просто потрібна маска, яку я можу використовувати згодом (наприклад, маскування залежної змінної, щоб отримати рівну кількість зразків на коробку).
MrFuppes

0

Ви можете використовувати grouby загальних елементів групи і список фільтрів, які більше ніж 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

Рішення

Ви можете використовувати numpy.unique. Змінна final_maskможе використовуватися для вилучення елементів масиву з масиву 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]

Вихід :

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

це потребує додаткового кроку, щоб отримати маску такої ж форми, як bins, правда?
MrFuppes

Правда: тільки якщо вам цікаво отримати маску спочатку. Якщо ви хочете, щоб final_valuesбезпосередньо, ви можете розкоментувати єдиним коментувала лінію в розчині і в цьому випадку ви можете відмовитися три лінії: mask = ..., final_mask = ...і bins[final_mask].
CypherX
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.