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つのオプションのみを表示: ここに画像の説明を入力してください

結論

Paul Panzerによって修正された、Florian Hによって提案されたアイデアは、この問題を解決する優れた方法であると思われます。ただし、使用に問題がなければ、MSeifertのソリューションは他のソリューションよりも優れています。numpynumba

より一般的な答えであるMSeifertの答えを解決策として受け入れることにしました。連続する繰り返し要素の(一意でない)ブロックを持つ任意の配列を正しく処理します。ケースnumbaノーゴーの場合、Divakarの答えも一見の価値があります!


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ソリューションは、Paul Panzerのソリューションに勝ることはできないようですおそらく、まともなサイズの範囲では高速です。そして、それはより強力です。私(@FlorianHのもの)を非固有のブロック値で動作させるには、速度を大幅に低下させる必要があります。興味深いことに、Pythran(通常はnumbaと同じように動作する)を使用してFloriansメソッドを複製したとしても、大規模な配列のnumpy実装と一致させることができませんでした。pythranはout引数(または演算子の関数形式)が気に入らないため、そのコピーを保存できませんでした。ところで私はかなり好きsimple_benchmarkです。
ポールパンツァー

すばらしいヒントがありますsimple_benchmark。それをありがとう、そしてもちろん答えをありがとう。私はnumba他のものにも使用しているので、ここでも使用する傾向があり、これを解決策にします。岩とそこの固い場所の間に...
MrFuppes '22 / 10/22

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 OPの例からこの種のことは起こり得ないと想定しましたが、OPの実際のコードがあなたの例を処理できるという点であなたは正しいです。まあ、OPだけがわかると思います。
Paul Panzer、

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

非常に良くする方法は、使用することですnumpyunique()-functionを。配列内の一意のエントリと、それらが出現する頻度の数を取得します。

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)
フローリアン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

True:最初にマスクを取得することに関心がある場合のみfinal_values直接必要な場合は、ソリューションでコメント化されている唯一の行のコメントを外し、その場合mask = ...、3つの行を破棄することができます:final_mask = ...bins[final_mask]
CypherX
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.