Numerical Python – Einführung in wissenschaftliches Rechnen mit NumPy
NumPy steht für Numerical Python und ist eines der bekanntesten Pakete für alle Python-Programmierer mit wissenschaftlichen Hintergrund. Von persönlichen Kontakten erfuhr ich, dass NumPy heute in der Astrophysik fast genauso verwendet wird wie auch von sogenannten Quants im Investment-Banking. Das NumPy-Paket ist sicherlich ein Grundstein des Erfolges für Python in der Wissenschaft und für den häufigen Einsatz für die Implementierung von Algorihtmen des maschinellen Lernens in Python.
Die zentrale Datenstruktur in NumPy ist das mehrdimensionale Array. Dieses n-dimensionale Array (ndarray) ist eine sehr mächtige Datenstruktur und verwende ich beispielsweise in meinem Artikel über den k-Nächste-Nachbarn-Algorithmus. Die Besonderheit des NumPy-Arrays ist, dass es ein mehrdimensionaler Container für homogene Daten ist. Ein Datentyp gilt also für das gesamte Array, nicht nur für bestimmte Zeilen oder Spalten!
import numpy as np
Wer ein NumPy-Array erstellen möchte, kann dies beispielsweise einfach über eine Liste (Standard-Python) tun, die in ein NumPy-Array umgewandelt werden kann:
dataList = [[2, 9],[3, 4, 7, 10, 2],[0, 9, 3, 2, 3]] # dies ist eine Liste (Standard-Python) dataList Out: [[2, 9], [3, 4, 7, 10, 2], [0, 9, 3, 2, 3]] array = np.array(dataList) # die Liste ist ein beliebter Ausgangspunkt für ein NumPy.Array array Out: array([[2, 9], [3, 4, 7, 10, 2], [0, 9, 3, 2, 3]], dtype=object) array.shape # Gibt die Dimensionsgrößen des Arrays aus Out: (3,) # 3 Zeilen, unterschiedlich viele Spalten
Eine zweite, einfachere Liste mit einheitlicher Spaltenzahl pro Zeile verdeutlicht das Prinzip noch mehr:
dataList2 = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]] array2 = np.array(dataList2) array2 Out: array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) array2.shape Out[15]: (4, 3) # Vier Zeilen, einheitliche drei Spalten
Während die Listen, die Python von Haus aus mitbringt, schwer zu indizieren sind (bei Mehrdimensionalität) und auch nicht wie eine Matrix dargestellt werden (und auch keine Matrix-Operationen anwendbar sind), haben wir all diese Features mit dem ndarray von NumPy.
Schnelle Erzeugung von Arrays
Schnell mal ein Array erzeugen? Kein Problem mit np.zeros() oder np.ones().
a1 = np.ones((5,5)) a2 = np.ones((5,5)) a3 = np.ones((5,5)) a1 + a2 + a3 Out: array([[ 3., 3., 3., 3., 3.], [ 3., 3., 3., 3., 3.], [ 3., 3., 3., 3., 3.], [ 3., 3., 3., 3., 3.], [ 3., 3., 3., 3., 3.]]) (a1 + a2 + a3).sum() Out: 75.0
Indizierung
ndarrays lassen sich wie gewohnt über einen Index (in eckigen Klammern) gezielt ansprechen:
array Out[28]: array([[ 1, 2, 3, 4, 5], [ 6, 7, 8, 9, 10]]) array[0] Out[29]: array([1, 2, 3, 4, 5]) array[1,3] Out[30]: 9 array[1][3] Out[31]: 9
Bei mehrdimensionalen Arrays wird es schnell unübersichtlich, allerdings ist dies in NumPy recht intelligent gelöst. Elemente können natürlich rekursiv angesprochen werden, wie wir es auch bei den Listen im Standard-Python gewohnt sind:
array = np.array([[1,2,3],[4,5,6],[7,8,9]]) array Out: array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) array[0] Out: array([1, 2, 3]) array[0][0] Out: 1
Darüber hinaus können NumPy-Arrays auch über ein Slicing (Ausschnitte über eine Range) indiziert angesprochen werden: array[Zeilen, Spalten].
array[1:2, 2] Out: array([6]) # zeige nur die mittlere Zeile, davon nur die letzte Spalte array[1,2] Out: 6 # gleiches Spiel wie oben (nur schöner) array[:, 2] Out: array([3, 6, 9]) # Alle Zeilen, aber nur aus der letzten Spalte array[:, :] # Alle Zeilen und davon alle Spalten Out: array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) array[0, :] Out: array([1, 2, 3]) # Nur die erste Zeile, über alle Spalten array[:, 0] Out: array([1, 4, 7]) # Alle Zeilen, aber nur aus der ersten Spalte
Fancy Indexing
Fancy Indexing (zu Deutsch etwa “ausgefallenes Indexing”) bietet zwei Funktionen zum Auswählen von Zeilen oder Zellen im mehrdimensionalen Array, die besonders praktisch sind, wenn man es mit sehr großen multidimensionalen Arrays zutun hat.
array = np.zeros((10,5)) array Out: array([[ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0.]]) for i in range(10): array[i] = i*3 array Out: array([[ 0., 0., 0., 0., 0.], [ 3., 3., 3., 3., 3.], [ 6., 6., 6., 6., 6.], [ 9., 9., 9., 9., 9.], [ 12., 12., 12., 12., 12.], [ 15., 15., 15., 15., 15.], [ 18., 18., 18., 18., 18.], [ 21., 21., 21., 21., 21.], [ 24., 24., 24., 24., 24.], [ 27., 27., 27., 27., 27.]]) array[[1]] # Selektiere die zweite Zeile Out: array([[ 3., 3., 3., 3., 3.]]) array[[1,2,3]] # Selektiere die zweite, dritte und vierte Zeile Out: array([[ 3., 3., 3., 3., 3.], [ 6., 6., 6., 6., 6.], [ 9., 9., 9., 9., 9.]]) array[[2,8,-1,-5]] # Selektiere die dritte und neunte Zeile, sowie die letzte und die fünf-letzte Zeile Out: array([[ 6., 6., 6., 6., 6.], [ 24., 24., 24., 24., 24.], [ 27., 27., 27., 27., 27.], [ 15., 15., 15., 15., 15.]])
Wenn wir mehrere Vektoren als Index liefern, werden diese als Koordinatensystem aufgefasst und so können wir auch über x und y zugreifen:
for i in range(10): for j in range(5): array[i,j] = str(i)+str(j) array Out[78]: array([[ 0., 1., 2., 3., 4.], [ 10., 11., 12., 13., 14.], [ 20., 21., 22., 23., 24.], [ 30., 31., 32., 33., 34.], [ 40., 41., 42., 43., 44.], [ 50., 51., 52., 53., 54.], [ 60., 61., 62., 63., 64.], [ 70., 71., 72., 73., 74.], [ 80., 81., 82., 83., 84.], [ 90., 91., 92., 93., 94.]]) array[[5],[3]] # Selektriere den einen Wert in der Zelle y = 5, x = 3 Out: array([ 53.]) array[[2,3,4],[0,1,4]] # Selektiere drei Zellen (y,x) = (2,0),(3,1),(4,4) Out: array([ 20., 31., 44.])
Hier als Beispiel (da gerade noch übersichtlich darstellbar) nur mit zwei Dimensionen (x, y). Es funktioniert aber auch mit drei oder noch mehr Dimensionen.
Achtung: NumPy-Arrays sind standardmäßig Referenzen!
Jeder Programmierer kennt den Unterschied zwischen der Referenz einer Variable und einer Kopie. Während im Standard-Python Listen standardmäßig kopiert werden, werden NumPy-Arrays standardmäßig referenziert. Wenn man dies nicht im Bewusstsein hat, provoziert schnell mal ungeahnte Fehler.
array = np.array([1,2,3,4,5,6,7,8,9,10,11,12]) array Out: array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) ausschnitt = array[4:9] ausschnitt Out: array([5, 6, 7, 8, 9]) ausschnitt[2:4] = 66666 ausschnitt Out: array([ 5, 6, 66666, 66666, 9]) array Out: array([ 1, 2, 3, 4, 5, 6, 66666, 66666, 9, 10, 11, 12]) ausschnitt Out: array([ 5, 6, 66666, 66666, 9])
Der Grund für diesen Umgang ist, dass NumPy für große Datenmengen konzipiert wurde, für die Kopien als Default besser vermieden werden. Wer also ein Array oder einen Ausschnitt aus diesem bewusst kopieren möchte, darf.copy() nicht vergessen.
array = np.array([1,2,3,4,5,6,7,8,9,10,11,12]) ausschnitt = array[4:11].copy() ausschnitt[:] = 989898 ausschnitt Out: array([989898, 989898, 989898, 989898, 989898, 989898, 989898]) array Out: array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
Datentypen festlegen und Casten
Numpy erkennt selbstständig, welcher Datentyp für den Arrayinhalt der vermutlich richtige ist. Wer sichergehen will, kann den Datentypen jedoch auch direkt bei der Erstellung des Arrays festlegen.
array1 = np.array([[1,2,3],[4, 5, 6],[7,8,9]], dtype=np.int16) array1 Out: array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=int16) array2 = np.array(array1, dtype=np.float64) array2 Out: array([[ 1., 2., 3.], [ 4., 5., 6.], [ 7., 8., 9.]])
Wer nicht gleich das ganze Array kopieren möchte, um den Datentypen zu wechseln, kann selbstverständlich auch Casting betreiben.
array3 = array2 * 4.5 array3 Out: array([[ 4.5, 9. , 13.5], [ 18. , 22.5, 27. ], [ 31.5, 36. , 40.5]]) array3.astype(np.int16) Out: array([[ 4, 9, 13], [18, 22, 27], [31, 36, 40]], dtype=int16)
array2 = array.astype('float64') array2 ** array # Jeder Wert an seiner jeweiligen Stelle von array2 (Basis) hoch dem Wert an der gleichen Stelle von array (Exponent) Out: array([[ 1.00000000e+00, 4.00000000e+00, 2.70000000e+01, 2.56000000e+02, 3.12500000e+03], [ 4.66560000e+04, 8.23543000e+05, 1.67772160e+07, 3.87420489e+08, 1.00000000e+10]])
Matrizenberechnungen mit NumPy
Vermutlich ist die Vektorberechnung der häufigste Grund, die NumPy-Bibliothek zu importieren. Früher hatte ich solche Operationen mit einer oder mehreren For-Schleifen durchführen müssen, mit NumPy können Arrays einfach untereinander “verrechnet” werden.
array = np.array([[1,2,3],[4,5,6],[7,8,9],[10,11,12]]) array Out[95]: array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]) array2 * 2 # Verdopplung aller Werte im Array Out: array([[ 2, 4, 6], [ 8, 10, 12], [14, 16, 18], [20, 22, 24]]) array2 ** 2 # entspricht array2 * array2 Out: array([[ 1, 4, 9], [ 16, 25, 36], [ 49, 64, 81], [100, 121, 144]])
Aber Achtung! Vor solchen Berechnungen, sollte das ndarray vorher den passenden Datentypen zugewiesen bekommen, sonst drohen ungeahnte Fehler:
array Out[169]: array = np.array([[ 1, 2, 3, 4, 5],[ 6, 7, 8, 9, 10]]) array ** array Out: array([[ 1, 4, 27, 256, 3125], [46656, 823543, 16777216, 387420489, -2147483648]], dtype=int32) #FEHLER! 10 ** 10 sprengen den Raum von Int32!
Mit dem Zuweisen des Float64-Datentypen wird die Kalkulation wieder korrekt:
array2 = array.astype('float64') array2 ** array Out: array([[ 1.00000000e+00, 4.00000000e+00, 2.70000000e+01, 2.56000000e+02, 3.12500000e+03], [ 4.66560000e+04, 8.23543000e+05, 1.67772160e+07, 3.87420489e+08, 1.00000000e+10]])
Leave a Reply
Want to join the discussion?Feel free to contribute!