- NumPy steht für Numerical Python und ist die Grundlage für wissenschaftliche Datenverarbeitung und stellt viele optimierte algebraische Methoden zur Verfügung

Motivation:

- Im Praktikum (und allgemein in der Physik) werden Datenpunkte gemessen, die anschließend ausgewertet werden
- NumPy ist eine Python-Bibliothek, die den Umgang mit Datenpunkten enorm vereinfacht

Die Dokumentation ist hier zu finden.

In [1]:

    %%javascript
    $.getScript('https://kmahelona.github.io/ipython_notebook_goodies/ipython_notebook_toc.js')

# Inhalt¶

# Grundlagen¶

In [2]:

    import numpy as np

- Grunddatentyp von NumPy ist das n-dimensionale Array (numpy.ndarray)
- Numpy Arrays speichern Werte eines Datentyps in einem zusammenhängenden Speicherbereich, wodurch mathematische Operationen auf allen Werten des Arrays effizienter sind
- Die Effizienz in den Berechnungen kommt durch NumPys Nutzung von optimiertem C/Cython Code statt purem Python Code

Hier sind einige erste Beispiele zur Nutzung dieser Arrays.

In [3]:

    x_arr = np.array([1, 2, 3, 4, 5])

In [4]:

    x_list = [1, 2, 3, 4, 5]

Arrays verhalten sich nicht wie Listen. Mathematischen Operationen werden komponentenweise auf die Elemente des Arrays angewendet.

In [5]:

    2 * x_arr

Out[5]:

    array([ 2,  4,  6,  8, 10])

In [6]:

    2 * x_list

Out[6]:

    [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

Fast alle mathematischen Operatoren aus Python funktionieren analog mit NumPy Arrays.

In [7]:

    x_arr**2

Out[7]:

    array([ 1,  4,  9, 16, 25])

In [8]:

    x_arr**x_arr

Out[8]:

    array([   1,    4,   27,  256, 3125])

Achtung: Bei besonderen Funktionen (cos, sin, exp, etc.) werden die NumPy Methoden benötigt, z.B. `np.cos()`!

In [9]:

    np.cos(x_arr)

Out[9]:

    array([ 0.54030231, -0.41614684, -0.9899925 , -0.65364362,  0.28366219])

In [10]:

    import math
    # This doesn't work

    #math.cos(x_arr)

Bei großen Datensätzen ist die Laufzeit relevant und NumPy ist einige Größenordnungen schneller:

In [11]:

    %%timeit
    x_pure = [42] * 10000
    x_pure2 = [x**2 for x in x_pure]

    2.58 ms ± 105 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [12]:

    %%timeit
    x = np.full(10000, 42)
    x2 = x**2

    15.7 µs ± 487 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Selbstgeschriebene Funktionen, die nur für eine Zahl geschrieben wurden, funktionieren oft ohne Änderung mit NumPy Arrays.

In [13]:

    def poly(y):
        return y + 2 * y**2 - y**3

    poly(np.pi)

Out[13]:

    -8.125475224531307

In [14]:

    poly(x_arr)

Out[14]:

    array([  2,   2,  -6, -28, -70])

Das erlaubt es einem unter anderem, sehr leicht physikalische Formeln auf seine Datenpunkte anzuwenden.

Arrays können beliebige Dimension haben:

In [15]:

    # two-dimensional array
    y = np.array([[1, 2, 3],
                  [4, 5, 6],
                  [7, 8, 9]])

    # element-wise summation, like matrix summation
    y + y

Out[15]:

    array([[ 2,  4,  6],
           [ 8, 10, 12],
           [14, 16, 18]])

Das erlaubt es z.B. eine ganze Tabelle von gleichen Datentypen als Array abzuspeichern.

Mit Arrays sind auch Matrixoperationen möglich:

In [16]:

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

    # element-wise product
    element_wise_product = A * B
    print("Elementweise Multiplikation:\n", element_wise_product)

    # matrix product
    matrix_product = A.T @ B
    print("Matrix Multiplikation:\n", matrix_product)

    # vector scalar product
    one_dim_vectors = np.array([1, 2, 3]).T @ np.array([4, 5, 6])
    print("Skalarprodukt von Vektoren:\n", one_dim_vectors)

    Elementweise Multiplikation:
     [[2 0]
     [0 4]]
    Matrix Multiplikation:
     [[2 0]
     [5 4]]
    Skalarprodukt von Vektoren:
     32

# Dimension von Arrays¶

In NumPy wird die Dimension auch Achse (axis) genannt und mit der `ndim`-Funktion abgerufen. Die `shape`-Funktion gibt an wie viele Tupel pro Dimension vorhanden sind. Die Gesamtzahl der Elemente in einem Array können mit der `size`-Funktion abgefragt werden. Der Datentyp eines Arrays muss innerhalb des Arrays der gleiche sein. Um den Datentyp eines Arrays abzufragen gibt es die `dtype`-Funktion.

In [17]:

    a = np.array([1.5, 3.0, 4.2])
    b = np.array([[1, 2], [3, 4]])

    print(f'Array a: \n\t a.ndim   {a.ndim} \n\t a.shape  {a.shape} \n\t a.size   {a.size} \n\t a.dtype  {a.dtype}')
    print(f'Array b: \n\t b.ndim   {b.ndim} \n\t b.shape  {b.shape} \n\t b.size   {b.size} \n\t b.dtype  {b.dtype}')

    Array a:
    	 a.ndim   1
    	 a.shape  (3,)
    	 a.size   3
    	 a.dtype  float64
    Array b:
    	 b.ndim   2
    	 b.shape  (2, 2)
    	 b.size   4
    	 b.dtype  int64

# Erstellen von Arrays¶

Es gibt viele nützliche Funktionen, die bei der Erstellung von Arrays helfen. Zum Verständnis der einzugebenden Argumente ist die NumPy Dokumentation zu empfehlen.

In [18]:

    np.zeros(10)

Out[18]:

    array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])

In [19]:

    np.ones((5, 2))

Out[19]:

    array([[1., 1.],
           [1., 1.],
           [1., 1.],
           [1., 1.],
           [1., 1.]])

In [20]:

    np.linspace(0, 1, 11)

Out[20]:

    array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])

In [21]:

    # like range() for arrays:
    np.arange(0, 10)

Out[21]:

    array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [22]:

    np.logspace(-4, 5, 10)

Out[22]:

    array([1.e-04, 1.e-03, 1.e-02, 1.e-01, 1.e+00, 1.e+01, 1.e+02, 1.e+03,
           1.e+04, 1.e+05])

# NumPy Indexing¶

NumPy erlaubt einem sehr bequem bestimmte Elemente aus einem Array auszuwählen und z.B. nur auf diesen Elementen Operationen auszuführen.

In [23]:

    x = np.arange(0, 10)
    print(x)

    # like lists:
    x[4]

    [0 1 2 3 4 5 6 7 8 9]

Out[23]:

    4

In [24]:

    # all elements with indices ≥1 and <4:
    x[1:4]

Out[24]:

    array([1, 2, 3])

In [25]:

    # negative indices count from the end
    x[-1], x[-2]

Out[25]:

    (9, 8)

In [26]:

    # combination:
    x[3:-2]

Out[26]:

    array([3, 4, 5, 6, 7])

In [27]:

    # step size
    x[::2]

Out[27]:

    array([0, 2, 4, 6, 8])

In [28]:

    # trick for reversal: negative step
    x[::-1]

Out[28]:

    array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

In [29]:

    y = np.array([x, x + 10, x + 20, x + 30])
    y

Out[29]:

    array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
           [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
           [20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
           [30, 31, 32, 33, 34, 35, 36, 37, 38, 39]])

In [30]:

    # comma between indices
    y[3, 2:-1]

Out[30]:

    array([32, 33, 34, 35, 36, 37, 38])

In [31]:

    # only one index ⇒ one-dimensional array
    y[2]

Out[31]:

    array([20, 21, 22, 23, 24, 25, 26, 27, 28, 29])

In [32]:

    # other axis: (`:` alone means the whole axis)
    y[:, 3]

Out[32]:

    array([ 3, 13, 23, 33])

In [33]:

    # inspecting the number of elements per axis:
    y.shape

Out[33]:

    (4, 10)

Ausgewählten Elementen kann man auch direkt einen Wert zuweisen.

In [34]:

    y[:, 3] = 0
    y

Out[34]:

    array([[ 0,  1,  2,  0,  4,  5,  6,  7,  8,  9],
           [10, 11, 12,  0, 14, 15, 16, 17, 18, 19],
           [20, 21, 22,  0, 24, 25, 26, 27, 28, 29],
           [30, 31, 32,  0, 34, 35, 36, 37, 38, 39]])

Man kann Indexing auch gleichzeitig auf der linken und rechten Seite benutzen.

In [35]:

    y[:,0] = x[3:7]
    y

Out[35]:

    array([[ 3,  1,  2,  0,  4,  5,  6,  7,  8,  9],
           [ 4, 11, 12,  0, 14, 15, 16, 17, 18, 19],
           [ 5, 21, 22,  0, 24, 25, 26, 27, 28, 29],
           [ 6, 31, 32,  0, 34, 35, 36, 37, 38, 39]])

Transponieren des Arrays kehrt die Reihenfolge der Indizes um.

In [36]:

    y

Out[36]:

    array([[ 3,  1,  2,  0,  4,  5,  6,  7,  8,  9],
           [ 4, 11, 12,  0, 14, 15, 16, 17, 18, 19],
           [ 5, 21, 22,  0, 24, 25, 26, 27, 28, 29],
           [ 6, 31, 32,  0, 34, 35, 36, 37, 38, 39]])

In [37]:

    y.T

Out[37]:

    array([[ 3,  4,  5,  6],
           [ 1, 11, 21, 31],
           [ 2, 12, 22, 32],
           [ 0,  0,  0,  0],
           [ 4, 14, 24, 34],
           [ 5, 15, 25, 35],
           [ 6, 16, 26, 36],
           [ 7, 17, 27, 37],
           [ 8, 18, 28, 38],
           [ 9, 19, 29, 39]])

In [38]:

    print(f"y \tShape: {y.shape} \ny.T \tShape: {y.T.shape}")

    y 	Shape: (4, 10)
    y.T 	Shape: (10, 4)

# Masken¶

Oft will man Elemente auswählen, die eine oder mehrere Bedingungen erfüllen. Hierzu wird eine Maske (Array aus True/False-Werten) mit der gleichen Dimension erstellt. Die Maske kann in eckigen Klammern übergeben werden.

In [39]:

    a = np.linspace(0, 2, 11)
    print(a)

    # create a mask with the condition: element >= 1
    mask = a >= 1
    print(mask)

    print(a[mask])
    # do it in one step:
    print(a[a>=1])

    [0.  0.2 0.4 0.6 0.8 1.  1.2 1.4 1.6 1.8 2. ]
    [False False False False False  True  True  True  True  True  True]
    [1.  1.2 1.4 1.6 1.8 2. ]
    [1.  1.2 1.4 1.6 1.8 2. ]

# Reduzieren von Arrays¶

Viele Rechenoperationen reduzieren ein Array auf einen einzelnen Wert.

In [40]:

    y

Out[40]:

    array([[ 3,  1,  2,  0,  4,  5,  6,  7,  8,  9],
           [ 4, 11, 12,  0, 14, 15, 16, 17, 18, 19],
           [ 5, 21, 22,  0, 24, 25, 26, 27, 28, 29],
           [ 6, 31, 32,  0, 34, 35, 36, 37, 38, 39]])

So z.B. die Summe aller Elemente oder die Multiplikation.

In [41]:

    np.sum(y)

Out[41]:

    666

In [42]:

    np.prod(y)

Out[42]:

    0

Bei vielen solchen Methoden kann die Dimension mit angegeben werden.

In [43]:

    np.sum(y, axis=1)  # sum of each row

Out[43]:

    array([ 45, 126, 207, 288])

In [44]:

    np.prod(y, axis=0) # multiplication of each column

Out[44]:

    array([   360,   7161,  16896,      0,  45696,  65625,  89856, 118881,
           153216, 193401])

Auch Mittelwert und Standardabweichung der Einträge kann einfach bestimmt werden.

In [45]:

    np.mean(y)

Out[45]:

    16.65

In [46]:

    np.std(y)

Out[46]:

    12.590770429167549

Oft wird im Praktikum aber nach der Unsicherheit des Mittelwerts gesucht.

In [47]:

    np.std(x, ddof=1) / np.sqrt(len(x))

Out[47]:

    0.9574271077563381

Dafür braucht man auch den Schätzer der Standardabweichung.

In [48]:

    np.std(x, ddof=1)

Out[48]:

    3.0276503540974917

Um die Differenzen zwischen benachbarten Elementen herauszufinden kann die Funktion `np.diff()` genutzt werden.

In [49]:

    z = x**2
    print('z ', z)

    np.diff(z)

    z  [ 0  1  4  9 16 25 36 49 64 81]

Out[49]:

    array([ 1,  3,  5,  7,  9, 11, 13, 15, 17])

# Input / Output¶

Um Datenpunkte aus einer Textdatei einzulesen wird die Funktion `np.genfromtxt()` genutzt. Sie gibt den Inhalt einer Textdatei als Array zurück.

Die Funktion die Datenpunkte in eine Datei abspeichert ist `np.savetxt()`.

In [50]:

    n = np.arange(11)
    x = np.linspace(0, 1, 11)

    np.savetxt('test.txt', [n, x])

Um den Inhalt der erstellten Datei zu öffnen kann man analog zu Aufgabe `1-python/6-readwrite` die `open`-Funktion benutzen.

In [51]:

    with open('test.txt', 'r') as f:
        print(f.read())

    0.000000000000000000e+00 1.000000000000000000e+00 2.000000000000000000e+00 3.000000000000000000e+00 4.000000000000000000e+00 5.000000000000000000e+00 6.000000000000000000e+00 7.000000000000000000e+00 8.000000000000000000e+00 9.000000000000000000e+00 1.000000000000000000e+01
    0.000000000000000000e+00 1.000000000000000056e-01 2.000000000000000111e-01 3.000000000000000444e-01 4.000000000000000222e-01 5.000000000000000000e-01 6.000000000000000888e-01 7.000000000000000666e-01 8.000000000000000444e-01 9.000000000000000222e-01 1.000000000000000000e+00

Für schönere Formatierung der Daten kann man auch `np.column_stack()` benutzen.

In [52]:

    data = np.array([n, x])

    np.savetxt('test.txt', np.column_stack([n, x]))

    with open('test.txt', 'r') as f:
        print(f.read())

    0.000000000000000000e+00 0.000000000000000000e+00
    1.000000000000000000e+00 1.000000000000000056e-01
    2.000000000000000000e+00 2.000000000000000111e-01
    3.000000000000000000e+00 3.000000000000000444e-01
    4.000000000000000000e+00 4.000000000000000222e-01
    5.000000000000000000e+00 5.000000000000000000e-01
    6.000000000000000000e+00 6.000000000000000888e-01
    7.000000000000000000e+00 7.000000000000000666e-01
    8.000000000000000000e+00 8.000000000000000444e-01
    9.000000000000000000e+00 9.000000000000000222e-01
    1.000000000000000000e+01 1.000000000000000000e+00

Am besten sollte aber auch immer erklären werden, was abspeichert wird:

In [53]:

    n = np.arange(11)
    x = np.linspace(0, 1, 11)

    # header schreibt eine Kommentarzeile in die erste Zeile der Datei
    np.savetxt('test.txt', np.column_stack([n, x]), header="n x")
    with open('test.txt', 'r') as f:
        print(f.read())

    # n x
    0.000000000000000000e+00 0.000000000000000000e+00
    1.000000000000000000e+00 1.000000000000000056e-01
    2.000000000000000000e+00 2.000000000000000111e-01
    3.000000000000000000e+00 3.000000000000000444e-01
    4.000000000000000000e+00 4.000000000000000222e-01
    5.000000000000000000e+00 5.000000000000000000e-01
    6.000000000000000000e+00 6.000000000000000888e-01
    7.000000000000000000e+00 7.000000000000000666e-01
    8.000000000000000000e+00 8.000000000000000444e-01
    9.000000000000000000e+00 9.000000000000000222e-01
    1.000000000000000000e+01 1.000000000000000000e+00

In [54]:

    a, b = np.genfromtxt('test.txt', unpack=True)
    a, b

Out[54]:

    (array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.]),
     array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ]))

Um die Datentypen beim Speichern zu erhalten, muss das Keyword Argument `fmt` wie im Folgenden Beispiel angegeben werden.

In [55]:

    np.savetxt(
        'test.txt',
        np.column_stack([n, x]),
        fmt=['%d', '%.4f'],       # first column integer, second 4 digits float
        delimiter=',',
        header='n,x',
    )

In [56]:

    data = np.genfromtxt(
        'test.txt',
        dtype=None,    # guess data types
        delimiter=',',
        names=True,
    )

Das resultierende Array `data` ist besonders, da es ein sogenanntes `structured array` ist. Dies ist ein Numpy Array in dem quasi mehrere Arrays in einem abgespeichert sind. Die einzelnen Arrays werden in der Dokumentation `fields` genannt und haben jeweils einen zugeordneten Namen und einen Datentyp.

In [57]:

    data

Out[57]:

    array([( 0, 0. ), ( 1, 0.1), ( 2, 0.2), ( 3, 0.3), ( 4, 0.4), ( 5, 0.5),
           ( 6, 0.6), ( 7, 0.7), ( 8, 0.8), ( 9, 0.9), (10, 1. )],
          dtype=[('n', '<i8'), ('x', '<f8')])

In [58]:

    data['n'], data.shape, data.dtype

Out[58]:

    (array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10]),
     (11,),
     dtype([('n', '<i8'), ('x', '<f8')]))

In [ ]: