Encontrar máximos / mínimos locales con Numpy en una matriz numpy 1D

116

¿Puede sugerir una función de módulo de numpy / scipy que pueda encontrar máximos / mínimos locales en una matriz numpy 1D? Obviamente, el enfoque más simple es echar un vistazo a los vecinos más cercanos, pero me gustaría tener una solución aceptada que sea parte de la distribución numpy.

Navi
fuente
1
No, eso es en 2D (estoy hablando de 1D) e involucra funciones personalizadas. Tengo mi propia implementación simple, pero me preguntaba si hay una mejor, que viene con los módulos Numpy / Scipy.
Navi
Tal vez pueda actualizar la pregunta para incluir que (1) tiene una matriz 1d y (2) qué tipo de mínimo local está buscando. ¿Solo una entrada más pequeña que las dos entradas adyacentes?
Sven Marnach
1
Puede echar un vistazo a scipy.signal.find_peaks_cwt si está hablando de datos con ruido
Lakshay Garg

Respuestas:

66

Si está buscando todas las entradas en la matriz 1d amás pequeñas que sus vecinas, puede intentar

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

También puede suavizar su matriz antes de este paso usando numpy.convolve().

No creo que haya una función dedicada a esto.

Sven Marnach
fuente
Hmm, ¿por qué tendría que suavizar? ¿Para eliminar el ruido? Eso suena interesante. Me parece que podría usar otro número entero en lugar de 1 en su código de ejemplo. También estaba pensando en calcular gradientes. De todos modos, si no hay función, eso es una lástima.
Navi
1
@Navi: El problema es que la noción de "mínimo local" varía enormemente de un caso de uso a otro, por lo que es difícil proporcionar una función "estándar" para este propósito. Suavizar ayuda a tener en cuenta algo más que el vecino más cercano. Usar un número entero diferente en lugar de 1, digamos 3, sería extraño ya que solo consideraría el tercer elemento siguiente en ambas direcciones, pero no los vecinos directos.
Sven Marnach
1
@Sven Marnach: la receta que enlazas retrasa la señal. hay una segunda receta que usa filtfilt de scipy.signal
bobrobbob
2
Por el simple hecho de hacerlo, reemplazar el <con >le dará los máximos locales en lugar de los mínimos
DarkCygnus
1
@SvenMarnach He utilizado su solución anterior para resolver mi problema publicado aquí stackoverflow.com/questions/57403659/… pero obtuve resultados [False False]¿Cuál podría ser el problema aquí?
Msquare
221

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

Produce

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

Tenga en cuenta que estos son los índices de x que son máximos / mínimos locales. Para obtener los valores, intente:

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

scipy.signaltambién proporciona argrelmaxy argrelminpara encontrar máximos y mínimos respectivamente.

danodonovan
fuente
1
¿Cuál es el significado de 12?
Malvavisco
7
@marshmallow: np.random.random(12)genera 12 valores aleatorios, se utilizan para demostrar la función argrelextrema.
sebix
2
si la entrada es test02=np.array([10,4,4,4,5,6,7,6]), entonces no funciona. No reconoce los valores consecutivos como mínimos locales.
Leos313
1
gracias, @Cleb. Quiero señalar otros problemas: ¿qué pasa con los puntos extremos de la matriz? el primer elemento también es un máximo local, ya que el último elemento de la matriz también es un mínimo local. Y, además, no devuelve cuántos valores consecutivos se fundan. Sin embargo, propuse una solución en el código de esta pregunta aquí . ¡¡Gracias!!
Leos313
1
Gracias, esta es una de las mejores soluciones que he encontrado hasta ahora
Noufal E
37

Para curvas sin demasiado ruido, recomiendo el siguiente fragmento de código pequeño:

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

El +1es importante, ya que diffreduce el número de índice original.

RC
fuente
1
buen uso de funciones numpy anidadas! pero tenga en cuenta que esto pierde los máximos en cualquier extremo de la matriz :)
danodonovan
2
Esto también actuará de forma extraña si hay valores repetitivos. por ejemplo, si toma la matriz [1, 2, 2, 3, 3, 3, 2, 2, 1], el máximo local está obviamente en algún lugar entre los 3 en el medio. Pero si ejecuta las funciones que proporcionó, obtendrá máximos en los índices 2,6 y mínimos en los índices 1,3,5,7, lo que para mí no tiene mucho sentido.
Korem
5
Para evitar esto en +1lugar de np.diff()usar np.gradient().
ankostis
Sé que este hilo tiene años, pero vale la pena agregar que si su curva es demasiado ruidosa, siempre puede probar primero el filtrado de paso bajo para suavizar. Para mí, al menos, la mayoría de mis usos máximos / mínimos locales son para máximos / mínimos globales dentro de un área local (p. Ej., Los grandes picos y valles, no todas las variaciones en los datos)
marcman
25

Otro enfoque (más palabras, menos código) que puede ayudar:

Las ubicaciones de los máximos y mínimos locales también son las ubicaciones de los cruces por cero de la primera derivada. En general, es mucho más fácil encontrar cruces por cero que encontrar directamente máximos y mínimos locales.

Desafortunadamente, la primera derivada tiende a "amplificar" el ruido, por lo que cuando hay ruido significativo en los datos originales, es mejor usar la primera derivada solo después de que se haya aplicado algún grado de suavizado a los datos originales.

Dado que el suavizado es, en el sentido más simple, un filtro de paso bajo, el suavizado a menudo se realiza mejor (bueno, más fácilmente) mediante el uso de un kernel de convolución, y "modelar" ese kernel puede proporcionar una sorprendente cantidad de capacidad de preservación / mejora de características . El proceso de encontrar un kernel óptimo se puede automatizar usando una variedad de medios, pero lo mejor puede ser la simple fuerza bruta (bastante rápido para encontrar kernels pequeños). Un buen kernel distorsionará (según lo previsto) masivamente los datos originales, pero NO afectará la ubicación de los picos / valles de interés.

Afortunadamente, con bastante frecuencia se puede crear un kernel adecuado mediante un simple SWAG ("conjetura fundamentada"). El ancho del núcleo de suavizado debe ser un poco más ancho que el pico "interesante" esperado más ancho en los datos originales, y su forma se parecerá a ese pico (una ondícula de una sola escala). Para los núcleos que preservan la media (lo que debería ser cualquier buen filtro de suavizado), la suma de los elementos del núcleo debe ser exactamente igual a 1,00, y el núcleo debe ser simétrico con respecto a su centro (lo que significa que tendrá un número impar de elementos.

Dado un kernel de suavizado óptimo (o un pequeño número de kernels optimizados para diferentes contenidos de datos), el grado de suavizado se convierte en un factor de escala para (la "ganancia") del kernel de convolución.

La determinación del grado "correcto" (óptimo) de suavizado (ganancia del núcleo de convolución) se puede incluso automatizar: Compare la desviación estándar de los datos de la primera derivada con la desviación estándar de los datos suavizados. Cómo se usa la relación de las dos desviaciones estándar con los cambios en el grado de suavizado para predecir valores de suavizado efectivos. Unas pocas ejecuciones de datos manuales (que sean verdaderamente representativas) deberían ser todo lo que se necesita.

Todas las soluciones anteriores publicadas anteriormente calculan la primera derivada, pero no la tratan como una medida estadística, ni las soluciones anteriores intentan realizar el suavizado de preservación / mejora de características (para ayudar a que los picos sutiles "salten por encima" del ruido).

Finalmente, las malas noticias: encontrar picos "reales" se convierte en un dolor real cuando el ruido también tiene características que parecen picos reales (ancho de banda superpuesto). La siguiente solución más compleja es generalmente usar un núcleo de convolución más largo (una "apertura de núcleo más amplia") que tenga en cuenta la relación entre picos "reales" adyacentes (como tasas mínimas o máximas para la ocurrencia de picos), o usar múltiples la convolución pasa utilizando núcleos que tienen diferentes anchos (pero solo si es más rápido: es una verdad matemática fundamental que las convoluciones lineales realizadas en secuencia siempre se pueden convolucionar juntas en una sola convolución). Pero a menudo es mucho más fácil encontrar primero una secuencia de kernels útiles (de diferentes anchos) y convertirlos juntos que encontrar directamente el kernel final en un solo paso.

Con suerte, esto proporciona suficiente información para permitir que Google (y quizás un buen texto de estadísticas) llene los vacíos. Realmente desearía tener el tiempo para proporcionar un ejemplo trabajado o un enlace a uno. Si alguien encuentra uno en línea, ¡publíquelo aquí!

BobC
fuente
24

A partir de la versión 1.1 de SciPy, también puede usar find_peaks . A continuación se muestran dos ejemplos tomados de la propia documentación.

Usando el heightargumento, uno puede seleccionar todos los máximos por encima de un cierto umbral (en este ejemplo, todos los máximos no negativos; esto puede ser muy útil si uno tiene que lidiar con una línea de base ruidosa; si desea encontrar mínimos, simplemente multiplique lo que ingresó 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()

ingrese la descripción de la imagen aquí

Otro argumento extremadamente útil es distance, que define la distancia mínima entre dos 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()

ingrese la descripción de la imagen aquí

Cleb
fuente
10

¿Por qué no utilizar la función integrada de Scipy signal.find_peaks_cwt para hacer el trabajo?

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]

Saludos

UN STEFANI
fuente
7
En lugar de hacer una división (con posible pérdida de precisión), ¿por qué no multiplicar por -1 para pasar de máximos a mínimos?
Livius
Intenté cambiar '1 / data' a 'data * -1', pero luego generó un error, ¿podría compartir cómo implementar su método?
A STEFANI
Quizás porque no queremos exigir que los usuarios finales también instalen scipy.
Damian Yerrick
5

Actualización: no estaba contento con el degradado, así que lo encontré más confiable de usar numpy.diff. Por favor avíseme si hace lo que quiere.

Con respecto al tema del ruido, el problema matemático es ubicar máximos / mínimos si queremos mirar el ruido podemos usar algo como convolve que se mencionó 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
fuente
¿Sabes cómo se calcula este gradiente? Si tiene datos ruidosos, probablemente el gradiente cambie mucho, pero eso no significa que haya un máximo / mínimo.
Navi
Sí, lo sé, sin embargo, los datos ruidosos son un problema diferente. Para eso supongo que usa convolve.
Mike Vella
Necesitaba algo similar para un proyecto en el que estaba trabajando y usé el método numpy.diff mencionado anteriormente, pensé que podría ser útil mencionar que para mis datos, el código anterior omitió algunos máximos y mínimos, al cambiar el término medio en ambos if declaraciones a <= y> = respectivamente, pude captar todos los puntos.
5

Si bien esta pregunta es realmente antigua. Creo que hay un enfoque mucho más simple en numpy (una línea).

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 un máximo o mínimo local, esencialmente queremos encontrar cuándo la diferencia entre los valores de la lista (3-1, 9-3 ...) cambia de positivo a negativo (máximo) o de negativo a positivo (mínimo). Por tanto, primero encontramos la diferencia. Luego encontramos el signo, y luego encontramos los cambios de signo tomando la diferencia nuevamente. (Algo así como una primera y segunda derivada en cálculo, solo que tenemos datos discretos y no tenemos una función continua).

La salida en mi ejemplo no contiene los extremos (el primer y último valor de la lista). Además, al igual que el cálculo, si la segunda derivada es negativa, tienes un máximo, y si es positivo tienes un mínimo.

Así tenemos el siguiente emparejamiento:

[1,  3,  9,  5,  2,  5,  6,  9,  7]
    [0, -2,  0,  2,  0,  0, -2]
        Max     Min         Max
Dave
fuente
1
Creo que esta (¡buena!) Respuesta es la misma que la respuesta de RC de 2012. Ofrece tres soluciones de una línea, dependiendo de si la persona que llama quiere minutos, máximos o ambos, si estoy leyendo su solución correctamente.
Brandon Rhodes
3

Ninguna de estas soluciones funcionó para mí, ya que también quería encontrar picos en el centro de los valores repetidos. por ejemplo, en

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

la respuesta debería ser

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

Hice esto usando un bucle. Sé que no está súper limpio, pero hace el trabajo.

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

minmy maxmcontienen índices de mínimos y máximos, respectivamente. Para un conjunto de datos enorme, proporcionará muchos máximos / mínimos, por lo que en ese caso primero suavice la curva y luego aplique este algoritmo.

prtkp
fuente
esto parece interesante. Sin bibliotecas. ¿Como funciona?
john ktejik
1
atraviesa la curva desde el punto de partida y mira si vas hacia arriba o hacia abajo continuamente, una vez que cambias de arriba a abajo significa que tienes un máximo, si vas de abajo a arriba, tienes un mínimo.
prtkp
1

Otra solución que utiliza esencialmente un operador dilatar:

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

y para los mínimos:

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

Además, desde scipy.ndimagepuede reemplazar rank_filter(x, -1, size=3)con grey_dilationy rank_filter(x, 0, size=3)con grey_erosion. Esto no requerirá una clasificación local, por lo que es un poco más rápido.

gnodab
fuente
funciona correctamente para este problema. Aquí la solución es perfecta (+1)
Leos313
0

Otro:


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
Pedro
fuente