Kapitel 4. Numerisches Rechnen mit NumPy

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Computer sind nutzlos. Sie können nur Antworten geben.

Pablo Picasso

Obwohl der Python-Interpreter selbst bereits eine Vielzahl von Datenstrukturen mitbringt, ergänzen NumPy und andere Bibliotheken diese auf wertvolle Weise. Dieses Kapitel konzentriert sich auf NumPy, das ein multidimensionales Array-Objekt zur Verfügung stellt, um homogene oder heterogene Datenarrays zu speichern und die Vektorisierung von Code zu unterstützen.

Das Kapitel behandelt die folgenden Datenstrukturen:

Objekttyp Bedeutung Verwendet für

ndarray (regulär)

n-dimensionales Array-Objekt

Große Arrays mit numerischen Daten

ndarray (Rekord)

2-dimensionales Array-Objekt

Tabellarische Daten, die in Spalten organisiert sind

Dieses Kapitel ist wie folgt gegliedert:

"Arrays of Data"

In diesem Abschnitt geht es um den Umgang mit Arrays von Daten mit reinem Python-Code.

"Reguläre NumPy-Arrays"

Dies ist der Kernabschnitt über die reguläre Klasse NumPy ndarray , das Arbeitspferd in fast allen datenintensiven Python-Anwendungen, die numerische Daten beinhalten.

"Strukturierte NumPy-Arrays"

In diesem kurzen Abschnitt werden strukturierte (oder Datensatz-) ndarray Objekte für die Bearbeitung von Tabellendaten mit Spalten vorgestellt.

"Vektorisierung von Code"

In diesem Abschnitt werden die Vorteile der Vektorisierung von Code und die Bedeutung der Speicheranordnung in bestimmten Szenarien diskutiert.

Arrays von Daten

Das vorherige Kapitel hat gezeigt, dass Python einige recht nützliche und flexible allgemeine Datenstrukturen bietet. Insbesondere list Objekte können als echte Arbeitstiere mit vielen praktischen Eigenschaften und Anwendungsbereichen angesehen werden. Die Verwendung einer solch flexiblen (veränderbaren) Datenstruktur hat ihren Preis, und zwar in Form eines relativ hohen Speicherverbrauchs, einer geringeren Leistung oder beidem. Wissenschaftliche und finanzielle Anwendungen benötigen jedoch in der Regel leistungsstarke Operationen mit speziellen Datenstrukturen. Eine der wichtigsten Datenstrukturen in diesem Zusammenhang ist das Array. Arrays strukturieren im Allgemeinen andere (grundlegende) Objekte desselben Datentyps in Zeilen und Spalten.

Nehmen wir einmal an, dass nur Zahlen relevant sind, obwohl sich das Konzept auch auf andere Arten von Daten übertragen lässt. Im einfachsten Fall stellt ein eindimensionales Array dann mathematisch gesehen einen Vektor von reellen Zahlen dar, die intern durch float Objekte repräsentiert werden. Es besteht dann nur aus einer einzigen Zeile oder Spalte von Elementen. Im häufigeren Fall stellt ein Array eine i × j-Matrix aus Elementen dar. Dieses Konzept lässt sich auf i × j × k Würfel von Elementen in drei Dimensionen sowie auf allgemeine n-dimensionale Arrays der Form i × j × k × l × ... .

Mathematische Disziplinen wie die lineare Algebra und die Vektorraumtheorie zeigen, dass solche mathematischen Strukturen in einer Reihe von wissenschaftlichen Disziplinen und Bereichen von großer Bedeutung sind. Daher kann es sich als nützlich erweisen, eine spezialisierte Klasse von Datenstrukturen zur Verfügung zu haben, die explizit für den bequemen und effizienten Umgang mit Arrays entwickelt wurde. Hier kommt die Python-Bibliothek NumPy ins Spiel, mit ihrer leistungsstarken Klasse ndarray. Bevor diese Klasse im nächsten Abschnitt vorgestellt wird, zeigt dieser Abschnitt zwei Alternativen für den Umgang mit Arrays.

Arrays mit Python-Listen

Arrays können mit den eingebauten Datenstrukturen konstruiert werden, die im vorherigen Kapitel vorgestellt wurden. list Objekte eignen sich besonders gut für diese Aufgabe. Ein einfaches list kann bereits als eindimensionales Array betrachtet werden:

In [1]: v = [0.5, 0.75, 1.0, 1.5, 2.0]  1
1

list Objekt mit Zahlen.

Da list Objekte beliebige andere Objekte enthalten können, können sie auch andere list Objekte enthalten. Auf diese Weise lassen sich zwei- und höherdimensionale Arrays leicht durch verschachtelte list Objekte aufbauen:

In [2]: m = [v, v, v]  1
        m  2
Out[2]: [[0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0]]
1

list Objekt mit list Objekten ...

2

... das Ergebnis ist eine Matrix aus Zahlen.

Man kann auch einfach Zeilen über eine einfache Indizierung oder einzelne Elemente über eine doppelte Indizierung auswählen (ganze Spalten sind jedoch nicht so einfach auszuwählen):

In [3]: m[1]
Out[3]: [0.5, 0.75, 1.0, 1.5, 2.0]

In [4]: m[1][0]
Out[4]: 0.5

Die Verschachtelung kann für noch allgemeinere Strukturen weiter vorangetrieben werden:

In [5]: v1 = [0.5, 1.5]
        v2 = [1, 2]
        m = [v1, v2]
        c = [m, m]  1
        c
Out[5]: [[[0.5, 1.5], [1, 2]], [[0.5, 1.5], [1, 2]]]

In [6]: c[1][1][0]
Out[6]: 1
1

Zahlenwürfel.

Beachte, dass das Kombinieren von Objekten auf die soeben vorgestellte Weise im Allgemeinen mit Referenzzeigern auf die ursprünglichen Objekte funktioniert. Was bedeutet das in der Praxis? Sieh dir die folgenden Operationen an:

In [7]: v = [0.5, 0.75, 1.0, 1.5, 2.0]
        m = [v, v, v]
        m
Out[7]: [[0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0]]

Ändere nun den Wert des ersten Elements des v Objekts und schau, was mit dem m Objekt passiert:

In [8]: v[0] = 'Python'
        m
Out[8]: [['Python', 0.75, 1.0, 1.5, 2.0],
         ['Python', 0.75, 1.0, 1.5, 2.0],
         ['Python', 0.75, 1.0, 1.5, 2.0]]

Dies kann vermieden werden, indem du die Funktion deepcopy() des Moduls copy verwendest:

In [9]: from copy import deepcopy
        v = [0.5, 0.75, 1.0, 1.5, 2.0]
        m = 3 * [deepcopy(v), ]  1
        m
Out[9]: [[0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0],
         [0.5, 0.75, 1.0, 1.5, 2.0]]

In [10]: v[0] = 'Python'  2
         m  3
Out[10]: [[0.5, 0.75, 1.0, 1.5, 2.0],
          [0.5, 0.75, 1.0, 1.5, 2.0],
          [0.5, 0.75, 1.0, 1.5, 2.0]]
1

Anstelle von Referenzzeigern werden physische Kopien verwendet.

2

Das hat zur Folge, dass eine Veränderung des ursprünglichen Objekts ...

3

... hat keinen Einfluss mehr.

Die Python-Array-Klasse

Mit gibt es ein eigenes array Modul in Python. In der Dokumentation steht:

Dieses Modul definiert einen Objekttyp, der ein Array von Basiswerten kompakt darstellen kann: Zeichen, Ganzzahlen, Fließkommazahlen. Arrays sind Sequenztypen und verhalten sich ähnlich wie Listen, mit dem Unterschied, dass der Typ der in ihnen gespeicherten Objekte eingeschränkt ist. Der Typ wird bei der Objekterstellung durch einen Typcode festgelegt, der aus einem einzigen Zeichen besteht.

Betrachte den folgenden Code, der ein array Objekt aus einem list Objekt instanziiert:

In [11]: v = [0.5, 0.75, 1.0, 1.5, 2.0]

In [12]: import array

In [13]: a = array.array('f', v)  1
         a
Out[13]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0])

In [14]: a.append(0.5)  2
         a
Out[14]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5])

In [15]: a.extend([5.0, 6.75])  2
         a
Out[15]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75])

In [16]: 2 * a  3
Out[16]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75, 0.5, 0.75, 1.0,
          1.5, 2.0, 0.5, 5.0, 6.75])
1

Die Instanziierung des array Objekts mit float als Typcode.

2

Die wichtigsten Methoden funktionieren ähnlich wie die des list Objekts.

3

Obwohl die "Skalarmultiplikation" im Prinzip funktioniert, ist das Ergebnis nicht das mathematisch erwartete; vielmehr werden die Elemente wiederholt.

Der Versuch, ein Objekt mit einem anderen Datentyp als dem angegebenen anzuhängen, löst eine TypeError aus:

In [17]: a.append('string')  1

         ---------------------------------------
         TypeErrorTraceback (most recent call last)
         <ipython-input-17-14cd6281866b> in <module>()
         ----> 1 a.append('string')  1

         TypeError: must be real number, not str

In [18]: a.tolist()  2
Out[18]: [0.5, 0.75, 1.0, 1.5, 2.0, 0.5, 5.0, 6.75]
1

Nur float Objekte können angehängt werden; andere Datentypen/Typcodes führen zu Fehlern.

2

Das array Objekt kann jedoch leicht in ein list Objekt zurückverwandelt werden, wenn eine solche Flexibilität erforderlich ist.

Ein Vorteil der Klasse array ist, dass sie über eine integrierte Speicherung und Abruffunktion verfügt:

In [19]: f = open('array.apy', 'wb')  1
         a.tofile(f)  2
         f.close()  3

In [20]: with open('array.apy', 'wb') as f:  4
             a.tofile(f)  4

In [21]: !ls -n arr*  5
         -rw-r--r--@ 1 503  20  32 Nov  7 11:46 array.apy
1

Öffnet eine Datei auf der Festplatte zum Schreiben von Binärdaten.

2

Schreibt die Daten von array in die Datei.

3

Schließt die Datei.

4

Alternative: verwendet einen with Kontext für denselben Vorgang.

5

Zeigt die Datei so an, wie sie auf die Festplatte geschrieben wurde.

Wie zuvor ist der Datentyp des array Objekts beim Lesen der Daten von der Festplatte von Bedeutung:

In [22]: b = array.array('f')  1

In [23]: with open('array.apy', 'rb') as f:  2
             b.fromfile(f, 5)  3

In [24]: b  3
Out[24]: array('f', [0.5, 0.75, 1.0, 1.5, 2.0])

In [25]: b = array.array('d')  4

In [26]: with open('array.apy', 'rb') as f:
             b.fromfile(f, 2)  5

In [27]: b  6
Out[27]: array('d', [0.0004882813645963324, 0.12500002956949174])
1

Erzeugt ein neues array Objekt mit dem Typcode float.

2

Öffnet die Datei zum Lesen von Binärdaten ...

3

... und liest fünf Elemente in dem Objekt b.

4

Erzeugt ein neues array Objekt mit dem Typcode double.

5

Liest zwei Elemente aus der Datei.

6

Der Unterschied in den Typencodes führt zu "falschen" Zahlen.

Reguläre NumPy-Arrays

Das Zusammensetzen von Array-Strukturen mit list Objekten funktioniert einigermaßen. Aber es ist nicht wirklich praktisch, und die Klasse list wurde nicht mit diesem speziellen Ziel im Hinterkopf entwickelt. Sie hat vielmehr einen viel breiteren und allgemeineren Anwendungsbereich. Die Klasse array ist etwas spezieller und bietet einige nützliche Funktionen für die Arbeit mit Arrays von Daten. Eine wirklich spezialisierte Klasse könnte jedoch sehr nützlich sein, um mit arrayartigen Strukturen zu arbeiten.

Die Grundlagen

numpy.ndarray ist eine solche Klasse, die mit dem Ziel entwickelt wurde, n-dimensionale Arrays bequem und effizient - d.h. hochperformant - zu verarbeiten. Die grundlegende Handhabung von Instanzen dieser Klasse lässt sich wiederum am besten anhand von Beispielen veranschaulichen:

In [28]: import numpy as np  1

In [29]: a = np.array([0, 0.5, 1.0, 1.5, 2.0])  2
         a
Out[29]: array([0. , 0.5, 1. , 1.5, 2. ])

In [30]: type(a)  2
Out[30]: numpy.ndarray

In [31]: a = np.array(['a', 'b', 'c'])  3
         a
Out[31]: array(['a', 'b', 'c'], dtype='<U1')

In [32]: a = np.arange(2, 20, 2)  4
         a
Out[32]: array([ 2,  4,  6,  8, 10, 12, 14, 16, 18])

In [33]: a = np.arange(8, dtype=np.float)  5
         a
Out[33]: array([0., 1., 2., 3., 4., 5., 6., 7.])

In [34]: a[5:]  6
Out[34]: array([5., 6., 7.])

In [35]: a[:2]  6
Out[35]: array([0., 1.])
1

Importiert das Paket numpy.

2

Erzeugt ein ndarray Objekt aus einem list Objekt mit floats.

3

Erzeugt ein ndarray Objekt aus einem list Objekt mit strs.

4

np.arange() funktioniert ähnlich wie range()...

5

... nimmt aber als zusätzliche Eingabe den Parameter dtype.

6

Bei eindimensionalen ndarray Objekten funktioniert die Indizierung wie gewohnt.

Ein wichtiges Merkmal der Klasse ndarray ist die Vielzahl der eingebauten Methoden. Zum Beispiel:

In [36]: a.sum()  1
Out[36]: 28.0

In [37]: a.std()  2
Out[37]: 2.29128784747792

In [38]: a.cumsum()  3
Out[38]: array([ 0.,  1.,  3.,  6., 10., 15., 21., 28.])
1

Die Summe aller Elemente.

2

Die Standardabweichung der Elemente.

3

Die kumulative Summe aller Elemente (beginnend bei Indexposition 0).

Ein weiteres wichtiges Merkmal von sind die (vektorisierten) mathematischen Operationen, die auf ndarray Objekten:

In [39]: l = [0., 0.5, 1.5, 3., 5.]
         2 * l  1
Out[39]: [0.0, 0.5, 1.5, 3.0, 5.0, 0.0, 0.5, 1.5, 3.0, 5.0]

In [40]: a
Out[40]: array([0., 1., 2., 3., 4., 5., 6., 7.])

In [41]: 2 * a  2
Out[41]: array([ 0.,  2.,  4.,  6.,  8., 10., 12., 14.])

In [42]: a ** 2  3
Out[42]: array([ 0.,  1.,  4.,  9., 16., 25., 36., 49.])

In [43]: 2 ** a  4
Out[43]: array([  1.,   2.,   4.,   8.,  16.,  32.,  64., 128.])

In [44]: a ** a  5
Out[44]: array([1.00000e+00, 1.00000e+00, 4.00000e+00, 2.70000e+01, 2.56000e+02,
                3.12500e+03, 4.66560e+04, 8.23543e+05])
1

Die Skalarmultiplikation mit list Objekten führt zu einer Wiederholung der Elemente.

2

Im Gegensatz dazu wird bei der Arbeit mit ndarray Objekten eine richtige skalare Multiplikation durchgeführt.

3

Damit werden die quadratischen Werte elementweise berechnet.

4

Damit werden die Elemente der ndarray als Kräfte interpretiert.

5

Damit wird die Leistung jedes Elements zu sich selbst berechnet.

Universelle Funktionen sind ein weiteres wichtiges Merkmal des NumPy Pakets. Sie sind "universell" in dem Sinne, dass sie im Allgemeinen sowohl auf ndarray Objekte als auch auf grundlegende Python-Datentypen wirken. Wenn du universelle Funktionen z. B. auf ein Python-Objekt float anwendest, musst du dich jedoch der geringeren Leistung im Vergleich zu den gleichen Funktionen im Modul math bewusst sein:

In [45]: np.exp(a)  1
Out[45]: array([1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
                5.45981500e+01, 1.48413159e+02, 4.03428793e+02, 1.09663316e+03])

In [46]: np.sqrt(a)  2
Out[46]: array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ,
                2.23606798, 2.44948974, 2.64575131])

In [47]: np.sqrt(2.5)  3
Out[47]: 1.5811388300841898

In [48]: import math  4

In [49]: math.sqrt(2.5)  4
Out[49]: 1.5811388300841898

In [50]: math.sqrt(a)  5

         ---------------------------------------
         TypeErrorTraceback (most recent call last)
         <ipython-input-50-b39de4150838> in <module>()
         ----> 1 math.sqrt(a)  5

         TypeError: only size-1 arrays can be converted to Python scalars

In [51]: %timeit np.sqrt(2.5)  6
         722 ns ± 13.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops
          each)

In [52]: %timeit math.sqrt(2.5)  7
         91.8 ns ± 4.13 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops
          each)
1

Berechnet die Exponentialwerte elementweise.

2

Berechnet die Quadratwurzel für jedes Element.

3

Berechnet die Quadratwurzel für ein Python float Objekt.

4

Die gleiche Berechnung, diesmal mit dem Modul math.

5

Die Funktion math.sqrt() kann nicht direkt auf das Objekt ndarray angewendet werden.

6

Die Anwendung der universellen Funktion np.sqrt() auf ein Python float Objekt ...

7

... ist viel langsamer als der gleiche Vorgang mit der Funktion math.sqrt().

Mehrere Dimensionen

Der Übergang zu mehr als einer Dimension ist nahtlos, und alle bisher vorgestellten Merkmale lassen sich auf die allgemeineren Fälle übertragen. Insbesondere wird das Indexierungssystem über alle Dimensionen hinweg einheitlich gestaltet:

In [53]: b = np.array([a, a * 2])  1
         b
Out[53]: array([[ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.],
                [ 0.,  2.,  4.,  6.,  8., 10., 12., 14.]])

In [54]: b[0]  2
Out[54]: array([0., 1., 2., 3., 4., 5., 6., 7.])

In [55]: b[0, 2]  3
Out[55]: 2.0

In [56]: b[:, 1]  4
Out[56]: array([1., 2.])

In [57]: b.sum()  5
Out[57]: 84.0

In [58]: b.sum(axis=0)  6
Out[58]: array([ 0.,  3.,  6.,  9., 12., 15., 18., 21.])

In [59]: b.sum(axis=1)  7
Out[59]: array([28., 56.])
1

Konstruiert ein zweidimensionales ndarray Objekt aus dem eindimensionalen.

2

Wählt die erste Zeile aus.

3

Wählt das dritte Element in der ersten Zeile aus; die Indizes werden innerhalb der Klammern durch ein Komma getrennt.

4

Wählt die zweite Spalte aus.

5

Berechnet die Summe aller Werte.

6

Berechnet die Summe entlang der ersten Achse, d. h. spaltenweise.

7

Berechnet die Summe entlang der zweiten Achse, d. h. zeilenweise.

Es gibt mehrere Möglichkeiten, ndarray Objekte zu initialisieren (instanziieren). Eine davon ist, wie zuvor vorgestellt, über np.array. Dabei wird jedoch davon ausgegangen, dass alle Elemente des Arrays bereits vorhanden sind. Im Gegensatz dazu kann es sinnvoll sein, die ndarray Objekte zuerst zu instanziieren, um sie später mit den Ergebnissen zu füllen, die bei der Ausführung des Codes entstehen. Zu diesem Zweck kann man die folgenden Funktionen verwenden:

In [60]: c = np.zeros((2, 3), dtype='i', order='C')  1
         c
Out[60]: array([[0, 0, 0],
                [0, 0, 0]], dtype=int32)

In [61]: c = np.ones((2, 3, 4), dtype='i', order='C')  2
         c
Out[61]: array([[[1, 1, 1, 1],
                 [1, 1, 1, 1],
                 [1, 1, 1, 1]],

                [[1, 1, 1, 1],
                 [1, 1, 1, 1],
                 [1, 1, 1, 1]]], dtype=int32)

In [62]: d = np.zeros_like(c, dtype='f16', order='C')  3
         d
Out[62]: 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.]]], dtype=float128)

In [63]: d = np.ones_like(c, dtype='f16', order='C')  3
         d
Out[63]: array([[[1., 1., 1., 1.],
                 [1., 1., 1., 1.],
                 [1., 1., 1., 1.]],

                [[1., 1., 1., 1.],
                 [1., 1., 1., 1.],
                 [1., 1., 1., 1.]]], dtype=float128)

In [64]: e = np.empty((2, 3, 2))  4
         e
Out[64]: array([[[0.00000000e+000, 0.00000000e+000],
                 [0.00000000e+000, 0.00000000e+000],
                 [0.00000000e+000, 0.00000000e+000]],

                [[0.00000000e+000, 0.00000000e+000],
                 [0.00000000e+000, 7.49874326e+247],
                 [1.28822975e-231, 4.33190018e-311]]])

In [65]: f = np.empty_like(c)  4
         f
Out[65]: array([[[         0,          0,          0,          0],
                 [         0,          0,          0,          0],
                 [         0,          0,          0,          0]],

                [[         0,          0,          0,          0],
                 [         0,          0,  740455269, 1936028450],
                 [         0,  268435456, 1835316017,       2041]]], dtype=int32)

In [66]: np.eye(5)  5
Out[66]: array([[1., 0., 0., 0., 0.],
                [0., 1., 0., 0., 0.],
                [0., 0., 1., 0., 0.],
                [0., 0., 0., 1., 0.],
                [0., 0., 0., 0., 1.]])

In [67]: g = np.linspace(5, 15, 12) 6
         g
Out[67]: array([ 5.        ,  5.90909091,  6.81818182,  7.72727273,  8.63636364,
                 9.54545455, 10.45454545, 11.36363636, 12.27272727, 13.18181818,
                14.09090909, 15.        ])
1

Erzeugt ein ndarray Objekt, das mit Nullen vorausgefüllt ist.

2

Erzeugt ein ndarray Objekt, das mit Einsen vorausgefüllt ist.

3

Das Gleiche, aber man braucht ein anderes ndarray Objekt, um auf die Form zu schließen.

4

Erzeugt ein ndarray Objekt, das mit nichts vorausgefüllt ist (die Zahlen hängen von den im Speicher vorhandenen Bits ab).

5

Erzeugt eine quadratische Matrix als ndarray Objekt, wobei die Diagonale mit Einsen gefüllt ist.

6

Erzeugt ein eindimensionales ndarray Objekt mit gleichmäßigen Abständen zwischen den Zahlen; die verwendeten Parameter sind start, end und num (Anzahl der Elemente).

Für alle diese Funktionen kannst du die folgenden Parameter angeben:

shape

Entweder ein int, eine Folge von int Objekten oder ein Verweis auf ein anderes ndarray

dtype (optional)

A dtype-das sind NumPy-spezifische Datentypen für ndarray Objekte

order (optional)

Die Reihenfolge, in der die Elemente im Speicher abgelegt werden: C für C-ähnliche (d.h. zeilenweise) oder F für Fortran-ähnliche (d.h. spaltenweise)

Hier wird deutlich, wie NumPy die Konstruktion von Arrays mit der Klasse ndarray im Vergleich zum list -basierten Ansatz spezialisiert:

  • Das Objekt ndarray hat eingebaute Abmessungen (Achsen).

  • Das Objekt ndarray ist unveränderlich; seine Länge (Größe) ist fest.

  • Sie erlaubt nur einen einzigen Datentyp (np.dtype) für das gesamte Array.

Die Klasse array hat dagegen nur die Eigenschaft, einen einzigen Datentyp zuzulassen (Typcode, dtype).

Die Rolle des Parameters order wird später in diesem Kapitel besprochen. Tabelle 4-1 gibt einen Überblick über ausgewählte np.dtype Objekte (d. h. die grundlegenden Datentypen, die NumPy erlaubt).

Tabelle 4-1. NumPy dtype Objekte
dtype Beschreibung Beispiel

?

Boolesche

? (True oder False)

i

Ganzzahl mit Vorzeichen

i8 (64-bit)

u

Ganzzahl ohne Vorzeichen

u8 (64-bit)

f

Fließkomma

f8 (64-bit)

c

Komplexes Gleitkomma

c32 (256-bit)

m

timedelta

m (64-bit)

M

datetime

M (64-bit)

O

Objekt

O (Zeiger auf Objekt)

U

Unicode

U24 (24 Unicode-Zeichen)

V

Rohdaten (ungültig)

V12 (12-Byte-Datenblock)

Metainformationen

Jedes ndarray Objekt bietet Zugang zu einer Reihe von nützlichen Attributen:

In [68]: g.size  1
Out[68]: 12

In [69]: g.itemsize  2
Out[69]: 8

In [70]: g.ndim  3
Out[70]: 1

In [71]: g.shape  4
Out[71]: (12,)

In [72]: g.dtype  5
Out[72]: dtype('float64')

In [73]: g.nbytes  6
Out[73]: 96
1

Die Anzahl der Elemente.

2

Die Anzahl der Bytes, die zur Darstellung eines Elements verwendet werden.

3

Die Anzahl der Dimensionen.

4

Die Form des ndarray Objekts.

5

Die dtype der Elemente.

6

Die Gesamtzahl der im Speicher verwendeten Bytes.

Umformung und Größenanpassung

Obwohl ndarray Objekte standardmäßig unveränderlich sind, gibt es mehrere Möglichkeiten, ein solches Objekt umzugestalten und seine Größe zu ändern. Während die Umformung im Allgemeinen nur eine andere Sicht auf dieselben Daten ermöglicht, wird durch die Größenänderung im Allgemeinen ein neues (temporäres) Objekt erstellt. Zunächst einige Beispiele für die Umformung:

In [74]: g = np.arange(15)

In [75]: g
Out[75]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

In [76]: g.shape  1
Out[76]: (15,)

In [77]: np.shape(g) 1
Out[77]: (15,)

In [78]: g.reshape((3, 5))  2
Out[78]: array([[ 0,  1,  2,  3,  4],
                [ 5,  6,  7,  8,  9],
                [10, 11, 12, 13, 14]])

In [79]: h = g.reshape((5, 3))  3
         h
Out[79]: array([[ 0,  1,  2],
                [ 3,  4,  5],
                [ 6,  7,  8],
                [ 9, 10, 11],
                [12, 13, 14]])

In [80]: h.T  4
Out[80]: array([[ 0,  3,  6,  9, 12],
                [ 1,  4,  7, 10, 13],
                [ 2,  5,  8, 11, 14]])

In [81]: h.transpose()  4
Out[81]: array([[ 0,  3,  6,  9, 12],
                [ 1,  4,  7, 10, 13],
                [ 2,  5,  8, 11, 14]])
1

Die Form des ursprünglichen ndarray Objekts.

2

Umformung in zwei Dimensionen (Speicheransicht).

3

Erstellen eines neuen Objekts.

4

Die Transponierung des neuen ndarray Objekts.

Während eines Umformungsvorgangs bleibt die Gesamtzahl der Elemente im ndarray Objekt unverändert. Bei einer Größenänderung ändert sich diese Zahl - sie wird entweder verringert ("down-sizing") oder erhöht ("up-sizing"). Hier einige Beispiele für Größenänderungen:

In [82]: g
Out[82]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

In [83]: np.resize(g, (3, 1))  1
Out[83]: array([[0],
                [1],
                [2]])

In [84]: np.resize(g, (1, 5))  1
Out[84]: array([[0, 1, 2, 3, 4]])

In [85]: np.resize(g, (2, 5))  1
Out[85]: array([[0, 1, 2, 3, 4],
                [5, 6, 7, 8, 9]])

In [86]: n = np.resize(g, (5, 4))  2
         n
Out[86]: array([[ 0,  1,  2,  3],
                [ 4,  5,  6,  7],
                [ 8,  9, 10, 11],
                [12, 13, 14,  0],
                [ 1,  2,  3,  4]])
1

Zwei Dimensionen, Verkleinerung.

2

Zwei Dimensionen, Up-Sizing.

Stapeln ist eine spezielle Operation, die die horizontale oder vertikale Kombination von zwei ndarray Objekten ermöglicht. Allerdings muss die Größe der "verbindenden" Dimension gleich sein:

In [87]: h
Out[87]: array([[ 0,  1,  2],
                [ 3,  4,  5],
                [ 6,  7,  8],
                [ 9, 10, 11],
                [12, 13, 14]])

In [88]: np.hstack((h, 2 * h))  1
Out[88]: array([[ 0,  1,  2,  0,  2,  4],
                [ 3,  4,  5,  6,  8, 10],
                [ 6,  7,  8, 12, 14, 16],
                [ 9, 10, 11, 18, 20, 22],
                [12, 13, 14, 24, 26, 28]])

In [89]: np.vstack((h, 0.5 * h))  2
Out[89]: array([[ 0. ,  1. ,  2. ],
                [ 3. ,  4. ,  5. ],
                [ 6. ,  7. ,  8. ],
                [ 9. , 10. , 11. ],
                [12. , 13. , 14. ],
                [ 0. ,  0.5,  1. ],
                [ 1.5,  2. ,  2.5],
                [ 3. ,  3.5,  4. ],
                [ 4.5,  5. ,  5.5],
                [ 6. ,  6.5,  7. ]])
1

Horizontales Stapeln von zwei ndarray Objekten.

2

Vertikales Stapeln von zwei ndarray Objekten.

Eine weitere spezielle Operation ist die Verflachung eines mehrdimensionalen ndarray Objekts in ein eindimensionales Objekt. Man kann wählen, ob die Verflachung zeilenweise (C Reihenfolge) oder spaltenweise (F Reihenfolge) erfolgt:

In [90]: h
Out[90]: array([[ 0,  1,  2],
                [ 3,  4,  5],
                [ 6,  7,  8],
                [ 9, 10, 11],
                [12, 13, 14]])

In [91]: h.flatten()  1
Out[91]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

In [92]: h.flatten(order='C')  1
Out[92]: array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

In [93]: h.flatten(order='F')  2
Out[93]: array([ 0,  3,  6,  9, 12,  1,  4,  7, 10, 13,  2,  5,  8, 11, 14])

In [94]: for i in h.flat:  3
             print(i, end=',')
         0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
In [95]: for i in h.ravel(order='C'):  4
             print(i, end=',')
         0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,
In [96]: for i in h.ravel(order='F'):  4
             print(i, end=',')
         0,3,6,9,12,1,4,7,10,13,2,5,8,11,14,
1

Die Standardreihenfolge für die Verflachung ist C.

2

Abflachung mit F bestellen.

3

Das Attribut flat bietet einen flachen Iterator (C order).

4

Die Methode ravel() ist eine Alternative zu flatten().

Boolesche Arrays

Der Vergleich und logische Operationen funktionieren auf ndarray Objekten genauso elementweise wie auf Standard-Python-Datentypen. Die Auswertung von Bedingungen ergibt standardmäßig ein boolesches ndarray Objekt (dtype ist bool):

In [97]: h
Out[97]: array([[ 0,  1,  2],
                [ 3,  4,  5],
                [ 6,  7,  8],
                [ 9, 10, 11],
                [12, 13, 14]])

In [98]: h > 8  1
Out[98]: array([[False, False, False],
                [False, False, False],
                [False, False, False],
                [ True,  True,  True],
                [ True,  True,  True]])

In [99]: h <= 7  2
Out[99]: array([[ True,  True,  True],
                [ True,  True,  True],
                [ True,  True, False],
                [False, False, False],
                [False, False, False]])

In [100]: h == 5  3
Out[100]: array([[False, False, False],
                 [False, False,  True],
                 [False, False, False],
                 [False, False, False],
                 [False, False, False]])

In [101]: (h == 5).astype(int)  4
Out[101]: array([[0, 0, 0],
                 [0, 0, 1],
                 [0, 0, 0],
                 [0, 0, 0],
                 [0, 0, 0]])

In [102]: (h > 4) & (h <= 12)  5
Out[102]: array([[False, False, False],
                 [False, False,  True],
                 [ True,  True,  True],
                 [ True,  True,  True],
                 [ True, False, False]])
1

Ist der Wert größer als ...?

2

Ist der Wert kleiner oder gleich als ...?

3

Ist der Wert gleich ...?

4

Stelle True und False als ganzzahlige Werte 0 und 1 dar.

5

Ist der Wert größer als ... und kleiner als oder gleich ...?

Solche booleschen Arrays können zur Indizierung und Datenauswahl verwendet werden. Beachte, dass die folgenden Operationen die Daten glätten:

In [103]: h[h > 8]  1
Out[103]: array([ 9, 10, 11, 12, 13, 14])

In [104]: h[(h > 4) & (h <= 12)]  2
Out[104]: array([ 5,  6,  7,  8,  9, 10, 11, 12])

In [105]: h[(h < 4) | (h >= 12)]  3
Out[105]: array([ 0,  1,  2,  3, 12, 13, 14])
1

Gib mir alle Werte größer als ...

2

Gib mir alle Werte größer als ... und kleiner oder gleich ...

3

Gib mir alle Werte größer als ... oder kleiner oder gleich ...

Ein mächtiges Werkzeug in dieser Hinsicht ist die Funktion np.where(), die die Definition von Aktionen/Operationen ermöglicht, je nachdem, ob eine Bedingung True oder False ist. Das Ergebnis der Anwendung von np.where() ist ein neues ndarray Objekt, das die gleiche Form wie das ursprüngliche hat:

In [106]: np.where(h > 7, 1, 0)  1
Out[106]: array([[0, 0, 0],
                 [0, 0, 0],
                 [0, 0, 1],
                 [1, 1, 1],
                 [1, 1, 1]])

In [107]: np.where(h % 2 == 0, 'even', 'odd')  2
Out[107]: array([['even', 'odd', 'even'],
                 ['odd', 'even', 'odd'],
                 ['even', 'odd', 'even'],
                 ['odd', 'even', 'odd'],
                 ['even', 'odd', 'even']], dtype='<U4')

In [108]: np.where(h <= 7, h * 2, h / 2)  3
Out[108]: array([[ 0. ,  2. ,  4. ],
                 [ 6. ,  8. , 10. ],
                 [12. , 14. ,  4. ],
                 [ 4.5,  5. ,  5.5],
                 [ 6. ,  6.5,  7. ]])
1

Setze im neuen Objekt 1, wenn True und 0, wenn nicht.

2

Setze im neuen Objekt even, wenn True und odd, wenn nicht.

3

Setze im neuen Objekt das zweifache Element h, wenn True, und ansonsten die Hälfte des Elements h.

In späteren Kapiteln findest du weitere Beispiele für diese wichtigen Operationen an ndarray Objekten.

Geschwindigkeitsvergleich

Wir werden in Kürze zu strukturierten Arrays mit NumPy übergehen, aber lass uns für einen Moment bei normalen Arrays bleiben und sehen, was die Spezialisierung in Bezug auf die Leistung bringt.

Ein einfaches Beispiel ist die Erstellung einer Matrix/eines Arrays der Form 5.000 × 5.000 Elemente, die mit pseudozufälligen, normalverteilten Zahlen gefüllt sind. Anschließend soll die Summe aller Elemente berechnet werden. Zunächst der reine Python-Ansatz, bei dem die list comprehensions verwendet werden:

In [109]: import random
          I = 5000

In [110]: %time mat = [[random.gauss(0, 1) for j in range(I)] \
                       for i in range(I)]  1
          CPU times: user 17.1 s, sys: 361 ms, total: 17.4 s
          Wall time: 17.4 s

In [111]: mat[0][:5]  2
Out[111]: [-0.40594967782329183,
           -1.357757478015285,
           0.05129566894355976,
           -0.8958429976582192,
           0.6234174778878331]

In [112]: %time sum([sum(l) for l in mat])  3
          CPU times: user 142 ms, sys: 1.69 ms, total: 144 ms
          Wall time: 143 ms

Out[112]: -3561.944965714259

In [113]: import sys
          sum([sys.getsizeof(l) for l in mat])  4
Out[113]: 215200000
1

Die Erstellung der Matrix über eine verschachtelte list comprehension.

2

Einige wählten Zufallszahlen aus den gezogenen Zahlen aus.

3

Die Summen der einzelnen list Objekte werden zuerst während eines Listenverständnisses berechnet; dann wird die Summe der Summen genommen.

4

Dabei wird der Speicherverbrauch aller list Objekte addiert.

Wenden wir uns nun NumPy zu und sehen wir uns an, wie das gleiche Problem dort gelöst wird. Der Einfachheit halber bietet das Unterpaket NumPy random eine Vielzahl von Funktionen, um ein ndarray Objekt zu instanziieren und es gleichzeitig mit Pseudo-Zufallszahlen zu füllen:

In [114]: %time mat = np.random.standard_normal((I, I))  1
          CPU times: user 1.01 s, sys: 200 ms, total: 1.21 s
          Wall time: 1.21 s

In [115]: %time mat.sum()  2
          CPU times: user 29.7 ms, sys: 1.15 ms, total: 30.8 ms
          Wall time: 29.4 ms

Out[115]: -186.12767026606448

In [116]: mat.nbytes  3
Out[116]: 200000000

In [117]: sys.getsizeof(mat)  3
Out[117]: 200000112
1

Erzeugt das Objekt ndarray mit standardmäßig normalverteilten Zufallszahlen; es ist etwa um den Faktor 14 schneller.

2

Berechnet die Summe aller Werte im Objekt ndarray; es ist um den Faktor 4,5 schneller.

3

Der NumPy Ansatz spart auch etwas Speicherplatz, da der Speicher-Overhead des ndarray Objekts im Vergleich zur Größe der Daten selbst winzig ist.

NumPy-Arrays verwenden

Die Verwendung von NumPy für Array-basierte Operationen und Algorithmen führt in der Regel zu kompaktem, leicht lesbarem Code und deutlichen Leistungssteigerungen gegenüber reinem Python-Code.

Strukturierte NumPy-Arrays

Die Spezialisierung der Klasse ndarray bringt natürlich eine Reihe von wertvollen Vorteilen mit sich. Eine zu enge Spezialisierung könnte sich jedoch für die meisten array-basierten Algorithmen und Anwendungen als zu große Last erweisen. Deshalb bietet NumPy strukturierte ndarray und Record recarray Objekte, die es dir ermöglichen, für jede Spalte ein anderes dtype zu haben. Was bedeutet "pro Spalte"? Betrachte die folgende Initialisierung eines strukturierten ndarray Objekts:

In [118]: dt = np.dtype([('Name', 'S10'), ('Age', 'i4'),
                         ('Height', 'f'), ('Children/Pets', 'i4', 2)])  1

In [119]: dt  1
Out[119]: dtype([('Name', 'S10'), ('Age', '<i4'), ('Height', '<f4'),
           ('Children/Pets', '<i4', (2,))])

In [120]: dt = np.dtype({'names': ['Name', 'Age', 'Height', 'Children/Pets'],
                       'formats':'O int float int,int'.split()})  2

In [121]: dt  2
Out[121]: dtype([('Name', 'O'), ('Age', '<i8'), ('Height', '<f8'),
           ('Children/Pets', [('f0', '<i8'), ('f1', '<i8')])])

In [122]: s = np.array([('Smith', 45, 1.83, (0, 1)),
                        ('Jones', 53, 1.72, (2, 2))], dtype=dt)  3

In [123]: s  3
Out[123]: array([('Smith', 45, 1.83, (0, 1)), ('Jones', 53, 1.72, (2, 2))],
          dtype=[('Name', 'O'), ('Age', '<i8'), ('Height', '<f8'),
           ('Children/Pets', [('f0', '<i8'), ('f1', '<i8')])])

In [124]: type(s)  4
Out[124]: numpy.ndarray
1

Der Komplex dtype besteht aus.

2

Eine alternative Syntax, um das gleiche Ergebnis zu erzielen.

3

Die strukturierte ndarray wird mit zwei Datensätzen instanziiert.

4

Der Objekttyp ist immer noch ndarray.

In gewisser Weise kommt diese Konstruktion dem Vorgang der Initialisierung von Tabellen in einer SQL-Datenbank recht nahe: Man hat Spaltennamen und Spaltendatentypen, vielleicht mit einigen zusätzlichen Informationen (z. B. die maximale Anzahl von Zeichen pro str Objekt). Auf die einzelnen Spalten kann nun einfach über ihre Namen und auf die Zeilen über ihre Indexwerte zugegriffen werden:

In [125]: s['Name']  1
Out[125]: array(['Smith', 'Jones'], dtype=object)

In [126]: s['Height'].mean()  2
Out[126]: 1.775

In [127]: s[0]  3
Out[127]: ('Smith', 45, 1.83, (0, 1))

In [128]: s[1]['Age']  4
Out[128]: 53
1

Auswählen einer Spalte nach Name.

2

Aufrufen einer Methode für eine ausgewählte Spalte.

3

Auswählen eines Datensatzes.

4

Auswählen eines Feldes in einem Datensatz.

Zusammenfassend lässt sich sagen, dass strukturierte Arrays eine Verallgemeinerung des regulären ndarray Objekttyps sind, da der Datentyp nur pro Spalte gleich sein muss, wie bei Tabellen in SQL-Datenbanken. Ein Vorteil von strukturierten Arrays ist, dass ein einzelnes Element einer Spalte ein anderes multidimensionales Objekt sein kann und nicht den grundlegenden NumPy Datentypen entsprechen muss.

Strukturierte Arrays

NumPy bietet zusätzlich zu regulären Arrays strukturierte (und Record-)Arrays, die die Beschreibung und Handhabung von tabellenähnlichen Datenstrukturen mit einer Vielzahl verschiedener Datentypen pro (benannter) Spalte ermöglichen. Sie bringen SQL-tabellenähnliche Datenstrukturen nach Python, mit den meisten Vorteilen der regulären ndarray Objekte (Syntax, Methoden, Leistung).

Vektorisierung des Codes

Vektorisierung ist eine Strategie, um kompakteren Code zu erhalten, der möglicherweise schneller ausgeführt wird. Die Grundidee besteht darin, eine Operation auf ein komplexes Objekt durchzuführen oder eine Funktion auf ein komplexes Objekt "auf einmal" anzuwenden und nicht in einer Schleife über die einzelnen Elemente des Objekts. In Python bieten funktionale Programmierwerkzeuge wie map() und filter() einige grundlegende Möglichkeiten zur Vektorisierung. NumPy hat die Vektorisierung jedoch tief in seinem Kern eingebaut.

Grundlegende Vektorisierung

Wie im vorherigen Abschnitt gezeigt wurde, können einfache mathematische Operationen - wie die Berechnung der Summe aller Elemente - direkt auf ndarray Objekten implementiert werden (über Methoden oder universelle Funktionen). Auch allgemeinere vektorisierte Operationen sind möglich. Zum Beispiel kann man zwei NumPy Arrays wie folgt elementweise addieren:

In [129]: np.random.seed(100)
          r = np.arange(12).reshape((4, 3))  1
          s = np.arange(12).reshape((4, 3)) * 0.5  2

In [130]: r  1
Out[130]: array([[ 0,  1,  2],
                 [ 3,  4,  5],
                 [ 6,  7,  8],
                 [ 9, 10, 11]])

In [131]: s  2
Out[131]: array([[0. , 0.5, 1. ],
                 [1.5, 2. , 2.5],
                 [3. , 3.5, 4. ],
                 [4.5, 5. , 5.5]])

In [132]: r + s  3
Out[132]: array([[ 0. ,  1.5,  3. ],
                 [ 4.5,  6. ,  7.5],
                 [ 9. , 10.5, 12. ],
                 [13.5, 15. , 16.5]])
1

Das erste ndarray Objekt mit Zufallszahlen.

2

Das zweite ndarray Objekt mit Zufallszahlen.

3

Elementweise Addition als vektorisierte Operation (keine Schleifenbildung).

NumPy unterstützt auch das so genannte Broadcasting. Damit kannst du Objekte mit unterschiedlichen Formen in einem einzigen Vorgang kombinieren. Die vorherigen Beispiele haben davon bereits Gebrauch gemacht. Betrachte die folgenden Beispiele:

In [133]: r + 3  1
Out[133]: array([[ 3,  4,  5],
                 [ 6,  7,  8],
                 [ 9, 10, 11],
                 [12, 13, 14]])

In [134]: 2 * r  2
Out[134]: array([[ 0,  2,  4],
                 [ 6,  8, 10],
                 [12, 14, 16],
                 [18, 20, 22]])

In [135]: 2 * r + 3  3
Out[135]: array([[ 3,  5,  7],
                 [ 9, 11, 13],
                 [15, 17, 19],
                 [21, 23, 25]])
1

Bei der Skalaraddition wird der Skalar übertragen und zu jedem Element hinzugefügt.

2

Bei der Skalarmultiplikation wird der Skalar auch an jedes Element gesendet und mit ihm multipliziert.

3

Diese lineare Transformation kombiniert beide Operationen.

Diese Vorgänge funktionieren bis zu einem gewissen Punkt auch mit anders geformten ndarray Objekten:

In [136]: r
Out[136]: array([[ 0,  1,  2],
                 [ 3,  4,  5],
                 [ 6,  7,  8],
                 [ 9, 10, 11]])

In [137]: r.shape
Out[137]: (4, 3)

In [138]: s = np.arange(0, 12, 4)  1
          s  1
Out[138]: array([0, 4, 8])

In [139]: r + s  2
Out[139]: array([[ 0,  5, 10],
                 [ 3,  8, 13],
                 [ 6, 11, 16],
                 [ 9, 14, 19]])

In [140]: s = np.arange(0, 12, 3)  3
          s  3
Out[140]: array([0, 3, 6, 9])

In [141]: r + s  4

          ---------------------------------------
          ValueErrorTraceback (most recent call last)
          <ipython-input-141-1890b26ec965> in <module>()
          ----> 1 r + s  4

          ValueError: operands could not be broadcast together
                      with shapes (4,3) (4,)

In [142]: r.transpose() + s  5
Out[142]: array([[ 0,  6, 12, 18],
                 [ 1,  7, 13, 19],
                 [ 2,  8, 14, 20]])

In [143]: sr = s.reshape(-1, 1)  6
          sr
Out[143]: array([[0],
                 [3],
                 [6],
                 [9]])

In [144]: sr.shape  6
Out[144]: (4, 1)

In [145]: r + s.reshape(-1, 1)  6
Out[145]: array([[ 0,  1,  2],
                 [ 6,  7,  8],
                 [12, 13, 14],
                 [18, 19, 20]])
1

Ein neues eindimensionales ndarray Objekt der Länge 3.

2

Die Objekte r (Matrix) und s (Vektor) können ganz einfach hinzugefügt werden.

3

Ein weiteres eindimensionales ndarray Objekt der Länge 4.

4

Die Länge des neuen s (Vektor)-Objekts unterscheidet sich nun von der Länge der zweiten Dimension des r Objekts.

5

Das erneute Transponieren des r Objekts ermöglicht die vektorisierte Addition.

6

Alternativ kann die Form von s in (4, 1) geändert werden, damit die Addition funktioniert (die Ergebnisse sind dann allerdings anders).

Oft funktionieren benutzerdefinierte Python-Funktionen auch mit ndarray Objekten. Wenn die Implementierung es zulässt, können Arrays genauso mit Funktionen verwendet werden wie int oder float Objekte. Betrachte die folgende Funktion:

In [146]: def f(x):
              return 3 * x + 5  1

In [147]: f(0.5)  2
Out[147]: 6.5

In [148]: f(r)  3
Out[148]: array([[ 5,  8, 11],
                 [14, 17, 20],
                 [23, 26, 29],
                 [32, 35, 38]])
1

Eine einfache Python-Funktion, die eine lineare Transformation des Parameters x durchführt.

2

Die Funktion f() wird auf ein Python float Objekt angewendet.

3

Dieselbe Funktion wird auf ein ndarray Objekt angewendet, was zu einer vektorisierten und elementweisen Auswertung der Funktion führt.

NumPy wendet einfach die Funktion f auf das Objekt an, und zwar elementweise. In diesem Sinne vermeidet man mit dieser Art von Operation keine Schleifen; man vermeidet sie nur auf der Python-Ebene und delegiert die Schleifenbildung an NumPy. Auf der Ebene NumPy wird die Schleifenbildung über das ndarray Objekt von optimiertem Code übernommen, der größtenteils in C geschrieben wurde und daher im Allgemeinen schneller ist als reines Python. Das erklärt den "secret", der hinter den Leistungsvorteilen der Verwendung von NumPy für array-basierte Anwendungsfälle steckt.

Speicher-Layout

Wenn ndarray Objekte durch die Verwendung von np.zeros() initialisiert werden, wie in "Multiple Dimensions", wird ein optionales Argument für die Speicheranordnung angegeben. Dieses Argument legt, grob gesagt, fest, welche Elemente eines Arrays nebeneinander (zusammenhängend) im Speicher abgelegt werden. Bei kleinen Arrays hat dies kaum messbare Auswirkungen auf die Leistung von Array-Operationen. Bei großen Arrays und je nach (Finanz-)Algorithmus, der darauf angewendet werden soll, sieht die Sache jedoch anders aus. Hier kommt das Speicherlayout ins Spiel (siehe z. B. Eli Benderskys Artikel "Memory Layout of Multi-Dimensional Arrays").

Um die potenzielle Bedeutung des Speicherlayouts von Arrays in der Wissenschaft und im Finanzwesen zu veranschaulichen, betrachte die folgende Konstruktion von mehrdimensionalen ndarray Objekten:

In [149]: x = np.random.standard_normal((1000000, 5))  1

In [150]: y = 2 * x + 3  2

In [151]: C = np.array((x, y), order='C')  3

In [152]: F = np.array((x, y), order='F')  4

In [153]: x = 0.0; y = 0.0  5

In [154]: C[:2].round(2)  6
Out[154]: array([[[-1.75,  0.34,  1.15, -0.25,  0.98],
                  [ 0.51,  0.22, -1.07, -0.19,  0.26],
                  [-0.46,  0.44, -0.58,  0.82,  0.67],
                  ...,
                  [-0.05,  0.14,  0.17,  0.33,  1.39],
                  [ 1.02,  0.3 , -1.23, -0.68, -0.87],
                  [ 0.83, -0.73,  1.03,  0.34, -0.46]],

                 [[-0.5 ,  3.69,  5.31,  2.5 ,  4.96],
                  [ 4.03,  3.44,  0.86,  2.62,  3.51],
                  [ 2.08,  3.87,  1.83,  4.63,  4.35],
                  ...,
                  [ 2.9 ,  3.28,  3.33,  3.67,  5.78],
                  [ 5.04,  3.6 ,  0.54,  1.65,  1.26],
                  [ 4.67,  1.54,  5.06,  3.69,  2.07]]])
1

Ein ndarray Objekt mit großer Asymmetrie in den beiden Dimensionen.

2

Eine lineare Transformation der ursprünglichen Objektdaten.

3

Dadurch wird ein zweidimensionales ndarray Objekt mit der Reihenfolge C (zeilen-major) erstellt.

4

Dadurch wird ein zweidimensionales ndarray Objekt mit der Reihenfolge F (column-major) erstellt.

5

Der Speicher wird freigegeben (abhängig von der Speicherbereinigung).

6

Einige Zahlen aus dem C Objekt.

Schauen wir uns einige grundlegende Beispiele und Anwendungsfälle für beide Arten von ndarray Objekten an und betrachten die Geschwindigkeit, mit der sie angesichts der unterschiedlichen Speicheraufteilung ausgeführt werden:

In [155]: %timeit C.sum()  1
          4.36 ms ± 89.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [156]: %timeit F.sum()  1
          4.21 ms ± 71.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [157]: %timeit C.sum(axis=0)  2
          17.9 ms ± 776 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [158]: %timeit C.sum(axis=1)  3
          35.1 ms ± 999 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [159]: %timeit F.sum(axis=0)  2
          83.8 ms ± 2.63 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [160]: %timeit F.sum(axis=1)  3
          67.9 ms ± 5.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [161]: F = 0.0; C = 0.0
1

Berechnet die Summe aller Elemente.

2

Berechnet die Summen pro Zeile ("many").

3

Berechnet die Summen pro Spalte ("few").

Wir können die Leistungsergebnisse wie folgt zusammenfassen:

  • Wenn du die Summe aller Elemente berechnest, spielt die Speicheranordnung keine Rolle.

  • Das Aufsummieren über die C-geordneten ndarray Objekte ist sowohl über Zeilen als auch über Spalten schneller (ein absoluter Geschwindigkeitsvorteil).

  • Mit dem Objekt C-ordered (row-major) ndarray ist das Aufsummieren über Zeilen relativ schneller als das Aufsummieren über Spalten.

  • Mit dem F-ordered (column-major) ndarray Objekt ist das Aufsummieren über Spalten relativ schneller als das Aufsummieren über Zeilen.

Fazit

NumPy ist das Paket der Wahl für numerische Berechnungen in Python. Die Klasse ndarray wurde speziell für den bequemen und effizienten Umgang mit (großen) numerischen Daten entwickelt. Leistungsstarke Methoden und die universellen Funktionen von NumPy ermöglichen einen vektorisierten Code, der langsame Schleifen auf Python-Ebene weitgehend vermeidet. Viele der in diesem Kapitel vorgestellten Ansätze lassen sich auch auf pandas und die dazugehörige Klasse DataFrame übertragen (siehe Kapitel 5).

Weitere Ressourcen

Viele hilfreiche Ressourcen findest du auf der Website NumPy:

Gute Einführungen in NumPy in Buchform sind:

  • McKinney, Wes (2017). Python für die Datenanalyse. Sebastopol, CA: O'Reilly.

  • VanderPlas, Jake (2016). Python Data Science Handbook. Sebastopol, CA: O'Reilly.

Get Python für Finanzen, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.