9 – Numerische Daten

Einführung in Python und PsychoPy

Autor

Clemens Brunner

Veröffentlicht

1. Dezember 2022

Allgemeines

Python wurde ursprünglich als einfach zu erlernende Lehr-Programmiersprache entwickelt. Aufgrund der Einfachheit und der Möglichkeit, tausende Zusatzpakete nutzen zu können, hat sich Python auch im wissenschaftlichen Bereich (Datenanalyse, Machine Learning) als Quasi-Standard etabliert. Folgende grundlegende Pakete sind für die Datenanalyse essentiell:

  • NumPy (Arbeiten mit numerischen Daten)
  • SciPy (Algorithmen für diverse wissenschaftliche Anwendungen)
  • Matplotlib (Erstellen von Grafiken)
  • Pandas (Arbeiten mit Tabellen)
  • IPython (erweiterter interaktiver Python-Interpreter)

Darüber hinaus gibt es aber noch eine große Anzahl an weiteren exzellenten Paketen für speziellere Anwendungen wie z.B. scikit-learn (Machine Learning), statsmodels (Statistik), scikit-image (Bildverarbeitung) oder SymPy (symbolisches Rechnen). Diese Pakete setzen meist zumindest NumPy als Grundlage voraus, welches wir in dieser Einheit näher kennenlernen werden.

Tipp

Der Python-Interpreter kann, wie bereits des öfteren erwähnt, im Script-Modus oder im interaktiven Modus verwendet werden. Letzterer eignet sich zum interaktiven Verarbeiten von Daten (also z.B. auch zum Ausprobieren von Ideen mittels REPL). Das Paket IPython fügt dem interaktiven Modus von Python einige zusätzliche Funktionen hinzu, die das Arbeiten komfortabler gestalten (z.B. farbliche Hervorhebung der Syntax, einfacher Zugriff auf bereits ausgeführte Befehle und Ergebnisse, hervorragende Vervollständigung von Befehlen mittels Tab-Completion, viele Zusatzfunktionen durch sogenannte Magic Functions, …). Deswegen sollte man eigentlich immer IPython und nicht den “nackten” Python-Interpreter für interaktive Aufgaben verwenden.

NumPy

NumPy ist die Basis der allermeisten wissenschaftlichen Pakete in Python. Es ist so wichtig und essentiell, dass Python ohne NumPy vermutlich nie für Datenanalysen verwendet worden wäre.

NumPy stellt einen hocheffizienten Datentyp für numerische Daten zur Verfügung, nämlich ein homogenes multidimensionales (n-dimensionales) Array (auch kurz ND-Array bzw. ndarray oder kurz Array genannt). Listen sind aufgrund ihrer Flexibilität dafür viel zu ineffizient, da sie unterschiedliche Elemente enthalten können. Homogen bedeutet nämlich, dass alle Elemente eines Arrays denselben Datentyp haben (z.B. lauter float-Zahlen) – dadurch können Berechnungen wesentlich effizienter und schneller ausgeführt werden. Multidimensional bedeutet, dass ein Array beliebig viele Dimensionen (auch Achsen genannt) besitzen kann. Jedes Element wird daher mit einem Tupel indiziert, welches seine genaue Position innerhalb des Arrays beschreibt.

Tipp

Das offizielle Tutorial NumPy: the absolute basics for beginners ergänzt bzw. vertieft die Inhalte dieser Unterlagen – die Lektüre ist sehr zu empfehlen!

Beginnen wir aber mit einem Beispiel (Details zu den einzelnen Befehlen folgen dann später). Der erste Schritt ist (wie bei jedem Paket bzw. Modul) das Importieren. Konventionell importiert man NumPy mit dem Kürzel np:

import numpy as np

Dies bedeutet, dass man das Paket mit np ansprechen kann statt mit numpy – man spart sich also ein paar Zeichen zu tippen. Wir erstellen nun eine Zahlensequenz aus 15 Zahlen:

a = np.arange(15)  # 15 Zahlen von 0 bis 14
a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

Der Typ des Objekts a ist nun keine Liste, sondern tatsächlich ein NumPy-Array, welches als numpy.ndarray bezeichnet wird:

type(a)
numpy.ndarray

NumPy-Arrays besitzen eine Form (Shape):

a.shape  # 15 Elemente in einer Dimension (Achse)
(15,)

Diese Form kann man auch verändern:

a = a.reshape(3, 5)  # umwandeln in 3 Zeilen und 5 Spalten
a  # 2 Dimensionen (Achsen) mit Längen 3 bzw. 5
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])
a.ndim  # Anzahl an Dimensionen (Achsen)
2
a.shape  # Länge der einzelnen Achsen
(3, 5)

Unabhängig von der Form kann man auch die Gesamtanzahl der Elemente im Array bestimmen:

a.size  # Anzahl aller Elemente im Array
15

NumPy-Arrays sind homogene Datentypen, d.h. alle Elemente im Array müssen denselben Typ haben. Diesen Typ kann man wie folgt bestimmen:

a.dtype  # Datentyp aller Elemente im Array (64 bit Ganzzahlen)
dtype('int64')

Erstellen von Arrays

Aus Listen

Arrays können mit der Funktion np.array aus Listen (oder Tupeln) erstellt werden. Im folgenden Beispiel übergeben wir der Funktion eine Liste als Argument, und daraus wird dann ein NumPy-Array erzeugt (zurückgegeben):

b = np.array([1.1, 3.14, 7.68, -12.69, -4.55])  # aus einer normalen Liste
b
array([  1.1 ,   3.14,   7.68, -12.69,  -4.55])

Eine Liste von Listen wird in ein 2D-Array (Tabelle, besteht aus Zeilen und Spalten) konvertiert:

c = np.array([[1, 2, 3], [4, 5, 6]])  # aus einer Liste von Listen
c
array([[1, 2, 3],
       [4, 5, 6]])
c.shape  # 2 Zeilen, 3 Spalten
(2, 3)

Konstant befüllte Arrays

Im Gegensatz zu Listen, welche dynamisch wachsen können, sollte die Größe von Arrays bereits bei der Erstellung bekannt sein, da das Hinzufügen von Zeilen oder Spalten relativ langsam ist. Hierfür gibt es einige praktische Konstruktionen, welche die Arrays mit Platzhalterelementen wie z.B. lauter Nullen erzeugen.

np.zeros((3, 4))  # Nullen
array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])
Hinweis

Die Funktion np.zeros wird mit einem Argument aufgerufen, nämlich mit dem Tupel (3, 4) im vorigen Beispiel. Deswegen benötigt man hier ein weiteres Klammernpaar, um ein Tupel zu übergeben und dieses von Funktionsargumenten zu unterscheiden.

np.ones((2, 4))  # Einsen
array([[1., 1., 1., 1.],
       [1., 1., 1., 1.]])

Arrays, die mit Zahlen ungleich 0 oder 1 gefüllt werden sollen, kann man mit der Funktion np.full erzeugen:

np.full((3, 4), 66)  # 3 Zeilen, 4 Spalten, alle Elemente gleich 66
array([[66, 66, 66, 66],
       [66, 66, 66, 66],
       [66, 66, 66, 66]])

Sequenzen

Analog zur Builtin-Funktion range können mit np.arange Arrays mit Folgen von Zahlen erstellt werden. Hier sind nicht nur ganze Zahlen möglich, sondern auch Kommazahlen. Auch hier zählt der letzte Wert (Stop-Wert) nicht mehr zum Bereich dazu.

np.arange(5, 17)
array([ 5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16])
np.arange(0.3, 5.4, 0.6)  # von 0.3 bis 5.4 mit Schrittweite 0.6
array([0.3, 0.9, 1.5, 2.1, 2.7, 3.3, 3.9, 4.5, 5.1])

Wenn man die Anzahl der Elemente (anstatt der Schrittweite) vorgeben will, verwendet man am besten die Funktion linspace (hier zählen sowohl Start- als auch Endwert zum Bereich):

np.linspace(1, 10, 10)  # 10 Elemente von 1 bis 10
array([ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.])
np.linspace(1, 10, 10, dtype=int)  # wie oben, nur Integer-Elemente
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
np.linspace(1, 10, 24)  # 24 Elemente von 1 bis 10
array([ 1.        ,  1.39130435,  1.7826087 ,  2.17391304,  2.56521739,
        2.95652174,  3.34782609,  3.73913043,  4.13043478,  4.52173913,
        4.91304348,  5.30434783,  5.69565217,  6.08695652,  6.47826087,
        6.86956522,  7.26086957,  7.65217391,  8.04347826,  8.43478261,
        8.82608696,  9.2173913 ,  9.60869565, 10.        ])

Sollen die Elemente nicht den gleichen (linearen) Abstand haben sondern logarithmisch unterteilt sein, gibt es analog dazu die Funktion logspace:

np.logspace(0, 4, 8)  # 8 Werte von 10**0 bis 10**4
array([1.00000000e+00, 3.72759372e+00, 1.38949549e+01, 5.17947468e+01,
       1.93069773e+02, 7.19685673e+02, 2.68269580e+03, 1.00000000e+04])

Zufallszahlen

Oft möchte man auch Zufallszahlen erzeugen. Dazu gibt es in NumPy einen sogenannten Zufallszahlengenerator, welcher Zahlen aus unterschiedlichsten Verteilungen generieren kann. Zunächst importiert man den Standard-Zufallzahlengenerator default_rng aus dem numpy.random-Modul:

from numpy.random import default_rng
Tipp

Die Abkürzung rng steht für Random Number Generator, also Zufallszahlengenerator.

Danach erzeugt man damit ein Generator-Objekt. Dieses kann man auch in einen definierten Zustand versetzen, indem man als Seed eine Zahl angibt:

rng = default_rng(seed=42)

In diesem Beispiel initialisieren wir den Generator mit 42 (wir könnten aber auch jede beliebige andere Zahl nehmen). Die Initialisierung bewirkt, dass der Generator danach dieselbe Folge an Zufallszahlen ausspuckt. Lässt man die Initialisierung mit einer Zahl weg, dann wird der Generator in einen nicht-reproduzierbaren Zustand initialisiert (der Seed wird z.B. aus der aktuellen Uhrzeit u.ä. gebildet). Das bedeutet, dass bei jeder erneuten Ausführung eines Scripts andere Zufallszahlen gezogen werden, was nicht im Sinne der Reproduzierbarkeit von Ergebnissen ist.

Durch Methoden des Generators kann man jetzt Zufallszahlen mit der gewünschten Verteilung erzeugen:

rng.standard_normal(10)
array([ 0.30471708, -1.03998411,  0.7504512 ,  0.94056472, -1.95103519,
       -1.30217951,  0.1278404 , -0.31624259, -0.01680116, -0.85304393])
rng.uniform(size=(2, 2))
array([[0.37079802, 0.92676499],
       [0.64386512, 0.82276161]])
rng.integers(low=-3, high=99, size=(2, 5))
array([[52, 42, 42, 20,  6],
       [53, 87,  3, 84, 81]])
Hinweis

Wenn Sie die obigen Beispiele auf Ihrem Rechner nachvollziehen, sollten Sie dieselben Zufallszahlen bekommen – vorausgesetzt Sie initialisieren den Generator so wie hier mit dem Wert 42.

Rechnen mit Arrays

Arithmetische Operationen

Arithmetische Operationen werden grundsätzlich elementweise angewendet.

a = np.arange(100, 700, 100).reshape((2, 3))
b = np.arange(1, 7).reshape((2, 3))
a
array([[100, 200, 300],
       [400, 500, 600]])
b
array([[1, 2, 3],
       [4, 5, 6]])
a * 2
array([[ 200,  400,  600],
       [ 800, 1000, 1200]])
a + b
array([[101, 202, 303],
       [404, 505, 606]])
a * b
array([[ 100,  400,  900],
       [1600, 2500, 3600]])
b**2
array([[ 1,  4,  9],
       [16, 25, 36]])
a < 300
array([[ True,  True, False],
       [False, False, False]])

Sind die zwei Arrays nicht gleich groß, wird das kleinere Array falls möglich vergrößert (d.h. Werte werden automatisch dupliziert) – dies nennt man Broadcasting. Das folgende Beispiel zeigt eine Multiplikation von einem Array der Shape (2, 3) mit der Zahl 5, was einer Shape von (1,) entspricht:

b.shape
(2, 3)
b * 5
array([[ 5, 10, 15],
       [20, 25, 30]])

Hier wird also die Zahl 5 automatisch vervielfältigt, sodass die Operation elementweise durchgeführt werden kann. Im Prinzip ist diese Operation äquivalent zu folgender Schreibweise:

b * np.array([[5, 5, 5], [5, 5, 5]])
array([[ 5, 10, 15],
       [20, 25, 30]])

Ein weiteres Beispiel für Broadcasting ist, wenn wir ein Array c wie folgt erstellen:

c = np.array([1, 2, 3])

Wir können nun das Array c direkt zum Array b addieren, weil die Dimensionen kompatibel sind: b hat 2 Zeilen und 3 Spalten und c hat 3 Elemente. Durch Broadcasting wird die Operation nun spaltenweise angewendet:

b
array([[1, 2, 3],
       [4, 5, 6]])
c
array([1, 2, 3])
b + c
array([[2, 4, 6],
       [5, 7, 9]])

Methoden

Viele Funktionen wie z.B. sum, mean, min oder max sind als Methoden von ndarray-Objekten verfügbar. Standardmäßig verarbeiten sie dabei alle Elemente so, als ob diese in einer Dimension wären. Alternativ zu den Methoden gibt es auch Funktionen mit dem gleichen Namen.

a.mean()  # Methode
350.0
np.mean(a)  # Funktion
350.0
b.sum()
21
b.max()
6
a.min()
100

Man kann diese Funktionen/Methoden aber auch auf einzelne Dimensionen (auch “Achsen” genannt) anwenden, z.B. auf Zeilen oder Spalten. Dabei entspricht 0 den Zeilen und 1 den Spalten.

a.mean(axis=0)  # Mittelwert entlang der Zeilen, d.h. Spaltenmittelwerte
array([250., 350., 450.])
a.mean(axis=1)  # Mittelwert entlang der Spalten, d.h. Zeilenmittelwerte
array([200., 500.])

Indizieren und Slicen

Analog zu anderen Sequenzdatentypen (z.B. String, Liste oder Tupel) können einzelne Elemente aus Arrays durch Indizieren bzw. Slicen herausgegriffen werden. Eindimensionale Arrays werden im Prinzip genau wie Listen indiziert.

a = np.arange(10)**3
a
array([  0,   1,   8,  27,  64, 125, 216, 343, 512, 729])
a[0]  # erstes Element (Indizierung beginnt bei 0)
0
a[-2]  # vorletztes Element
512
a[2:5]  # drei Elemente, beginnend mit Position 2
array([ 8, 27, 64])
a[::2]  # jedes zweite Element
array([  0,   8,  64, 216, 512])

Mehrdimensionale Arrays haben einen Index pro Achse:

b = np.random.randint(0, 100, (5, 4))
b
array([[37, 12, 72,  9],
       [75,  5, 79, 64],
       [16,  1, 76, 71],
       [ 6, 25, 50, 20],
       [18, 84, 11, 28]])
b.shape
(5, 4)
b[2, 3]  # 3. Zeile, 4. Spalte
71
b[:, -1]  # alle Zeilen, letzte Spalte
array([ 9, 64, 71, 20, 28])
b[0, :]  # erste Zeile
array([37, 12, 72,  9])
b[1:3, 2:]
array([[79, 64],
       [76, 71]])

Mit Listen kann man mehrere (auch gleiche) spezifische Positionen aus einem Array indizieren. Der Einfachkeit halber sei dies an einem 1D-Array veranschaulicht:

a = np.arange(12)**2
a
array([  0,   1,   4,   9,  16,  25,  36,  49,  64,  81, 100, 121])
idx = [2, 7, 10, 10]  # wir wollen das 2., 7., 10. und 10. Element
idx
[2, 7, 10, 10]
a[idx]
array([  4,  49, 100, 100])

Den Namen idx für die Liste benötigt man nicht, man kann diese direkt innerhalb der eckigen Klammern angeben (das erste eckige Klammernpaar ist die Indizierung, und das zweite eckige Klammernpaar kennzeichnet die Liste):

a[[2, 7, 10, 10]]
array([  4,  49, 100, 100])

Mit Bool’schen Indexarrays kann man Arrays ebenfalls indizieren (maskieren):

a % 2 == 0  # wird elementweise verglichen
array([ True, False,  True, False,  True, False,  True, False,  True,
       False,  True, False])
a[a % 2 == 0]
array([  0,   4,  16,  36,  64, 100])

Es werden hier also nur jene Elemente herausgegriffen, für die das Indexarray True ist.

Form (Shape)

Die Form eines Arrays lässt sich auf folgende Arten einsehen bzw. ändern:

c = np.random.randint(-500, 500, (3, 4))
c
array([[ 425, -102,   62,   80],
       [-285,  483,  253,    3],
       [ -22,  364, -414, -359]])
c.shape  # gibt die aktuelle Form aus
(3, 4)
c.shape = 2, 6  # ändert die Form in place
c
array([[ 425, -102,   62,   80, -285,  483],
       [ 253,    3,  -22,  364, -414, -359]])
c.resize((4, 3))  # ändert die Form in place
c
array([[ 425, -102,   62],
       [  80, -285,  483],
       [ 253,    3,  -22],
       [ 364, -414, -359]])
c.reshape((1, -1))  # reshape ändert das Objekt nicht
c
array([[ 425, -102,   62],
       [  80, -285,  483],
       [ 253,    3,  -22],
       [ 364, -414, -359]])
Tipp

Man muss bei reshape nicht alle Dimensionen angeben – eine kann man auf -1 setzen, was bedeutet, dass NumPy die korrekte Anzahl automatisch bestimmt. Dies ist möglich, da die Anzahl aller Elemente konstant bleiben muss.

c = c.reshape((1, -1))  # daher sollte man einen Namen zuweisen
c
array([[ 425, -102,   62,   80, -285,  483,  253,    3,  -22,  364, -414,
        -359]])

Der Unterschied zwischen resize und reshape ist also, das resize das Array direkt modifiziert und reshape lediglich ein neues geändertes Array zurückgibt.

Kombinieren von Arrays

Mit den Funktionen hstack und vstack können Arrays horizontal bzw. vertikal miteinander kombiniert werden.

a = np.random.randint(-100, 100, (2, 3))
b = np.random.randint(-100, 100, (2, 3))
a
array([[ 37, -93, -37],
       [-39, -78, -43]])
b
array([[-99,  28, -40],
       [-92,  41,  15]])
np.hstack((a, b))
array([[ 37, -93, -37, -99,  28, -40],
       [-39, -78, -43, -92,  41,  15]])
np.vstack((a, b))
array([[ 37, -93, -37],
       [-39, -78, -43],
       [-99,  28, -40],
       [-92,  41,  15]])

Die Funktionen column_stack und row_stack liefern bei 2D-Arrays dieselben Ergebnisse wie hstack und vstack. Es gibt jedoch Unterschiede bei 1D-Arrays.

np.column_stack((a, b))
array([[ 37, -93, -37, -99,  28, -40],
       [-39, -78, -43, -92,  41,  15]])
np.row_stack((a, b))
array([[ 37, -93, -37],
       [-39, -78, -43],
       [-99,  28, -40],
       [-92,  41,  15]])
c = np.random.randint(-100, 100, 5)
c
array([ 75,  21, -70, -29,  31])
d = np.random.randint(-100, 100, 5)
d
array([ 98,  49, -51, -43, -97])
c.shape
(5,)
d.shape
(5,)
np.row_stack((c, d))
array([[ 75,  21, -70, -29,  31],
       [ 98,  49, -51, -43, -97]])
np.column_stack((c, d))
array([[ 75,  98],
       [ 21,  49],
       [-70, -51],
       [-29, -43],
       [ 31, -97]])
np.hstack((c, d))
array([ 75,  21, -70, -29,  31,  98,  49, -51, -43, -97])
np.vstack((c, d))
array([[ 75,  21, -70, -29,  31],
       [ 98,  49, -51, -43, -97]])

Übungen

Übung 1

Erstellen Sie ein eindimensionales Array mit den Zahlen von 0 (inklusive) bis 10 (exklusive) in Schritten von 0.1. Weisen Sie diesem Array den Namen t zu. Wie viele Elemente hat das Array? Wie lautet die Form (Shape) des Arrays?

Übung 2

Erstellen Sie aus dem Array t aus Übung 1 ein zweidimensionales Array s, welches die gleichen Elemente beinhaltet, jedoch aus 20 Zeilen (und der entsprechenden Anzahl an Spalten) besteht.

Übung 3

Erstellen Sie ein zweidimensionales Array u der Form (100, 8), welches aus zufälligen Ganzzahlen im Bereich [−1000, 1000) besteht (d.h. −1000 ist dabei, 1000 nicht). Setzen Sie vorher den Seed des Generators auf 18. Berechnen Sie dann folgende Größen von u:

  • Summe aller Elemente
  • Mittelwert aller Elemente
  • Zeilenmittelwerte
  • Spaltenmittelwerte
  • Maxima und Minima jeder Spalte
  • Maxima und Minima jeder Zeile
  • Maximum der Zeilenmittelwerte

Übung 4

Erstellen Sie ein dreidimensionales Array x der Form (3, 10, 5), welches die Zahlen von 1 bis 150 enthält. Wie lauten die drei Mittelwerte wenn Sie über die letzten beiden Dimensionen mitteln?

Hinweis

Das Array x kann man sich als 3 Tabellen à 10 Zeilen und 5 Spalten vorstellen. Der Mittelwert aller Elemente von x[0, :, :] ist der erste gesuchte Mittelwert, jener von x[1, :, :] der zweite, und der Mittelwert von x[2, :, :] ist der dritte gesuchte Wert. Sie können so die drei Mittelwerte berechnen, oder kürzer wenn Sie das axis-Argument von np.mean auf ein Tupel setzen, welches die Achsen beschreibt, über die Sie mitteln möchten (also die Achsen 1 und 2, da Python bei 0 zu zählen beginnt).

Übung 5

Erstellen Sie ein (8, 8)-Array namens chess mit einem Schachbrettmuster (verwenden Sie dafür die Werte 0 und 1). Es gibt viele mögliche Lösungen, gerne können Sie auch mehrere Varianten anführen. Sehen Sie sich z.B. die Hilfe zur Funktion np.tile an, oder erzeugen Sie zuerst ein Array aus lauter Nullen und fügen Sie dann an den entsprechenden Stellen Einsen ein (z.B. durch entsprechendes Indizieren oder mit for-Schleifen).