Wie kann man eine Kurve richtig glätten?


200

Nehmen wir an, wir haben einen Datensatz, der ungefähr von gegeben sein könnte

import numpy as np
x = np.linspace(0,2*np.pi,100)
y = np.sin(x) + np.random.random(100) * 0.2

Daher haben wir eine Variation von 20% des Datensatzes. Meine erste Idee war, die UnivariateSpline-Funktion von scipy zu verwenden, aber das Problem ist, dass dies das kleine Rauschen nicht gut berücksichtigt. Wenn Sie die Frequenzen berücksichtigen, ist der Hintergrund viel kleiner als das Signal, sodass ein Spline nur des Cutoffs eine Idee sein könnte, aber dies würde eine Hin- und Her-Fourier-Transformation beinhalten, die zu schlechtem Verhalten führen könnte. Ein anderer Weg wäre ein gleitender Durchschnitt, aber dies würde auch die richtige Wahl der Verzögerung erfordern.

Irgendwelche Hinweise / Bücher oder Links, wie man dieses Problem angeht?

Beispiel


1
Wird Ihr Signal immer eine Sinuswelle sein, oder haben Sie das nur als Beispiel verwendet?
Mark Ransom

nein, ich werde unterschiedliche Signale haben, auch in diesem einfachen Beispiel ist es offensichtlich, dass meine Methoden nicht ausreichen
varantir

Die Kalman-Filterung ist für diesen Fall optimal. Und das Pykalman-Python-Paket ist von guter Qualität.
Toine

Vielleicht werde ich es zu einer vollständigen Antwort erweitern, wenn ich etwas mehr Zeit habe, aber die einzige leistungsstarke Regressionsmethode, die noch nicht erwähnt wurde, ist die GP-Regression (Gaußscher Prozess).
Ori5678

Antworten:


261

Ich bevorzuge einen Savitzky-Golay-Filter . Es verwendet die kleinsten Quadrate, um ein kleines Fenster Ihrer Daten auf ein Polynom zu regressieren, und verwendet dann das Polynom, um den Punkt in der Mitte des Fensters zu schätzen. Schließlich wird das Fenster um einen Datenpunkt nach vorne verschoben und der Vorgang wiederholt. Dies setzt sich fort, bis jeder Punkt im Verhältnis zu seinen Nachbarn optimal angepasst wurde. Es funktioniert auch mit verrauschten Samples aus nicht periodischen und nicht linearen Quellen.

Hier ist ein ausführliches Kochbuchbeispiel . In meinem Code unten finden Sie eine Vorstellung davon, wie einfach die Verwendung ist. Hinweis: Ich habe den Code zum Definieren der savitzky_golay()Funktion weggelassen , da Sie ihn buchstäblich aus dem oben verlinkten Kochbuchbeispiel kopieren / einfügen können.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0,2*np.pi,100)
y = np.sin(x) + np.random.random(100) * 0.2
yhat = savitzky_golay(y, 51, 3) # window size 51, polynomial order 3

plt.plot(x,y)
plt.plot(x,yhat, color='red')
plt.show()

Glätten einer lauten Sinuskurve optimal

UPDATE: Mir ist aufgefallen, dass das von mir verlinkte Kochbuchbeispiel entfernt wurde. Glücklicherweise wurde der Savitzky-Golay-Filter in die SciPy-Bibliothek aufgenommen , wie @dodohjk hervorhob . Geben Sie Folgendes ein, um den obigen Code mithilfe der SciPy-Quelle anzupassen:

from scipy.signal import savgol_filter
yhat = savgol_filter(y, 51, 3) # window size 51, polynomial order 3

Ich habe den Fehler Traceback (letzter Aufruf zuletzt) ​​erhalten: Datei "hp.py", Zeile 79, in <module> ysm2 = savitzky_golay (y_data, 51,3) Datei "hp.py", Zeile 42, in savitzky_golay firstvals = y [0] - np.abs (y [1: half_window + 1] [:: - 1] - y [0])
März Ho


14
Vielen Dank für die Einführung des Savitzky-Golay-Filters! Im Grunde ist dies wie ein normaler Filter für den "gleitenden Durchschnitt", aber anstatt nur den Durchschnitt zu berechnen, wird für jeden Punkt eine Polynomanpassung (normalerweise 2. oder 4. Ordnung) vorgenommen und nur der "mittlere" Punkt ausgewählt. Da Informationen zu 2. (oder 4.) Ordnung an jedem Punkt betroffen sind, wird die Verzerrung, die beim Ansatz des "gleitenden Durchschnitts" bei lokalen Maxima oder Minima eingeführt wird, umgangen. Wirklich elegant.
np8

2
Ich möchte mich nur dafür bedanken. Ich bin verrückt geworden, als ich versucht habe, Wavelet-Zerlegungen herauszufinden, um geglättete Daten zu erhalten, und das ist viel schöner.
Eldar M.

5
Wenn die x-Daten nicht regelmäßig verteilt sind, können Sie den Filter auch auf die x anwenden : savgol_filter((x, y), ...).
Tim Kuipers

127

Ein schneller und schmutziger Weg, um die von mir verwendeten Daten zu glätten, basierend auf einer Box mit gleitendem Durchschnitt (durch Faltung):

x = np.linspace(0,2*np.pi,100)
y = np.sin(x) + np.random.random(100) * 0.8

def smooth(y, box_pts):
    box = np.ones(box_pts)/box_pts
    y_smooth = np.convolve(y, box, mode='same')
    return y_smooth

plot(x, y,'o')
plot(x, smooth(y,3), 'r-', lw=2)
plot(x, smooth(y,19), 'g-', lw=2)

Geben Sie hier die Bildbeschreibung ein


9
Dies hat ein paar nette Vorteile: (1) funktioniert für jede Funktion, nicht nur für periodische, und (2) keine Abhängigkeiten oder großen Funktionen zum Kopieren und Einfügen. Sie können es sofort mit reinem Numpy tun. Außerdem ist es nicht zu schmutzig - es ist der einfachste Fall einiger der anderen oben beschriebenen Methoden (wie LOWESS, aber der Kernel ist ein scharfes Intervall und wie Savitzky-Golay, aber der Polynomgrad ist Null).
Jim Pivarski

2
Das einzige Problem mit dem gleitenden Durchschnitt ist, dass er hinter den Daten zurückbleibt. Sie können dies am deutlichsten am Ende sehen, wo oben mehr Punkte und unten weniger Punkte vorhanden sind, aber die grüne Kurve liegt derzeit unter dem Durchschnitt, da die Fensterfunktion vorwärts gehen muss, um diese zu berücksichtigen.
Nurettin

Und das funktioniert nicht auf nd Array, nur 1d. scipy.ndimage.filters.convolve1d()Mit dieser Option können Sie eine Achse eines nd-Arrays angeben, um die Filterung durchzuführen. Aber ich denke, beide leiden unter einigen Problemen bei maskierten Werten.
Jason

1
@nurettin Ich denke, was Sie beschreiben, sind Randeffekte. Solange der Faltungskern in der Lage ist, seine Ausdehnung innerhalb des Signals abzudecken, bleibt er im Allgemeinen nicht "zurück", wie Sie sagen. Am Ende sind jedoch keine Werte über 6 im Durchschnitt enthalten, sodass nur der "linke" Teil des Kernels verwendet wird. Kanteneffekte sind in jedem Glättungskern vorhanden und müssen separat behandelt werden.
Jon

4
@nurettin Nein, ich habe versucht, für andere, die dies lesen, zu verdeutlichen, dass Ihr Kommentar "Das einzige Problem mit dem gleitenden Durchschnitt ist, dass er hinter den Daten zurückbleibt" irreführend ist. Bei jeder Fensterfiltermethode tritt dieses Problem auf, nicht nur beim gleitenden Durchschnitt. Savitzky-Golay leidet auch unter diesem Problem. Ihre Aussage "Was ich beschreibe, ist das, was savitzky_golay durch Schätzung löst" ist also einfach falsch. Jede Glättungsmethode erfordert eine Methode zur Behandlung von Kanten, die von der Glättungsmethode selbst unabhängig ist.
Jon

79

Wenn Sie an einer "glatten" Version eines Signals interessiert sind, das periodisch ist (wie in Ihrem Beispiel), ist eine FFT der richtige Weg. Nehmen Sie die Fourier-Transformation und subtrahieren Sie die Frequenzen mit niedrigem Beitrag:

import numpy as np
import scipy.fftpack

N = 100
x = np.linspace(0,2*np.pi,N)
y = np.sin(x) + np.random.random(N) * 0.2

w = scipy.fftpack.rfft(y)
f = scipy.fftpack.rfftfreq(N, x[1]-x[0])
spectrum = w**2

cutoff_idx = spectrum < (spectrum.max()/5)
w2 = w.copy()
w2[cutoff_idx] = 0

y2 = scipy.fftpack.irfft(w2)

Geben Sie hier die Bildbeschreibung ein

Selbst wenn Ihr Signal nicht vollständig periodisch ist, wird dadurch das weiße Rauschen hervorragend subtrahiert. Es gibt viele Arten von Filtern (Hochpass, Tiefpass usw.). Der geeignete Filter hängt davon ab, wonach Sie suchen.


Welches Diagramm ist für welche Variable? Ich versuche, die Koordinaten für den Tennisball bei einer Rallye zu glätten, d. H. Nehmen Sie alle Bounces heraus, die wie kleine Parabeln auf meiner Handlung
aussehen

44

Wenn Sie einen gleitenden Durchschnitt an Ihre Daten anpassen, wird das Rauschen ausgeglichen. In dieser Antwort erfahren Sie, wie das geht.

Wenn Sie LOWESS verwenden möchten , um Ihre Daten anzupassen (es ähnelt einem gleitenden Durchschnitt, ist jedoch komplexer ), können Sie dies mithilfe der Statistikmodellbibliothek tun :

import numpy as np
import pylab as plt
import statsmodels.api as sm

x = np.linspace(0,2*np.pi,100)
y = np.sin(x) + np.random.random(100) * 0.2
lowess = sm.nonparametric.lowess(y, x, frac=0.1)

plt.plot(x, y, '+')
plt.plot(lowess[:, 0], lowess[:, 1])
plt.show()

Wenn Sie die funktionale Form Ihres Signals kennen, können Sie eine Kurve an Ihre Daten anpassen, was wahrscheinlich das Beste ist.


Wenn nur das loessumgesetzt hätte.
Prüfung

18

Eine weitere Option ist die Verwendung von KernelReg in Statistikmodellen :

from statsmodels.nonparametric.kernel_regression import KernelReg
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0,2*np.pi,100)
y = np.sin(x) + np.random.random(100) * 0.2

# The third parameter specifies the type of the variable x;
# 'c' stands for continuous
kr = KernelReg(y,x,'c')
plt.plot(x, y, '+')
y_pred, y_std = kr.fit(x)

plt.plot(x, y_pred)
plt.show()

7

Schau dir das an! Es gibt eine klare Definition der Glättung eines 1D-Signals.

http://scipy-cookbook.readthedocs.io/items/SignalSmooth.html

Abkürzung:

import numpy

def smooth(x,window_len=11,window='hanning'):
    """smooth the data using a window with requested size.

    This method is based on the convolution of a scaled window with the signal.
    The signal is prepared by introducing reflected copies of the signal 
    (with the window size) in both ends so that transient parts are minimized
    in the begining and end part of the output signal.

    input:
        x: the input signal 
        window_len: the dimension of the smoothing window; should be an odd integer
        window: the type of window from 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'
            flat window will produce a moving average smoothing.

    output:
        the smoothed signal

    example:

    t=linspace(-2,2,0.1)
    x=sin(t)+randn(len(t))*0.1
    y=smooth(x)

    see also: 

    numpy.hanning, numpy.hamming, numpy.bartlett, numpy.blackman, numpy.convolve
    scipy.signal.lfilter

    TODO: the window parameter could be the window itself if an array instead of a string
    NOTE: length(output) != length(input), to correct this: return y[(window_len/2-1):-(window_len/2)] instead of just y.
    """

    if x.ndim != 1:
        raise ValueError, "smooth only accepts 1 dimension arrays."

    if x.size < window_len:
        raise ValueError, "Input vector needs to be bigger than window size."


    if window_len<3:
        return x


    if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']:
        raise ValueError, "Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'"


    s=numpy.r_[x[window_len-1:0:-1],x,x[-2:-window_len-1:-1]]
    #print(len(s))
    if window == 'flat': #moving average
        w=numpy.ones(window_len,'d')
    else:
        w=eval('numpy.'+window+'(window_len)')

    y=numpy.convolve(w/w.sum(),s,mode='valid')
    return y




from numpy import *
from pylab import *

def smooth_demo():

    t=linspace(-4,4,100)
    x=sin(t)
    xn=x+randn(len(t))*0.1
    y=smooth(x)

    ws=31

    subplot(211)
    plot(ones(ws))

    windows=['flat', 'hanning', 'hamming', 'bartlett', 'blackman']

    hold(True)
    for w in windows[1:]:
        eval('plot('+w+'(ws) )')

    axis([0,30,0,1.1])

    legend(windows)
    title("The smoothing windows")
    subplot(212)
    plot(x)
    plot(xn)
    for w in windows:
        plot(smooth(xn,10,w))
    l=['original signal', 'signal with noise']
    l.extend(windows)

    legend(l)
    title("Smoothing a noisy signal")
    show()


if __name__=='__main__':
    smooth_demo()

3
Ein Link zu einer Lösung ist willkommen, aber stellen Sie sicher, dass Ihre Antwort ohne sie nützlich ist: Fügen Sie dem Link einen Kontext hinzu, damit Ihre Mitbenutzer eine Vorstellung davon haben, was es ist und warum es dort ist, und zitieren Sie dann den relevantesten Teil der Seite, die Sie verwenden. erneutes Verknüpfen mit, falls die Zielseite nicht verfügbar ist. Antworten, die kaum mehr als ein Link sind, können gelöscht werden.
Shree

-4

Wenn Sie ein Zeitreihendiagramm zeichnen und mtplotlib zum Zeichnen von Diagrammen verwendet haben, verwenden Sie die Medianmethode, um das Diagramm zu glätten

smotDeriv = timeseries.rolling(window=20, min_periods=5, center=True).median()

Wo timeseriesIhr Datensatz übergeben wird, können Sie windowsizefür eine bessere Glättung ändern .

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.