web-dev-qa-db-fra.com

Comment ajuster une courbe sinusoïdale à mes données avec pylab et numpy?

Pour un projet d'école, j'essaie de montrer que les économies suivent un schéma de croissance relativement sinusoïdal. Au-delà des aspects économiques de celui-ci, qui sont certes douteux, je construis une simulation python pour montrer que même lorsque nous laissons un certain degré de hasard, nous pouvons toujours produire quelque chose de relativement sinusoïdal. Je suis satisfait de mes données que je produis mais maintenant j'aimerais trouver un moyen d'obtenir un sinus qui correspond assez étroitement aux données. Je sais que vous pouvez faire l'ajustement polynomial, mais pouvez-vous l'ajustement sinus?

Merci d'avance pour votre aide. Faites-moi savoir s'il y a des parties du code que vous souhaitez voir.

24
ChapmIndustries

Vous pouvez utiliser la fonction optimisation des moindres carrés dans scipy pour adapter n'importe quelle fonction arbitraire à une autre. En cas d'ajustement d'une fonction sin, les 3 paramètres à ajuster sont le décalage ('a'), l'amplitude ('b') et la phase ('c').

Tant que vous fournissez une première estimation raisonnable des paramètres, l'optimisation devrait bien converger.Heureusement pour une fonction sinus, les premières estimations de 2 d'entre elles sont faciles: le décalage peut être estimé en prenant la moyenne des données et l'amplitude via le RMS (3 * écart-type/sqrt (2)).

Remarque: comme modification ultérieure, l'ajustement de fréquence a également été ajouté. Cela ne fonctionne pas très bien (peut conduire à des ajustements extrêmement pauvres). Ainsi, utilisez à votre discrétion, mon conseil serait de ne pas utiliser l'ajustement de fréquence sauf si l'erreur de fréquence est inférieure à quelques pour cent.

Cela conduit au code suivant:

import numpy as np
from scipy.optimize import leastsq
import pylab as plt

N = 1000 # number of data points
t = np.linspace(0, 4*np.pi, N)
f = 1.15247 # Optional!! Advised not to use
data = 3.0*np.sin(f*t+0.001) + 0.5 + np.random.randn(N) # create artificial data with noise

guess_mean = np.mean(data)
guess_std = 3*np.std(data)/(2**0.5)/(2**0.5)
guess_phase = 0
guess_freq = 1
guess_amp = 1

# we'll use this to plot our first estimate. This might already be good enough for you
data_first_guess = guess_std*np.sin(t+guess_phase) + guess_mean

# Define the function to optimize, in this case, we want to minimize the difference
# between the actual data and our "guessed" parameters
optimize_func = lambda x: x[0]*np.sin(x[1]*t+x[2]) + x[3] - data
est_amp, est_freq, est_phase, est_mean = leastsq(optimize_func, [guess_amp, guess_freq, guess_phase, guess_mean])[0]

# recreate the fitted curve using the optimized parameters
data_fit = est_amp*np.sin(est_freq*t+est_phase) + est_mean

# recreate the fitted curve using the optimized parameters

fine_t = np.arange(0,max(t),0.1)
data_fit=est_amp*np.sin(est_freq*fine_t+est_phase)+est_mean

plt.plot(t, data, '.')
plt.plot(t, data_first_guess, label='first guess')
plt.plot(fine_t, data_fit, label='after fitting')
plt.legend()
plt.show()

enter image description here

Edit: je suppose que vous connaissez le nombre de périodes dans l'onde sinusoïdale. Si vous ne le faites pas, il est un peu plus difficile à adapter. Vous pouvez essayer de deviner le nombre de périodes en traçant manuellement et essayer de l'optimiser comme votre 6ème paramètre.

45
Dhara

Voici une fonction d'ajustement sans paramètre fit_sin() qui ne nécessite pas de deviner manuellement la fréquence:

import numpy, scipy.optimize

def fit_sin(tt, yy):
    '''Fit sin to the input time sequence, and return fitting parameters "amp", "omega", "phase", "offset", "freq", "period" and "fitfunc"'''
    tt = numpy.array(tt)
    yy = numpy.array(yy)
    ff = numpy.fft.fftfreq(len(tt), (tt[1]-tt[0]))   # assume uniform spacing
    Fyy = abs(numpy.fft.fft(yy))
    guess_freq = abs(ff[numpy.argmax(Fyy[1:])+1])   # excluding the zero frequency "peak", which is related to offset
    guess_amp = numpy.std(yy) * 2.**0.5
    guess_offset = numpy.mean(yy)
    guess = numpy.array([guess_amp, 2.*numpy.pi*guess_freq, 0., guess_offset])

    def sinfunc(t, A, w, p, c):  return A * numpy.sin(w*t + p) + c
    popt, pcov = scipy.optimize.curve_fit(sinfunc, tt, yy, p0=guess)
    A, w, p, c = popt
    f = w/(2.*numpy.pi)
    fitfunc = lambda t: A * numpy.sin(w*t + p) + c
    return {"amp": A, "omega": w, "phase": p, "offset": c, "freq": f, "period": 1./f, "fitfunc": fitfunc, "maxcov": numpy.max(pcov), "rawres": (guess,popt,pcov)}

La conjecture de fréquence initiale est donnée par la fréquence de crête dans le domaine fréquentiel en utilisant la FFT. Le résultat d'ajustement est presque parfait en supposant qu'il n'y a qu'une seule fréquence dominante (autre que le pic de fréquence zéro).

import pylab as plt

N, amp, omega, phase, offset, noise = 500, 1., 2., .5, 4., 3
#N, amp, omega, phase, offset, noise = 50, 1., .4, .5, 4., .2
#N, amp, omega, phase, offset, noise = 200, 1., 20, .5, 4., 1
tt = numpy.linspace(0, 10, N)
tt2 = numpy.linspace(0, 10, 10*N)
yy = amp*numpy.sin(omega*tt + phase) + offset
yynoise = yy + noise*(numpy.random.random(len(tt))-0.5)

res = fit_sin(tt, yynoise)
print( "Amplitude=%(amp)s, Angular freq.=%(omega)s, phase=%(phase)s, offset=%(offset)s, Max. Cov.=%(maxcov)s" % res )

plt.plot(tt, yy, "-k", label="y", linewidth=2)
plt.plot(tt, yynoise, "ok", label="y with noise")
plt.plot(tt2, res["fitfunc"](tt2), "r-", label="y fit curve", linewidth=2)
plt.legend(loc="best")
plt.show()

Le résultat est bon même avec un bruit élevé:

Amplitude = 1,00660540618, Angular fréq. = 2,03370472482, phase = 0,360276844224, décalage = 3,95747467506, cov. Max. = 0,0122923578658

With noiseLow frequencyHigh frequency

43
unsym

La fonction curvefit est plus conviviale pour nous. Voici un exemple:

import numpy as np
from scipy.optimize import curve_fit
import pylab as plt

N = 1000 # number of data points
t = np.linspace(0, 4*np.pi, N)
data = 3.0*np.sin(t+0.001) + 0.5 + np.random.randn(N) # create artificial data with noise

guess_freq = 1
guess_amplitude = 3*np.std(data)/(2**0.5)
guess_phase = 0
guess_offset = np.mean(data)

p0=[guess_freq, guess_amplitude,
    guess_phase, guess_offset]

# create the function we want to fit
def my_sin(x, freq, amplitude, phase, offset):
    return np.sin(x * freq + phase) * amplitude + offset

# now do the fit
fit = curve_fit(my_sin, t, data, p0=p0)

# we'll use this to plot our first estimate. This might already be good enough for you
data_first_guess = my_sin(t, *p0)

# recreate the fitted curve using the optimized parameters
data_fit = my_sin(t, *fit[0])

plt.plot(data, '.')
plt.plot(data_fit, label='after fitting')
plt.plot(data_first_guess, label='first guess')
plt.legend()
plt.show()
12
Vasco

Les méthodes actuelles pour adapter une courbe sin à un ensemble de données donné nécessitent une première estimation des paramètres, suivie d'un processus interactif. Il s'agit d'un problème de régression non linéaire.

Une autre méthode consiste à transformer la régression non linéaire en une régression linéaire grâce à une équation intégrale pratique. Ensuite, il n'y a pas besoin de conjecture initiale et pas besoin de processus itératif: l'ajustement est directement obtenu.

Dans le cas de la fonction y = a + r*sin(w*x+phi) ou y=a+b*sin(w*x)+c*cos(w*x), voir pages 35-36 du document "Régression sinusoidale" Publié le Scribd

Dans le cas de la fonction y = a + p*x + r*sin(w*x+phi): pages 49-51 du chapitre "Régressions mixtes linéaires et sinusoïdales".

Dans le cas de fonctions plus compliquées, le processus général est expliqué dans le chapitre "Generalized sinusoidal regression" Pages 54-61, suivi d'un exemple numérique y = r*sin(w*x+phi)+(b/x)+c*ln(x), pages 62-63

4
JJacquelin

Le script Python effectue l'ajustement des moindres carrés des sinusoïdes comme décrit dans Bloomfield (2000), "Analyse de Fourier des séries chronologiques", Wiley. Les étapes clés sont les suivantes:

  1. Définissez une gamme de différentes fréquences possibles.
  2. Pour chacune des fréquences spécifiées au point 1 ci-dessus, estimez tous les paramètres de la sinusoïde (moyenne, amplitude et phase) par les moindres carrés ordinaires (OLS).
  3. Parmi tous les différents ensembles de paramètres estimés au point 2 ci-dessus, sélectionnez celui qui minimise la somme résiduelle des carrés (RSS).
import pandas as pd
import numpy as np
import statsmodels.api as sm
import matplotlib.pyplot as plt
######################################################################################
# (1) generate the data
######################################################################################
omega=4.5  # frequency
theta0=0.5 # mean
theta1=1.5 # amplitude
phi=-0.25  # phase

n=1000 # number of observations
sigma=1.25 # error standard deviation
e=np.random.normal(0,sigma,n) # Gaussian error

t=np.linspace(1,n,n)/n # time index
y=theta0+theta1*np.cos(2*np.pi*(omega*t+phi))+e # time series
######################################################################################
# (2) estimate the parameters
######################################################################################
# define the range of different possible frequencies
f=np.linspace(1,12,100)

# create a data frame to store the estimated parameters and the associated
# residual sum of squares for each of the considered frequencies
coefs=pd.DataFrame(data=np.zeros((len(f),5)), columns=["omega","theta0","theta1","phi","RSS"])

for i in range(len(f)): # iterate across the different considered frequencies

    x1=np.cos(2*np.pi*f[i]*t) # define the first regressor
    x2=np.sin(2*np.pi*f[i]*t) # define the second regressor

    X=sm.add_constant(np.transpose(np.vstack((x1,x2)))) # add the intercept
    reg=sm.OLS(y,X).fit() # fit the regression by OLS

    A=reg.params[1] # estimated coefficient of first regressor
    B=reg.params[2] # estimated coefficient of second regressor

    # estimated mean
    mean=reg.params[0]

    # estimated amplitude
    amplitude=np.sqrt(A**2+B**2)

    # estimated phase
    if A>0:          phase=np.arctan(-B/A)/(2*np.pi)
    if A<0 and B>0:  phase=(np.arctan(-B/A)-np.pi)/(2*np.pi)
    if A<0 and B<=0: phase=(np.arctan(-B/A)+np.pi)/(2*np.pi)
    if A==0 and B>0: phase=-1/4
    if A==0 and B<0: phase=1/4

    # fitted sinusoid
    s=mean+amplitude*np.cos(2*np.pi*(f[i]*t+phase))

    # residual sum of squares
    RSS=np.sum((y-s)**2)

    # save the estimation results
    coefs["omega"][i]=f[i]
    coefs["theta0"][i]=mean
    coefs["theta1"][i]=amplitude
    coefs["phi"][i]=phase
    coefs["RSS"][i]=RSS

    del x1, x2, X, reg, A, B, mean, amplitude, phase, s, RSS
######################################################################################
# (3) analyze the results
######################################################################################
# extract the set of parameters that minimizes the residual sum of squares
coefs=coefs.loc[coefs["RSS"]==coefs["RSS"].min(),]

# calculate the fitted sinusoid
s=coefs["theta0"].values+coefs["theta1"].values*np.cos(2*np.pi*(coefs["omega"].values*t+coefs["phi"].values))

# plot the fitted sinusoid
plt.plot(y,color="black",linewidth=1,label="actual")
plt.plot(s,color="lightgreen",linewidth=3,label="fitted")
plt.ylim(ymax=np.max(y)*1.3)
plt.legend(loc=1)
plt.show()
0
flaviag