Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Einführung in das Arbeiten mit NumPy

In vielen mathematischen Anwendungen muss mit Vektoren und Matrizen gerechnet werden, beispielsweise bei der numerischen Berechnung von Integralen, Differentialgleichungen oder Problemen aus der Graphentheorie. Wir werden in diesem Kurs ausführlich mit linearen Modellen, zu denen beispielsweise die Regressionsmodelle gehören, beschäftigen und diese mit Hilfe von Matrizenrechnungen in Python umsetzen. Wesentlicher Bestandteil des Kurses ist die Zusammenstellung und Analyse von Portfolien. Sowohl die Berechnung von Portfoliowerten als auch -Renditen können bequem mittels Matrix-Vektor-Operationen der linearen Algebra effizient berechnet werden. Daher beschäftigen wir uns in diesem Kapitel mit der Python-Bibliothek NumPy, die die entsprechenden Datentypen und Rechenoperationen für Matrizen und Vektoren bereitstellt.

NumPy installieren

Einmalig muss vor der ersten Nutzung die NumPy-Bibliothek installiert werden, welches wieder einfach in der Konsole mit aktivierter Umgebung mittels Pip schnell erledigt ist, falls das Paket noch nicht in Ihrer virtuellen Umgebung installiert ist:

pip install numpy

Nun muss die Bibliothek in unser Python-Skript eingebunden werden. Wir könnten dies wie in Abschnitt Variablen und Datentypen mit der Zeile from numpy import * machen, was aber nicht empfehlenswert ist, da die NumPy-Bibliothek die Funktionen sin, cos, sqrt, etc. bereit stellt, welche die aus der math-Bibliothek überschreiben würden. Daher nutzen wir folgende Variante:

import numpy as np

Der Zusatz as np gibt nur an, dass wir die Bibliothek in Zukunft unter dem kürzeren Namen np und nicht unter dem langen numpy ansprechen können.

Arbeiten mit Vektoren

Vektoren erzeugen

Ein Vektor bzw. NumPy-Array kann mit der Funktion np.array(...) von einem beliebigen iterierbaren Objekt (Liste, Tupel, ...), deren Elemente vom gleichen Typ sind, initialisiert werden:

a = np.array([1.,2.,3.])
b = np.array((6.,5.,4.))
print("a ist ein", type(a), "mit Wert", a)
print("b ist ein", type(b), "mit Wert", b)
a ist ein <class 'numpy.ndarray'> mit Wert [1. 2. 3.]
b ist ein <class 'numpy.ndarray'> mit Wert [6. 5. 4.]

Die Klasse ndarray repräsentiert ein mehrdimensionales Array. In unserem Fall ein Vektor, also ein Array der Dimension

a.ndim
1

mit Daten vom Typ

a.dtype
dtype('float64')

der Dimension

a.shape
(3,)

2 weitere Funktionen um NumPy-Arrays zu erzeugen sind:

x = np.linspace(0,3,6) # Äquidistantes Punktgitter für [0,3] aus 6 Punkten
x
array([0. , 0.6, 1.2, 1.8, 2.4, 3. ])
x = np.arange(0, 3, 0.5) # Äquidistantes Punktgitter für [0,3) mit Inkrement 0.5
x
array([0. , 0.5, 1. , 1.5, 2. , 2.5])

Elementare Vektoroperationen

Ein ndarray ist, wie eine Liste oder ein Tupel, ein iterierbares Objekt. Wir können also einfach mit einer for-Schleife über alle Elemente gehen:

val = 0
for e in a:
    val += e
print("Die Summe der Einträge von", a, "ist", val)
Die Summe der Einträge von [1. 2. 3.] ist 6.0

Wir können einige elementare Rechenoperationen für mit unseren Vektoren durchführen und es kommt das erwartete Ergebnis raus:

a+b
array([7., 7., 7.])
b-a
array([5., 3., 1.])
a*b
array([ 6., 10., 12.])
a/b
array([0.16666667, 0.4 , 0.75 ])

Wir beobachten, dass die Grundrechenarten einfach elementweise durchgeführt werden. Bei Addition und Subtraktion ist es auch das, was wir aus der Linearen Algebra-Perspektive erwarten würden, aber bei der Multiplikation und Division ist dies nicht klar. Eventuell hätten wir die Berechnung des Skalar- oder Kreuzproduktes erwartet. Dazu später mehr.

Die Rechenoperation +, -, *, / werfen auch einen Fehler, wenn die Größen der beiden Vektoren nicht kompatibel sind:

c = np.array([8,7,6,5]) 
# Dies wird einen Fehler werfen
a+c
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[13], line 3
      1 c = np.array([8,7,6,5])
      2 # Dies wird einen Fehler werfen
----> 3 a+c

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

Untersuchen wir zunächst welche Attribute und Methoden von der Klasse ndarray bereitgestelt werden. Dazu geben wir a.<TAB> bzw. dir(a) ein und bekommen eine lange Liste angezeigt. Testen wir 3 dieser Methoden aus:

print("Der kleinste Eintrag von a ist", a.min(), "bei Index", a.argmin())
print("Das Skalarprodukt <a,b> ist", a.dot(b))
print("Die Summe aller Einträge von a ist", a.sum())
Der kleinste Eintrag von a ist 1.0 bei Index 0
Das Skalarprodukt <a,b> ist 28.0
Die Summe aller Einträge von a ist 6.0

Weitere Rechenoperationen

Neben diesen elementaren Funktionen, welche die Klasse ndarray direkt bereitstellt, finden wir noch weitere Rechenoperation, welche als freie Funktionen in der Bibliothek numpy implementiert sind. Wir tippen np.<TAB> ein und erhalten eine Liste all dieser Funktionen. Uns fällt auf, dass hier nochmals die Funktionen sqrt, exp, sin, cos definiert sind. Diese sind zwar schon in der math-Bibliothek vorhanden, lassen sich aber nicht auf Objekte vom Typ ndarray anwenden:

import math
# math.exp(a)

Die Exponentialfunktion aus der numpy-Bibliothek hingegen wendet die Exponentialfunktion komponentenweise auf den Vektor an:

np.exp(a)
array([ 2.71828183, 7.3890561 , 20.08553692])

Auch die Funktionen log, sin, cos, tan, abs, ..., werden elementweise auf den Vektor angewendet. Genau so werden auch Vergleichsoperationen elementweise angewendet:

x = np.array([0,1,2,3,4])
y = (x <=2)
y
array([ True, True, True, False, False])

Neben diesen elementaren Rechenoperationen finden wir im Modul numpy aber auch vektorspezifische Operationen, wie beispielsweise das Skalar- und Kreuzprodukt:

np.dot(a,b)
np.float64(28.0)
np.cross(a,b)
array([-7., 14., -7.])

Bei einer genaueren Betrachtung der Funktionen aus np stellt man fest, dass Funktionen für beispielsweise Vektornormen fehlen. Diese finden wir im Submodul numpy.linalg. Wir tippen np.linalg.<TAB> ein und erhalten wieder eine Liste aller angebotenen Funktionen.

Wir können beispielsweise wie folgt die üblichen Normen berechnen:

print("Euklidische Norm :", np.linalg.norm(a))
print("Maximumnorm      :", np.linalg.norm(a, np.inf))
print("1-Norm           :", np.linalg.norm(a, 1))
Euklidische Norm : 3.7416573867739413
Maximumnorm      : 3.0
1-Norm           : 6.0

Zugriff auf Vektoreinträge

Schauen wir uns zuletzt noch an, wie auf einzelne oder mehrere Elemente des Vektors in einem bestimmten Bereich zugegriffen werden kann. Dies geschieht mit dem []-Operator:

a = np.linspace(0,1,11)
a
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])
print("Vierter Eintrag:") # Die Zählung des Index' beginnt bei 0
a[3]
Vierter Eintrag:
np.float64(0.30000000000000004)

Wir können auch einen Teil des Arrays extrahieren, indem wir einen Indexbereich i:j angeben:

a[3:6] # Indizes zwischen 3 und 5
array([0.3, 0.4, 0.5])
a[:6] # Indizes bis 5
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5])
a[6:] # Indizes ab 6
array([0.6, 0.7, 0.8, 0.9, 1. ])

Wir können auch Teilvektoren überschreiben, natürlich unter Beachtung der Dimension:

a[1:4] = np.ones((3,))  # Schreibe Einsen in die Einträge 1 bis 3
a[6:8] = np.zeros((2,)) # Schreibe Nullen in die Einträge 6 und 7
a
array([0. , 1. , 1. , 1. , 0.4, 0.5, 0. , 0. , 0.8, 0.9, 1. ])

Nebenbei haben wir hier auch 2 Methoden kennengelernt um Arrays mit Einträgen 0 oder 1 zu initialisieren.

Arbeiten mit Matrizen

Erzeugen von Matrizen

Auch für Matrizen nutzen wir den Datentyp numpy.ndarray. Zur Initialisierung übergeben wir der np.array-Funktion eine Liste bestehend aus Listen bestehend aus Zahlen. NumPy erkennt automatisch, dass wir offensichtlich ein zweidimensionales Array, also eine Matrix, erstellen wollen:

A = np.array([[1.,4.,2.],[2.,0.,3.],[1.,1.,4.]])
A
array([[1., 4., 2.], [2., 0., 3.], [1., 1., 4.]])

Um eine Einheitsmatrix zu erzeugen nutzen wir

B = np.eye(3)
B
array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])

Wir können auch eine leere Matrix erzeugen und die Einträge anschließend direkt eintragen:

C = np.zeros((3,3)) # Das Argument ist ein Tupel und gibt die Größer der Matrix an
C[0,0] = 1
C[1,1] = 2
C[1,2] = 5
C[2,1] = 4
C[2,2] = 1
C
array([[1., 0., 0.], [0., 2., 5.], [0., 4., 1.]])

Zu einem gegebenen Vektor kann man auch die zugehörige Diagonalmatrix erzeugen mit

D = np.diag(np.array([1.,2.,3.]))
D
array([[1., 0., 0.], [0., 2., 0.], [0., 0., 3.]])

Rechnen mit Matrizen

Wie schon bei den Vektoren beobachtet werden die Grundrechenarten sowie Funktionen exp, sin, cos, log, etx. aus dem Paket numpy elementweise angewendet:

C = A+B
C
array([[2., 4., 2.], [2., 1., 3.], [1., 1., 5.]])
D = A*B
D
array([[1., 0., 0.], [0., 0., 0.], [0., 0., 4.]])
E = np.exp(A)
E
array([[ 2.71828183, 54.59815003, 7.3890561 ], [ 7.3890561 , 1. , 20.08553692], [ 2.71828183, 2.71828183, 54.59815003]])

Weitere Rechenoperationen werden von der Klasse numpy.ndarray bereitgestellt, wie das Transponieren einer Matrix:

C.transpose()
array([[2., 2., 1.], [4., 1., 1.], [2., 3., 5.]])

Die Matrixmultiplikation wird als freie Funktion vom Modul numpy bereitgestellt:

np.matmul(A,B)
array([[1., 4., 2.], [2., 0., 3.], [1., 1., 4.]])

oder alternativ mit dem @-Operator

A@B
array([[1., 4., 2.], [2., 0., 3.], [1., 1., 4.]])

Auch in der Bibliothek np.linalg finden wir weitere Funktionen aus der linearen Algebra, beispielsweise eine Funktion zur Berechnung der Determinante

np.linalg.det(C)
np.float64(-22.000000000000004)

der Inversen

np.linalg.inv(C)
array([[-0.09090909, 0.81818182, -0.45454545], [ 0.31818182, -0.36363636, 0.09090909], [-0.04545455, -0.09090909, 0.27272727]])

und der Eigenwerte- und Vektoren

E,V = np.linalg.eig(C)
print("Eigenwerte    :\n", E)
print("Eigenvektoren :\n", V)
Eigenwerte    :
 [ 6.97413644+0.j -1.33574651+0.j  2.36161007+0.j]
Eigenvektoren :
 [[ 0.63932126+0.j  0.77319109+0.j -0.8514755 +0.j]
 [ 0.50515119+0.j -0.63379132+0.j -0.29406667+0.j]
 [ 0.57973321+0.j -0.02200211+0.j  0.43418229+0.j]]

Die Eigenvektoren stehen spaltenweise in V. Testen wir die Rechnung indem wir Cvλv=0C\,v - \lambda\,v=0 überprüfen:

for i in range(3):
    res = np.matmul(C, V[:,i]) - E[i]*V[:,i]  # C*v-lambda*v    
    print("Fehler: ", np.linalg.norm(res))
Fehler:  3.66205343881779e-15
Fehler:  1.5862621070749701e-15
Fehler:  2.434909185329438e-15

Auch für die Lösung linearer Gleichungssysteme gibt es eine Funktion aus dem Modul numpy.linalg:

b = np.array([24.,23.,30.])
x = np.linalg.solve(C, b)
x
array([3., 2., 5.])

Eine einfache Probe ergibt

np.matmul(C,x)-b
array([ 0.00000000e+00, 0.00000000e+00, -3.55271368e-15])

Zugriff auf Matrixeinträge

Auch der Zugriff auf einzelne Einträge bzw. auf Teilmatrizen funktioniert analog zu eindimensionalen NumPy-Arrays:

print("Eintrag C_11           :", C[0,0])
print("Zweite Spalte          :", C[:,1])
print("Dritte Zeile           :", C[2,:])
print("Letzte (=dritte) Zeile :", C[-1,:])
Eintrag C_11           : 2.0
Zweite Spalte          : [4. 1. 1.]
Dritte Zeile           : [1. 1. 5.]
Letzte (=dritte) Zeile : [1. 1. 5.]

Matrizen stapeln

Mit den Befehlen numpy.vstack und numpy.hstack lassen sich Matrizen vertikal bzw. horizontal “zusammenkleben”:

np.vstack([A,B,C]) # Stapelt A, B, C vertikal
array([[1., 4., 2.], [2., 0., 3.], [1., 1., 4.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.], [2., 4., 2.], [2., 1., 3.], [1., 1., 5.]])
np.hstack([A,B,C]) # Stapelt A, B, C vertikal
array([[1., 4., 2., 1., 0., 0., 2., 4., 2.], [2., 0., 3., 0., 1., 0., 2., 1., 3.], [1., 1., 4., 0., 0., 1., 1., 1., 5.]])

Um einen Vektor an eine Matrix zu kleben müssen wir bei numpy.hstack den Vektor zunächst in eine n×1n\times 1-Matrix umwandeln wir folgendes Beispiel zeigt:

np.vstack([A,b])
array([[ 1., 4., 2.], [ 2., 0., 3.], [ 1., 1., 4.], [24., 23., 30.]])
np.hstack([A,b.reshape((3,1))])
array([[ 1., 4., 2., 24.], [ 2., 0., 3., 23.], [ 1., 1., 4., 30.]])

Mutable oder Immutable?

Zuletzt wollen wir noch überprüfen, wie sich NumPy-Arrays als Funktionsparameter verhalten. Sind diese mutable oder immutable? Ein einfacher Test gibt:

def modify_matrix(A):
    A[1,1] = 1.
    
A = np.zeros((3,3))
modify_matrix(A)
A
array([[0., 0., 0.], [0., 1., 0.], [0., 0., 0.]])

Objekte vom Typ numpy.ndarray sind offensichtlich mutable. Man muss an dieser Stelle auch bei einer Zuweisung von Matrizen aufpassen:

B = A
B[1,1] = 2.
A
array([[0., 0., 0.], [0., 2., 0.], [0., 0., 0.]])

Wir haben hier den Namen B an das gleiche Objekt wie A gebunden. Nachdem wir B verändert haben, hat sich diese Änderung offensichtlich auch auf die Matrix A ausgewirkt. Es wird also keine Kopie der Matrix angezeigt.

Möchte man tatsächlich eine Kopie einer Matrix erstellen, so nutzt man

B = np.copy(A)
B[1,1] = 3.
A
array([[0., 0., 0.], [0., 2., 0.], [0., 0., 0.]])

Wir haben hier B verändert, die Änderung wirkt sich aber nicht auf die Matrix A aus.

Finanzmathematik mit NumPy

NumPy, genauer gesagt das Modul numpy-financial, stellt Standardfunktionen für Finanzmathematik bereit, die man z. B. für Investitionsrechnungen, Kredite oder Cashflow-Analysen braucht.

Wir installieren numpy-financial mit folgendem Befehl in unserer Konsole bzw. Terminal.

conda install -c conda-forge numpy-financial 

In Python importieren wir das Modul dann wie folgt:

import numpy_financial as npf

Finanzfunktionen in NumPy

FunktionBeschreibung
fv(rate, nper, pmt, pv[, when])Endwert (Future Value) berechnen
ipmt(rate, per, nper, pv[, fv, when])Zinsanteil einer Zahlung berechnen
irr(values)Interne Verzinsung (IRR) berechnen
mirr(values, finance_rate, reinvest_rate)Modifizierte interne Verzinsung (MIRR) berechnen
nper(rate, pmt, pv[, fv, when])Anzahl der Perioden berechnen
npv(rate, values)Kapitalwert (Net Present Value, NPV) berechnen
pmt(rate, nper, pv[, fv, when])Zahlungsrate (Annuität = Zins + Tilgung) berechnen
ppmt(rate, per, nper, pv[, fv, when])Tilgungsanteil einer Zahlung berechnen
pv(rate, nper, pmt[, fv, when])Barwert (Present Value) berechnen
rate(nper, pmt, pv, fv[, when, guess, tol, ...])Zinssatz pro Periode berechnen

Nachfolgend sollen einige Codebeispiele den Einsatz von numpy_financial demonstrieren.

Beginnen wir mit dem Endwert (forward value) fv(). Der Endwert beantwortet die Frage, wie viel sind 10.000 € bei 5 % Verzinsung nach 10 Jahren wert?

import numpy_financial as npf

endwert = npf.fv(
    rate=0.05,   # Zinssatz pro Jahr
    nper=10,     # 10 Jahre
    pmt=0,       # keine regelmäßigen Zahlungen
    pv=-10000    # Anfangsinvestition
)

print(f"Endwert: {endwert:.2f} €")
Endwert: 16288.95 €

Der Zinsanteil einer Kreditrate (Annuität) besteht aus Tilgungs- und Zinsanteil. Sollte zum Beispiel, betrachten wir einen Kredit mit folgenden Eckdaten:

  • Kredithöhe: 100.000 €

  • Laufzeit 20 Jahren

  • 4 % Zinsen.

Der Befehl ipmt() beantwortet die Frage, wie hoch ist der Zinsanteil (Interest Payment) der ersten Jahresrate?

zinsanteil = npf.ipmt(
    rate=0.04,
    per=1,        # erste Periode
    nper=20,
    pv=100000
)

print(f"Zinsanteil: {-zinsanteil:.2f} €")
Zinsanteil: 4000.00 €

Der interne Zinsfuß (internal rate of return) irr() ist der Zinssatz, bei dem der Kapitalwert der Zahlungsreihe null wird. Eine Investition kostet heute 10.000 € und liefert anschließend vier Rückzahlungen:

cashflows = [-10000, 3000, 3500, 4000, 2500]
irr = npf.irr(cashflows)

print(f"IRR: {irr:.2%}")
IRR: 11.54%

Die Modified internal rate of return (modifizierter interner Zinsfuß) mirr() trifft Annahmen über die Finanzierungskostensatz (Fremdkapitalzinsen) und Wiederanlagesatz.

cashflows = [-10000, 3000, 3500, 4000, 2500]

mirr = npf.mirr(
    cashflows,
    finance_rate=0.05, # Fremdkapitalzinsen
    reinvest_rate=0.03 # Wiederanlagesatz
)

print(f"MIRR: {mirr:.2%}")
MIRR: 8.01%

Der Befehl nper() steht für number of periods und berechnet die Kreditlaufzeit eines Darlehens. In unserem Beispiel suchen wir die Darlehenslaufzeit, eines Kredits über 100.000 € bei 4 % Zinsen und einer jährlichen Rate von 8.000 € zurückzuzahlen?

laufzeit = npf.nper(
    rate=0.04,
    pmt=-8000,
    pv=100000
)

print(f"Laufzeit: {laufzeit:.2f} Jahre")
Laufzeit: 17.67 Jahre

Der Kapitalwert (net present value) npv() bewertet die Rentabilität eines Projekts oder einer Investition. Ein positiver Kapitalwert bedeutet grundsätzlich, dass die Investition bei dem verwendeten Kalkulationszins vorteilhaft ist. Nehmen wir an, eine Investition von 10.000 € liefert vier zukünftige Zahlungen und der Kalkulationszins beträgt 5 %.

cashflows = [3000, 3500, 4000, 2500]
kapitalwert = -10000 + npf.npv(0.05, cashflows)

print(f"Kapitalwert: {kapitalwert:.2f} €")
Kapitalwert: 2121.05 €

Die Annuität/Kreditrate (payment) pmt() bezeichnet die Höhe einer jährlichen (Kredit-)Rate für einen Kredit von 100.000 € bei 4 % Zinsen und 20 Jahren Laufzeit.

rate = npf.pmt(
    rate=0.04,
    nper=20,
    pv=100000
)

print(f"Jährliche Rate: {-rate:.2f} €")
Jährliche Rate: 7358.18 €

Wie bereits erwähnt, besteht eine Kreditrate aus Zins- und Tilgungsanteil. Den Zinsanteil berechnen wir mit ipmt(). Den Tilgungsanteil (Principal Payment) berechnen wir mit dem Befehl ppmt(). Aber: mit ipmt() berechnen wir nur die erste Tilgungsrate und können nur bei einem Tilgungsdarlehen die gleichbleibende Tilgungsrate voraussetzen. Ganz im Gegensatz zum Annuitätendarlehen.

tilgung = npf.ppmt(
    rate=0.04,
    per=1,
    nper=20,
    pv=100000
)

print(f"Tilgungsanteil: {-tilgung:.2f} €")
Tilgungsanteil: 3358.18 €

Der Barwert (present value) pv() beantwortet beispielhaft die Frage: Wie viel müsste man heute anlegen, um bei 5 % Verzinsung in 10 Jahren 20.000 € zu besitzen?

barwert = npf.pv(
    rate=0.05,
    nper=10,
    pmt=0,
    fv=20000
)

print(f"Barwert: {-barwert:.2f} €")
Barwert: 12278.27 €

Der Befehl rate() berechnet den jährlichen Zinssatz (interest rate). Dazu folgendes Beispiel: Ein Kapital von 10.000 € wächst innerhalb von 10 Jahren auf 15.000 €. Welcher jährliche Zinssatz steckt dahinter?

zinssatz = npf.rate(
    nper=10,
    pmt=0,
    pv=-10000,
    fv=15000
)

print(f"Zinssatz: {zinssatz:.2%}")
Zinssatz: 4.14%

Mit when kann festgelegt werden, ob eine Zahlungen vor- oder nachschüssig erfolgen. Für vorschüssig gilt when=begin und für nachschüssig when=end.

zins_vor = npf.ipmt(
    rate=0.05,        
    per=2, # bei vorschüssig per = 2 setzen, da zu per=1 ja noch keine Zeit vergnagen ist und somit das Ergebnis 0 wäre.   
    nper=10,          
    pv=50000,        
    when="begin"      
)

print(f"Zinsanteil: {-zins_vor:.2f} €")
Zinsanteil: 2191.66 €
import numpy_financial as npf

zins_end = npf.ipmt(
    rate=0.04,
    per=1,
    nper=20,
    pv=100000,
    when="end"
)

print(f"Zinsanteil: {-zins_end:.2f} €")
Zinsanteil: 4000.00 €