Datentypen, Datenformate und der ganze Kram...

In dieser Lektion wollen wir nochmal einen Schritt zurücktreten und betrachten, womit wir es zu tun haben, wenn wir von Daten sprechen. Dabei gibt es unterschiedliche Perspektiven:

Datentypen

In Python werden Daten durch bestimmte Datentypen repräsentiert. Es gibt primitive Datentypen, die es auch außerhalb der Pythonwelt bekannt sind (z.B. float, int usw.). Andere Datentypen gibt es nur in Python (z.B. dict).

Dateien

Außerhalb von Python existieren Daten typischerweise in Dateien. Was muss man über diese wissen?

  • Kodierung: Um eine Datei lesen zu können, musst Du die "Kodierung" kennen. Uns vertraut ist uns z.B. die Kodierung als Text.
  • Struktur: Um den Inhalt einer Datei interpretieren oder verarbeiten zu können, musst Du ihre Struktur kennen - wie sind die Daten organisiert? Zum Beispiel können die Daten in Spalten organisiert sein (Tabelle): Jede Spalte hat dann einen Datentyp, der sich aber nicht immer vollkommen intuitiv erschließt.

Format

Meist spricht man von einem Format als einer Kombination aus einer Kodierung und einer Struktur. Dieses Format wird manchmal, aber nicht immer, durch die Dateiendung angezeigt. Beispiele:

  • .csv: Die Endung csv zeigt eine Tabelle an, die als Text kodiert ist. Wie die Spalten getrennt sind und was für Datentypen die Spalten enthalten, sagt uns die Endung nicht.
  • .xlsx: Diese Endung ("Excel-Datei") zeigt eine Tabelle an, die in einem binären Spezialformat kodiert ist.

Spröde?

Ja! Dieser einführende Textbatzen signalisiert bereits, dass wir es mit einer sehr spröden Materie zu tun haben. Aber wer mit Umweltdaten arbeitet, muss sich tagtäglich mit genau solchen Fragen beschäftigen...also los!

Datentypen

Aufgabe

Ihr habt in Python bereits viele Datentypen kennengelernt. Versucht (mit Euren Nachbar:innen?) eine möglichst vollständige Liste der Euch bekannten Datentypen in Python zusammenzustellen, mit einem Beispiel für jeden Datentyp. Tipp: Nutzt die type-Funktion. Und Ihr dürft auch Pakete importieren.

In [1]:
# Beispiel integer
type(1)
Out[1]:
int

Lösung

In [2]:
import datetime as dt

data = [True, 1, 1., "umwelt", [92, 93, 94], {"a": 1, "b": 2}, dt.datetime.now(),dt.datetime.now,
       dt.datetime.now()-dt.datetime.now(), range(5)]

for item in data:
    print("- %s is %s" % (item, type(item)))
- True is <class 'bool'>
- 1 is <class 'int'>
- 1.0 is <class 'float'>
- umwelt is <class 'str'>
- [92, 93, 94] is <class 'list'>
- {'a': 1, 'b': 2} is <class 'dict'>
- 2021-11-25 17:34:24.323115 is <class 'datetime.datetime'>
- <built-in method now of type object at 0x7fc852322bc0> is <class 'builtin_function_or_method'>
- -1 day, 23:59:59.999999 is <class 'datetime.timedelta'>
- range(0, 5) is <class 'range'>

Datentypen umwandeln (type conversion)

Primitive Datentypen können wir teils ineinander umwandeln. Man nennt dies "type conversion".

In [3]:
# int zu float
float(5)
Out[3]:
5.0
In [4]:
# float zu int
int(5.0)
Out[4]:
5
In [5]:
# str zu int
int("5")
Out[5]:
5
In [6]:
# str zu float
float("5.0")
Out[6]:
5.0
In [7]:
# bool zu float
float(True)
Out[7]:
1.0
In [8]:
# list zu str
str([1,2,3])
Out[8]:
'[1, 2, 3]'
In [9]:
# float zu bool (alles außer 0 ist True)
bool(5.)
Out[9]:
True

Warum interessiert uns das?

Die Entscheidung, welchen Datentyp eine Variable annimmt, trifft Python oft im Hintergrund, ohne dass wir uns darum kümmern müssen. Manchmal müssen wir uns aber bewusst machen, womit wir es zu tun haben...

Beispiel: Präzision

Der vorliegende Fall stellt unser gewohntes Verständis von True und False auf den Kopf...

In [10]:
(0.3 - 0.2 - 0.1 ) == 0
Out[10]:
False
In [11]:
0.3 - 0.2 - 0.1
Out[11]:
-2.7755575615628914e-17

Der Grund für dieses überraschende Ergebnis: Computer speichern Zahlen nicht im Dezimalsystem, sondern im Binärsystem ab. Bestimmte Dezimalzahlen lassen sich so nicht präzise abbilden. Man sich vorstellen, dass so etwas in einem Programm zu schwer auffindbaren Fehlern führen kann...

Beispiel: Indizierung

Die Indizierung einer Liste oder eines Arrays funktioniert nur mit Integern.

In [15]:
nottodo = ["Frühzeitig an die Rente denken.",
           "Sich an Kleinkunstprojekten beteiligen.",
           "Aus Langeweile ein Detektivbüro gründen"]
In [16]:
nottodo[1.]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_11096/2750298419.py in <module>
----> 1 nottodo[1.]

TypeError: list indices must be integers or slices, not float
In [17]:
nottodo[1]
Out[17]:
'Sich an Kleinkunstprojekten beteiligen.'

Beispiel: Gefährliche Zeiger

Eine Liste ist eigentlich eine übersichtliche Angelegenheit.

In [18]:
bewerberinnen = ["alf", "eric", "bart", "kyle", "stan", "kenny"]

Wir wollen nun aus den Bewerber:innen (?!) eine Vorauswahl treffen. Dazu erzeugen wir aus der Liste bewerberinnen eine Liste namens vorauswahl. alf und bart fliegen raus, sie passen nicht ins gewünschte Profil.

In [19]:
vorauswahl = bewerberinnen
vorauswahl.remove("alf")
vorauswahl.remove("bart")
vorauswahl
Out[19]:
['eric', 'kyle', 'stan', 'kenny']

alf hat allerdings den Rechtsweg bestritten und sich eingeklagt. Wir sind also gezwungen, uns die ursprüngliche Liste der bewerberinnen nochmal anzuschauen..doch...auweia:

In [20]:
bewerberinnen
Out[20]:
['eric', 'kyle', 'stan', 'kenny']

Wie ist das passiert?? Das geht doch sonst auch:

In [21]:
a = 1
b = a
b = 2
a, b
Out[21]:
(1, 2)

Aber eben nicht mit Listen oder Arrays. Diese werden vom System nämlich als "Zeiger" repräsentiert. Was das ist? Oje, das besprechen wir ein anderes Mal. Wie man das Problem vermeiden kann?

In [22]:
liste1 = [1,2,3]
liste2 = liste1.copy()
liste2.remove(2)
liste1
Out[22]:
[1, 2, 3]

Beispiel: Datumsangaben

Nehmen wir an, Ihr habt eine Textdatei eingelesen und erhaltet folgende Zeichenkette zurück:

In [23]:
x = "01/02/03"
x
Out[23]:
'01/02/03'

Tja...wahrscheinlich handelt es sich um ein Datumsformat. Das sollte sich hoffentlich aus dem Kontext oder noch besser der Dokumentation ergeben. Aber was ist jetzt Tag, Monat und Jahr? In den USA wäre es wohl der 2. Januar 2003, in Großbritannien eher der 1. Februar 2003 - manche Schlaumeier:innen schreiben vielleicht das Jahr nach vorne, weil sie das mal "irgendwo so gesehen haben". Klar ist: Die Interpretation der Zeichenkette als Datum ist kontextabhängig.

In [24]:
# the American way of style
date = dt.datetime.strptime(x, "%m/%d/%y")
str(date), date
Out[24]:
('2003-01-02 00:00:00', datetime.datetime(2003, 1, 2, 0, 0))

Aufgabe: Datumsangabe I

Interpretiere den obenstehenden Datumsstring x auf britische Weise und formatiere das resultierende Datum y mit y.strftime("...") so, dass eine Datumsangabe im Stil von 24. Dezember 2021 entsteht. Hier kannst Du sehen, wie Du den Format-String schreiben musst, um das gewünschte Ergebnis zu erzielen.

Lösung

In [7]:
# the British way of style
y = dt.datetime.strptime(x, "%d/%m/%y")
y
Out[7]:
datetime.datetime(2003, 2, 1, 0, 0)
In [8]:
y.strftime("%d. %B %Y")
Out[8]:
'01. February 2003'

Aufgabe: Datumsangaben II

Aber auch bei folgender Integer handelt es sich um eine Datumsangabe.

In [25]:
x = 1640304000

Hast Du eine Idee, wie hier das Datum kodiert ist und wie man es mit Hilfe des datetime-packages in eine verständliche Zeitangabe umwandeln kann?

Lösung

Es handelt sich um einen so genannten UNIX (POSIX) Timestamp (Zeitstempel): ein weit verbreitetes Format, in dem Datum und Zeit als die Zahl der Sekunden dargestellt wird, die seit dem 1. Januar 1970 vergangen sind.

In [26]:
dt.datetime.utcfromtimestamp(x)
Out[26]:
datetime.datetime(2021, 12, 24, 0, 0)

Überprüfung der Richtigkeit:

In [27]:
dt.datetime.utcfromtimestamp(x) - dt.timedelta(seconds=x)
Out[27]:
datetime.datetime(1970, 1, 1, 0, 0)

Daten brauchen Kontext!

Der letzte Teil über Datumsangaben hat uns bewusst gemacht, dass die Interpretation und Nutzung von Daten immer auch einen Kontext bzw. eine Dokumentation erfordert. Dieses Zusammenspiel zwischen Typ, Struktur und Format wollen wir in der Folge weiter vertiefen.

Text vs. Nicht-Text

Die am weitesten verbreitete Kodierung ist der Text. Textdateien kann man in einem Texteditor wie notepad++ betrachten und erkunden. Auf diese Weise erhält man im besten Fall einen Eindruck über die Struktur der Daten.

Schau Dir z.B. mal die Datei data/data.txt an. Aha, spannend: Zahlen von 0 bis 19, organisiert in Zeilen und Spalten. Man kann diese z.B. mit numpy.loadtxt einlesen und erhält einen zweidimensionalen Array:

In [28]:
import numpy as np
In [29]:
x = np.loadtxt("data/data.txt")
x
Out[29]:
array([[ 0.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  7.],
       [ 8.,  9., 10., 11.],
       [12., 13., 14., 15.],
       [16., 17., 18., 19.]])
In [30]:
type(x)
Out[30]:
numpy.ndarray

Aufgabe: int statt float

Wie könnt Ihr sicherstellen, dass diese Datei als Array von int-Werten, nicht float-Werten eingelesen wird?

Lösung

In [31]:
x = np.loadtxt("data/data.txt", dtype="int")
x
Out[31]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19]])

Nicht-Text-Aufgabe

Öffnet nun einmal die Datei data/data.npy in Eurem Texteditor. Was seht Ihr? Bei mir (Ubuntu, gedit) Folgendes:

Das bezeichnet man landläufig wohl als "Kraut und Rüben". Aber ist gibt immerhin ein kleines Textfragment, das uns erklärt, womit wir es zu tun haben: einer binären Numpy-Datei. Erfahrene Nutzer:innen wissen, was sie zu tun haben. Andere müssen die Suchmaschine anschmeißen: Wie könnt Ihr diese Datei lesen?

Lösung

Ich bin mal wieder bei stackoverflow fündig geworden.

In [32]:
x = np.load("data/data.npy")
x
Out[32]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19]])

Fazit

Die gleichen Daten können als Text oder binär gespeichert werden. Was genau "binär" heißt, werden wir hier nicht behandeln. Sagen wir mal so: Wenn es in unserem Texteditor wie Kraut und Rüben aussieht, haben wir es wahrscheinlich mit einem Binärformat zu tun.

Um eine Binärdatei zu lesen, benötigt man eine Funktion/ein Package/eine Software, welche die Datei "versteht" (Kodierung und Struktur). Gelegentlich steht man als Wissenschaftler:in vor einer nicht dokumentierten Binärdatei und weiß nicht, wie man diese lesen soll - blöd!

Dabei kann durchaus sinnvoll sein, Daten in einem Binärformat abzuspeichern, z.B. um den Speicherbedarf zu reduzieren oder die Lesegeschwindigkeit zu erhöhen. In manchen Bereichen sind binäre Formate die Regel, nicht die Ausnahme (z.B. bei Bildern, Geodaten, uvm.).

Dateiendungen

Eine Dateiendung wie .csv, .txt, .png, oder .jpg zeigt uns zwar an, womit wir wir es zu tun haben, ist aber ansonsten unerheblich. Die Dateiendung wird erst dadurch relevant, dass sie auf unseren Rechnern teils mit bestimmten Anwendungen verknüpft ist.

Aufgabe: Endung manipulieren

Probier es mal anhand der Datei ../data/06_Matjpg.jpg aus (einer Bilddatei).

  • Lege bitte zunächst eine Kopie an und nenne diese ../data/logo.jpg.
  • Ein Doppelklick auf die Datei sollte auf Deinem System nun das Unilogo in irgendeinem Bildbetrachter öffnen.
  • Ändere die Dateinamen von logo.jpg zu logo.txt
  • Versuche nun die Datei zu öffnen. Was passiert?
  • Wie kannst Du sie trotzdem öffnen?

Lösung

Wenn auf Deinem System .txt-Dateien mit einem Texteditor verknüpft sind, wird sich logo.txt in Deinem Texteditor öffnen. Wenn Du aber die Datei im Bildbetrachter öffnest (Shift + rechte Maustaste > "Öffnen mit..."), wird dennoch das Bild angezeigt. Wir können das auch direkt in Python zeigen:

In [33]:
import matplotlib.image as mim
import matplotlib.pyplot as plt
In [34]:
im = mim.imread("../data/logo.txt")
plt.imshow(im)
_ = plt.axis("off")

Dateien mit bestimmter Endung suchen

Man kann zwar logo.jpg in logo.txt umbennen - sollte es aber lassen. Denn wie gesagt: Die Endung signalisiert uns, womit wir es zu tun haben. Auf diese Weise können wir z.B. ganze Verzeichnisse nach bestimmten Dateitypen durchforsten, um diese nacheinander zur verarbeiten. Dazu gibt es das geniale Modul glob.

In [35]:
import glob

Wir haben für Dich Ostereier im Verzeichnis data versteckt. Diese werden durch die Dateiendung .ei angezeigt und enthalten jeweils eine Farbe. Wir können jetzt mit glob alle Eier im Verzeichnis finden. Dafür nutzen wir sogenannte "Wildcards".

Zunächst suchen wir nur alle Eier im Verzeichnis data.

In [36]:
for name in glob.glob('data/*.ei'):
    print(name)
data/ein.ei

Nun suchen wir auch in allen Unterverzeichnissen nach Eiern. Man nennt dies auch "rekursive" Suche.

In [37]:
for name in glob.iglob('data/**/*.ei', recursive = True):
    print(name)
data/ein.ei
data/warm/mehr.ei
data/warm/nochein.ei
data/warm/waermer/letztes.ei

Aufgabe: Farbe bekennen

Jede .ei Datei enthält die Farbe des Eies (als String). Anstatt den Dateinamen zu drucken: Lies die Datei ein und drucke die Farbe. Wie liest man eine Textdatei?

f = open(pfadname)
txt = f.read()
f.close()

Lösung

In [38]:
for name in glob.iglob('data/**/*.ei', recursive = True):
    f = open(name)
    txt = f.read()
    f.close()
    print(txt)
print("(Happy Christmas!)")
Kastanienbraun
Schweinfurter Gruen
Tizianrot
Habsburggelb
(Happy Christmas!)

Typische Probleme mit Tabellen

Nur weil man Textdateien im Editor betrachten kann, ist das Einlesen der Daten in Python nicht automatisch "leicht". Aber wir können auf diese Weise ggf. Rückschlüsse auf die Struktur der Daten ziehen und so Probleme lösen.

Aufgabe: komischer Spaltentrenner, kein Header

Der DWD stellt in diesem Verzeichnis die Daten der Webcam an der Station Hohenpeißenberg zur Verfügung. Dazu gibt es auch die Metadaten der Webcam (also Infos über das Instrument).

Lies die Metadaten in der jüngsten Webcam-Aufzeichnung als DataFrame ein. Schönes Feature: Du brauchst die Datei gar nicht runterzuladen, sonden kannst gleich die URL als Dateipfad verwenden: https://opendata.dwd.de/weather/webcam/Hohenpeissenberg-S/Hohenpeissenberg-S_latest.txt

In [39]:
dateipfad = "https://opendata.dwd.de/weather/webcam/Hamburg-SW/Hamburg-SW_latest.txt"
In [40]:
import pandas as pd

Lösung

In [67]:
pd.read_table(dateipfad, sep="|", header=None)
Out[67]:
0 1
0 Model Canon EOS 1300D
1 Exposure Time 1/1600 sec.
2 FNumber f/5.6
3 ISO 100
4 Exposure Bias 1.0 eV
5 Focal Length 14mm
6 MeasuredEV 11.5 eV
7 Lens Name 11-16mm
8 Serial Number ME1404168
9 Luminance 58.8 %

Aufgabe: Typberatung für Spalten

Die Datei data/schnee_potsdam.csv enthält eine Tabelle mit Schneemessungen an der DWD-Station Potsdam. Die Spalte hoehe steht für die gemessene Schneehöhe (in cm), die Spalte weq für das Schmelzwasseräquivalent (in mm). Wir lesen die Datei wie üblich ein:

In [41]:
df = pd.read_csv("data/snow_potsdam.csv", sep=";")
df
Out[41]:
datum hoehe weq
0 19660104 1 1.6
1 19660105 4 5.2
2 19660106 4 4.0
3 19660107 3 2.7
4 19660108 2 2.0
... ... ... ...
17256 20130403 9 22.5
17257 20130404 9 25.2
17258 20130405 9 26.1
17259 20130406 8 26.4
17260 20130407 6 19.8

17261 rows × 3 columns

Sieht gut aus. Lassen wir uns für den Dataframe doch mal die üblichen statistischen Kennwerte ausgeben:

In [42]:
df.describe()
Out[42]:
datum hoehe
count 1.726100e+04 17261.000000
mean 1.989206e+07 1.071491
std 1.364289e+05 4.410247
min 1.966010e+07 0.000000
25% 1.977103e+07 0.000000
50% 1.989082e+07 0.000000
75% 2.001061e+07 0.000000
max 2.013041e+07 70.000000

Hm... keine Werte für die Spalte weq? Dafür aber für das Datum? Was ist da schiefgegangen??

Lösung

In der Spalte weq wurde ein unüblicher Wert für fehlende Daten verwendet: "novalue". Weil pandas solch einen exotischen Platzhalter nicht kennt (im Gegensatz zu z.B. nan), geht es davon aus, dass es sich bei der Spalte weq durchweg um Strings handelt. Bei der Datumsspalte hingegen müssen wir erstmal ansagen, wie dieses zu interpretieren ist.

In [48]:
df = pd.read_csv("data/snow_potsdam.csv", sep=";", na_values="novalue")
df.datum = pd.to_datetime(df.datum, format="%Y%m%d")
df.describe()
Out[48]:
hoehe weq
count 17261.000000 867.000000
mean 1.071491 22.281084
std 4.410247 24.577783
min 0.000000 0.500000
25% 0.000000 6.400000
50% 0.000000 14.000000
75% 0.000000 27.000000
max 70.000000 161.000000
In [46]:
df
Out[46]:
datum hoehe weq
0 19660104 1 1.6
1 19660105 4 5.2
2 19660106 4 4.0
3 19660107 3 2.7
4 19660108 2 2.0
... ... ... ...
17256 20130403 9 22.5
17257 20130404 9 25.2
17258 20130405 9 26.1
17259 20130406 8 26.4
17260 20130407 6 19.8

17261 rows × 3 columns

Aufgabe: Ärger in der Besenscheune

"Brooms Barn" ist eine landwirtschaftliche Forschungsstation in England. Die Datei ../data/brooms_barn.txt enthält Bodenkennwerte für eine dortige Versuchsfläche. Versuche, die Datei als DataFrame einzulesen. Welche Probleme treten auf? Kannst Du sie beheben?

Lösung

Es liegt ein so genanntes "fixed width format" vor, es gibt also keinen Spaltentrenner im bekannten Sinne. Freundlicherweise kann pandas damit umgehen, aber nur mit der Funktion pandas.read_fwd. Obendrein muss man noch bekanntgeben, dass die ersten 20 Zeilen ignoriert werden müssen und es keinen Header gibt.

In [68]:
df = pd.read_fwf("../data/brooms_barn.txt", skiprows=20, header=None)
df.columns = "x", "y", "K", "log10K", "pH","P", "log10P"
df
Out[68]:
x y K log10K pH P log10P
0 1 24 26.0 1.41497 7.2 5.5 0.74036
1 1 25 22.0 1.34242 7.2 5.2 0.71600
2 1 26 18.0 1.25527 6.8 2.6 0.41497
3 1 27 19.0 1.27875 6.4 1.3 0.11394
4 1 28 26.0 1.41497 6.1 1.3 0.11394
... ... ... ... ... ... ... ...
430 8 25 29.0 1.46240 7.9 9.0 0.95424
431 8 26 23.0 1.36173 7.9 7.5 0.87506
432 8 27 20.0 1.30103 8.0 6.6 0.81954
433 8 28 38.0 1.57978 7.9 -9.0 -9.00000
434 8 29 36.0 1.55630 8.6 5.4 0.73239

435 rows × 7 columns

Optional: Spezialformate

Eigentlich könnte alles so einfach sein...es gibt standardisierte Dateiformate für so ziemlich jeden denkbaren Anwendungsfall. Und um diese Formate zu lesen, gibt es Pakete und Funktionen. So wie das altbekannte csv-Format (hier ein Beispiel von $C_{org}$-Werten an vier Punkten im Raum:

x;y;Corg
12.5;20.1;1.5
14.7;9.3;2.2
13.2;12.2;2.1
14.1;15.3;1.0

Aus unerfindlichen Gründen entscheiden sich aber manchmal Menschen, ihre Daten einfach "anders" abzuspeichern. Wenn sie so freundlich sind, die Nutzung der Daten zu erlauben, nimmt man das meist stillschweigend hin.

Die Daten von oben habe ich einfach mal wie folgt in der Datei ../data/miniexample.txt abgespeichert:

x
12.5 14.7 13.2 14.1
y
20.1 9.3 12.2 15.3
Corg
1.5 2.2 2.1 1.0

pandas kann das so nicht verstehen, aber wir wollen trotzdem einen Dataframe daraus machen. Was tun? Wir müssen uns eine eigene Funktion schreiben.

In [124]:
# Old school
def read_weirdo_format(weirdofile):
    # Datei öffnen
    f = open(weirdofile)
    # Datei lesen
    x = f.read()
    # Datei schließen
    f.close()
    # Ggf. letzten Zeilenumbruch entfernen
    x = x.strip("\n")
    # Text entlang der verbleibenden Zeilenumbrüche zerschneiden
    x = x.split("\n")
    # Jede zweite Zeile ist ein Spaltenname
    columns = x[::2]
    # Jede andere zweite Zeile sind Daten
    rows = x[1::2]
    # Hierin wollen wir die Daten ablegen
    data = []
    for row in rows:
        # Jede Zeile wird entlang der Leerzeichen zerschnitten
        data.append(row.split(" "))
    # Aus der Liste von Strings machen wir einen Array
    array = np.array(data).astype("float")
    # Aus dem transponierten Array machen wir einen Dataframe
    df = pd.DataFrame(array.T, columns=columns)
    return(df)
In [123]:
read_weirdo_format("../data/miniexample.txt")
Out[123]:
X Y Corg
0 12.5 20.1 1.5
1 14.7 9.3 2.2
2 13.2 12.2 2.1
3 14.1 15.3 1.0

Naja, geht bestimmt auch eleganter...aber es funktioniert. Alternativ können wir auch read_csv zweckentfremden.

In [125]:
# Dreister Missbrauch von Funktionen
def read_weirdo_format(weirdo_file):
    df = pd.read_csv(weirdo_file, sep=" ", skiprows=[0,2,4], header=None).transpose()
    df.columns = pd.read_csv(weirdo_file, skiprows=[1,3,5], header=None)[0]
    return(df)
In [126]:
read_weirdo_format("../data/miniexample.txt")
Out[126]:
X Y Corg
0 12.5 20.1 1.5
1 14.7 9.3 2.2
2 13.2 12.2 2.1
3 14.1 15.3 1.0

Noch eine Kleinigkeit: xml, json, kml, geojson, shp, tiff, png, jpg, yaml, netcdf, hdf5, usw

Mit dieser Lektion haben wir gerade mal an der Oberfläche gekratzt. Es gibt so viele Datenformate, dass man leicht den Überblick verlieren kann...

  • Bilddaten (Bitmaps/Raster), z.B.
    • jpg
    • bmp
    • png
    • tiff
    • ...
  • Geodaten (Raster):
    • geotiff
    • ESRI ASCII raster
    • bip, bil
    • ...
  • Geodaten (Vektor):
    • shp
    • geojson
    • kml
    • ...
  • Hierarchische und strukturierte Textformate, z.B.
    • json
    • xml
    • yaml
    • ...
  • Hierarchische Formate für multi-dimensionale Arrays
    • NetCDF
    • HDF5

Das NetCDF-Format nehmen wir uns in Lektion 5 vor, die Geodatenformate in Lektion 6.

Ansonsten gilt stets: Falls Ihr es mit einem Euch unbekannten Dateiformat zu tun habt, dann sucht nach einer Lösung.

Optionale Aufgabe: json Lesen

Die Datei ../data/biowetter.json kommt vom Open Data Server des DWD und fasst die Gefahreneinschätzung des DWD für Wetterfühlige zu einem bestimmten Zeitpunkt zusammen.

Findet heraus, wie Ihr die Datei ../data/biowetter.json in Python einlesen könnt.

Lösung

In [66]:
import json
In [70]:
f = open("../data/biowetter.json", "r")
result = json.load(f)
f.close()
In [101]:
type(result)
Out[101]:
dict

Die Variable result ist vom Typ dict, also ein Dictionary. Ein dict kann nach Schlüsselwörtern (keys) abgefragt werden.

In [106]:
print(result.keys())
dict_keys(['author', 'last_update', 'zone', 'next_update', 'name'])
In [110]:
result["author"]
Out[110]:
'Medizin-Meteorologie'

Die eigentlichen Daten liegen unter den Schlüssel zone, und zwar als Liste (list).

In [103]:
type(result["zone"])
Out[103]:
list
In [111]:
len(result["zone"])
Out[111]:
11

Die elf Elemente der Liste bestehen wiederum aus dicts. Wir bauen eine kleine Schleife, um uns einen Überblick über die Zonen zu verschaffen:

In [119]:
for i, item in enumerate(result["zone"]):
    print(i, ":", item["id"], "-", item["name"])
0 : A - Schleswig-Holstein, Hamburg, nördl. Niedersachsen, Bremen
1 : B - Mecklenburg-Vorpommern
2 : C - Südwestliches Niedersachsen, Nordrhein-Westfalen
3 : D - Östliches und südliches Niedersachsen
4 : E - Berlin, Brandenburg und im nördlichen Sachsen-Anhalt
5 : G - südl. Sachsen-Anhalt, Thüringen, Sachsen
6 : F - Hessen, Rheinland-Pfalz, Saarland
7 : H - Baden
8 : I - Württemberg, Franken ohne östl. Oberfranken
9 : J - Niederbayern, Oberpfalz, östl. Oberfranken
10 : K - Schwaben, Oberbayern

Aha, wir befinden uns also in Zone E, die wir mit dem Index 4 aus der Liste extrahieren können. Nun schauen wir mal, ob eine Erklärung für eine heutige, wetter-bedingte unzureichende Leistungsfähigkeit im Kurs vorliegt.

In [142]:
print(result["zone"][4]["name"])
print("Heute nachmittag ('today_afternoon')")
print("   ", result["zone"][4]["today_afternoon"]["effect"][0]["subeffect"][0]["name"], end=": ")
print(result["zone"][4]["today_afternoon"]["effect"][0]["subeffect"][0]["value"])
print("    Empfehlungen?")
for item in result["zone"][4]["today_afternoon"]["recomms"]:
    print("       ", item["name"], ":", item["value"])
Berlin, Brandenburg und im nördlichen Sachsen-Anhalt
Heute nachmittag ('today_afternoon')
    psychisch-geistige Leistungsfähigkeit: geringe Gefährdung
    Empfehlungen?
        Wettereinfluss auf das allgemeine Befinden : keine
        Wettereinfluss auf das Herz- und Kreislaufgeschehen : kreislaufanregende Maßnahmen bei niedrigem Blutdruck
        Wettereinfluss auf rheumatische Beschwerden : keine
        Wettereinfluss auf Asthma : Bewegung im Freien zur Stärkung der Abwehrkräfte

Na dann.

Fazit: JSON-Daten (und ähnlich XML-Daten) sind sehr strukturiert, haben aber auch einen sehr starken Overhead durch redundante Schlüsselwörter. Man muss außerdem die Struktur gut kennen(lernen), um die Daten sinnvoll verarbeiten zu können.

Coding-Werkstatt

Nachsitzen

Hast Du noch nicht alle Aufgaben aus dieser Lektion geschafft? Dann versuche doch, diese noch nachzuarbeiten. Falls es nicht klappt, nimm die ausklappbaren Lösungsvorschläge zur Hilfe.

GRDC-Daten einlesen

Die Datei pvm_6457010.txt beinhaltet Abflussdaten der Oder am Pegel Gozdowice. Die Datei stammt vom "Global Data Runoff Center" (GRDC). Schreibe eine möglichst allgemeingültige Funktion, mit der Du aus der Datei folgende Daten in einen DataFrame überführst.

  • Stationsname
  • Längengrad und Breitengrard
  • Einzugsgebietsgröße
  • Höhe
  • Anzahl der Jahre mit verfügbaren Daten
  • Für den Gesamtzeitraum: LQ, MQ_1, MQ_2, MQ_3, HQ

Dir könnten dabei folgende Funktionen für den Vergleich von Zeichenfolgen helfen: startsWith, endsWith, identical.

Teste, ob Dein Code auch mit einer anderen Datei (pvm_6457100.txt) funktioniert.