Gesetzliche Grenzwerte (Teil 1)

Zeitreihenanalyse der Luftqualität

Data Literacy
Grenzwerte
Schadstoffe
Luftqualität
Zeitreihe
pandas
plotly

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

30. Mai 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.

# Bibliothek für das Arbeiten mit tabellarischen Daten importieren
import pandas as pd

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

# Daten anzeigen
df
NO NO2 O3 PM10 SO2
time
1989-07-01 0.521739 32.739130 32.000000 NaN 0.000000
1989-07-02 1.347826 13.130435 47.291667 NaN 0.000000
1989-07-03 19.545455 26.227273 46.130435 NaN 2.636364
1989-07-04 18.652174 29.826087 43.500000 NaN 6.347826
1989-07-05 20.300000 38.300000 49.619048 NaN 21.352941
... ... ... ... ... ...
2022-12-27 0.000000 11.695652 51.541667 4.541667 NaN
2022-12-28 0.000000 1.869565 58.608696 1.708333 NaN
2022-12-29 0.000000 1.913043 61.043478 0.000000 NaN
2022-12-30 0.000000 3.130435 52.000000 0.000000 NaN
2022-12-31 0.000000 0.000000 55.391304 0.000000 NaN

12237 rows × 5 columns

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

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

import plotly.express as px

# Liniendiagramm 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()
Tipp

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

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

# Boxplot erzeugen
fig = px.box(df)

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

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 Grenzwerte 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
time
1989-07-01         NaN
1989-07-02         NaN
1989-07-03         NaN
1989-07-04         NaN
1989-07-05         NaN
                ...   
2022-12-27    4.541667
2022-12-28    1.708333
2022-12-29    0.000000
2022-12-30    0.000000
2022-12-31    0.000000
Name: PM10, Length: 12237, dtype: float64

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
time
1989-07-01    False
1989-07-02    False
1989-07-03    False
1989-07-04    False
1989-07-05    False
              ...  
2022-12-27    False
2022-12-28    False
2022-12-29    False
2022-12-30    False
2022-12-31    False
Name: PM10, Length: 12237, dtype: bool

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 filtern
filtered = values[cond]
filtered
time
2002-12-11    54.166667
2002-12-12    57.125000
2002-12-13    57.750000
2002-12-14    68.958333
2002-12-15    59.458333
                ...    
2018-04-02    74.708333
2019-01-24    51.375000
2019-01-25    50.791667
2020-01-01    51.916667
2021-02-25    53.375000
Name: PM10, Length: 214, dtype: float64

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

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
time
2002-01-01     5
2003-01-01    31
2004-01-01    14
2005-01-01    11
2006-01-01    18
2007-01-01    16
2008-01-01     7
2009-01-01    18
2010-01-01    14
2011-01-01    23
2012-01-01     7
2013-01-01     6
2014-01-01    10
2015-01-01     9
2016-01-01     4
2017-01-01    11
2018-01-01     6
2019-01-01     2
2020-01-01     1
2021-01-01     1
Freq: YS-JAN, Name: PM10, dtype: int64

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

Aufgabe 1

  • Wurden die gesetzlichen Bestimmungen hinsichtlich des Tagesgrenzwertes eingehalten?

(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 listet die Häufigkeit der Grenzwertüberschreitungen pro Jahr auf. 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.

5.5 Jahresmittelwert

Aufgabe 2

  • Überprüfe, ob der Grenzwert für den Jahresmittelwert (40 µg/m3) eingehalten wurde!

(10 Minuten)

Tipp

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

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

Aufgabe 3

  • Überprüfe, ob die folgenden gesetzlichen Vorgaben 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 ).

  • 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-05-30},
  url = {https://climate-data-entrepreneurial-club.netlify.app/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).” May 30, 2025. https://climate-data-entrepreneurial-club.netlify.app/modul_2/submodules/01_luftqualitaet/01_grenzwerte_luftqualitaet.html.
close all nutshells