Numpy Tutorial



Einführung

Visualisierung einer Matrix als Hinton-Diagram

NumPy ist ein Akronym für "Numeric Python" oder "Numerical Python". Bei diesem Modul handelt es sich um eine OpenSource Erweiterung für Python, die schnelle vorkompilierte Funktionen für mathematische und numerische Routinen bereitstellt. Außerdem bereichert NumPy die Programmiersprache Python um mächtige Datenstrukturen für das effiziente Rechnen mit großen Arrays und Matrizen. Die Implementierung zielt sogar auf extrem große ("big data") Matrizen und Arrays. Ferner bietet das Modul eine riesige Anzahl von hochwertigen mathematischen Funktionen, um mit diesen Matrizen und Arrays zu bearbeiten.

SciPy (Scientific Python) wird oft im gleichen Atemzug wie NumPy genannt. Scipy erweitert die Leistungsfähigkeit von NumPy um weitere nützliche Funktionen, wie zum Beispiel Minimierung, Regression, Fouriertransformation und vielen anderen.

Sowohl NumPy als auch SciPy sind üblicherweise bei einer Standarinstallation von Python nicht installiert. NumPy muss vor SciPy installiert werden. NumPy kann von folgender Webseite heruntergelanden werden:

http://www.numpy.org

(Kommentar: Das Diagramm im Bild auf der rechten Seite ist eine grafische Visualisierung einer Matrix mit 14 Reihen und 20 Spalten. Es handelt sich um ein sogenanntes Hinton-Diagramm. Die Größe eines Quadrates innerhalb dieses Diagrammes korrespondiert zu der Größe des entsprechenden Wertes in der darzustellenden Matrix. Die Farbe bestimmt dabei, ob es sich um einen positiven oder negativen Wert handelt. In unserem Beispiel: Die Farbe Rot bezeichnet die negativen Werte und die Farbe Grün bezeichnet die positiven Werte.)

NumPy basiert auf zwei früheren Python-Modulen, die mit Arrays zu tun hatten. Eines von diesen ist Numeric. Numeric ist wie Numpy ein Python-Modul für leistungsstarke numerische Berechnungen, aber es ist heute überholt. Ein anderer Vorgänger von NumPy ist Numarray, bei dem es sich um eine vollständige Überarbeitung von Numeric handelt, aber auch dieses Modul ist heute veraltet. NumPy ist die Verschmelzung dieser beiden, d.h. es ist auf dem Code von Numeric und den Funktionalitäten von Numarray aufgebaut.



Die Python-Alternative zu MATLAB

Python in Kombination mit Numpy, Scipy und Matplotlib kann als vollwertiger Ersatz für MATLAB genutzt werden. Bei Python und seinen Modulen handelt es sich um freie Software ("free Software" oder "open source"), frei steht hier im Sinne von "Frei"heit und nicht von "Frei"bier, auch wenn Python kostenlos ist.

Obwohl für MATLAB eine riesige Anzahl von zusätzlichen Toolboxen verfügbar sind, hat Python in Verbindung mit oben erwähnten Modulen den Vorteil, dass es sich bei Python um die modernere und umfassendere Programmiersprache handelt.

Vergleich zwischen Python und Matlab



Vergleich zwischen Kern-Python und Numpy

Wenn wir von Kern-Python (engl. "Core Python") sprechen, dann meinen wir das reine Python ohne seine speziellen Module, also in unserem Fall NumPy.

Die Vorteile von Kern-Python:

Vorteile von NumPy gegenüber Python:


Ein einfaches Numpy-Beispiel

Bevor wir Numpy benutzen können, müssen wir es importieren. Es wird importiert wie jedes andere Modul auch:

import numpy

Die obige import-Anweisung wird man aber nur sehr selten zu sehen bekommen. Üblicherweise wird Numpy in np umbenannt:

import numpy as np

Wir haben eine Liste mit Werten, zum Beispiel Temperaturen in Celsius:

cvalues = [20.1, 20.8, 21.9, 22.5, 22.7, 22.3, 21.8, 21.2, 20.9, 20.1]

Aus unserer Liste "cvalues" erzeugen wir nun ein eindimensionales Numpy-Array:

C = np.array(cvalues)
print(C)
[ 20.1  20.8  21.9  22.5  22.7  22.3  21.8  21.2  20.9  20.1]

Nehmen wir nun an, dass wir die Werte in Grad Fahrenheit benötigen. Dies kann sehr einfach mit einem NumPy-Array bewerkstelligt werden. Die Lösung unseres Problems besteht in einfachen skalaren Operationen:

print(C * 9 / 5 + 32)
[ 68.18  69.44  71.42  72.5   72.86  72.14  71.24  70.16  69.62  68.18]

Das Array C selbst wurde dabei jedoch nicht verändert:

print(C)
[ 20.1  20.8  21.9  22.5  22.7  22.3  21.8  21.2  20.9  20.1]

Verglichen zu diesem Vorgehen stellt sich die Python-Lösung mit Listen umständlicher dar:

fvalues = [ x*9/5 + 32 for x in cvalues] 
print(fvalues)
[68.18, 69.44, 71.42, 72.5, 72.86, 72.14, 71.24000000000001, 70.16, 69.62, 68.18]

Wir haben bisher C als ein Array bezeichnet. Die interne Typbezeichung bedeutet "ndarray" oder noch genauer "C ist eine Instanz der Klasse numpy.ndarray":

type(C)
Wir können die folgenden Ergebnisse erwarten, wenn wir den obigen Python-Code ausführen:
numpy.ndarray

Im folgenden werden wir die Begriffe "Array" und "ndarray" meistens synonym verwenden.



Grafische Darstellung der Werte

Obwohl wir das Modul matplotlib erst später im Detail besprechen werden, wollen wir zeigen, wie wir mit diesem Modul die obigen Temperaturwerte ausgeben können. Dazu benutzen wir das Paket pyplot aus matplotlib. Wenn man mit dem Jupyter-Notebook arbeitet, empfiehlt es sich die folgende Codezeile zu verwenden, damit der Plot nicht als separates Fenster erzeugt wird:

%matplotlib inline

Der Code zum Erzeugen eines Plots für unsere Werte sieht wie folgt aus:

import matplotlib.pyplot as plt
plt.plot(C)
plt.show()

Die Funktion "plot" hat das Array C als Werte für die Ordinate, also die Y-Achse übernommen. Als Werte für die Abszisse wurden die Indizes de Arrays C genommen.



Speicherbedarf

Der wesentliche Vorteil von Numpy-Arrays sollte ein kleinerer Speicherverbrauch und ein besseres Laufzeitverhalten sein. Wir wollen uns den Speicherverbrauch von Numpy-Arrays in diesem Kapitel unseres Tutorials anschauen und ihn mit dem Speicherverbrauch von Python-Listen vergleichen.

Python Listen: Interne Speicherstruktur

Um den Speicherverbrauch der Liste aus dem vorigen Bild zu berechnen, werden wir die Funktion "getsizeof" aus dem Modul "sys" benutzen.

from sys import getsizeof as size
lst = [24, 12, 57]
size_of_list_object = size(lst)   # only green box
size_of_elements = len(lst) * size(lst[0]) # 24, 12, 57
total_list_size = size_of_list_object + size_of_elements
print("Größe ohne Größe der Elemente: ", size_of_list_object)
print("Größe aller Elemente: ", size_of_elements)
print("Gesamtgröße der Liste: ", total_list_size)
Größe ohne Größe der Elemente:  88
Größe aller Elemente:  84
Gesamtgröße der Liste:  172

Der Speicherbedarf einer Python-Liste besteht aus der Größe der allgemeinen Listeninformation, dem Speicherbedarf für die Referenzen auf die Listenelemente und der Größe aller Elemente der Liste. Wenn wir sys.getsizeof auf eine Liste anwenden, erhalten wir nur den Speicherbedarf ohne die Größe der Listenelemente. Im obigen Beispiel sind wir davon ausgegangen, dass alle Integerelemente unserer Liste die gleiche Größe haben. Dies stimmt natürlich nicht im allgemeinen Fall, da Integer bei steigender Größe auch einen größeren Speicherbedarf haben.

We will check now, how the memory usage changes, if we add another integer element to the list. We also look at an empty list:

lst = [24, 12, 57, 42]
size_of_list_object = size(lst)   # only green box
size_of_elements = len(lst) * size(lst[0]) # 24, 12, 57, 42
total_list_size = size_of_list_object + size_of_elements
print("Größe ohne Größe der Elemente: ", size_of_list_object)
print("Größe aller Elemente: ", size_of_elements)
print("Gesamtgröße der Liste: ", total_list_size)
 
lst = []
print("Speicherbedarf einer leeren Liste: ", size(lst))
Größe ohne Größe der Elemente:  96
Größe aller Elemente:  112
Gesamtgröße der Liste:  208
Speicherbedarf einer leeren Liste:  64

Aus dem vorigen Code können wir folgern, dass wir für jedes Integer-Element 8 Bytes für die Referenz benötigen. Ein Integer-Objekt selbst benötigt in unserem Fall 28 Bytes. Die Größe der Liste "lst" ohne den Speicherbedarf für die Elemente selbst kann also wie folgt berechnet werden:

64 + 8 * len(lst)

Um den kompletten Speicherbedarf einer Integerliste auszurechnen, müssen wir noch den Speicherbedarf aller Integer hinzuaddieren.

Nun werden wir den Speicherbedarf eines Numpy-Arrays berechnen. Zu diesem Zweck schauen wir uns zunächst die Implementierung im folgenden Bild an:

Numpy-Arrays: Interne Speicherstruktur

Wir erzeugen nun das Array aus dem vorigen Bild und berechnen seinen Speicherbedarf:

a = np.array([24, 12, 57])
print(size(a))
120

Den Speicherbedarf für die allgemeine Array-Information können wir berechnen, indem wir ein leeres Array erzeugen:

e = np.array([])
print(size(e))
96

Wir können sehen, dass die Differenz zwischen dem leeren Array "e" und dem Array "a", bestehend aus 3 Integern, 24 Bytes beträgt. Dies bedeutet dass sich der Speicherbedarf für ein beliebiges Integer-Array "n" wir folgt ergibt:

96 + n * 8 Bytes

Im vergleich dazu berechnet sich der Speicherbedarf einer Integerlist, wie wir gesehen haben als:

64 + 8 len(lst) + len(lst) * 28

Dies ist ein untere Schranke, da Python-Integer größer als 29 Bytes werden können!

Wenn wir ein Numpy-Array definieren, wählt numpy automatisch eine feste Integergröße, in unserem Fall "int64".

Diese Größe können wir auch bei der Definition eines Arrays festlegen. Damit ändert sich natürlich auch der Gesamtspeicherbedarf des Arrays:

a = np.array([24, 12, 57], np.int8)
print(size(a) - 96)
a = np.array([24, 12, 57], np.int16)
print(size(a) - 96)
a = np.array([24, 12, 57], np.int32)
print(size(a) - 96)
a = np.array([24, 12, 57], np.int64)
print(size(a) - 96)
3
6
12
24
import numpy as np
samples, spacing = np.linspace(1, 10, 
                               retstep=True)
print(spacing)
samples, spacing = np.linspace(1, 10, 20, 
                               endpoint=True, 
                               retstep=True)
print(spacing)
samples, spacing = np.linspace(1, 10, 20, 
                               endpoint=False, 
                               retstep=True)
print(spacing)
0.1836734693877551
0.47368421052631576
0.45

Zeitvergleich zwischen Python-Listen und Numpy-Arrays

Einer der Hauptvorteile von NumPy ist sein Zeitvorteil gegenüber Standardpython. Schauen wir uns die folgenden Funktionen an:

import time
size_of_vec = 1000
def pure_python_version():
    t1 = time.time()
    X = range(size_of_vec)
    Y = range(size_of_vec)
    Z = []
    for i in range(len(X)):
        Z.append(X[i] + Y[i])
    return time.time() - t1
def numpy_version():
    t1 = time.time()
    X = np.arange(size_of_vec)
    Y = np.arange(size_of_vec)
    Z = X + Y
    return time.time() - t1

Wir rufen diese Funktionen auf und können den Zeitvorteil sehen:

t1 = pure_python_version()
t2 = numpy_version()
print(t1, t2)
print("Numpy is in this example " + str(t1/t2) + " faster!")
0.0003075599670410156 6.723403930664062e-05
Numpy is in this example 4.574468085106383 faster!

Die Zeitmessung gestaltet sich einfacher und vor allen Dingen besser, wenn wir dazu das Modul timeit verwenden. In dem folgenden Skript werden wir die Timer-Klasse nutzen.

Dem Konstruktor eines Timer-Objektes können zwei Anweisungen übergeben werden: eine die gemessen werden soll und eine, die als Setup fungiert. Beide Anweisungen sind auf 'pass' per Default gesetzt. Ansonsten kann noch eine Timer-Funktion übergeben werden.

Ein Timer-Objekt hat eine timeit-Methode. Das Argument der timeit-Methode ist die Anzahl der Schleifendruchläufe, die der Code wiederholt werden soll.

timeit(number=1000000)

timeit liefert als Ergebnis die benötigte Zeit für "number"-Durchläufe.

import numpy as np
from timeit import Timer
size_of_vec = 1000
def pure_python_version():
    X = range(size_of_vec)
    Y = range(size_of_vec)
    Z = []
    for i in range(len(X)):
        Z.append(X[i] + Y[i])
def numpy_version():
    X = np.arange(size_of_vec)
    Y = np.arange(size_of_vec)
    Z = X + Y
#timer_obj = Timer("x = x + 1", "x = 0")
timer_obj1 = Timer("pure_python_version()", 
                   "from __main__ import pure_python_version")
timer_obj2 = Timer("numpy_version()", 
                   "from __main__ import numpy_version")
print(timer_obj1.timeit(10))
print(timer_obj2.timeit(10))
0.0036315619945526123
0.00044670194620266557

Die repeat-Method ist eine vereinfachte Möglichkeit die Methode timeit mehrmals aufzurufen und eine Liste der Ergebnisse zu erhalten:

print(timer_obj1.repeat(repeat=3, number=10))
print(timer_obj2.repeat(repeat=3, number=10))
[0.004898615996353328, 0.002878547995351255, 0.0029337090090848505]
[0.0004389690002426505, 8.19870037958026e-05, 4.942499799653888e-05]