Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Berechnung der Typ A Messunsicherheit von Regressionskoeffizienten mit Python

Georg Ramer/TU Wien
Binder

Wir verwenden hier Messwerte einer Flammen OES (optische Emissions Spektroskopie) Messung von Kalzium. Eine Stammlösung wurde durch Einwaage von Ca(NOX3)X2 4HX2O\ce{Ca(NO3)2 \cdot 4 H2O} in 1% Salpetersäure eingewogen.

Es wurden 584 mg eingewogen und auf 100 mL verdünnt. Anschließend wurden mit einer Kolbenhubpipette Volumina von 50 µL bis 500 µL ( in 50 µL Schritten) entnommen und in 25 mL Kolben weiter verdünnt. Diese Standardreihe wurde dann vermessen. Die Daten sind ein einem Excel File “Ca_Kalibration.xlsx” im Sheet “Kalibration” gespeichert.

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from IPython.display import Markdown
from uncertainties import ufloat, unumpy

Wir laden die Datei mittels Pandas

calibration = pd.read_excel(
    "https://gitlab.tuwien.ac.at/cookbooks/data/analytical_chemistry/-/raw/main/Messunsicherheit/Ca_Kalibration.xlsx?inline=false",
    sheet_name="Kalibration",
    index_col=0,
)

calibration
Loading...

We plot the calibration

fig, ax = plt.subplots()
ax.plot(calibration["c"], calibration["I"], "x")

ax.set_xlabel("Ca Konzentration / (mg/L)")
ax.set_ylabel("I / counts")
<Figure size 640x480 with 1 Axes>

Wir könnten numpy.polyfit verwenden um diese Punkte mit einer Gerade zu fitten, aber zur Übung werden wir stattdessen die Gleichungen (2) und (3) in Python code gießen:

def linfit(x, y):
    """creates a least-squares fit of data pairs (x,y)"""
    k = np.sum((x - np.mean(x)) * (y - np.mean(y))) / (np.sum((x - np.mean(x)) ** 2))
    d = np.mean(y) - k * np.mean(x)
    return k, d
k, d = linfit(calibration["c"], calibration["I"])

fig, ax = plt.subplots()
ax.plot(calibration["c"], calibration["I"], "x", label="Messungen")
ax.plot(calibration["c"], k * calibration["c"] + d, label="Regression")

ax.set_xlabel("Ca Konzentration / (mg/L)")
ax.set_ylabel("I / counts")

ax.annotate(f"I = {k:.3f} L/g c + {d:.3f}", (0.5, 0.3), xycoords="axes fraction")

ax.legend()
<Figure size 640x480 with 1 Axes>

Bei einer Regression macht es auch immer Sinn die Residuen zu überprüfen. Sie würden Auskunft darüber geben, ob wir noch im linearen Bereich der Kalibration sind:

k, d = linfit(calibration["c"], calibration["I"])

fig, ax = plt.subplots()
ax.axhline(0, color="lightgrey")
ax.plot(
    calibration["c"],
    calibration["I"] - (k * calibration["c"] + d),
    "x",
    label="Residuen",
)

ax.set_xlabel("Ca Konzentration / (mg/L)")
ax.set_ylabel("Residuen I / counts")

ax.legend()
<Figure size 640x480 with 1 Axes>

Nun verpacken wir auch die Formeln zu Berechnung des Standardfehlers von Steigung und Achsenabschnitt in eine Funktion

def s_linfit(x, y):
    k, d = linfit(x, y)
    sk = np.sqrt(
        1 / (len(x) - 2) * np.sum((y - k * x - d) ** 2) / np.sum((x - np.mean(x)) ** 2)
    )
    sd = sk * np.sqrt(1 / len(x) * np.sum(x**2))
    return sk, sd
sk, sd = s_linfit(calibration["c"], calibration["I"])

fig, ax = plt.subplots()
ax.plot(calibration["c"], calibration["I"], "x", label="Messungen")
ax.plot(calibration["c"], k * calibration["c"] + d, label="Regression")


ax.fill_between(
    calibration["c"],
    (k - sk) * calibration["c"] + (d - sd),
    (k + sk) * calibration["c"] + (d + sd),
    color="lightblue",
)

ax.set_xlabel("Ca Konzentration / (mg/L)")
ax.set_ylabel("I / counts")

ax.annotate(
    rf"I = ({k:.3f} $\pm$ {sk:.3f}) L/mg c + ({d:.2f} $\pm$ {sd:.2f})",
    (0.3, 0.2),
    xycoords="axes fraction",
)

ax.legend()
<Figure size 640x480 with 1 Axes>

Die Ergebnisse für die Steigung und Achsenabschnitt sind daher:

Markdown(f"k=({k:.3f}±{sk:.3f}) L/mg\n d=({d:.2f}±{sd:.2f})")
Loading...

Fehler der Standards

Wir ignorieren bisher, dass die Herstellung der Standards auch fehlerbehaftet ist. Wir machen Fehler beim Einwägen, beim Pipettieren und beim Verdünnen von Lösungen. Es kann sein, dass diese Fehler zu Fehler in den Konzentrationen führen, die Vergleichbar mit dem Fehler durch die Messung sind. Das überprüfen wir nun mit Hilfe des uncertainties Pakets.

Zuerst berechnen wir die Konzentration der Stammlösung. Wir wiegen ein, rechnen dann in Mol Kalzium um. Da es sich dabei um chemische Formeln handelt, müssen wir dabei keine Messunsicherheit beachten. Schließlich verdünnen wir in einem 100 mL Kolben. Die Konzentration in Milligram pro Liter:

EW_CaNO34H2O = (
    584
    + ufloat(0, np.sqrt(1 / 3) * (0.1 / 2), tag="Digitalisierung Waage")
    + ufloat(0, 0.1, tag="Repeatability Waage")
    + ufloat(0, 0.2, tag="Linearität Waage")
)

V_stamm = 100 + ufloat(0, 0.1 / 2 / 3, tag="Fehlerbereich Kolben")

M_Ca = 40.078  # g/mol
M_CaNO34H2O = 236.15  # g/mol

c0_Ca = EW_CaNO34H2O / M_CaNO34H2O * M_Ca / V_stamm * 1000
c0_Ca
991.1307219987297+/-0.4167756653684643

Nun pipettieren wir. Da wir dabei mehrere Werte gleich behandeln müssen, verwenden wir uarrays. Diese funktionieren wie numpy arrays haben aber für jeden Wert einen eigene Messunsicherheit. Die pipettierten Volumina sind in Mikrolitern, wir erhalten dann Ergebnisse in Milligram pro Liter.

pipette_volumen = unumpy.uarray(np.arange(0, 550, 50), [0] + [0.6] * 10) + ufloat(
    0, 0.6, tag="systematische Fehler Pipette"
    ) * np.array([0] + [1] * 10)


c_stds = pipette_volumen / 1000 * c0_Ca / unumpy.uarray([25] * 11, [0.1 / 2 / 3] * 11)

I_calc = k * c_stds + d

Sowohl für die Konzentrationen als auch für die daraus berechneten Intensitäten sind die Fehler klein, im Vergleich zu den Abweichungen der Messpunkte von der Regressiongerade. Es ist also anzunehmen, dass die Messunsicherheit durch die Schwankungen bei der OES Messung zu stande kommt. Wir machen uns das Leben daher einfach, und vernachlässigen die Beiträge der Probenherstellung zur Messunsicherheit von k und d.

pd.DataFrame(
    {
        "E(c)": unumpy.nominal_values(c_stds),
        "s(c)": unumpy.std_devs(c_stds),
        "E(I)": unumpy.nominal_values(I_calc),
        "s(I)": unumpy.std_devs(I_calc),
    }
)
Loading...
fig, ax = plt.subplots()


ax.plot(calibration["c"], calibration["I"], "x", label="Messungen")

ax.errorbar(
    calibration["c"],
    unumpy.nominal_values(I_calc),
    marker="x",
    linestyle="none",
    xerr=unumpy.std_devs(c_stds),
    capsize=1,
    elinewidth=1,
    yerr=unumpy.std_devs(I_calc),
    label="Fehler",
)


ax.plot(calibration["c"], k * calibration["c"] + d, label="Regression")


ax.set_xlabel("Ca Konzentration / (mg/L)")
ax.set_ylabel("I / counts")

ax.annotate(
    rf"I = ({k:.3f} $\pm$ {sk:.3f}) L/g c + ({d:.2f} $\pm$ {sd:.2f})",
    (0.3, 0.2),
    xycoords="axes fraction",
)

ax.legend()
<Figure size 640x480 with 1 Axes>