import numpy as np
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.
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.
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
:
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:
= np.arange(15) # 15 Zahlen von 0 bis 14 a
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):
# 15 Elemente in einer Dimension (Achse) a.shape
(15,)
Diese Form kann man auch verändern:
= a.reshape(3, 5) # umwandeln in 3 Zeilen und 5 Spalten a
# 2 Dimensionen (Achsen) mit Längen 3 bzw. 5 a
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
# Anzahl an Dimensionen (Achsen) a.ndim
2
# Länge der einzelnen Achsen a.shape
(3, 5)
Unabhängig von der Form kann man auch die Gesamtanzahl der Elemente im Array bestimmen:
# Anzahl aller Elemente im Array a.size
15
NumPy-Arrays sind homogene Datentypen, d.h. alle Elemente im Array müssen denselben Typ haben. Diesen Typ kann man wie folgt bestimmen:
# Datentyp aller Elemente im Array (64 bit Ganzzahlen) a.dtype
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):
= np.array([1.1, 3.14, 7.68, -12.69, -4.55]) # aus einer normalen Liste
b 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:
= np.array([[1, 2, 3], [4, 5, 6]]) # aus einer Liste von Listen
c c
array([[1, 2, 3],
[4, 5, 6]])
# 2 Zeilen, 3 Spalten c.shape
(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.
3, 4)) # Nullen np.zeros((
array([[0., 0., 0., 0.],
[0., 0., 0., 0.],
[0., 0., 0., 0.]])
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.
2, 4)) # Einsen np.ones((
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:
3, 4), 66) # 3 Zeilen, 4 Spalten, alle Elemente gleich 66 np.full((
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.
5, 17) np.arange(
array([ 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])
0.3, 5.4, 0.6) # von 0.3 bis 5.4 mit Schrittweite 0.6 np.arange(
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):
1, 10, 10) # 10 Elemente von 1 bis 10 np.linspace(
array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.])
1, 10, 10, dtype=int) # wie oben, nur Integer-Elemente np.linspace(
array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
1, 10, 24) # 24 Elemente von 1 bis 10 np.linspace(
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
:
0, 4, 8) # 8 Werte von 10**0 bis 10**4 np.logspace(
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
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:
= default_rng(seed=42) rng
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:
10) rng.standard_normal(
array([ 0.30471708, -1.03998411, 0.7504512 , 0.94056472, -1.95103519,
-1.30217951, 0.1278404 , -0.31624259, -0.01680116, -0.85304393])
=(2, 2)) rng.uniform(size
array([[0.37079802, 0.92676499],
[0.64386512, 0.82276161]])
=-3, high=99, size=(2, 5)) rng.integers(low
array([[52, 42, 42, 20, 6],
[53, 87, 3, 84, 81]])
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.
= np.arange(100, 700, 100).reshape((2, 3))
a = np.arange(1, 7).reshape((2, 3)) b
a
array([[100, 200, 300],
[400, 500, 600]])
b
array([[1, 2, 3],
[4, 5, 6]])
* 2 a
array([[ 200, 400, 600],
[ 800, 1000, 1200]])
+ b a
array([[101, 202, 303],
[404, 505, 606]])
* b a
array([[ 100, 400, 900],
[1600, 2500, 3600]])
**2 b
array([[ 1, 4, 9],
[16, 25, 36]])
< 300 a
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)
* 5 b
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:
* np.array([[5, 5, 5], [5, 5, 5]]) b
array([[ 5, 10, 15],
[20, 25, 30]])
Ein weiteres Beispiel für Broadcasting ist, wenn wir ein Array c
wie folgt erstellen:
= np.array([1, 2, 3]) c
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])
+ c b
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.
# Methode a.mean()
350.0
# Funktion np.mean(a)
350.0
sum() b.
21
max() b.
6
min() a.
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.
=0) # Mittelwert entlang der Zeilen, d.h. Spaltenmittelwerte a.mean(axis
array([250., 350., 450.])
=1) # Mittelwert entlang der Spalten, d.h. Zeilenmittelwerte a.mean(axis
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.
= np.arange(10)**3
a a
array([ 0, 1, 8, 27, 64, 125, 216, 343, 512, 729])
0] # erstes Element (Indizierung beginnt bei 0) a[
0
-2] # vorletztes Element a[
512
2:5] # drei Elemente, beginnend mit Position 2 a[
array([ 8, 27, 64])
2] # jedes zweite Element a[::
array([ 0, 8, 64, 216, 512])
Mehrdimensionale Arrays haben einen Index pro Achse:
= np.random.randint(0, 100, (5, 4)) b
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)
2, 3] # 3. Zeile, 4. Spalte b[
71
-1] # alle Zeilen, letzte Spalte b[:,
array([ 9, 64, 71, 20, 28])
0, :] # erste Zeile b[
array([37, 12, 72, 9])
1:3, 2:] b[
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:
= np.arange(12)**2
a a
array([ 0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121])
= [2, 7, 10, 10] # wir wollen das 2., 7., 10. und 10. Element
idx 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):
2, 7, 10, 10]] a[[
array([ 4, 49, 100, 100])
Mit Bool’schen Indexarrays kann man Arrays ebenfalls indizieren (maskieren):
% 2 == 0 # wird elementweise verglichen a
array([ True, False, True, False, True, False, True, False, True,
False, True, False])
% 2 == 0] a[a
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:
= np.random.randint(-500, 500, (3, 4))
c c
array([[ 425, -102, 62, 80],
[-285, 483, 253, 3],
[ -22, 364, -414, -359]])
# gibt die aktuelle Form aus c.shape
(3, 4)
= 2, 6 # ändert die Form in place
c.shape c
array([[ 425, -102, 62, 80, -285, 483],
[ 253, 3, -22, 364, -414, -359]])
4, 3)) # ändert die Form in place
c.resize(( c
array([[ 425, -102, 62],
[ 80, -285, 483],
[ 253, 3, -22],
[ 364, -414, -359]])
1, -1)) # reshape ändert das Objekt nicht
c.reshape(( c
array([[ 425, -102, 62],
[ 80, -285, 483],
[ 253, 3, -22],
[ 364, -414, -359]])
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.reshape((1, -1)) # daher sollte man einen Namen zuweisen
c 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.
= np.random.randint(-100, 100, (2, 3))
a = np.random.randint(-100, 100, (2, 3)) b
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]])
= np.random.randint(-100, 100, 5)
c c
array([ 75, 21, -70, -29, 31])
= np.random.randint(-100, 100, 5)
d 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?
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).