Trovare i massimi / minimi locali con Numpy in un array numpy 1D


116

Potete suggerire una funzione del modulo da numpy / scipy che può trovare i massimi / minimi locali in un array numpy 1D? Ovviamente l'approccio più semplice in assoluto è quello di dare un'occhiata ai vicini più vicini, ma mi piacerebbe avere una soluzione accettata che faccia parte della distribuzione numpy.



1
No, è in 2D (sto parlando di 1D) e coinvolge funzioni personalizzate. Ho la mia semplice implementazione, ma mi chiedevo se ce n'è una migliore, fornita con i moduli Numpy / Scipy.
Navi

Forse potresti aggiornare la domanda per includere che (1) hai un array 1d e (2) che tipo di minimo locale stai cercando. Solo una voce più piccola delle due voci adiacenti?
Sven Marnach

1
Puoi dare un'occhiata a scipy.signal.find_peaks_cwt se stai parlando di dati con rumore
Lakshay Garg

Risposte:


66

Se stai cercando tutte le voci nell'array 1d apiù piccole delle loro vicine, puoi provare

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

Puoi anche smussare il tuo array prima di questo passaggio usando numpy.convolve().

Non credo che ci sia una funzione dedicata per questo.


Hmm, perché dovrei lisciare? Per rimuovere il rumore? Sembra interessante. Mi sembra di poter usare un altro numero intero invece di 1 nel tuo codice di esempio. Stavo anche pensando di calcolare i gradienti. Ad ogni modo, se non esiste una funzione, è un peccato.
Navi

1
@Navi: Il problema è che la nozione di "minimo locale" varia notevolmente da caso d'uso a caso d'uso, quindi è difficile fornire una funzione "standard" per questo scopo. La levigatura aiuta a tenere in considerazione più del vicino più vicino. Usare un numero intero diverso invece di 1, diciamo 3, sarebbe strano in quanto considererebbe solo il terzo elemento successivo in entrambe le direzioni, ma non i vicini diretti.
Sven Marnach

1
@Sven Marnach: la ricetta che colleghi ritarda il segnale. c'è una seconda ricetta che usa filtfilt da scipy.signal
bobrobbob

2
Solo per il gusto di farlo, la sostituzione del <con >ti darà i massimi locali invece dei minimi
DarkCygnus

1
@SvenMarnach Ho usato la soluzione sopra per risolvere il mio problema pubblicato qui stackoverflow.com/questions/57403659/… ma ho ottenuto un output [False False]Quale potrebbe essere il problema qui?
Msquare

221

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

Nota, questi sono gli indici di x che sono max / min locali. Per ottenere i valori, prova:

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

scipy.signalprevede anche argrelmaxe argrelminper trovare rispettivamente massimi e minimi.


1
Qual è il significato di 12?
marshmallow

7
@marshmallow: np.random.random(12)genera 12 valori casuali, vengono utilizzati per dimostrare la funzione argrelextrema.
sebix

2
se l'input è test02=np.array([10,4,4,4,5,6,7,6]), allora non funziona. Non riconosce i valori consecutivi come minimi locali.
Leos 313

1
grazie, @ Cleb. Voglio segnalare altri problemi: che dire dei punti estremi dell'array? anche il primo elemento è un massimo locale poiché anche l'ultimo elemento dell'array è un minimo locale. Inoltre, non restituisce quanti valori consecutivi sono fondati. Tuttavia, ho proposto una soluzione nel codice di questa domanda qui . Grazie!!
Leos313

1
Grazie, questa è una delle migliori soluzioni che ho trovato finora
Noufal E

37

Per le curve con non troppo rumore, consiglio il seguente piccolo frammento di codice:

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

Il +1è importante, perché diffriduce il numero indice originale.


1
buon uso delle funzioni numpy annidate! ma nota che questo non riesce a raggiungere i massimi alle estremità dell'array :)
danodonovan

2
Questo agirà anche in modo strano se ci sono valori ripetitivi. ad esempio, se prendi l'array [1, 2, 2, 3, 3, 3, 2, 2, 1], il massimo locale è ovviamente da qualche parte tra i 3 nel mezzo. Ma se esegui le funzioni che hai fornito ottieni massime agli indici 2,6 e minime agli indici 1,3,5,7, il che per me non ha molto senso.
Korem

5
Per evitare questo +1invece di np.diff()usare np.gradient().
ankostis

So che questo thread ha anni, ma vale la pena aggiungere che se la tua curva è troppo rumorosa, puoi sempre provare prima il filtro passa-basso per smussare. Almeno per me, la maggior parte dei miei usi max / min locali sono per max / min globali all'interno di alcune aree locali (ad es., I grandi picchi e valli, non tutte le variazioni nei dati)
marcman

25

Un altro approccio (più parole, meno codice) che può aiutare:

Le posizioni dei massimi e dei minimi locali sono anche le posizioni degli incroci per lo zero della derivata prima. In genere è molto più facile trovare incroci per lo zero che trovare direttamente massimi e minimi locali.

Sfortunatamente, la derivata prima tende ad "amplificare" il rumore, quindi quando è presente un rumore significativo nei dati originali, la derivata prima è utilizzata al meglio solo dopo che ai dati originali è stato applicato un certo grado di attenuazione.

Poiché lo smoothing è, nel senso più semplice, un filtro passa-basso, lo smoothing è spesso il migliore (beh, più facilmente) fatto usando un kernel di convoluzione, e "modellando" quel kernel può fornire una quantità sorprendente di capacità di preservare / migliorare le funzionalità . Il processo per trovare un kernel ottimale può essere automatizzato utilizzando una varietà di mezzi, ma il migliore potrebbe essere la semplice forza bruta (molto veloce per trovare piccoli kernel). Un buon kernel distorcerà (come previsto) in maniera massiccia i dati originali, ma NON influenzerà la posizione dei picchi / avvallamenti di interesse.

Fortunatamente, molto spesso un kernel adatto può essere creato tramite un semplice SWAG ("ipotesi istruita"). La larghezza del kernel levigante dovrebbe essere un po 'più ampia del picco "interessante" atteso più ampio nei dati originali, e la sua forma assomiglierà a quel picco (un wavelet a scala singola). Per i kernel che conservano la media (quello che dovrebbe essere un buon filtro smoothing) la somma degli elementi del kernel dovrebbe essere esattamente uguale a 1.00, e il kernel dovrebbe essere simmetrico rispetto al suo centro (il che significa che avrà un numero dispari di elementi.

Dato un kernel smoothing ottimale (o un piccolo numero di kernel ottimizzati per diversi contenuti di dati), il grado di smoothing diventa un fattore di scala per (il "guadagno" del) kernel di convoluzione.

La determinazione del grado "corretto" (ottimale) di livellamento (guadagno del kernel di convoluzione) può anche essere automatizzata: confrontare la deviazione standard dei dati della prima derivata con la deviazione standard dei dati livellati. Il modo in cui il rapporto tra le due deviazioni standard cambia con i cambiamenti nel grado di camma di livellamento può essere utilizzato per prevedere i valori di livellamento effettivi. Alcune esecuzioni manuali di dati (che sono veramente rappresentative) dovrebbero essere tutto ciò che serve.

Tutte le soluzioni precedenti pubblicate sopra calcolano la derivata prima, ma non la trattano come una misura statistica, né le soluzioni di cui sopra tentano di eseguire il livellamento di conservazione / miglioramento delle caratteristiche (per aiutare i picchi sottili a "saltare sopra" il rumore).

Infine, la cattiva notizia: trovare picchi "reali" diventa un vero dolore quando il rumore ha anche caratteristiche che sembrano picchi reali (larghezza di banda sovrapposta). La prossima soluzione più complessa è generalmente quella di utilizzare un kernel di convoluzione più lungo (una "apertura del kernel più ampia") che tenga conto della relazione tra picchi "reali" adiacenti (come velocità minime o massime per l'occorrenza dei picchi), o di utilizzare più la convoluzione passa utilizzando kernel di larghezza diversa (ma solo se è più veloce: è una verità matematica fondamentale che le convoluzioni lineari eseguite in sequenza possono sempre essere convolte insieme in una singola convoluzione). Ma spesso è molto più facile trovare prima una sequenza di kernel utili (di larghezza variabile) e convolgerli insieme piuttosto che trovare direttamente il kernel finale in un unico passaggio.

Si spera che questo fornisca informazioni sufficienti per consentire a Google (e forse un buon testo delle statistiche) di colmare le lacune. Vorrei davvero avere il tempo di fornire un esempio funzionante o un collegamento a uno. Se qualcuno ne trova uno online, pubblicalo qui!


24

A partire dalla versione 1.1 di SciPy, puoi anche utilizzare find_peaks . Di seguito sono riportati due esempi tratti dalla documentazione stessa.

Utilizzando l' heightargomento, è possibile selezionare tutti i massimi al di sopra di una certa soglia (in questo esempio, tutti i massimi non negativi; questo può essere molto utile se si ha a che fare con una linea di base rumorosa; se si desidera trovare i minimi, è sufficiente moltiplicare l'immissione da -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()

inserisci qui la descrizione dell'immagine

Un altro argomento estremamente utile è distance, che definisce la distanza minima tra due picchi:

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

inserisci qui la descrizione dell'immagine


10

Perché non utilizzare la funzione incorporata di Scipy signal.find_peaks_cwt per fare il lavoro?

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]

i risultati:

maxima [ 0.9995736]
minima [ 0.09146464]

Saluti


7
Invece di fare la divisione (con possibile perdita di precisione), perché non moltiplicare semplicemente per -1 per passare dai massimi ai minimi?
Livius

Ho provato a cambiare '1 / data' in 'data * -1', ma poi solleva un errore, potresti condividere come implementare il tuo metodo?
A STEFANI

Forse perché non vogliamo richiedere che gli utenti finali installino anche scipy.
Damian Yerrick

5

Aggiornamento: non ero soddisfatto del gradiente, quindi l'ho trovato più affidabile da usarenumpy.diff . Per favore fatemi sapere se fa quello che volete.

Per quanto riguarda la questione del rumore, il problema matematico è individuare i massimi / minimi se vogliamo guardare al rumore possiamo usare qualcosa come convolve che è stato menzionato in precedenza.

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

Sai come viene calcolato questo gradiente? Se hai dati rumorosi probabilmente il gradiente cambia molto, ma questo non significa che ci sia un massimo / minimo.
Navi

Sì, lo so, per quanto i dati rumorosi siano un problema diverso. Per questo immagino che usi convolve.
Mike Vella

Avevo bisogno di qualcosa di simile per un progetto su cui stavo lavorando e ho utilizzato il metodo numpy.diff sopra menzionato, ho pensato che potesse essere utile menzionare che per i miei dati il ​​codice sopra ha perso alcuni massimi e minimi, cambiando il medio termine in entrambi if dichiarazioni a <= e> = rispettivamente, sono stato in grado di cogliere tutti i punti.

5

Anche se questa domanda è davvero vecchia. Credo che ci sia un approccio molto più semplice in numpy (one 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])

Per trovare un massimo o un minimo locale, vogliamo essenzialmente trovare quando la differenza tra i valori nell'elenco (3-1, 9-3 ...) cambia da positivo a negativo (max) o da negativo a positivo (min). Pertanto, prima troviamo la differenza. Quindi troviamo il segno, e poi troviamo i cambiamenti nel segno riprendendo la differenza. (Un po 'come una derivata prima e seconda nel calcolo, solo che abbiamo dati discreti e non abbiamo una funzione continua.)

L'output nel mio esempio non contiene gli estremi (il primo e l'ultimo valore nell'elenco). Inoltre, proprio come il calcolo, se la derivata seconda è negativa, hai max, e se è positiva hai un min.

Quindi abbiamo il seguente abbinamento:

[1,  3,  9,  5,  2,  5,  6,  9,  7]
    [0, -2,  0,  2,  0,  0, -2]
        Max     Min         Max

1
Penso che questa (buona!) Risposta sia la stessa della risposta di RC del 2012? Offre tre soluzioni di una riga, a seconda che il chiamante desideri i minimi, i massimi o entrambi, se sto leggendo correttamente la sua soluzione.
Brandon Rhodes

3

Nessuna di queste soluzioni ha funzionato per me poiché volevo trovare i picchi anche al centro dei valori ripetuti. ad esempio, in

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

la risposta dovrebbe essere

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

L'ho fatto usando un loop. So che non è super pulito, ma fa il suo lavoro.

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 

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 maxmcontengono indici di minimi e massimi, rispettivamente. Per un enorme set di dati, darà molte massime / minime, quindi in tal caso smussa prima la curva e poi applica questo algoritmo.


questo sembra interessante. Nessuna libreria. Come funziona?
john ktejik

1
attraversa la curva dal punto di partenza e vedi se stai andando verso l'alto o verso il basso continuamente, una volta che cambi da su a giù significa che hai un massimo, se stai andando dal basso verso l'alto, hai un minimo.
prtkp

1

Un'altra soluzione che utilizza essenzialmente un operatore dilatato:

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 per i minimi:

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

Inoltre, da scipy.ndimagepuoi sostituire rank_filter(x, -1, size=3)con grey_dilatione rank_filter(x, 0, size=3)con grey_erosion. Questo non richiederà un ordinamento locale, quindi è leggermente più veloce.


funziona correttamente per questo problema. Qui la soluzione è perfetta (+1)
Leos313

0

Un altro:


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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.