"Numpy und andere Bibliotheken des Scientific Computing in Python sind groß, man kann kaum alle Funktionen kennen.\n",
"In diesem Skript liefert die händisch bereinigte Ausgabe von `cat *.ipynb | egrep -o \"np\\.([^(]+)\" | sort | uniq` diese vollständige Liste. Diese muss nicht auswendig gelernt werden, gibt aber eine klare Abschätzung nach oben. Mehr muss man definitiv auf keinen Fall können! Außerdem sollte man bedenken, dass diese Liste für das gesamte Skript gilt, und wir vieles davon noch nicht kennen gelernt haben."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"np.abs\n",
"np.all\n",
"np.allclose\n",
"np.arange\n",
"np.argmin\n",
"np.array\n",
"np.atleast_1d\n",
"np.byte\n",
"np.column_stack\n",
"np.concatenate\n",
"np.copy\n",
"np.corrcoef\n",
"np.cos\n",
"np.cov\n",
"np.diff\n",
"np.dot\n",
"np.dstack\n",
"np.equal\n",
"np.exp\n",
"np.eye\n",
"np.float64\n",
"np.frombuffer\n",
"np.full\n",
"np.genfromtxt\n",
"np.heaviside\n",
"np.hstack\n",
"np.identity\n",
"np.isclose\n",
"np.isnan\n",
"np.linalg.eig\n",
"np.linalg.inv\n",
"np.linalg.norm\n",
"np.linalg.svd\n",
"np.linspace\n",
"np.log\n",
"np.matmul\n",
"np.max\n",
"np.maximum\n",
"np.may_share_memory\n",
"np.mean\n",
"np.median\n",
"np.meshgrid\n",
"np.mgrid\n",
"np.min\n",
"np.minimum\n",
"np.nansum\n",
"np.ones\n",
"np.ones_like\n",
"np.outer\n",
"np.pi\n",
"np.power\n",
"np.ptp\n",
"np.random.choice\n",
"np.random.multivariate_normal\n",
"np.random.permutation\n",
"np.random.rand\n",
"np.random.randint\n",
"np.random.randn\n",
"np.random.random\n",
"np.random.seed\n",
"np.random.uniform\n",
"np.round\n",
"np.sin\n",
"np.size\n",
"np.sort\n",
"np.sqrt\n",
"np.square\n",
"np.stack\n",
"np.std\n",
"np.sum\n",
"np.uint8\n",
"np.unique\n",
"np.var\n",
"np.vstack\n",
"np.zeros\n",
"np.zeros_like"
]
}
],
"metadata": {
...
...
%% Cell type:markdown id: tags:
# Numpy
%% Cell type:markdown id: tags:
In diesem kurzen Abschnitt lernen wir die wesentlichen Ideen bei Numpy kennen.
Eine längere und bessere (und englischere) Fassung davon sind die [Numpy fundamentals](https://numpy.org/devdocs/user/basics.html) im Numpy User Guide, die Sie am besten im Anschluss überfliegen sollten.
Ein richtiges (und sehr gutes) Lehrbuch für alle, die mit Python vertraut sind (und als das betrachten wir uns zu diesem Punkt der Vorlesung) ist [Nicolas Rougier's From Python to Numpy](https://www.labri.fr/perso/nrougier/from-python-to-numpy/), wo für uns zunächst Kapitel 3 relevant ist.
Kurz lässt sich sagen, dass mit Numpy [Array-orientierte Programmierung](https://en.wikipedia.org/wiki/Array_programming)(auch:*Vektorisierung*) in Python möglich wird.
## Arrays
<!--
Zur Probability mit Numpy ist das hier (auf deutsch) gut geeignet:
Es gibt auch eine `arange`-Methode, sie erzeugt aber keine Range-Objekte in Numpy:
%% Cell type:code id: tags:
``` python
myRange=np.arange(1,7)
print(myRange,type(myRange),myRange.dtype)
myRange
```
%% Output
[1 2 3 4 5 6] <class 'numpy.ndarray'> int64
array([1, 2, 3, 4, 5, 6])
%% Cell type:markdown id: tags:
Der Numpy-Array `ndarray` trägt im Gegensatz zur Python-Liste einen festen Datentyp, den alle Elemente gemeinsam haben. Das können alle Numpy-datatypes (dtypes) sein, z.B. `double` (kompatibel mit dem Python-`float`) oder `float32` (auf den meisten Plattformen kompatibel mit dem C-`float`) oder `long` (kompatibel mit dem Python-`int`).
Es gibt noch mehr Wege, Arrays zu erzeugen, außer mit `arange` oder durch konvertieren einer Python-Sequenz. Z.B. lässt sich mit `ones` ein Array gefüllt mit $1$ und mit `zeros` ein Array gefüllt mit $0$ erzeugen.
%% Cell type:markdown id: tags:
Der Grund dafür, dass das Array den Namen `ndarray` trägt, ist, dass es für \`\`$n$-dimensional array'' steht.
Wenn man in Python eine Matrix speichern möchte, würde man das als Liste der Zeilenvektoren (oder der Spaltenvektoren) tun, etwa
%% Cell type:code id: tags:
``` python
matrix=[[1,0,0],[0,1,0],[0,0,1]]# Einheitsmatrix
print(matrix,type(matrix))
quarkix=matrix# eine Kopie
print(quarkix[0][0])
quarkix[0][0]=0# wir ändern den oberen linken Eintrag
print(matrix[0][0])
```
%% Output
[[1, 0, 0], [0, 1, 0], [0, 0, 1]] <class 'list'>
1
0
%% Cell type:markdown id: tags:
Daran sehen wir ein Problem: Python behandelt unsere Matrix wie eine Liste (so haben wir es ja auch hingeschrieben), also wird beim kopieren der Liste der Inhalt (die Zeilenvektoren) nicht mitkopiert (sondern nur die Pointer darauf).
%% Cell type:code id: tags:
``` python
npmatrix=np.identity(3,int)# Einheitsmatrix
print(npmatrix,type(npmatrix))
npquarkix=npmatrix[:]
print(npquarkix[0][0])
npquarkix[0][0]=2# wir ändern den oberen linken Eintrag
print(npmatrix[0][0])
# Mit einer echten Kopie wäre das nicht passiert:
real_copy=npmatrix.copy()
real_copy[0][0]=1
assertnpmatrix[0][0]!=1
```
%% Output
[[1 0 0]
[0 1 0]
[0 0 1]] <class 'numpy.ndarray'>
1
2
%% Cell type:markdown id: tags:
Es ist wichtig, festzustellen, dass der Numpy-Array das gleiche Verhalten an den Tag legt wie unsere Python-Liste-von-Listen. Wir können gleich damit indizieren und slicen, und es gibt das gleiche Problem beim Kopieren über die Slicing-Syntax.
Der `shape`-Parameter sagt uns, welche Form unser Numpy-Array hat. Dabei handelt es sich um ein $d$-Tupel, wobei $d$ die Dimension ist. Eine Matrix ist $2$-dimensional, ein Vektor $1$-dimensional und ein Skalar $0$-dimensional.
%% Cell type:code id: tags:
``` python
print(npmatrix.shape)
print(npmatrix[0],npmatrix[0].shape)
print(npmatrix[0][0],npmatrix[0][0].shape)
```
%% Output
(3, 3)
[2 0 0] (3,)
2 ()
%% Cell type:markdown id: tags:
In Numpy kann man noch etwas feiner slicen.
Die Allgemeine Syntax ist `[start:stop:step, ..]` wobei man mit dem Komma getrennt über die Achsen geht. Ein zweidimensionaler Array hat zwei Achsen, wobei Achse $0$ von oben nach unten und Achse $1$ von links nach rechts indiziert ist. Während "step" auch mit Python-Listen funktioniert, ist das indizieren mit mehreren Achsen eine Spezialität von Numpy.
In klassischem Python-Code würden wir auf einen Eintrag einer Matrix zugreifen mit `matrix[x][y]`, und das funktioniert so auch in Numpy. Allerdings wird dabei zunächst ein weiteres Listenobjekt `matrix[x]` erzeugt (beim Slicing auch zusätzlicher Speicher dafür belegt) und dann darauf `[y]` aufgerufen. Es ist daher grundsätzlich effizienter, direkt Numpy's `[x,y]` zu verwenden.
Numpy erzeugt bewusst keine Kopien beim Slicing, sondern nur eine andere Sichtweise auf den gleichen Speicherbereich (daher auch das oben beobachtete Verhalten bei `[:]`). Ob zwei Arrays auf den gleichen Speicherbereich verweisen, lässt sich mit `np.may_share_memory` prüfen. Dabei bedeutet ein positives Ergebnis keineswegs, dass die Arrays voneinander abhängig sind - so verweisen die erste und die zweite Spalte einer Matrix auch auf den gleichen Speicherbereich, nämlich die ganze Matrix. Wenn man nun einen der beiden Vektoren ändert, bleibt der andere unverändert - die ganze Matrix aber ändert sich mit.
%% Cell type:code id: tags:
``` python
npmatrix=np.array(list(range(1,10))).reshape(3,3)
candidates=(npmatrix[0::2],npmatrix[1])
print("May share memory (but actually don't):",
candidates,np.may_share_memory(*candidates))
print(type([0,0,0]))# vor der Zuweisung
npmatrix[1]=[0,0,0]
print(type(npmatrix[1]))# nach der Zuweisung
print(npmatrix)# die ganze Matrix ist wie verändert
```
%% Output
May share memory (but actually don't): (array([[1, 2, 3],
[7, 8, 9]]), array([4, 5, 6])) True
<class 'list'>
<class 'numpy.ndarray'>
[[1 2 3]
[0 0 0]
[7 8 9]]
%% Cell type:markdown id: tags:
Ausführliche Informationen zum Slicing und Indizieren liefert [die Dokumentation](https://numpy.org/doc/stable/user/basics.indexing.html).
%% Cell type:markdown id: tags:
## Broadcasting
Während für Python-Listen der Additionsoperator die Listenkonkatenation ist, und damit die Multiplikation von Listen mit Skalaren definiert ist, ist die Multiplikation von zwei Listen undefiniert.
Für Numpy-Arrays sind deutlich mehr arithmetische Operationen verfügbar:
%% Cell type:code id: tags:
``` python
E=np.identity(3,int)
print(E,"= E")
A=np.ones((3,3),int)
A[0]=[0,0,0]
print(A,"= A")
print(E+A,"= E + A")
print(E*A,"= EA")
print((E+A)**2,"= (E+A)(E+A)")
```
%% Output
[[1 0 0]
[0 1 0]
[0 0 1]] = E
[[0 0 0]
[1 1 1]
[1 1 1]] = A
[[1 0 0]
[1 2 1]
[1 1 2]] = E + A
[[0 0 0]
[0 1 0]
[0 0 1]] = EA
[[1 0 0]
[1 4 1]
[1 1 4]] = (E+A)(E+A)
%% Cell type:markdown id: tags:
Wenn man das aufmerksam nachverfolgt, stellt man fest, dass diese Rechnungen keine Matrizenmultiplikationen sind,
sondern schlicht elementweise erfolgt sind - so sind die Operationen auf Arrays definiert.
Besonders tückisch ist dies:
%% Cell type:code id: tags:
``` python
v=np.ones(3,int)
print(v,"= v")
print(A*v,"= A*v (aber nicht die Matrixmultiplikation)")
```
%% Output
[1 1 1] = v
[[0 0 0]
[1 1 1]
[1 1 1]] = A*v (aber nicht die Matrixmultiplikation)
%% Cell type:markdown id: tags:
Um explizit mit Matrizenkalkül zu rechnen, hat man früher den Numpy-Datentyp `matrix` verwendet, aber dieser ist als `deprecated` (veraltet) markiert und wird in zukünftigen Numpy-Versionen abgeschafft. Heutzutage nutzt man die Methode `np.matmul` oder den Infix-Operator `@`.
%% Cell type:code id: tags:
``` python
print(np.matmul(A,v))
print(A@v)
```
%% Output
[0 3 3]
[0 3 3]
%% Cell type:markdown id: tags:
Für viele Probleme ist es sehr hilfreich, nicht den Matrizenkalkül zu verwenden, sondern elementweise arithmetische Operationen auszuführen.
*Broadcasting* ist ein Mechanismus, der diesen elementweisen Kalkül etwas praktischer macht.
So ist die Operation ($n \times n$-array) * ($n$-Vektor) automatisch interpretiert, indem der $n$-Vektor $n$-fach kopiert wird, sodass die Multiplikation einer jeden Zeile des linken Arrays mit dem Vektor (elementweise) durchgeführt wird.
Dazu ist es wirklich hilfreich, einmal [die Dokumentation](https://numpy.org/doc/stable/user/basics.broadcasting.html) zu überfliegen.
%% Cell type:markdown id: tags:
## ufuncs
`ufunc` steht für "universal function" und bezeichnet eine Methode, die auf Numpy Arrays vektorisiert laufen kann.
Mit jeder `ufunc` lässt sich z.B. `reduce` durchführen, wo entlang einer Achse des Arrays die `ufunc` auf die resultierenden kleineren Arrays angewandt wird.
Um selbst eine `ufunc` zu schreiben, muss man [C-Code programmieren](https://numpy.org/doc/stable/user/c-info.ufunc-tutorial.html) oder aber [einen Wrapper um eine Python-Methode legen](https://numpy.org/doc/stable/reference/generated/numpy.frompyfunc.html).
Es lohnt sich, einen kurzen Blick auf alle bereits definierten `ufunc`s zu werfen:
Numpy und andere Bibliotheken des Scientific Computing in Python sind groß, man kann kaum alle Funktionen kennen.
In diesem Skript liefert die händisch bereinigte Ausgabe von `cat *.ipynb | egrep -o "np\.([^(]+)" | sort | uniq` diese vollständige Liste. Diese muss nicht auswendig gelernt werden, gibt aber eine klare Abschätzung nach oben. Mehr muss man definitiv auf keinen Fall können! Außerdem sollte man bedenken, dass diese Liste für das gesamte Skript gilt, und wir vieles davon noch nicht kennen gelernt haben.