Comment lisser une courbe de la bonne manière?


200

Supposons que nous ayons un ensemble de données qui pourrait être donné approximativement par

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

Nous avons donc une variation de 20% de l'ensemble de données. Ma première idée a été d'utiliser la fonction UnivariateSpline de scipy, mais le problème est que cela ne considère pas le petit bruit dans le bon sens. Si vous considérez les fréquences, l'arrière-plan est beaucoup plus petit que le signal, donc une spline uniquement de la coupure pourrait être une idée, mais cela impliquerait une transformation de Fourier d'avant en arrière, ce qui pourrait entraîner un mauvais comportement. Une autre façon serait une moyenne mobile, mais cela nécessiterait également le bon choix du retard.

Des conseils / livres ou des liens sur la façon de résoudre ce problème?

exemple


1
Votre signal sera-t-il toujours une onde sinusoïdale, ou l'utilisiez-vous uniquement à titre d'exemple?
Mark Ransom

non, j'aurai des signaux différents, même dans cet exemple simple il est évident que mes méthodes ne sont pas suffisantes
varantir

le filtrage kalman est optimal dans ce cas. Et le paquet pykalman python est de bonne qualité.
toine

Je vais peut-être l'étendre à une réponse complète lorsque j'aurai un peu plus de temps, mais la seule méthode de régression puissante qui n'a pas encore été mentionnée est la régression GP (Gaussian Process).
Ori5678

Réponses:


262

Je préfère un filtre Savitzky-Golay . Il utilise les moindres carrés pour régresser une petite fenêtre de vos données sur un polynôme, puis utilise le polynôme pour estimer le point au centre de la fenêtre. Enfin, la fenêtre est décalée d'un point de données vers l'avant et le processus se répète. Cela continue jusqu'à ce que chaque point ait été ajusté de manière optimale par rapport à ses voisins. Cela fonctionne très bien même avec des échantillons bruyants provenant de sources non périodiques et non linéaires.

Voici un exemple complet de livre de cuisine . Voir mon code ci-dessous pour avoir une idée de la facilité d'utilisation. Remarque: J'ai omis le code pour définir la savitzky_golay()fonction car vous pouvez littéralement le copier / coller à partir de l'exemple de livre de cuisine que j'ai lié ci-dessus.

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

lissage optimal d'une sinusoïde bruyante

MISE À JOUR: Il est venu à mon attention que l'exemple de livre de cuisine auquel j'ai lié a été retiré. Heureusement, le filtre Savitzky-Golay a été intégré à la bibliothèque SciPy , comme l'a souligné @dodohjk . Pour adapter le code ci-dessus à l'aide de la source SciPy, tapez:

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

J'ai reçu l'erreur Traceback (dernier appel en date): fichier "hp.py", ligne 79, dans <module> ysm2 = savitzky_golay (y_data, 51,3) Fichier "hp.py", ligne 42, dans savitzky_golay firstvals = y [0] - np.abs (y [1: half_window + 1] [:: - 1] - y [0])
Mars Ho


14
Merci d'avoir présenté le filtre Savitzky-Golay! Donc, fondamentalement, cela ressemble à un filtre "Moyenne mobile" normal, mais au lieu de simplement calculer la moyenne, un ajustement polynomial (généralement de 2e ou 4e ordre) est effectué pour chaque point, et seul le point "central" est choisi. Étant donné que les informations d'ordre 2 (ou 4) sont concernées à chaque point, le biais introduit dans l'approche de la "moyenne mobile" aux maxima ou minima locaux est contourné. Vraiment élégant.
np8

2
Je veux juste vous remercier pour cela, je suis devenu fou en essayant de comprendre les décompositions en ondelettes pour obtenir des données lissées, et c'est tellement plus agréable.
Eldar M.

5
Si les données de x ne sont pas espacées régulièrement vous pourriez vouloir appliquer le filtre aux x ainsi: savgol_filter((x, y), ...).
Tim Kuipers

127

Un moyen rapide et sale de lisser les données que j'utilise, basé sur une boîte moyenne mobile (par convolution):

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)

entrez la description de l'image ici


9
Cela a quelques avantages: (1) fonctionne pour n'importe quelle fonction, pas seulement périodique, et (2) aucune dépendance ou grande fonction à copier-coller. Vous pouvez le faire tout de suite avec Numpy pur. De plus, ce n'est pas trop sale --- c'est le cas le plus simple de certaines des autres méthodes décrites ci-dessus (comme LOWESS mais le noyau est un intervalle pointu et comme Savitzky-Golay mais le degré polynomial est zéro).
Jim Pivarski

2
le seul problème avec la moyenne mobile est qu'elle est en retard sur les données. Vous pouvez le voir le plus apparemment à la fin où il y a plus de points en haut et moins en bas, mais la courbe verte est actuellement inférieure à la moyenne car la fonction de fenêtre doit avancer pour en tenir compte.
nurettin

Et cela ne fonctionne pas sur nd array, seulement 1d. scipy.ndimage.filters.convolve1d()vous permet de spécifier un axe d'un nd-array pour effectuer le filtrage. Mais je pense que les deux souffrent de problèmes de valeurs masquées.
Jason

1
@nurettin Je pense que ce que vous décrivez sont des effets de bord. En général, tant que le noyau de convolution est capable de couvrir son étendue dans le signal, il ne "traîne" pas comme vous le dites. À la fin, cependant, il n'y a aucune valeur au-delà de 6 à inclure dans la moyenne, donc seule la partie "gauche" du noyau est utilisée. Les effets de contour sont présents dans chaque noyau de lissage et doivent être gérés séparément.
Jon

4
@nurettin Non, j'essayais de clarifier pour les autres personnes lisant ceci que votre commentaire "le seul problème avec la moyenne mobile est qu'il est en retard sur les données" est trompeur. Toute méthode de filtrage des fenêtres souffre de ce problème, pas seulement de la moyenne mobile. Savitzky-golay souffre également de ce problème. Donc, votre affirmation "Ce que je décris est ce que savitzky_golay résout par estimation" est tout simplement fausse. L'une ou l'autre méthode de lissage nécessite un moyen de gérer les bords qui est indépendant de la méthode de lissage elle-même.
Jon

79

Si vous êtes intéressé par une version "fluide" d'un signal périodique (comme votre exemple), alors une FFT est la bonne solution. Prenez la transformée de Fourier et soustrayez les fréquences à faible contribution:

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)

entrez la description de l'image ici

Même si votre signal n'est pas complètement périodique, cela fera un excellent travail pour soustraire le bruit blanc. Il existe de nombreux types de filtres à utiliser (passe-haut, passe-bas, etc ...), celui qui convient dépend de ce que vous recherchez.


Quel graphique est pour quelle variable? J'essaie de lisser les coordonnées de la balle de tennis dans un rallye, c'est-à-dire. éliminer tous les rebonds qui ressemblent à de petites paraboles sur mon intrigue
mLstudent33

44

Ajuster une moyenne mobile à vos données atténuerait le bruit, voyez ceci cette réponse pour savoir comment procéder.

Si vous souhaitez utiliser LOWESS pour ajuster vos données (c'est similaire à une moyenne mobile mais plus sophistiqué), vous pouvez le faire en utilisant la bibliothèque de modèles de statistiques :

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

Enfin, si vous connaissez la forme fonctionnelle de votre signal, vous pouvez adapter une courbe à vos données, ce qui serait probablement la meilleure chose à faire.


Si seulement le avait loessmis en œuvre.
scrutari

18

Une autre option consiste à utiliser KernelReg dans les modèles de statistiques :

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

Regarde ça! Il existe une définition claire du lissage d'un signal 1D.

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

Raccourci:

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
Un lien vers une solution est le bienvenu, mais assurez-vous que votre réponse est utile sans elle: ajoutez du contexte autour du lien pour que vos collègues utilisateurs aient une idée de ce que c'est et pourquoi il est là, puis citez la partie la plus pertinente de la page que vous '' relier à au cas où la page cible n'est pas disponible. Les réponses qui ne sont guère plus qu'un lien peuvent être supprimées.
Shree

-4

Si vous tracez un graphique de séries chronologiques et si vous avez utilisé mtplotlib pour dessiner des graphiques, utilisez la méthode médiane pour lisser le graphique

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

timeseriesest passé votre ensemble de données que vous pouvez modifier windowsizepour plus de lissage.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.