Encontrar máximos / mínimos locais com Numpy em uma matriz numpy 1D

116

Você pode sugerir uma função de módulo de numpy / scipy que pode encontrar máximos / mínimos locais em uma matriz numpy 1D? Obviamente, a abordagem mais simples é dar uma olhada nos vizinhos mais próximos, mas eu gostaria de ter uma solução aceita que faça parte da distro numpy.

Navi
fonte
1
Não, isso é em 2D (estou falando de 1D) e envolve funções personalizadas. Eu tenho minha própria implementação simples, mas queria saber se existe uma melhor, que vem com módulos Numpy / Scipy.
Navi
Talvez você possa atualizar a questão para incluir que (1) você tem um array 1d e (2) que tipo de mínimo local você está procurando. Apenas uma entrada menor que as duas entradas adjacentes?
Sven Marnach
1
Você pode dar uma olhada em scipy.signal.find_peaks_cwt se estiver falando de dados com ruído
Lakshay Garg

Respostas:

66

Se você estiver procurando por todas as entradas no array 1d amenores do que seus vizinhos, você pode tentar

numpy.r_[True, a[1:] < a[:-1]] & numpy.r_[a[:-1] < a[1:], True]

Você também pode suavizar sua matriz antes desta etapa usando numpy.convolve().

Não acho que haja uma função dedicada para isso.

Sven Marnach
fonte
Hmm, por que eu preciso suavizar? Para remover o ruído? Isso parece interessante. Parece-me que poderia usar outro inteiro em vez de 1 em seu código de exemplo. Também estava pensando em calcular gradientes. De qualquer forma, se não houver função, isso é muito ruim.
Navi
1
@Navi: O problema é que a noção de "mínimo local" varia muito de caso de uso para caso de uso, então é difícil fornecer uma função "padrão" para esse propósito. A suavização ajuda a levar em consideração mais do que apenas o vizinho mais próximo. Usar um número inteiro diferente em vez de 1, digamos 3, seria estranho, pois consideraria apenas o terceiro próximo elemento em ambas as direções, mas não os vizinhos diretos.
Sven Marnach
1
@Sven Marnach: a receita que você vincula atrasa o sinal. há uma segunda receita que usa filtfilt de scipy.signal
bobrobbob
2
Apenas por causa disso, substituir o <por >fornecerá os máximos locais em vez dos mínimos
DarkCygnus
1
@SvenMarnach Usei a solução acima para resolver meu problema postado aqui stackoverflow.com/questions/57403659/… mas recebi uma saída [False False]Qual poderia ser o problema aqui?
Msquare de
221

Em SciPy> = 0,11

import numpy as np
from scipy.signal import argrelextrema

x = np.random.random(12)

# for local maxima
argrelextrema(x, np.greater)

# for local minima
argrelextrema(x, np.less)

Produz

>>> x
array([ 0.56660112,  0.76309473,  0.69597908,  0.38260156,  0.24346445,
    0.56021785,  0.24109326,  0.41884061,  0.35461957,  0.54398472,
    0.59572658,  0.92377974])
>>> argrelextrema(x, np.greater)
(array([1, 5, 7]),)
>>> argrelextrema(x, np.less)
(array([4, 6, 8]),)

Observe que esses são os índices de x que são máx. / Mín. Locais. Para obter os valores, tente:

>>> x[argrelextrema(x, np.greater)[0]]

scipy.signaltambém fornece argrelmaxe argrelminpara encontrar máximos e mínimos, respectivamente.

danodonovan
fonte
1
Qual é o significado de 12?
marshmallow de
7
@marshmallow: np.random.random(12)gera 12 valores aleatórios, eles são usados ​​para demonstrar a função argrelextrema.
sebix
2
se a entrada for test02=np.array([10,4,4,4,5,6,7,6]), então ele não funciona. Ele não reconhece os valores consecutivos como mínimos locais.
Leos313
1
obrigado, @Cleb. Quero apontar outros problemas: e quanto aos pontos extremos da matriz? o primeiro elemento também é um máximo local, já que o último elemento da matriz também é um mínimo local. E, também, não retorna quantos valores consecutivos são fundados. No entanto, propus uma solução no código desta questão aqui . Obrigado!!
Leos313
1
Obrigado, esta é uma das melhores soluções que encontrei até agora
Noufal E
37

Para curvas sem muito ruído, recomendo o seguinte pequeno snippet de código:

from numpy import *

# example data with some peaks:
x = linspace(0,4,1e3)
data = .2*sin(10*x)+ exp(-abs(2-x)**2)

# that's the line, you need:
a = diff(sign(diff(data))).nonzero()[0] + 1 # local min+max
b = (diff(sign(diff(data))) > 0).nonzero()[0] + 1 # local min
c = (diff(sign(diff(data))) < 0).nonzero()[0] + 1 # local max


# graphical output...
from pylab import *
plot(x,data)
plot(x[b], data[b], "o", label="min")
plot(x[c], data[c], "o", label="max")
legend()
show()

O +1é importante porque diffreduz o número do índice original.

RC
fonte
1
bom uso de funções numpy aninhadas! mas note que isso não perde o máximo em cada extremidade da matriz :)
danodonovan
2
Isso também parecerá estranho se houver valores repetitivos. por exemplo, se você pegar o array [1, 2, 2, 3, 3, 3, 2, 2, 1], o máximo local está obviamente em algum lugar entre os 3's no meio. Mas se você executar as funções fornecidas, obterá máximos nos índices 2,6 e mínimos nos índices 1,3,5,7, o que para mim não faz muito sentido.
Korem
5
Para evitar isso, em +1vez de np.diff()usar np.gradient().
ankostis
Eu sei que este segmento tem anos, mas vale a pena acrescentar que, se sua curva for muito barulhenta, você sempre pode tentar a filtragem de passagem baixa primeiro para suavizar. Para mim, pelo menos, a maioria dos meus usos máx. / Mín. Locais são para máx. / Mín. Global dentro de alguma área local (por exemplo, os grandes picos e vales, nem todas as variações nos dados)
marcman
25

Outra abordagem (mais palavras, menos código) que pode ajudar:

As localizações dos máximos e mínimos locais também são as localizações dos cruzamentos por zero da primeira derivada. Geralmente é muito mais fácil encontrar cruzamentos de zero do que encontrar diretamente máximos e mínimos locais.

Infelizmente, a primeira derivada tende a "amplificar" o ruído, portanto, quando um ruído significativo está presente nos dados originais, a primeira derivada é melhor usada somente depois que os dados originais tiverem algum grau de suavização aplicado.

Uma vez que a suavização é, no sentido mais simples, um filtro passa-baixa, a suavização é geralmente melhor (bem, mais facilmente) feita usando um kernel de convolução, e "modelar" esse kernel pode fornecer uma quantidade surpreendente de capacidade de preservação / aprimoramento de recursos . O processo de encontrar um kernel ideal pode ser automatizado usando uma variedade de meios, mas o melhor pode ser a força bruta simples (bastante rápido para encontrar pequenos kernels). Um bom kernel irá (como pretendido) distorcer maciçamente os dados originais, mas NÃO afetará a localização dos picos / vales de interesse.

Felizmente, muitas vezes um kernel adequado pode ser criado por meio de um SWAG simples ("suposição educada"). A largura do kernel de suavização deve ser um pouco maior do que o pico "interessante" mais largo esperado nos dados originais, e sua forma será semelhante a esse pico (uma wavelet de escala única). Para kernels que preservam a média (o que qualquer bom filtro de suavização deve ser), a soma dos elementos do kernel deve ser precisamente igual a 1,00, e o kernel deve ser simétrico em relação ao seu centro (o que significa que terá um número ímpar de elementos.

Dado um kernel de suavização ideal (ou um pequeno número de kernels otimizados para diferentes conteúdos de dados), o grau de suavização torna-se um fator de escala para (o "ganho" do) kernel de convolução.

Determinar o grau "correto" (ótimo) de suavização (ganho de kernel de convolução) pode até ser automatizado: Compare o desvio padrão dos primeiros dados derivados com o desvio padrão dos dados suavizados. Como a razão dos dois desvios padrão muda com as mudanças no grau de suavização pode ser usado para prever valores de suavização eficazes. Algumas execuções manuais de dados (que são verdadeiramente representativas) devem ser suficientes.

Todas as soluções anteriores postadas acima calculam a primeira derivada, mas não a tratam como uma medida estatística, nem as soluções acima tentam realizar a preservação / aprimoramento de recursos (para ajudar os picos sutis a "saltarem acima" do ruído).

Finalmente, a má notícia: encontrar picos "reais" torna-se uma dor de cabeça quando o ruído também tem características que parecem picos reais (largura de banda sobreposta). A próxima solução mais complexa é geralmente usar um kernel de convolução mais longo (uma "abertura de kernel mais ampla") que leva em conta a relação entre picos "reais" adjacentes (como taxas mínimas ou máximas para ocorrência de pico), ou usar vários a convolução passa usando núcleos com larguras diferentes (mas apenas se for mais rápida: é uma verdade matemática fundamental que as convoluções lineares realizadas em sequência podem sempre ser convoluídas juntas em uma única convolução). Mas geralmente é muito mais fácil primeiro encontrar uma sequência de kernels úteis (de larguras variadas) e convolvê-los juntos do que encontrar diretamente o kernel final em uma única etapa.

Esperançosamente, isso fornece informações suficientes para permitir que o Google (e talvez um bom texto de estatísticas) preencha as lacunas. Eu realmente gostaria de ter tempo para fornecer um exemplo funcional ou um link para um. Se alguém encontrar um online, poste aqui!

BobC
fonte
24

A partir da versão 1.1 do SciPy, você também pode usar find_peaks . Abaixo estão dois exemplos retirados da própria documentação.

Usando o heightargumento, pode-se selecionar todos os máximos acima de um certo limite (neste exemplo, todos os máximos não negativos; isso pode ser muito útil se for necessário lidar com uma linha de base ruidosa; se você quiser encontrar os mínimos, basta multiplicar os dados de entrada por -1):

import matplotlib.pyplot as plt
from scipy.misc import electrocardiogram
from scipy.signal import find_peaks
import numpy as np

x = electrocardiogram()[2000:4000]
peaks, _ = find_peaks(x, height=0)
plt.plot(x)
plt.plot(peaks, x[peaks], "x")
plt.plot(np.zeros_like(x), "--", color="gray")
plt.show()

insira a descrição da imagem aqui

Outro argumento extremamente útil é distance, que define a distância mínima entre dois picos:

peaks, _ = find_peaks(x, distance=150)
# difference between peaks is >= 150
print(np.diff(peaks))
# prints [186 180 177 171 177 169 167 164 158 162 172]

plt.plot(x)
plt.plot(peaks, x[peaks], "x")
plt.show()

insira a descrição da imagem aqui

Cleb
fonte
10

Por que não usar a função embutida Scipy signal.find_peaks_cwt para fazer o trabalho?

from scipy import signal
import numpy as np

#generate junk data (numpy 1D arr)
xs = np.arange(0, np.pi, 0.05)
data = np.sin(xs)

# maxima : use builtin function to find (max) peaks
max_peakind = signal.find_peaks_cwt(data, np.arange(1,10))

# inverse  (in order to find minima)
inv_data = 1/data
# minima : use builtin function fo find (min) peaks (use inversed data)
min_peakind = signal.find_peaks_cwt(inv_data, np.arange(1,10))

#show results
print "maxima",  data[max_peakind]
print "minima",  data[min_peakind]

resultados:

maxima [ 0.9995736]
minima [ 0.09146464]

Saudações

A STEFANI
fonte
7
Em vez de fazer a divisão (com possível perda de precisão), por que não simplesmente multiplicar por -1 para ir do máximo para o mínimo?
Livius
Eu tentei alterar '1 / data' para 'data * -1', mas então surgiu um erro, você poderia compartilhar como implementar seu método?
A STEFANI de
Talvez porque não queremos exigir que os usuários finais instalem adicionalmente o scipy.
Damian Yerrick de
5

Atualização: não fiquei feliz com o gradiente, então achei mais confiável de usar numpy.diff. Por favor, deixe-me saber se ele faz o que você deseja.

Em relação à questão do ruído, o problema matemático é localizar máximos / mínimos. Se quisermos olhar para o ruído, podemos usar algo como convolver, que foi mencionado anteriormente.

import numpy as np
from matplotlib import pyplot

a=np.array([10.3,2,0.9,4,5,6,7,34,2,5,25,3,-26,-20,-29],dtype=np.float)

gradients=np.diff(a)
print gradients


maxima_num=0
minima_num=0
max_locations=[]
min_locations=[]
count=0
for i in gradients[:-1]:
        count+=1

    if ((cmp(i,0)>0) & (cmp(gradients[count],0)<0) & (i != gradients[count])):
        maxima_num+=1
        max_locations.append(count)     

    if ((cmp(i,0)<0) & (cmp(gradients[count],0)>0) & (i != gradients[count])):
        minima_num+=1
        min_locations.append(count)


turning_points = {'maxima_number':maxima_num,'minima_number':minima_num,'maxima_locations':max_locations,'minima_locations':min_locations}  

print turning_points

pyplot.plot(a)
pyplot.show()
Mike Vella
fonte
Você sabe como esse gradiente é calculado? Se você tiver dados ruidosos, provavelmente o gradiente muda muito, mas isso não significa que haja um máximo / mínimo.
Navi
Sim, eu sei, porém dados barulhentos são um problema diferente. Para isso, acho que uso convolve.
Mike Vella
Eu precisava de algo semelhante para um projeto no qual estava trabalhando e usei o método numpy.diff mencionado acima, achei que pode ser útil mencionar que para meus dados o código acima perdeu alguns máximos e mínimos, alterando o termo do meio em ambos declarações if para <= e> = respectivamente, eu consegui pegar todos os pontos.
5

Embora esta questão seja muito antiga. Acredito que haja uma abordagem muito mais simples no numpy (um liner).

import numpy as np

list = [1,3,9,5,2,5,6,9,7]

np.diff(np.sign(np.diff(list))) #the one liner

#output
array([ 0, -2,  0,  2,  0,  0, -2])

Para encontrar um máximo ou mínimo local, queremos essencialmente encontrar quando a diferença entre os valores na lista (3-1, 9-3 ...) muda de positivo para negativo (máximo) ou negativo para positivo (mínimo). Portanto, primeiro encontramos a diferença. Então encontramos o sinal, e então encontramos as mudanças no sinal pegando a diferença novamente. (Mais ou menos como uma primeira e segunda derivadas em cálculo, só que temos dados discretos e não temos uma função contínua.)

A saída em meu exemplo não contém os extremos (o primeiro e o último valores na lista). Além disso, assim como o cálculo, se a segunda derivada for negativa, você tem o máximo e, se for positiva, você terá o mínimo.

Portanto, temos a seguinte comparação:

[1,  3,  9,  5,  2,  5,  6,  9,  7]
    [0, -2,  0,  2,  0,  0, -2]
        Max     Min         Max
Dave
fonte
1
Eu acho que esta (boa!) Resposta é igual à resposta do RC de 2012? Ele oferece três soluções de uma linha, dependendo se o chamador deseja minutos, máximos ou ambos, se estou lendo a solução corretamente.
Brandon Rhodes
3

Nenhuma dessas soluções funcionou para mim, pois eu também queria encontrar picos no centro de valores repetidos. por exemplo, em

ar = np.array([0,1,2,2,2,1,3,3,3,2,5,0])

a resposta deve ser

array([ 3,  7, 10], dtype=int64)

Eu fiz isso usando um loop. Eu sei que não é muito limpo, mas dá conta do recado.

def findLocalMaxima(ar):
# find local maxima of array, including centers of repeating elements    
maxInd = np.zeros_like(ar)
peakVar = -np.inf
i = -1
while i < len(ar)-1:
#for i in range(len(ar)):
    i += 1
    if peakVar < ar[i]:
        peakVar = ar[i]
        for j in range(i,len(ar)):
            if peakVar < ar[j]:
                break
            elif peakVar == ar[j]:
                continue
            elif peakVar > ar[j]:
                peakInd = i + np.floor(abs(i-j)/2)
                maxInd[peakInd.astype(int)] = 1
                i = j
                break
    peakVar = ar[i]
maxInd = np.where(maxInd)[0]
return maxInd 
Misha Smirnov
fonte
1
import numpy as np
x=np.array([6,3,5,2,1,4,9,7,8])
y=np.array([2,1,3,5,3,9,8,10,7])
sortId=np.argsort(x)
x=x[sortId]
y=y[sortId]
minm = np.array([])
maxm = np.array([])
i = 0
while i < length-1:
    if i < length - 1:
        while i < length-1 and y[i+1] >= y[i]:
            i+=1

        if i != 0 and i < length-1:
            maxm = np.append(maxm,i)

        i+=1

    if i < length - 1:
        while i < length-1 and y[i+1] <= y[i]:
            i+=1

        if i < length-1:
            minm = np.append(minm,i)
        i+=1


print minm
print maxm

minme maxmcontêm índices de mínimos e máximos, respectivamente. Para um grande conjunto de dados, ele fornecerá muitos máximos / mínimos, então, nesse caso, suavize a curva primeiro e, em seguida, aplique este algoritmo.

prtkp
fonte
isso parece interessante. Sem bibliotecas. Como funciona?
john ktejik
1
atravesse a curva do ponto de partida e veja se você está indo para cima ou para baixo continuamente, uma vez que você muda de cima para baixo significa que você tem um máximo, se você está indo para baixo, você tem um mínimo.
prtkp
1

Outra solução usando essencialmente um operador dilate:

import numpy as np
from scipy.ndimage import rank_filter

def find_local_maxima(x):
   x_dilate = rank_filter(x, -1, size=3)
   return x_dilate == x

e para os mínimos:

def find_local_minima(x):
   x_erode = rank_filter(x, -0, size=3)
   return x_erode == x

Além disso, scipy.ndimagevocê pode substituir rank_filter(x, -1, size=3)por grey_dilatione rank_filter(x, 0, size=3)por grey_erosion. Isso não requer uma classificação local, por isso é um pouco mais rápido.

gnodab
fonte
ele funciona corretamente para este problema. Aqui a solução é perfeita (+1)
Leos313
0

Outro:


def local_maxima_mask(vec):
    """
    Get a mask of all points in vec which are local maxima
    :param vec: A real-valued vector
    :return: A boolean mask of the same size where True elements correspond to maxima. 
    """
    mask = np.zeros(vec.shape, dtype=np.bool)
    greater_than_the_last = np.diff(vec)>0  # N-1
    mask[1:] = greater_than_the_last
    mask[:-1] &= ~greater_than_the_last
    return mask
Peter
fonte