Lineare Regression mit Fehlerbalken#

Gegeben sei eine Messreihe von 10 Messwerten \(y_i\) mit annäherend konstanten Verhalten, d.h. es wurde 10 mal ein Wert um 10 herum gemessen. Diese Messwerte sind im folgenden Diagramm inklusive Fehlerbalken (Anzeige der Messunsicherheit) dargestellt. Die Unsicherheit der \(x\)-Achse wird vernachlässigt.

import numpy as np
import matplotlib.pyplot as plt
# MatplotLib Settings:
plt.style.use('default') # Matplotlib Style wählen
plt.figure(figsize=(7,5)) # Plot-Größe
plt.rcParams['font.size'] = 10; # Schriftgröße

y = [11.55, 9.8, 9.82, 9.15, 10.57, 9.58, 10.44, 10.55, 8.23, 10.93] #Messwerte y_i
s_y = [0.8, 0.9, 0.9, 1.1, 1.0, 1.2, 0.7, 1.1, 1.0, 0.9] #Unsicherheiten Fall 1
x = [0., 1., 2., 3., 4., 5., 6., 7., 8., 9.] #Messwerte y_i

y = np.array(y) #konvertiere die Messwerte in ein Numpy-Array
x = np.array(x) #konvertiere die Messwerte in ein Numpy-Array
s_y = np.array(s_y) #konvertiere die Unsicherheiten in ein Numpy-Array

plt.errorbar(x,y, fmt='o', xerr = None, yerr = s_y, label = 'Messwerte')
plt.xlabel('Nr der Messung')
plt.ylabel('y')
plt.ylim([5,13])
plt.legend()
plt.show()
../_images/673b1fad805a4c5e8ecb7a974f5a3518161323c0bdd5d90a53ee41e80b88193e.png

Wir wollen nun überprüfen, ob das Verhalten wirklich konstant oder linear steigend oder doch eher linear abfallend ist. Hierfür werden wir im Folgenden eine lineare Ausgleichsgerade an die Daten anpassen (Kurvenanpassung/Lineare Regression).

Recap: Lineare Regression#

Im Vorlesungsskript findet ihr die Vorgehensweise, wie lineare Ausgleichsgeraden berechnet werden:

  • Gegeben: \(y_i\) Messwerte (hier 10)

  • Gesucht: lineare Ausgleichsgerade der Form \(f(x) = y = a\cdot x + b\), welche bestmöglich die Messdaten \(y_i\) beschreiben soll

  • Konsequenz: Modell \(f(x_i)\) und Messdaten \(y_i\) werden voneinander abweichen. Die Abweichung beträgt \(\epsilon_i = \left( f(x_i) - y_i\right)\)

  • Ziel: Die Abweichung \(\epsilon\) so klein wie möglich machen

  • Strategie: Methode der kleinsten Quadrate verwenden:

    • Für alle \(N=10\) Messwerte erhalten wir einen Fehler \(\epsilon_i\).

    • Die jeweiligen Fehler summieren wir auf um dem Gesamtfehler der kompletten Messreihe zu erhalten: \(\sum_{i = 0}^{9} \epsilon_i\)

    • Wir definieren die Gütefunktion, indem wir die Fehler quadrieren, dadurch können sich diese nicht versehentlich aufheben. Außerdem werden stärkere Abweichungen stärker gewichtet: \(Q =\sum_{i=0}^9 \left( f(x_i) - y_i\right)^2 := \sum_{i=9}^9 \epsilon_i^2 = \mathrm{min!}\)

Der Einfluss von Fehlerbalken auf den Fit #

Im Allgemeinen gilt, je kleiner die Unsicherheit eines Messwertes ist, desto wichtiger ist dieser Messwert für die Mittelwertbildung. Die analytische Lösung würde wiefolgt aussehen:

  • \(S(c) = \displaystyle\sum_{i=1}^{N} \frac{(y_i -c)^2}{{s_i}^2}\)

  • \(0= \frac{dS}{dc} = \displaystyle\sum_{i=1}^{N} \frac{2 (y_i -c)}{{s_i}^2}\)

\(\Rightarrow {\hat c} = \frac{1}{\sum{1/s_i}^2} \displaystyle\sum_{i=1}^{N}\frac{1}{{s_i}^2}y_i \)

Mit der Definition \( w_i \equiv \frac{1} {{s_i}^2}\) erhalten wir als wichtiges Ergebnis die Formel für den gewichteten Mittelwert:

\({\hat c}= \frac{1} {\sum {w_i}} \displaystyle\sum_{i=1}^{N} {w_i} y_i\)

Der Mittelwert ist die mit \(1/s_i^2\) gewichtete Summe der Einzelmessungen. Die Gewichte entsprechen der obigen Erwartung: Messwerte mit den kleinsten \(s_i\) bekommen bei der Mittelwertbildung das größte Gewicht. Am Vorfaktor \(1/\sum w_i\) kann man ablesen, dass die Zahl der Messwerte \(N\) in diesem Fall durch die Summe der Gewichte \(\sum w_i\) ersetzt werden muss.

Ist der Fehler für alle Messwerte konstant, so kann \(s_i\) einfach auf die andere Seite multipliziert werden und verschwindet aus der Gleichung. Ist der Fehler Null, nutzen wir die Gütefunktion, welche minimiert werden muss:

  • \(Q(c) := \sum_{i=1}^n (y_i - f(x))^2 = \sum_{i=1}^n (y_i - c)^2 = \textrm{min?}\)

def S(y,c): # Minimierungsfunktion mit Fehler
    return ((y-c)**2 / s_y).sum()

def Q(y,c): # Minimierungsfehler ohne Fehler
    return ((y-c)**2).sum()

Die Minimierung kann einfach ausgeführt werden, indem die Gütefunktion für verschiedene Funktionsparameter (\(c\)) ausprobiert wird, im Folgenden werden für \(c\) 100 Werte zwischen 8 und 12 ausprobiert:

c_val = np.linspace(8,12,100)
S_c = []
Q_c = []

for c in c_val:
    S_c.append(S(y,c))
    Q_c.append(Q(y,c))
    
id_min_S = np.argmin(S_c)
id_min_Q = np.argmin(Q_c)

print("Minimum unter Einbezug von Unsicherheiten ist bei c =", c_val[id_min_S])
print("Minimum ohne  Einbezug von Unsicherheiten ist bei c =", c_val[id_min_Q])

plt.figure()
plt.plot(c_val, S_c, label='Minimierungsfunktion inkl. Unsicherheiten')
plt.plot(c_val, Q_c, label = 'Minimierungsfunktion ohne/konstante Unsicherheiten')
plt.xlabel('c')
plt.ylabel('Minimierungsfunktion')
plt.legend()
plt.show()
Minimum unter Einbezug von Unsicherheiten ist bei c = 10.141414141414142
Minimum ohne  Einbezug von Unsicherheiten ist bei c = 10.06060606060606
../_images/93661c47dd5ce13b43391d07dd36646664cfd6605f15775d81523205bc6c63b3.png

Schon bei dieser schnellen Analyse sehen wir, dass es einen Unterschied gibt, ob wir die Fehlerbalken miteinbeziehen, oder ob keine Unsicherheiten vorhanden sind. Sind die absoluten Unsicherheiten für alle Messwerte die gleichen, so trifft der Fall ohne Unsicherheiten zu.

Kurvenanpassung mit Python: scipy #

Im Folgenden Code-Block wollen wir die Analyse dieser einfachen Messreihe noch einmal mittel scipy-Paket wiederholen und eine lineare Regression und konstante Regression auf die Messdaten anwenden. Wir definieren also zwei Fit-Funktionen:

def fit_lin(x, b, a): # Funktion für lineare Regression
    return b*x + a

def fit_const(x, a): # Funktion für konstante Regression
    return a + x - x

def f(x, c_val): # Funktion um Minimierungsfunktion in Diagramm zu zeichnen
    return c_val + x-x

Diese beiden Funktionen werden nun benutzt, um die Daten zu modellieren. Wir testen jeweils beide Fälle, nämlich mit und ohne Fehlerbalken:

from scipy.optimize import curve_fit
# ----- Mit Fehlerbalken: ---- #

fit_lin_out_err = curve_fit(fit_lin, x, y, sigma=s_y)
fit_const_out_err = curve_fit(fit_const, x, y, sigma=s_y)

plt.errorbar(x,y, fmt='o', xerr = None, yerr = s_y, label = 'Messwerte')
plt.plot(x,f(x,  c_val[id_min_S]), label = 'Minimierungsfunktion: c = %5.3f'%(c_val[id_min_S]))
plt.plot(x,fit_lin(x, fit_lin_out_err[0][0], fit_lin_out_err[0][1]), label = 'Lin. Fit: y = %5.3f*x+%5.3f'%(fit_lin_out_err[0][0], fit_lin_out_err[0][1]))
plt.plot(x,fit_const(x, fit_const_out_err[0][0]), label = 'Const. Fit: y = %5.3f'%(fit_const_out_err[0][0]))
plt.xlabel('Nr der Messung')
plt.ylabel('y')
plt.ylim([5,13])
plt.legend()
plt.title('Fit mit Fehlerbalken')
plt.show()


# ----- Ohne Fehlerbalken: ---- #

fit_lin_out = curve_fit(fit_lin, x, y)
fit_const_out = curve_fit(fit_const, x, y)

plt.plot(x,y, 'o', label = 'Messwerte')
plt.plot(x,f(x,  c_val[id_min_Q]), label = 'Minimierungsfunktion: c = %5.3f'%(c_val[id_min_Q]))
plt.plot(x,fit_lin(x, fit_lin_out[0][0], fit_lin_out[0][1]), label = 'Lin. Fit: y = %5.3f*x+%5.3f'%(fit_lin_out[0][0], fit_lin_out[0][1]))
plt.plot(x,fit_const(x, fit_const_out[0][0]), label = 'Const. Fit: y = %5.3f'%(fit_const_out[0][0]))
plt.xlabel('Nr der Messung')
plt.ylabel('y')
plt.ylim([5,13])
plt.legend()
plt.title('Fit ohne Fehlerbalken')
plt.show()
../_images/33e71de3de8624acd3a77ea1b47b62106ec43973966c999d29cec5de01cd271a.png ../_images/843aa250786fad19ceff36317a0593b23b9a23338295ecbcd04f85b404be8a8c.png

Kurvenanpassung mit Python: lmfit #

Das vorangegangene Beispiel mit scipy zeigt auf, wie aufwändig es ist eine Fitparameter zu fixieren. Es muss eine neue Funktion mit weniger Freiheitsgeraden definiert werden. Wie oben schon angeükndigt, lässt sich dies mit dem lmfit-Paket etwas einfacher lösen und soll hier anhand des Beispiels noch einmal visualisiert werden.

from lmfit import minimize, Parameters
from lmfit import Model

# ----- Mit Fehlerbalken: ---- #

def f_lin(x, steigung, abschnitt):
    return steigung*x + abschnitt

model = Model(f_lin)
params = model.make_params(steigung=0.0, abschnitt=10.0)
result_lin = model.fit(y, params, x=x, weights = 1/s_y) # Fehlerbalken werden über die 'weights = 1/sigma' Option berücksichtigt

params['steigung'].set(0.0, vary=False)
result_const = model.fit(y, params, x=x, weights = 1/s_y)

#print('Fit Parameter der Linearen Regression:')
result_lin.params.pretty_print()
#print('Fit Report:')
print(result_lin.fit_report())

#print('Fit Parameter der Konstanten Regression:')
#result_const.params.pretty_print()
#print('Fit Report:')
#print(result_const.fit_report())

plt.errorbar(x,y, fmt='o', xerr = None, yerr = s_y, label = 'Messwerte')
plt.plot(x,f(x,  c_val[id_min_S]), label = 'Minimierungsfunktion: c = %5.3f'%(c_val[id_min_S]))
plt.plot(x,fit_lin(x, fit_lin_out_err[0][0], fit_lin_out_err[0][1]), label = 'Lin. Fit: y = %5.3f*x+%5.3f'%(fit_lin_out_err[0][0], fit_lin_out_err[0][1]))
plt.plot(x,fit_const(x, fit_const_out_err[0][0]), label = 'Const. Fit: y = %5.3f'%(fit_const_out_err[0][0]))
plt.plot(x,result_lin.best_fit, '--',label = 'lmfit Lin-Fit: y = %5.3f*x+%5.3f' %(result_lin.params['steigung'].value,result_lin.params['abschnitt'].value))  # plot Fitfunktion mit b und a
plt.plot(x,result_const.best_fit, '--',label = 'lmfit Const-Fit: y = %5.3f*x+%5.3f' %(result_const.params['steigung'].value,result_const.params['abschnitt'].value))  # plot Fitfunktion mit b und a
plt.xlabel('Nr der Messung')
plt.ylabel('y')
plt.ylim([5,13])
plt.legend()
plt.title('Fit mit Fehlerbalken')
plt.show()


# ----- Ohne Fehlerbalken: ---- #

def f_lin(x, steigung, abschnitt):
    return steigung*x + abschnitt

model = Model(f_lin)
params = model.make_params(steigung=0.0, abschnitt=10.0)
result_lin = model.fit(y, params, x=x)

params['steigung'].set(0.0, vary=False)
result_const = model.fit(y, params, x=x)

#print('Fit Parameter der Linearen Regression:')
result_lin.params.pretty_print()
#print('Fit Report:')
print(result_lin.fit_report())

#print('Fit Parameter der Konstanten Regression:')
result_const.params.pretty_print()
#print('Fit Report:')
print(result_const.fit_report())

plt.plot(x,y, 'o', label = 'Messwerte')
plt.plot(x,f(x,  c_val[id_min_Q]), label = 'Minimierungsfunktion: c = %5.3f'%(c_val[id_min_Q]))
plt.plot(x,fit_lin(x, fit_lin_out[0][0], fit_lin_out[0][1]), label = 'scipy Lin. Fit: y = %5.3f*x+%5.3f'%(fit_lin_out[0][0], fit_lin_out[0][1]))
plt.plot(x,fit_const(x, fit_const_out[0][0]), label = 'scipy const. Fit: y = %5.3f'%(fit_const_out[0][0]))
plt.plot(x,result_lin.best_fit, '--',label = 'lmfit Lin-Fit: y = %5.3f*x+%5.3f' %(result_lin.params['steigung'].value,result_lin.params['abschnitt'].value))  # plot Fitfunktion mit b und a
plt.plot(x,result_const.best_fit, '--',label = 'lmfit Const-Fit: y = %5.3f*x+%5.3f' %(result_const.params['steigung'].value,result_const.params['abschnitt'].value))  # plot Fitfunktion mit b und a
plt.xlabel('Nr der Messung')
plt.ylabel('y')
plt.ylim([5,13])
plt.legend()
plt.title('Fit ohne Fehlerbalken')
plt.show()
Name          Value      Min      Max   Stderr     Vary     Expr Brute_Step
abschnitt     10.49     -inf      inf   0.5446     True     None     None
steigung   -0.06839     -inf      inf   0.1031     True     None     None
[[Model]]
    Model(f_lin)
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 6
    # data points      = 10
    # variables        = 2
    chi-square         = 8.81129304
    reduced chi-square = 1.10141163
    Akaike info crit   = 2.73449106
    Bayesian info crit = 3.33966124
[[Variables]]
    steigung:  -0.06839389 +/- 0.10313985 (150.80%) (init = 0)
    abschnitt:  10.4919542 +/- 0.54460553 (5.19%) (init = 10)
[[Correlations]] (unreported correlations are < 0.100)
    C(steigung, abschnitt) = -0.825
../_images/00a26ae19cf2df26ec72fb8eddddf9be09626190132dfada343a64209614ff25.png
Name          Value      Min      Max   Stderr     Vary     Expr Brute_Step
abschnitt     10.34     -inf      inf   0.5822     True     None     None
steigung   -0.06085     -inf      inf   0.1091     True     None     None
[[Model]]
    Model(f_lin)
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 6
    # data points      = 10
    # variables        = 2
    chi-square         = 7.84870061
    reduced chi-square = 0.98108758
    Akaike info crit   = 1.57762897
    Bayesian info crit = 2.18279916
[[Variables]]
    steigung:  -0.06084848 +/- 0.10905032 (179.22%) (init = 0)
    abschnitt:  10.3358182 +/- 0.58216937 (5.63%) (init = 10)
[[Correlations]] (unreported correlations are < 0.100)
    C(steigung, abschnitt) = -0.843
Name          Value      Min      Max   Stderr     Vary     Expr Brute_Step
abschnitt     10.06     -inf      inf    0.301     True     None     None
steigung          0     -inf      inf        0    False     None     None
[[Model]]
    Model(f_lin)
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 4
    # data points      = 10
    # variables        = 1
    chi-square         = 8.15416000
    reduced chi-square = 0.90601778
    Akaike info crit   = -0.04056867
    Bayesian info crit = 0.26201643
[[Variables]]
    steigung:   0 (fixed)
    abschnitt:  10.0620000 +/- 0.30100129 (2.99%) (init = 10)
../_images/eb4129050826ca627e9eaf278153985ff45f9e426aaaa9ed177cd458966b6286.png