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 numpyNun 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 npDer 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.ndim1mit Daten vom Typ
a.dtypedtype('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
xarray([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
xarray([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+barray([7., 7., 7.])b-aarray([5., 3., 1.])a*barray([ 6., 10., 12.])a/barray([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)
yarray([ 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)
aarray([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 5array([0.3, 0.4, 0.5])a[:6] # Indizes bis 5array([0. , 0.1, 0.2, 0.3, 0.4, 0.5])a[6:] # Indizes ab 6array([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
aarray([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.]])
Aarray([[1., 4., 2.],
[2., 0., 3.],
[1., 1., 4.]])Um eine Einheitsmatrix zu erzeugen nutzen wir
B = np.eye(3)
Barray([[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
Carray([[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.]))
Darray([[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
Carray([[2., 4., 2.],
[2., 1., 3.],
[1., 1., 5.]])D = A*B
Darray([[1., 0., 0.],
[0., 0., 0.],
[0., 0., 4.]])E = np.exp(A)
Earray([[ 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@Barray([[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 ü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)
xarray([3., 2., 5.])Eine einfache Probe ergibt
np.matmul(C,x)-barray([ 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 vertikalarray([[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 vertikalarray([[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 -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)
Aarray([[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.
Aarray([[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.
Aarray([[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 npfFinanzfunktionen in NumPy
| Funktion | Beschreibung |
|---|---|
| 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 €