Skip to content
Snippets Groups Projects
Commit c5b9dd46 authored by Konrad Völkel's avatar Konrad Völkel
Browse files

incorporating feedback

parent 357c2586
No related branches found
No related tags found
No related merge requests found
%% 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:
https://www.python-kurs.eu/python_numpy_wahrscheinlichkeit.php
-->
Numeric Python (Numpy) wird meist als `np` abgekürzt importiert:
%% Cell type:code id: tags:
``` python
import numpy as np
sample = np.random.random(10)
print(sample)
print(type(sample))
sample
```
%% Output
[0.42327034 0.18083199 0.98939518 0.25440161 0.34240241 0.37819465
0.11030633 0.9302959 0.07195987 0.18855029]
[0.49929771 0.59160776 0.77432696 0.80597013 0.18907085 0.11844771
0.70945366 0.87172152 0.8522308 0.42780462]
<class 'numpy.ndarray'>
array([0.42327034, 0.18083199, 0.98939518, 0.25440161, 0.34240241,
0.37819465, 0.11030633, 0.9302959 , 0.07195987, 0.18855029])
array([0.49929771, 0.59160776, 0.77432696, 0.80597013, 0.18907085,
0.11844771, 0.70945366, 0.87172152, 0.8522308 , 0.42780462])
%% Cell type:markdown id: tags:
Viele nützliche Hilfsfunktionen sind in Numpy enthalten, die wiederum Numpy-eigene Datenstrukturen (den Array) verarbeiten.
%% Cell type:code id: tags:
``` python
sample = np.random.randint(low=0, high=10, size=5000)
print("mean =", np.mean(sample), "\nexpected=", 9/2)
print("std² =", np.std(sample)**2, "\nvariance=", 99/12)
```
%% Output
mean = 4.4748
mean = 4.529
expected= 4.5
std² = 8.27496496
std² = 8.260759
variance= 8.25
%% Cell type:markdown id: tags:
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`).
%% Cell type:code id: tags:
``` python
import timeit
ordinary_list = [1,2,3,4,5,6,5,4,3,2,1]*10
def sort_array(dtype):
a = np.array(ordinary_list, dtype)
a.sort()
print(timeit.timeit(lambda : sort_array(np.byte)))
print(timeit.timeit(lambda : sort_array(np.float64)))
```
%% Output
3.9101130639901385
4.628806502034422
4.177020760005689
5.418045660000644
%% Cell type:markdown id: tags:
Dadurch, dass der Datentyp präzise bekannt ist, kann Numpy darauf optimierte Algorithmen, direkt in C implementiert, verwenden.
Was es noch für `dtype`s gibt und wie sie eingesetzt werden, können wir der Dokumentation entnehmen:
* ["Structured Arrays"](https://numpy.org/doc/stable/user/basics.rec.html)
* ["Data types" in den "Numpy fundamentals"](https://numpy.org/doc/stable/user/basics.types.html)
* ["Scalars"](https://numpy.org/doc/stable/reference/arrays.scalars.html)
* ["Data type objects (dtype)"](https://numpy.org/doc/stable/reference/arrays.dtypes.html)
* ["numpy.dtype"](https://numpy.org/doc/stable/reference/generated/numpy.dtype.html)
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
assert npmatrix[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.
%% Cell type:code id: tags:
``` python
matrix = list(range(1,10))
npmatrix = np.array(matrix)
print("not in shape:", npmatrix)
npmatrix.shape = (3,3)
print("in much better shape:\n"+ str(npmatrix))
print("Zeilenvektor Zeile 0:", npmatrix[0])
print("Spaltenvektor Spalte 0:", npmatrix[:,0])
print("Spalten 1-2:\n"+ str(npmatrix[:,1:]))
print("Alle Zeilen, Schrittweite 2\n"+ str(npmatrix[0::2]))
```
%% Output
not in shape: [1 2 3 4 5 6 7 8 9]
in much better shape:
[[1 2 3]
[4 5 6]
[7 8 9]]
Zeilenvektor Zeile 0: [1 2 3]
Spaltenvektor Spalte 0: [1 4 7]
Spalten 1-2:
[[2 3]
[5 6]
[8 9]]
Alle Zeilen, Schrittweite 2
[[1 2 3]
[7 8 9]]
%% Cell type:markdown id: tags:
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:
[Available ufuncs](https://numpy.org/doc/stable/reference/ufuncs.html#available-ufuncs)
......
%% Cell type:markdown id:04598fd5-c190-479e-a252-457a3ed02c52 tags:
# Stochastik programmieren
%% Cell type:markdown id:9b4163a3-d84b-42ec-b927-6888e3bc159b tags:
Wir wollen uns nun kurz damit beschäftigen, wie sich mit Python Stichproben (en. Samples) von Zufallsexperimenten simulieren lassen.
Verwendete Python-Konstrukte: [Module/import](https://docs.python.org/3/tutorial/modules.html), [Methoden/def](https://docs.python.org/3/tutorial/controlflow.html#defining-functions), [Länge/len](https://docs.python.org/3/library/functions.html#len), [Mengen/set](https://docs.python.org/3/tutorial/datastructures.html#sets), [Generator-Ausdruck](https://docs.python.org/3/tutorial/classes.html#generator-expressions), [Range](https://docs.python.org/3/tutorial/controlflow.html#the-range-function), [Assertion/assert](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement)
## Wahrscheinlichkeitsmaß implementieren
%% Cell type:code id:1b9aa059-0486-4d8b-aff3-37a5f9cbb5d8 tags:
``` python
from fractions import Fraction
def P(A, Omega):
"""Die Wahrscheinlichkeit für das Ereignis A,
gegeben gleich wahrscheinliche Ergebnisse aus einem Ergebnisraum Ω."""
return Fraction(len(A and Omega), len(Omega))
return Fraction(len(A & Omega), len(Omega))
```
%% Cell type:markdown id:2080149c-7800-4728-acb3-579a89f46971 tags:
Wir werden damit nun einen Würfelwurf programmieren:
%% Cell type:code id:a03a347d-86fb-4098-98ba-c7c1cd3ed715 tags:
``` python
W = {1, 2, 3, 4, 5, 6}
gerade = set((x*2 for x in range(1,4)))
print("Gerade Würfelaugen:",gerade)
print("Wahrscheinlichkeit für gerade Augenzahl:", P(gerade, W))
```
%% Output
Gerade Würfelaugen: {2, 4, 6}
Wahrscheinlichkeit für gerade Augenzahl: 1
Wahrscheinlichkeit für gerade Augenzahl: 1/2
%% Cell type:markdown id:1a17807c-4adb-4482-8836-1a892b6af8d6 tags:
Damit haben wir nun das Wahrscheinlichkeitsmaß. Wenn wir eine Stichprobe ziehen wollen, müssen wir noch irgendwo den "Zufall" her bekommen.
## Zufall importieren
Python bietet mit dem `random`-Modul eine Schnittstelle zu Pseudozufallszahlen. Die Methode `random.random` ist ein direkt in C implementierter [Mersenne Twister](https://de.wikipedia.org/wiki/Mersenne-Twister). Wenn man "echte" Zufallszahlen braucht, etwa für kryptografische Zwecke, gibt es dazu das `secrets`-Modul. Den Seed für den Mersenne Twister kann man angeben, und sollte man auch, um Zufallssimulationen reproduzierbar zu machen.
%% Cell type:code id:05a3b6f7-e7b6-4034-b3f9-807b20b47d6c tags:
``` python
from random import random as r
help(r)
print(r())
print("1", r())
print("2", r())
import random
random.seed(1)
very_random = r()
print(very_random)
print("3", very_random)
assert very_random == 0.13436424411240122
print("4", r())
random.seed(1)
print("5", r())
```
%% Output
Help on built-in function random:
random() method of random.Random instance
random() -> x in the interval [0, 1).
0.19357801680866282
0.13436424411240122
1 0.5636526399887509
2 0.5708891871271717
3 0.13436424411240122
4 0.8474337369372327
5 0.13436424411240122
%% Cell type:markdown id:c862bd54-0c4f-4100-8654-4e86a6e3d3e1 tags:
![XKCD 221: Random Number](images/random_number.png "RFC 1149.5 specifies 4 as the standard IEEE-vetted random number.")
[Link zum Comic (Randall Munroe, CC-BY-NC 2.5)](https://xkcd.com/221)
Aus einer (Pseudo)zufallszahl zwischen $0$ und $1$ (man beachte: evtl. $0$ aber nie $1$) lassen sich zufällige Würfelwürfe erzeugen:
%% Cell type:code id:62096c2f-6911-4c7a-b243-b8661e83ddb3 tags:
``` python
from math import floor
def transform_unit_to_dice(x):
return floor(1 + 6*x)
assert list(range(1,7)) == [transform_unit_to_dice((x-1)/6)
for x in range(1,7)]
N = 100
würfe = [transform_unit_to_dice(r()) for n in range(N)]
print(würfe)
```
%% Output
[6, 5, 2, 3, 3, 4, 5, 1, 1, 6, 3, 5, 1, 3, 5, 2, 6, 6, 1, 1, 4, 6, 3, 2, 3, 1, 2, 3, 3, 2, 2, 2, 3, 2, 1, 6, 4, 4, 2, 6, 6, 1, 2, 5, 5, 6, 3, 5, 5, 2, 4, 6, 6, 4, 4, 1, 2, 5, 3, 2, 4, 5, 5, 3, 3, 4, 5, 4, 3, 3, 1, 1, 5, 6, 4, 3, 2, 4, 6, 5, 4, 6, 2, 4, 6, 4, 3, 2, 4, 6, 1, 5, 5, 6, 5, 5, 4, 4, 3, 1]
%% Cell type:markdown id:beac5e48-9b2d-410b-9ddd-a9898347d71b tags:
Damit man solche Transformationen nicht andauernd programmieren muss, kann man hier auch auf `random.randint(1,6)` oder auch auf `random.choice(range(1,7))` oder `random.randrange(1,7)` zurückgreifen. Dabei ist `randint` ein Kürzel für das entsprechende `randrange` und `choice` ist etwas allgemeiner.
Wir wollen aber festhalten: gegeben eine gleichverteilte "Zufallsvariable" $X=$`random.random` mit Werten in $[0,1)$ haben wir eine Abbildung $t =$`transform_unit_to_dice` konstruiert und implementiert, die Werte in $\Omega := \{1,2,3,4,5,6\}$ hat und $t(X)$ ist gleichverteilt. Die mathematische Abbildung $t \colon [0,1) \to \Omega$ ist eine Zufallsvariable, wir behandeln die [Verknüpfung](https://de.wikipedia.org/wiki/Komposition_(Mathematik)) $t \circ X$ als Zufallsvariable, die den Würfel modelliert.
Nun könnte man sich beschweren: `random.random()` nimmt gar keinen Parameter, ist also keine mathematische Abbildung von einem Definitionsbereich in die Menge $[0,1)$. Tatsächlich müssen wir uns vorstellen, dass es eine Abbildung $X \colon \Omega' \to [0,1)$ ist, und auf $\Omega'$ ein irgendwie geartetes Wahrscheinlichkeitsmaß definiert ist, sodass durch $X$ auf $[0,1)$ die Gleichverteilung induziert wird. Die Menge $\Omega'$ spielt für uns keine konkrete Rolle - da "kommt der Zufall her" und in der Notation `random.random()` sehen wir schon, dass wir eben kein konkretes Element von $\Omega'$ einsetzen, sondern pseudozufällig eins ziehen und das in $X$ einsetzen.
%% Cell type:markdown id:77aab03c-2b4f-4e83-bf3c-bc7b8cb3d5f4 tags:
## Größere Stichproben
%% Cell type:markdown id:d2571d1b-b44c-436d-a72f-f8385a1e4255 tags:
Die Methode `random.sample(population, k)` erlaubt es eine Stichprobe der Größe $k$ aus einer Population (einer Urne) zu ziehen - ziehen mit Zurücklegen. Für $k=1$ entspricht das einer Gleichverteilung auf der Population.
Mit der Methode `random.choices(population, weights=None, *, cum_weights=None, k=1)` kann man das ziehen aus der Population $k$-mal sampeln (und dabei anstelle einer geeigneten Population auch Gewichte vergeben).
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment