{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Numpy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In diesem kurzen Abschnitt lernen wir die wesentlichen Ideen bei Numpy kennen.\n", "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.\n", "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.\n", "\n", "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.\n", "\n", "## Arrays\n", "<!--\n", "Zur Probability mit Numpy ist das hier (auf deutsch) gut geeignet:\n", "https://www.python-kurs.eu/python_numpy_wahrscheinlichkeit.php\n", "-->\n", "Numeric Python (Numpy) wird meist als `np` abgekürzt importiert:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[0.49929771 0.59160776 0.77432696 0.80597013 0.18907085 0.11844771\n", " 0.70945366 0.87172152 0.8522308 0.42780462]\n", "<class 'numpy.ndarray'>\n" ] }, { "data": { "text/plain": [ "array([0.49929771, 0.59160776, 0.77432696, 0.80597013, 0.18907085,\n", " 0.11844771, 0.70945366, 0.87172152, 0.8522308 , 0.42780462])" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "sample = np.random.random(10)\n", "print(sample)\n", "print(type(sample))\n", "sample" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Viele nützliche Hilfsfunktionen sind in Numpy enthalten, die wiederum Numpy-eigene Datenstrukturen (den Array) verarbeiten." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "mean = 4.529 \n", "expected= 4.5\n", "std² = 8.260759 \n", "variance= 8.25\n" ] } ], "source": [ "sample = np.random.randint(low=0, high=10, size=5000)\n", "print(\"mean =\", np.mean(sample), \"\\nexpected=\", 9/2)\n", "print(\"std² =\", np.std(sample)**2, \"\\nvariance=\", 99/12)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Es gibt auch eine `arange`-Methode, sie erzeugt aber keine Range-Objekte in Numpy:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[1 2 3 4 5 6] <class 'numpy.ndarray'> int64\n" ] }, { "data": { "text/plain": [ "array([1, 2, 3, 4, 5, 6])" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "myRange = np.arange(1, 7)\n", "print(myRange, type(myRange), myRange.dtype)\n", "myRange" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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`)." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "4.177020760005689\n", "5.418045660000644\n" ] } ], "source": [ "import timeit\n", "ordinary_list = [1,2,3,4,5,6,5,4,3,2,1]*10\n", "def sort_array(dtype):\n", " a = np.array(ordinary_list, dtype)\n", " a.sort()\n", "print(timeit.timeit(lambda : sort_array(np.byte)))\n", "print(timeit.timeit(lambda : sort_array(np.float64)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Dadurch, dass der Datentyp präzise bekannt ist, kann Numpy darauf optimierte Algorithmen, direkt in C implementiert, verwenden.\n", "\n", "Was es noch für `dtype`s gibt und wie sie eingesetzt werden, können wir der Dokumentation entnehmen:\n", "* [\"Structured Arrays\"](https://numpy.org/doc/stable/user/basics.rec.html)\n", "* [\"Data types\" in den \"Numpy fundamentals\"](https://numpy.org/doc/stable/user/basics.types.html)\n", "* [\"Scalars\"](https://numpy.org/doc/stable/reference/arrays.scalars.html)\n", "* [\"Data type objects (dtype)\"](https://numpy.org/doc/stable/reference/arrays.dtypes.html)\n", "* [\"numpy.dtype\"](https://numpy.org/doc/stable/reference/generated/numpy.dtype.html)\n", "\n", "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", "metadata": {}, "source": [ "Der Grund dafür, dass das Array den Namen `ndarray` trägt, ist, dass es für \\`\\`$n$-dimensional array'' steht.\n", "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", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[1, 0, 0], [0, 1, 0], [0, 0, 1]] <class 'list'>\n", "1\n", "0\n" ] } ], "source": [ "matrix = [[1,0,0], [0,1,0], [0,0,1]] # Einheitsmatrix\n", "print(matrix, type(matrix))\n", "quarkix = matrix # eine Kopie\n", "print(quarkix[0][0])\n", "quarkix[0][0] = 0 # wir ändern den oberen linken Eintrag\n", "print(matrix[0][0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[1 0 0]\n", " [0 1 0]\n", " [0 0 1]] <class 'numpy.ndarray'>\n", "1\n", "2\n" ] } ], "source": [ "npmatrix = np.identity(3, int) # Einheitsmatrix\n", "print(npmatrix, type(npmatrix))\n", "npquarkix = npmatrix[:]\n", "print(npquarkix[0][0])\n", "npquarkix[0][0] = 2 # wir ändern den oberen linken Eintrag\n", "print(npmatrix[0][0])\n", "# Mit einer echten Kopie wäre das nicht passiert:\n", "real_copy = npmatrix.copy()\n", "real_copy[0][0] = 1\n", "assert npmatrix[0][0] != 1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "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", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(3, 3)\n", "[2 0 0] (3,)\n", "2 ()\n" ] } ], "source": [ "print(npmatrix.shape)\n", "print(npmatrix[0], npmatrix[0].shape)\n", "print(npmatrix[0][0], npmatrix[0][0].shape)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In Numpy kann man noch etwas feiner slicen.\n", "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." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "not in shape: [1 2 3 4 5 6 7 8 9]\n", "in much better shape:\n", "[[1 2 3]\n", " [4 5 6]\n", " [7 8 9]]\n", "Zeilenvektor Zeile 0: [1 2 3]\n", "Spaltenvektor Spalte 0: [1 4 7]\n", "Spalten 1-2:\n", "[[2 3]\n", " [5 6]\n", " [8 9]]\n", "Alle Zeilen, Schrittweite 2\n", "[[1 2 3]\n", " [7 8 9]]\n" ] } ], "source": [ "matrix = list(range(1,10))\n", "npmatrix = np.array(matrix)\n", "print(\"not in shape:\", npmatrix)\n", "npmatrix.shape = (3,3)\n", "print(\"in much better shape:\\n\"+ str(npmatrix))\n", "print(\"Zeilenvektor Zeile 0:\", npmatrix[0])\n", "print(\"Spaltenvektor Spalte 0:\", npmatrix[:,0])\n", "print(\"Spalten 1-2:\\n\"+ str(npmatrix[:,1:]))\n", "print(\"Alle Zeilen, Schrittweite 2\\n\"+ str(npmatrix[0::2]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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.\n", "\n", "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", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "May share memory (but actually don't): (array([[1, 2, 3],\n", " [7, 8, 9]]), array([4, 5, 6])) True\n", "<class 'list'>\n", "<class 'numpy.ndarray'>\n", "[[1 2 3]\n", " [0 0 0]\n", " [7 8 9]]\n" ] } ], "source": [ "npmatrix = np.array(list(range(1,10))).reshape(3,3)\n", "candidates = (npmatrix[0::2], npmatrix[1])\n", "print(\"May share memory (but actually don't):\",\n", " candidates, np.may_share_memory(*candidates))\n", "print(type([0,0,0])) # vor der Zuweisung\n", "npmatrix[1] = [0,0,0]\n", "print(type(npmatrix[1])) # nach der Zuweisung\n", "print(npmatrix) # die ganze Matrix ist wie verändert" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ausführliche Informationen zum Slicing und Indizieren liefert [die Dokumentation](https://numpy.org/doc/stable/user/basics.indexing.html)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Broadcasting\n", "\n", "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.\n", "Für Numpy-Arrays sind deutlich mehr arithmetische Operationen verfügbar:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[1 0 0]\n", " [0 1 0]\n", " [0 0 1]] = E\n", "[[0 0 0]\n", " [1 1 1]\n", " [1 1 1]] = A\n", "[[1 0 0]\n", " [1 2 1]\n", " [1 1 2]] = E + A\n", "[[0 0 0]\n", " [0 1 0]\n", " [0 0 1]] = EA\n", "[[1 0 0]\n", " [1 4 1]\n", " [1 1 4]] = (E+A)(E+A)\n" ] } ], "source": [ "E = np.identity(3, int)\n", "print(E, \"= E\")\n", "A = np.ones((3,3), int)\n", "A[0] = [0,0,0]\n", "print(A, \"= A\")\n", "print(E + A, \"= E + A\")\n", "print(E * A, \"= EA\")\n", "print((E+A)**2, \"= (E+A)(E+A)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Wenn man das aufmerksam nachverfolgt, stellt man fest, dass diese Rechnungen keine Matrizenmultiplikationen sind,\n", "sondern schlicht elementweise erfolgt sind - so sind die Operationen auf Arrays definiert.\n", "Besonders tückisch ist dies:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[1 1 1] = v\n", "[[0 0 0]\n", " [1 1 1]\n", " [1 1 1]] = A*v (aber nicht die Matrixmultiplikation)\n" ] } ], "source": [ "v = np.ones(3, int)\n", "print(v, \"= v\")\n", "print(A*v, \"= A*v (aber nicht die Matrixmultiplikation)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[0 3 3]\n", "[0 3 3]\n" ] } ], "source": [ "print(np.matmul(A,v))\n", "print(A@v)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Für viele Probleme ist es sehr hilfreich, nicht den Matrizenkalkül zu verwenden, sondern elementweise arithmetische Operationen auszuführen.\n", "*Broadcasting* ist ein Mechanismus, der diesen elementweisen Kalkül etwas praktischer macht.\n", "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.\n", "\n", "Dazu ist es wirklich hilfreich, einmal [die Dokumentation](https://numpy.org/doc/stable/user/basics.broadcasting.html) zu überfliegen." ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "## ufuncs\n", "\n", "`ufunc` steht für \"universal function\" und bezeichnet eine Methode, die auf Numpy Arrays vektorisiert laufen kann.\n", "\n", "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.\n", "\n", "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).\n", "\n", "Es lohnt sich, einen kurzen Blick auf alle bereits definierten `ufunc`s zu werfen:\n", "[Available ufuncs](https://numpy.org/doc/stable/reference/ufuncs.html#available-ufuncs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Was muss man kennen?\n", "\n", "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": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.2" } }, "nbformat": 4, "nbformat_minor": 4 }