Gesetzliche Grenzwerte (Teil 1)

Zeitreihenanalyse der Luftqualität

In der Europäischen Union gelten verbindliche Grenzwerte für Luftschadstoffe wie Feinstaub, Stickstoffdioxid und Schwefeldioxid. Doch werden diese Vorgaben in Deutschland tatsächlich eingehalten? Um diese Frage zu beantworten, werden in diesem Notebook Daten einer Messstation aus der Großstadt Bielefeld ausgewertet. Die Daten stammen vom Landesamt für Natur, Umwelt und Klima Nordrhein-Westfalen (https://www.lanuk.nrw.de/) und enthalten Tagesmittelwerte verschiedener Luftschadstoffe über einen Zeitraum von 30 Jahren. Analysiert werden die Konzentrationen von Feinstaub (PM10), Stickstoffmonoxid (NO), Stickstoffdioxid (NO2), Ozon (O3), Feinstaub PM10 sowie Schwefeldioxid (SO2). Zentrale Fragestellungen sind: Wurden die gesetzlichen Grenzwerte eingehalten? Und wie haben sich die Schadstoffkonzentrationen im Laufe der letzten drei Jahrzehnte entwickelt? Die Analyse soll ein besseres Verständnis der langfristigen Luftqualitätsentwicklung ermöglichen und potenziellen Handlungsbedarf aufzeigen.

Autor:in
Zugehörigkeit

Sören Sparmann

Universität Paderborn

Veröffentlichungsdatum

14. September 2025

1 Luftqualität

Feinstaub, Stickstoffoxide, Benzol, Schwefeldioxid, Ozon - das sind Schadstoffe in der Luft, denen wir täglich ausgesetzt sind und die gesundheitsschädlich sind.

Wegen der Gesundheitsgefahr gelten für diese Schadstoffe gesetzliche Grenzwerte. Aber werden diese Grenzwerte in der Realität auch eingehalten?

Um die Einhaltung der Grenzwerte zu kontrollieren, gibt es an vielen Orten in Deutschland Messstationen, die die Schadstoffkonzentration in der Luft messen.

In diesem interaktiven Arbeitsblatt werden die Daten der Messstation Bielefeld-Ost (BIEL) analysiert. Dabei wird untersucht, ob die gesetzlich vorgeschriebenen Grenzwerte tatsächlich eingehalten wurden.

Quelle: Deutsche Umwelthilfe

Foto „Industrielle Schornsteine - Luftverschmutzung“ von Tim Reckmann unter der Lizenz CC BY 2.0 via Flickr.

Foto „Industrielle Schornsteine - Luftverschmutzung“ von Tim Reckmann unter der Lizenz CC BY 2.0 via Flickr.

2 Messstation - Bielefeld-Ost (BIEL)

Der Messcontainer steht auf einer Grünfläche am Rande des Ravensberger Parks, in der Nähe der Kreuzung Hermann-Delius-Straße / Bleichstraße. Das Umfeld ist locker bebaut mit vielen Grünflächen. Das Stadtzentrum Bielefeld liegt etwa 1,2 km westlich. Die autobahnähnlich ausgebaute B 61 verläuft ca. 1,5 km westlich in Nord-Süd-Richtung.

Quelle: LANUK

Foto der Station

Foto der Station

In der Karte ist die genaue Position der Messstation markiert.

Code
# Code zum Erzeugen der Karte
from folium import Map, Marker

# Breiten- und Längengrad des Containers
location = 52.023155, 8.548362

# Karte erzeugen
map = Map(location, zoom_start=14)

# Marker hinzufügen
marker = Marker(location=location)
# Marker zur Karte hinzufügen
marker.add_to(map)

# Karte anzeigen
map
Make this Notebook Trusted to load map: File -> Trust Notebook

3 Daten einlesen

Die Daten der Messstation sind bereits heruntergeladen und werden nun mit dem Befehl read_csv() eingelesen.

Dabei werden die Daten in ein sogenanntes DataFrame mit dem Namen df geschrieben. Ein DataFrame ist eine tabellarische Datenstruktur, die aus Spalten und Zeilen besteht – ähnlich den Tabellen, wie du sie möglicherweise aus Programmen wie Excel kennst.

Um die Daten anschließend anzuzeigen, muss der Variablennamen df (engl. DataFrame) am Ende der Zelle eingegeben werden.

Die Daten umfassen verschiedene Schadstoffe:

Kürzel Name Einheit
NO Stickstoffmonoxid µg/m3
NO2 Stickstoffdioxid µg/m3
O3 Ozon µg/m3
PM10 Feinstaub (Particulate Matter) µg/m3
SO2 Schwefeldioxid µg/m3

Die Messwerte liegen jeweils als Tagesmittelwert vor.

Die Daten sind in µg/m3 (Mikrogramm pro Kubikmeter) angegeben.

Die Daten umfassen den Zeitraum von 1989 bis 2022.

Solch eine zeitliche Abfolge von Messdaten wird auch Zeitreihe genannt.

Daten einlesen
# Data Science Bibliothek importieren
import pandas as pd

# Daten einlesen
df = pd.read_csv('data/air_quality.csv', index_col='time', parse_dates=True)

# Daten anzeigen
df
_pyodide_editor_2 = Object {code: null, options: Object, indicator: it}
Hinweis

Der Eintrag NaN (Not a Number) bedeutet, dass an der betreffenden Stelle kein Messwert vorliegt (z.B. weil keine Messung durchgeführt wurde oder ein Messfehler vorliegt).

4 Daten explorieren

4.1 Liniendiagramm

Mit der Funktion px.line(...) der Bibliothek plotly können die Daten in einem Liniendiagramm visualisiert werden.

Liniendiagramm erzeugen
# Bibliothek zum Erstellen von Diagrammen (plots) importieren
import plotly.express as px

# Liniendiagramm aus den Daten df erzeugen
fig = px.line(df)

# Achsenbeschriftungen einfügen
fig.update_xaxes(title='Zeitpunkt der Messung')
fig.update_yaxes(title='Schadstoffkonzentration<br>[µg/m<sup>3</sup>]')
fig.update_legends(title='Schadstoff')

fig.show()
_pyodide_editor_3 = Object {code: null, options: Object, indicator: it}
Tipp

Bewege die Maus über das Diagramm, um einzelne Datenpunkte genauer zu betrachten. Durch Klicken auf eine Variable kann diese ein- oder ausgeblendet werden.

4.2 Boxplot

Eine weitere Möglichkeit zur Darstellung der Messwerte sind sogenannte Boxplots. Diese eignen sich besonders, um die Verteilung und Streuung von Messwerte darzustellen.

Aufgabe 1

  • Erstelle ein Boxplot, das die Verteilung der Schadstoffe zeigt.
  • Verwende dafür die Funktion px.box(...), analog zu px.line(...) aus dem Beispiel oben.
  • Füge geeignete Achsenbeschriftungen hinzu.

(5 Minuten)

# Boxplot erzeugen
fig =

# Achenbeschriftungen hinzufügen


# Diagramm anzeigen
fig.show()
_pyodide_editor_4 = Object {code: null, options: Object, indicator: it}
_pyodide_value_4 = Object {result: null, evaluate_result: null, evaluator: Ea}
Lösung
# Boxplot erzeugen
fig = px.box(df)

# Achenbeschriftungen einfügen
fig.update_traces() 
fig.update_xaxes(title='Schadstoff')
fig.update_yaxes(title='Schadstoffkonzentration<br>[µg/m<sup>3</sup>]')

# Diagramm anzeigen
fig.show()

5 Feinstaub

Feinstaub besteht aus kleinsten Partikeln, die sich neben ihrer Größe auch durch ihre chemischen Eigenschaften unterscheiden. Zu Feinstaub zählen alle Partikel, deren Durchmesser kleiner als 10 Mikrometer (µm) ist. Häufig wird dafür das Kürzel PM10 (PM = Particulate Matter) verwendet.

5.1 Gesetzliche Grenzwerte für Feinstaub

Zum Schutz der menschlichen Gesundheit gelten seit dem 1. Januar 2005 europaweit Grenzwerte für Feinstaub:

  • Der Tagesgrenzwert beträgt 50 µg/m3 und darf nicht öfter als 35mal im Jahr überschritten werden.
  • Der zulässige Jahresmittelwert beträgt 40 µg/m3.

Um zu überprüfen, ob die gesetzlichen Bestimmungen hinsichtlich des Tagesgrenzwertes eingehalten wurden, muss die Anzahl der Tage bestimmt werden, an denen der Grenzwert über 50 µg/m3 lag.

Hinweis

Die Weltgesundheitsorganisation WHO empfiehlt einen Richtwert von 5 µg/m3 im Jahresmittel für PM2,5 und 15 µg/m3 im Jahresmittel für PM10 (WHO 2021).

Es gibt keine Feinstaubkonzentration, unterhalb derer eine schädigende Wirkung ausgeschlossen werden kann

5.2 Daten selektieren

Zunächst wird die Spalte PM10, welche die Feinstaubmesswerte enthält, ausgewählt (selektiert) und in einer Variable mit dem Namen values gespeichert.

Die Variable values enthält somit eine Reihe (engl. Series) von Messwerten der Feinstaubkonzentration.

# Spalte PM10 selektieren
values = df['PM10']
values
_pyodide_editor_5 = Object {code: null, options: Object, indicator: it}
Hinweis

Der Wert NaN(Not a number) bedeutet, dass für den jeweiligen Zeitpunkt kein Wert vorliegt.

Um nun die Anzahl der Überschreitungen pro Jahr zu ermitteln, wird wie folgt vorgegangen:

  1. Zunächst werden alle Tage ermittelt, an denen der Tagesgrenzwert überschritten wurde. Dazu werden die Daten anhand der Bedingung für eine Grenzwertüberschreitung gefiltert.
  2. Anschließend werden die Grenzwertüberschreitungen nach Jahren gruppiert und die Anzahl der Überschreitungen pro Jahr gezählt.

5.3 Daten filtern

Anschließend werden die Daten gefiltert. Dazu wird zunächst eine Bedingung festgelegt (values > 50). Die Bedingung wird dabei für jeden Wert der Reihe einzeln geprüft.

  • Die Bedingung ist erfüllt (True), wenn der jeweilige Wert größer als 50 ist.
  • Die Bedingung ist nicht erfüllt (False), wenn der jeweilige Wert kleiner oder gleich 50 ist.
# Bedingung festlegen
cond = values > 50
cond
_pyodide_editor_6 = Object {code: null, options: Object, indicator: it}

Anschließend werden die Werte entsprechend der Bedingung gefiltert. Die Variable filtered enthält nun nur noch die Messwerte, bei denen der Tagesgrenzwert überschritten wurde.

Der Ausdruck values[cond] filtert die Werte so, dass nur noch jene in der Reihe verbleiben, bei denen die Bedingung cond erfüllt ist (True) – also eine Grenzwertüberschreitung vorliegt.

# Daten enstprechend der Bedingung filtern
filtered = values[cond]
filtered
_pyodide_editor_7 = Object {code: null, options: Object, indicator: it}

Mit der Funktion px.scatter() können wir ein Streudiagramm (engl. scatter plot) der gefilterten Daten erzeugen. Darauf sehen wir alle Messzeitpunkte, an denen der Tagesgrenzwert überschritten wurde.

# Streudiagramm erzeugen
fig = px.scatter(filtered, title='Grenzwertüberschreitungen')

# Achsenbeschriftungen anpassen
fig.update_xaxes(title='Zeitpunkt')
fig.update_yaxes(title='Messwert')

# Streudiagramm anzeigen
fig.show()
_pyodide_editor_8 = Object {code: null, options: Object, indicator: it}

5.4 Gruppieren und aggregieren

Um nun die Anzahl der Tage pro Jahr zu bestimmen, an denen der Grenzwert überschritten wurde, müssen die Daten nach Jahren gruppiert und anschließend die Anzahl der Einträge gezählt werden.

Mit der Methode resample() können die Daten in Zeitintervalle zusammengefasst (gruppiert) werden. In diesem Beispiel wird die Regel 'YS' (year start) angewandt, um die Daten nach Jahren zu gruppieren. Die Gruppe mit dem Eintrag 2002-01-01 umfasst demnach beispielsweise die Daten aus dem Zeitraum vom 01.01.2002 bis zum 31.12.2002.

Anschließend wird mit der Methode count() die Anzahl der Tage pro Jahr ermittelt, an denen der Grenzwert überschritten wurde. Dieser Schritt wird auch als Aggregation bezeichnet. Das bedeutet, dass mehrere Einträge zu einem Wert (in diesem Fall der Anzahl) zusammengefasst werden.

Weitere Aggregationsfunktionen sind z.B. mean() für den Mittelwert, sum() für die Summe der Werte, min() für den kleinsten Wert (Minimum) oder max() für den größten Wert (Maximum).

# Daten nach Jahren gruppieren
grouped = filtered.resample('YS')

# Anzahl der Einträge je Gruppe bestimmen (aggregieren)
aggregated = grouped.count()
aggregated
_pyodide_editor_9 = Object {code: null, options: Object, indicator: it}

Die resample Funktion akzeptiert unter anderem die folgenden Regeln:

Kürzel Bedeutung
D Tag (day)
W Woche (week)
MS Monat (month start)
QS Quartal (quarter start)
YS Jahr (year start)

Hier nochmal alle Schritte zusammengefasst:

Codefragment 1: Selektieren, filtern, gruppieren und aggregieren
# Feinstaub-Spalte selektieren
values = df['PM10']

# Bedingung für Überschreitung des Tagesgrenzwerts festlegen
cond = values > 50

# Daten anhand der Bedingung filtern
filtered = values[cond]

# Daten nach Jahren gruppieren
grouped = filtered.resample('YS')

# Anzahl der Einträge je Gruppe bestimmen (aggregieren)
aggregated = grouped.count()

Mit der Funktion bar() können die aggregierten Daten nun in einem Säulendiagramm (engl. bar chart) dargestellt werden.

# Daten in einem Säulendiagram anzeigen
fig = px.bar(aggregated, title='Anzahl der Tage mit Grenzwertüberschreitung (Feinstaub)')

fig.update_xaxes(title='Jahr')
fig.update_yaxes(title='Anzahl Tage')
fig.update_legends(title='Schadstoff')

fig.show()
_pyodide_editor_10 = Object {code: null, options: Object, indicator: it}

Aufgabe 2

  • Betrachte das Säulendiagramm und überprüfe, ob die gesetzlichen Bestimmungen hinsichtlich des Tagesgrenzwertes eingehalten wurden!

(5 Minuten)

Zur Erinnerung

Der Tagesgrenzwert beträgt 50 µg/m3 und darf nicht öfter als 35-mal im Jahr überschritten werden.

Das vorliegende Säulendiagramm zeigt die Anzahl der Grenzwertüberschreitungen pro Jahr an. Es ist zu erkennen, dass die maximale Anzahl der Grenzwertüberschreitungen pro Jahr im Zeitraum von 2002 bis 2021 im Jahr 2003 bei 31 lagen. Der Tagesgrenzwert wurde in keinem Jahr häufiger als 35 mal überschritten. Daher wurden die gesetzlichen Bestimmungen eingehalten.

5.5 Jahresmittelwert

Aufgabe 3

  • Überprüfe, ob der Grenzwert für den Jahresmittelwert (40 µg/m3) der Feinstaubkonzentration eingehalten wurde!
  • Ermittel dafür die Jahresmittelwerte der Feinstaubkonzentration.

(10 Minuten)

Tipp

Du benötigst dafür die Funktion resample() und die Aggregatfunktion .mean().

# Jahresmittelwerte bestimmen
annual_mean =

# Jahresmittelwerte ausgeben
annual_mean
_pyodide_editor_11 = Object {code: null, options: Object, indicator: it}
_pyodide_value_11 = Object {result: null, evaluate_result: null, evaluator: Ea}
Lösung

Der Grenzwert für den Jahresmittelwert wurde in keinem Jahr überschritten!

values.resample('YS').mean()
time
1989-01-01          NaN
1990-01-01          NaN
1991-01-01          NaN
1992-01-01          NaN
1993-01-01          NaN
1994-01-01          NaN
1995-01-01          NaN
1996-01-01          NaN
1997-01-01          NaN
1998-01-01          NaN
1999-01-01          NaN
2000-01-01          NaN
2001-01-01          NaN
2002-01-01    29.799510
2003-01-01    25.965259
2004-01-01    24.248289
2005-01-01    23.280031
2006-01-01    24.177340
2007-01-01    22.737365
2008-01-01    20.749723
2009-01-01    22.770302
2010-01-01    23.199413
2011-01-01    22.892461
2012-01-01    18.740982
2013-01-01    19.620356
2014-01-01    19.399189
2015-01-01    17.988354
2016-01-01    18.177187
2017-01-01    17.969657
2018-01-01    18.185818
2019-01-01    14.987553
2020-01-01    12.246859
2021-01-01    12.051199
2022-01-01    12.917954
Freq: YS-JAN, Name: PM10, dtype: float64

6 Weitere Schadstoffe in der Luft

6.1 Stickstoffdioxid

Stickstoffdioxid (NO2) entsteht als Produkt unerwünschter Nebenreaktionen bei Verbrennungsprozessen. Die Hauptquellen von Stickstoffoxiden sind Verbrennungsmotoren und Feuerungsanlagen für Kohle, Öl, Gas, Holz und Abfälle. In Ballungsgebieten ist der Straßenverkehr die bedeutendste Quelle für Stickstoffoxide (NOx).

In der Umwelt vorkommende Stickstoffdioxid-Konzentrationen sind vor allem für Asthmatiker ein Problem. Zudem erhöht eine jahrzehntelange Belastung durch NO2 das Risiko an Herz-Kreislauferkrankungen zu versterben (Quelle: Umweltbundesamt).

Stickstoffdioxid kann Pflanzen schädigen und unter anderem ein Gelbwerden der Blätter, vorzeitiges Altern und Kümmerwuchs bewirken. Zudem trägt Stickstoffdioxid zur Überdüngung und ⁠Versauerung⁠ von Böden und in geringem Maße auch von Gewässern bei.

6.2 Schwefeldioxid

Schwefeldioxid (SO2) ist ein farbloses, stechend riechendes, wasserlösliches Gas, das Mensch und Umwelt beeinträchtigt.

Schwefeldioxid entsteht überwiegend bei Verbrennungsvorgängen fossiler Energieträger wie Kohle und Öl durch Oxidation des im Brennstoff enthaltenen Schwefels.

Schwefeldioxid reizt die Schleimhäute und kann zu Augenreizungen und Atemwegsproblemen führen.

6.3 Grenzwerte

Auch für Stickstoffdioxid und Schwefeldioxid existieren gesetzliche Grenzwerte.

Aufgabe 4

  • Überprüfe, ob die gesetzlichen Bestimmungen hinsichtlich des Tages- und Jahresgrenzwerte für die Schadstoffe NO2 und SO2 eingehalten wurden!

(15 Minuten)

Schadstoff Tagesgrenzwert Jahresgrenzwert
NO2 - Der Grenzwert für den Jahresmittelwert beträgt 40 µg/m3
SO2 Der Tagesgrenzwert von 125 µg/m3 darf nicht öfter als dreimal im Kalenderjahr überschritten werden. Der Zum Schutz der Vegetation beträgt der kritische Wert als Jahresmittelwert 20 µg/m3.
Tipp

Orientiere dich an dem Beispiel oben, um den Tagesgrenzwert zu überprüfen(siehe ).

# Tagesgrenzwert SO2

# Jahresgrenzwert NO2

# Jahresgrenzwert SO2
_pyodide_editor_13 = Object {code: null, options: Object, indicator: it}
_pyodide_value_13 = Object {result: null, evaluate_result: null, evaluator: Ea}
Lösung
  • Der Grenzwert für den Jahresmittelwert der NO2 Konzentration wurde 1991 einmalig überschritten.
  • Der Grenzwert für den Jahresmittelwert der SO2 Konzentration wurde nicht überschritten.
  • Der Grenzwert für den Tagesmittelwert der SO2 Konzentration wurde 1991 6 mal überschritten. Damit wurden die gesetlichen Vorgaben nicht eingehalten.

Die gesetlichen Vorgaben gelten jedoch erst ab 2005.

no2 = df['NO2']
so2 = df['SO2']
no2.resample('YS').mean()
time
1989-01-01    40.881467
1990-01-01    34.719262
1991-01-01    40.822363
1992-01-01    38.375462
1993-01-01    35.334288
1994-01-01    31.063849
1995-01-01    27.849006
1996-01-01    28.182599
1997-01-01    26.958979
1998-01-01    26.144047
1999-01-01    28.335787
2000-01-01    25.463651
2001-01-01    25.819286
2002-01-01    25.229629
2003-01-01    30.465887
2004-01-01    24.471287
2005-01-01    25.630279
2006-01-01    25.995971
2007-01-01    25.552724
2008-01-01    29.632934
2009-01-01    29.019984
2010-01-01    26.024708
2011-01-01    24.122963
2012-01-01    22.644553
2013-01-01    23.299178
2014-01-01    20.924798
2015-01-01    21.242126
2016-01-01    24.950927
2017-01-01    21.170492
2018-01-01    22.317858
2019-01-01    19.331646
2020-01-01    14.865625
2021-01-01    15.200308
2022-01-01    15.502891
Freq: YS-JAN, Name: NO2, dtype: float64
so2.resample('YS').mean()
time
1989-01-01    14.387635
1990-01-01    12.882733
1991-01-01    18.131772
1992-01-01    13.462768
1993-01-01    13.321636
1994-01-01     9.138736
1995-01-01     6.564896
1996-01-01     7.799553
1997-01-01     4.113066
1998-01-01     2.339002
1999-01-01     1.495626
2000-01-01     1.630804
2001-01-01     1.524641
2002-01-01     1.891455
2003-01-01     1.481274
2004-01-01     0.773982
2005-01-01     0.569058
2006-01-01     0.561162
2007-01-01     0.704071
2008-01-01     0.288349
2009-01-01     2.534460
2010-01-01     0.435951
2011-01-01     0.566622
2012-01-01     0.813730
2013-01-01     0.392067
2014-01-01          NaN
2015-01-01          NaN
2016-01-01          NaN
2017-01-01          NaN
2018-01-01          NaN
2019-01-01          NaN
2020-01-01          NaN
2021-01-01          NaN
2022-01-01          NaN
Freq: YS-JAN, Name: SO2, dtype: float64
so2[so2 > 125].resample('YS').count()
time
1989-01-01    3
1990-01-01    2
1991-01-01    6
1992-01-01    0
1993-01-01    1
Freq: YS-JAN, Name: SO2, dtype: int64

7 Zusammenfassung

In diesem Notebook hast du gelernt, wie du:

  • Daten mit der Funktion read_csv() einlesen kannst,
  • ein Liniendiagramm mit line() und ein Säulendiagramm mit bar() erstellst,
  • eine bestimmte Spalte aus den Daten selektierst, z. B. mit df[<Spalte>],
  • Daten anhand einer Bedingung filterst, z. B. mit df[cond],
  • Zeitreihen mit resample() in Zeitintervalle gruppierst und mit Funktionen wie count(), mean(), min() oder max() aggregierst.

Wiederverwendung

Zitat

Mit BibTeX zitieren:
@online{sparmann2025,
  author = {Sparmann, Sören},
  title = {Gesetzliche Grenzwerte (Teil 1)},
  date = {2025-09-14},
  url = {https://material.cdec.io/modul_2/submodules/01_luftqualitaet/01_grenzwerte_luftqualitaet.html},
  langid = {de}
}
Bitte zitieren Sie diese Arbeit als:
Sparmann, Sören. 2025. “Gesetzliche Grenzwerte (Teil 1).” September 14, 2025. https://material.cdec.io/modul_2/submodules/01_luftqualitaet/01_grenzwerte_luftqualitaet.html.
close all nutshells