Python 在一维NumPy数组中寻找局部极大值/极小值的奇点/集合(再次)

Python 在一维NumPy数组中寻找局部极大值/极小值的奇点/集合(再次),python,arrays,python-3.x,algorithm,numpy,Python,Arrays,Python 3.x,Algorithm,Numpy,我希望有一个函数可以检测局部极大值/极小值在数组中的位置(即使存在一组局部极大值/极小值)。例如: 给定数组 test03 = np.array([2,2,10,4,4,4,5,6,7,2,6,5,5,7,7,1,1]) 我希望有如下输出: set of 2 local minima => array[0]:array[1] set of 3 local minima => array[3]:array[5] local minima, i = 9 set of 2 local m

我希望有一个函数可以检测局部极大值/极小值在数组中的位置(即使存在一组局部极大值/极小值)。例如:

给定数组

test03 = np.array([2,2,10,4,4,4,5,6,7,2,6,5,5,7,7,1,1])
我希望有如下输出:

set of 2 local minima => array[0]:array[1]
set of 3 local minima => array[3]:array[5]
local minima, i = 9
set of 2 local minima => array[11]:array[12]
set of 2 local minima => array[15]:array[16]
从示例中可以看出,不仅检测到奇异值,而且还检测到局部极大值/极小值集

我知道有很多很好的答案和想法,但没有一个能完成描述的工作:其中一些只是忽略了数组的极值点,而所有的都忽略了局部极小值/极大值集

在提出问题之前,我自己编写了一个函数,它完全符合我上面描述的内容(函数位于问题的末尾:
local\u min(a)
。通过我所做的测试,它工作正常)

问题:但是,我也确信这不是使用Python的最佳方式。是否有我可以使用的内置函数、API、库等?还有其他功能建议吗?一行指令?完全矢量化的解决方案

def local_min(a):
    candidate_min=0
    for i in range(len(a)):

        # Controlling the first left element
        if i==0 and len(a)>=1:
            # If the first element is a singular local minima
            if a[0]<a[1]:
                print("local minima, i = 0")
            # If the element is a candidate to be part of a set of local minima
            elif a[0]==a[1]:
                candidate_min=1
        # Controlling the last right element
        if i == (len(a)-1) and len(a)>=1:
            if candidate_min > 0:
                if a[len(a)-1]==a[len(a)-2]:
                    print("set of " + str(candidate_min+1)+ " local minima => array["+str(i-candidate_min)+"]:array["+str(i)+"]")
            if a[len(a)-1]<a[len(a)-2]:
                print("local minima, i = " + str(len(a)-1))
        # Controlling the other values in the middle of the array
        if i>0 and i<len(a)-1 and len(a)>2:
            # If a singular local minima
            if (a[i]<a[i-1] and a[i]<a[i+1]):
                print("local minima, i = " + str(i))
                # print(str(a[i-1])+" > " + str(a[i]) + " < "+str(a[i+1])) #debug
            # If it was found a set of candidate local minima
            if candidate_min >0:
                # The candidate set IS a set of local minima
                if a[i] < a[i+1]:
                    print("set of " + str(candidate_min+1)+ " local minima => array["+str(i-candidate_min)+"]:array["+str(i)+"]")
                    candidate_min = 0
                # The candidate set IS NOT a set of local minima
                elif a[i] > a[i+1]:
                    candidate_min = 0
                # The set of local minima is growing
                elif a[i] == a[i+1]:
                    candidate_min = candidate_min + 1
                # It never should arrive in the last else
                else:
                    print("Something strange happen")
                    return -1
            # If there is a set of candidate local minima (first value found)
            if (a[i]<a[i-1] and a[i]==a[i+1]):
                candidate_min = candidate_min + 1
拥有这样的东西才是我真正想要的。然而,当局部极小值/极大值集合有两个以上的值时,它不能正常工作。例如:

test03 = np.array([2,2,10,4,4,4,5,6,7,2,6,5,5,7,7,1,1])

print(local_max_scipy(test03))
输出为:

[ 0  2  4  8 10 13 14 16]

当然,在
test03[4]
中,我有一个最小值,而不是最大值。如何修复此行为?(我不知道这是不是另一个问题,也不知道这是不是该问的地方。)

有多种方法可以解决这个问题。这里列出了一种方法。 您可以创建一个自定义函数,并在查找mimima时使用最大值处理边缘情况

import numpy as np
a = np.array([2,2,10,4,4,4,5,6,7,2,6,5,5,7,7,1,1])

def local_min(a):
    temp_list = list(a)
    maxval = max(a) #use max while finding minima
    temp_list = temp_list + [maxval] #handles last value edge case.

    prev = maxval #prev stores last value seen
    loc = 0 #used to store starting index of minima
    count = 0 #use to count repeated values
    #match_start = False
    matches = []
    for i in range(0, len(temp_list)): #need to check all values including the padded value
        if prev == temp_list[i]:
            if count > 0: #only increment for minima candidates
                count += 1
        elif prev > temp_list[i]:
            count = 1
            loc = i
    #        match_start = True
        else: #prev < temp_list[i]
            if count > 0:
                matches.append((loc, count))
            count = 0
            loc = i
        prev = temp_list[i]
    return matches

result = local_min(a)

for match in result:
    print ("{} minima found starting at location {} and ending at location {}".format(
            match[1], 
            match[0],
            match[0] + match[1] -1))
将numpy导入为np
a=np.数组([2,2,10,4,4,4,5,6,7,2,6,5,5,7,7,1])
def本地_最小值(a):
临时列表=列表(a)
maxval=max(a)#在寻找极小值时使用max
temp_list=temp_list+[maxval]#处理最后一个值边缘大小写。
prev=maxval#prev存储最后看到的值
loc=0#用于存储最小值的起始索引
计数=0#用于对重复值进行计数
#match_start=False
匹配项=[]
对于范围(0,len(temp_list))中的i:#需要检查所有值,包括填充值
如果prev==临时列表[i]:
如果计数>0:#仅最小候选值的增量
计数+=1
elif prev>临时列表[i]:
计数=1
loc=i
#match_start=True
其他:#上一个<临时列表[i]
如果计数>0:
匹配。追加((loc,计数))
计数=0
loc=i
prev=临时列表[i]
复赛
结果=局部最小值(a)
对于匹配结果:
打印(“{}最小值从位置{}开始,在位置{}结束”。格式(
匹配[1],
匹配[0],
匹配[0]+匹配[1]-1))

如果这对你有用,请告诉我。想法很简单,您希望在列表中迭代一次,并在看到它们时保持存储极小值。通过在每一端填充最大值来处理边。(或填充最后一个端点,并使用最大值进行初始比较)

完整的矢量化解决方案:

test03 = np.array([2,2,10,4,4,4,5,6,7,2,6,5,5,7,7,1,1])  # Size 17
extended = np.empty(len(test03)+2)  # Rooms to manage edges, size 19
extended[1:-1] = test03
extended[0] = extended[-1] = np.inf

flag_left = extended[:-1] <= extended[1:]  # Less than successor, size 18
flag_right = extended[1:] <= extended[:-1]  # Less than predecessor, size 18

flagmini = flag_left[1:] & flag_right[:-1]  # Local minimum, size 17
mini = np.where(flagmini)[0]  # Indices of minimums
spl = np.where(np.diff(mini)>1)[0]+1  # Places to split
result = np.split(mini, spl)
编辑

不幸的是,当它们至少有3个项目大时,这也会检测到最大值,因为它们被视为平坦的局部最小值。这样的话,一块裸体补丁会很难看

为了解决这个问题,我提出了另外两种解决方案,一种是numpy,另一种是numba

使用
np.diff
的数值:

import numpy as np
test03=np.array([12,13,12,4,4,4,5,6,7,2,6,5,5,7,7,17,17])
extended=np.full(len(test03)+2,np.inf)
extended[1:-1]=test03

slope = np.sign(np.diff(extended))  # 1 if ascending,0 if flat, -1 if descending
not_flat,= slope.nonzero() # Indices where data is not flat.   
local_min_inds, = np.where(np.diff(slope[not_flat])==2) 

#local_min_inds contains indices in not_flat of beginning of local mins. 
#Indices of End of local mins are shift by +1:   
start = not_flat[local_min_inds]
stop =  not_flat[local_min_inds+1]-1

print(*zip(start,stop))
#(0, 1) (3, 5) (9, 9) (11, 12) (15, 16)    
与numba acceleration兼容的直接解决方案:

#@numba.njit
def localmins(a):
    begin= np.empty(a.size//2+1,np.int32)
    end  = np.empty(a.size//2+1,np.int32)
    i=k=0
    begin[k]=0
    search_end=True
    while i<a.size-1:
         if a[i]>a[i+1]:
                begin[k]=i+1
                search_end=True
         if search_end and a[i]<a[i+1]:   
                end[k]=i
                k+=1
                search_end=False
        i+=1
    if search_end and i>0  : # Final plate if exists 
        end[k]=i
        k+=1 
    return begin[:k],end[:k]

    print(*zip(*localmins(test03)))
    #(0, 1) (3, 5) (9, 9) (11, 12) (15, 16)  
#@numba.njit
def localmins(a):
begin=np.empty(a.size//2+1,np.int32)
end=np.empty(a.size//2+1,np.int32)
i=k=0
开始[k]=0
search\u end=True
而ia[i+1]:
开始[k]=i+1
search\u end=True
如果搜索结束且[i]0:#如果存在最终板
结束[k]=i
k+=1
返回开始[:k],结束[:k]
打印(*zip(*localmins(test03)))
#(0, 1) (3, 5) (9, 9) (11, 12) (15, 16)  

以下是一个基于将数组限制为一组窗口的答案:

import numpy as np
from numpy.lib.stride_tricks import as_strided

def windowstride(a, window):
    return as_strided(a, shape=(a.size - window + 1, window), strides=2*a.strides)

def local_min(a, maxwindow=None, doends=True):
    if doends: a = np.pad(a.astype(float), 1, 'constant', constant_values=np.inf)
    if maxwindow is None: maxwindow = a.size - 1

    mins = []
    for i in range(3, maxwindow + 1):
        for j,w in enumerate(windowstride(a, i)):
            if (w[0] > w[1]) and (w[-2] < w[-1]):
                if (w[1:-1]==w[1]).all():
                    mins.append((j, j + i - 2))

    mins.sort()
    return mins
输出:

[(0, 2), (3, 6), (9, 10), (11, 13), (15, 17)]
[2 2]
[4 4 4]
[2]
[5 5]
[1 1]
[ True  True False  True  True  True False False False  True False  True
  True False False  True  True]
[ True False  True False  True False False False  True False  True False
 False  True  True False  True]
[array([0, 1]), array([3, 4, 5]), array([9]), array([11, 12]), array([15, 16])]
不是最有效的算法,但至少它很短。我很确定这是
O(n^2)
,因为大约有
1/2*(n^2+n)
窗口可以迭代。这只是部分矢量化,因此可能有一种方法可以改进它

编辑 为了澄清,输出是包含局部最小值运行的切片索引。他们在跑步结束后跑了一圈是故意的(有人只是试图在编辑中“修正”)。您可以使用输出在输入数组中的最小值切片上进行迭代,如下所示:

for s in local_mins(test03):
    print(test03[slice(*s)])
输出:

[(0, 2), (3, 6), (9, 10), (11, 13), (15, 17)]
[2 2]
[4 4 4]
[2]
[5 5]
[1 1]
[ True  True False  True  True  True False False False  True False  True
  True False False  True  True]
[ True False  True False  True False False False  True False  True False
 False  True  True False  True]
[array([0, 1]), array([3, 4, 5]), array([9]), array([11, 12]), array([15, 16])]

我认为来自
scipy.signal
的另一个函数会很有趣

from scipy.signal import find_peaks

test03 = np.array([2,2,10,4,4,4,5,6,7,2,6,5,5,7,7,1,1])
find_peaks(test03)

Out[]: (array([ 2,  8, 10, 13], dtype=int64), {})
有很多选择,可能非常有用,特别是对于嘈杂的信号

更新 该功能非常强大,用途广泛。您可以为峰值最小宽度、高度、彼此之间的距离等设置多个参数。例如:

test04 = np.array([1,1,5,5,5,5,5,5,5,5,1,1,1,1,1,5,5,5,1,5,1,5,1])
find_peaks(test04, width=1)

Out[]: 
(array([ 5, 16, 19, 21], dtype=int64),
 {'prominences': array([4., 4., 4., 4.]),
  'left_bases': array([ 1, 14, 18, 20], dtype=int64),
  'right_bases': array([10, 18, 20, 22], dtype=int64),
  'widths': array([8., 3., 1., 1.]),
  'width_heights': array([3., 3., 3., 3.]),
  'left_ips': array([ 1.5, 14.5, 18.5, 20.5]),
  'right_ips': array([ 9.5, 17.5, 19.5, 21.5])})

有关更多示例,请参见。

一种非常简单且快速的方法是使用秩过滤器作为腐蚀或扩展。 为了找到最小值,首先腐蚀你的数组。然后,找到它与原始数组相等的位置

import numpy as np
from scipy.ndimage import rank_filter, grey_erosion, grey_dilation

test03 = np.array([2,2,10,4,4,4,5,6,7,2,6,5,5,7,7,1,1])

test_erode = rank_filter(test03, 0, size=3)
print(test_erode == test03)
输出:

[(0, 2), (3, 6), (9, 10), (11, 13), (15, 17)]
[2 2]
[4 4 4]
[2]
[5 5]
[1 1]
[ True  True False  True  True  True False False False  True False  True
  True False False  True  True]
[ True False  True False  True False False False  True False  True False
 False  True  True False  True]
[array([0, 1]), array([3, 4, 5]), array([9]), array([11, 12]), array([15, 16])]
同样的过程也可用于找到最大值。用最大值进行扩张。只需将排名更改为
-1

test_dilate = rank_filter(test03, -1, size=3)
print(test_dilate == test03)

输出:

[(0, 2), (3, 6), (9, 10), (11, 13), (15, 17)]
[2 2]
[4 4 4]
[2]
[5 5]
[1 1]
[ True  True False  True  True  True False False False  True False  True
  True False False  True  True]
[ True False  True False  True False False False  True False  True False
 False  True  True False  True]
[array([0, 1]), array([3, 4, 5]), array([9]), array([11, 12]), array([15, 16])]
编辑 我意识到
rank\u filter
比其他操作要慢。您也可以使用SciPy的
grey\u腐蚀
查找最小值或
grey\u膨胀
查找最大值:

test_erode = grey_erosion(test03, size=3)
test_min = test_erode == test03

test_dil = grey_dilation(test03, size=3)
test_max = test_dil == test03
编辑2


要转换为集合(贷记用户B.M.)

输出:

[(0, 2), (3, 6), (9, 10), (11, 13), (15, 17)]
[2 2]
[4 4 4]
[2]
[5 5]
[1 1]
[ True  True False  True  True  True False False False  True False  True
  True False False  True  True]
[ True False  True False  True False False False  True False  True False
 False  True  True False  True]
[array([0, 1]), array([3, 4, 5]), array([9]), array([11, 12]), array([15, 16])]
只要没有多个连续的相等元素,就可以使用,因此首先需要对数组进行游程编码,然后使用argrelmax(或):

输出

set of 1 maxima start:2 end:3
set of 1 maxima start:8 end:9
set of 1 maxima start:10 end:11
set of 2 maxima start:13 end:15
纯numpy解决方案(修订答案):

np.searchsorted(