Numpy

  • NumPy steht für Numerical Python und ist die Grundlage für wissenschaftliche Datenverarbeitung
  • NumPy 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]:
from IPython.display import Image
In [2]:
%%javascript
$.getScript('https://kmahelona.github.io/ipython_notebook_goodies/ipython_notebook_toc.js')

Inhalt

Grundlagen

In [3]:
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 [4]:
x_arr = np.array([1, 2, 3, 4, 5])
In [5]:
x_list = [1, 2, 3, 4, 5]

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

In [6]:
2 * x_arr
Out[6]:
array([ 2,  4,  6,  8, 10])
In [7]:
2 * x_list
Out[7]:
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

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

In [8]:
x_arr**2
Out[8]:
array([ 1,  4,  9, 16, 25])
In [9]:
x_arr**x_arr
Out[9]:
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 [10]:
np.cos(x_arr)
Out[10]:
array([ 0.54030231, -0.41614684, -0.9899925 , -0.65364362,  0.28366219])
In [11]:
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 [12]:
%%timeit
x_pure = [42] * 10000
x_pure2 = [x**2 for x in x_pure]
401 μs ± 23.2 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
In [13]:
%%timeit
x = np.full(10000, 42)
x2 = x**2
19.7 μs ± 4.02 μs per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

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

In [14]:
def poly(y):
    return y + 2 * y**2 - y**3


poly(np.pi)
Out[14]:
-8.125475224531307
In [15]:
poly(x_arr)
Out[15]:
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 [16]:
# two-dimensional array
y = np.array(
    [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9],
    ]
)

# element-wise summation, like matrix summation
y + y
Out[16]:
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 [17]:
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 @ 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:
 [[5 4]
 [3 4]]
Skalarprodukt von Vektoren:
 32

Eigenschaften von Arrays

NumPy-Arrays tragen neben den Daten noch zusätzliche Informationen über die Eigenschaften des Arrays.

Die Dimension eines Arrays kann mit der ndim-Funktion abgerufen werden. In NumPy werden die Dimensionen von 0 aufsteigend durchnummeriert. Wird über einzelne Dimensionen eines Arrays gesprochen, werden im NumPy Kontext die Bezeichnungen Achse/Achsen (axis/axes) verwendet. Die Dimension ist also die Anzahl aller Achsen.

Die shape-Funktion gibt in einem Tupel an, wie viele Elemente in jeder 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 [18]:
a = np.array([1.5, 3.0, 4.2])
b = np.array(
    [
        [1, 2, 3],
        [4, 5, 6],
    ]
)

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, 3) 
	 b.size   6 
	 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 [19]:
np.zeros(10)
Out[19]:
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
In [20]:
np.ones((5, 2))
Out[20]:
array([[1., 1.],
       [1., 1.],
       [1., 1.],
       [1., 1.],
       [1., 1.]])
In [21]:
np.linspace(0, 1, 11)
Out[21]:
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])
In [22]:
# like range() for arrays:
np.arange(0, 10)
Out[22]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [23]:
np.logspace(-4, 5, 10)
Out[23]:
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])

Aufgabe 1 kann bearbeitet werden.

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 [24]:
Image(filename="images/Indexing1D.png")
Out[24]:
No description has been provided for this image
In [25]:
x = np.arange(0, 10)
print(x)

# like lists:
x[4]
[0 1 2 3 4 5 6 7 8 9]
Out[25]:
np.int64(4)
In [26]:
# all elements with indices ≥1 and <4:
x[1:4]
Out[26]:
array([1, 2, 3])
In [27]:
# negative indices count from the end
x[-1], x[-2]
Out[27]:
(np.int64(9), np.int64(8))
In [28]:
# combination:
x[3:-2]
Out[28]:
array([3, 4, 5, 6, 7])
In [29]:
# step size
x[::2]
Out[29]:
array([0, 2, 4, 6, 8])
In [30]:
# trick for reversal: negative step
x[::-1]
Out[30]:
array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
In [31]:
y = np.array([x, x + 10, x + 20, x + 30])
y
Out[31]:
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 [32]:
# comma between indices
y[3, 2:-1]
Out[32]:
array([32, 33, 34, 35, 36, 37, 38])
In [33]:
# only one index ⇒ one-dimensional array
y[2]
Out[33]:
array([20, 21, 22, 23, 24, 25, 26, 27, 28, 29])
In [34]:
# other axis: (`:` alone means the whole axis)
y[:, 3]
Out[34]:
array([ 3, 13, 23, 33])
In [35]:
# inspecting the number of elements per axis:
y.shape
Out[35]:
(4, 10)
In [36]:
Image(filename="images/Indexing2D.png")
Out[36]:
No description has been provided for this image

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

In [37]:
y[:, 3] = 0
y
Out[37]:
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 [38]:
y[:, 0] = x[3:7]
y
Out[38]:
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 [39]:
y
Out[39]:
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 [40]:
y.T
Out[40]:
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 [41]:
print(f"y \tShape: {y.shape} \ny.T \tShape: {y.T.shape}")
y 	Shape: (4, 10) 
y.T 	Shape: (10, 4)

Aufgabe 2 kann bearbeitet werden.

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 [42]:
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 [43]:
y
Out[43]:
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 [44]:
np.sum(y)
Out[44]:
np.int64(666)
In [45]:
np.prod(y)
Out[45]:
np.int64(0)
In [46]:
np.prod(y[y != 0])
Out[46]:
np.int64(6652872369767448576)

Bei vielen solchen Methoden kann die Dimension mit angegeben werden.

In [47]:
np.sum(y, axis=1)  # sum of each row
Out[47]:
array([ 45, 126, 207, 288])
In [48]:
np.prod(y, axis=0)  # multiplication of each column
Out[48]:
array([   360,   7161,  16896,      0,  45696,  65625,  89856, 118881,
       153216, 193401])

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

In [49]:
np.mean(y)
Out[49]:
np.float64(16.65)
In [50]:
np.std(y)
Out[50]:
np.float64(12.590770429167549)

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

In [51]:
np.std(x, ddof=1) / np.sqrt(len(x))
Out[51]:
np.float64(0.9574271077563381)

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

In [52]:
np.std(x, ddof=1)
Out[52]:
np.float64(3.0276503540974917)

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

In [53]:
z = x**2
print("z ", z)

np.diff(z)
z  [ 0  1  4  9 16 25 36 49 64 81]
Out[53]:
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 [54]:
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 [55]:
with open("test.txt") 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 eine schönere Formatierung der Daten kann man auch np.column_stack() benutzen.

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

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

with open("test.txt") 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 [57]:
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") 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 [58]:
a, b = np.genfromtxt("test.txt", unpack=True)
a, b
Out[58]:
(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 gezeigt, angegeben werden.

In [59]:
np.savetxt(
    "test.txt",
    np.column_stack([n, x]),
    fmt=["%d", "%.4f"],  # first column integer, second 4 digits float
    delimiter=",",
    header="n,x",
)
In [60]:
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 [61]:
data
Out[61]:
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 [62]:
data["n"], data.shape, data.dtype
Out[62]:
(array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10]),
 (11,),
 dtype([('n', '<i8'), ('x', '<f8')]))

Aufgaben 3, 4 und 5 können bearbeitet werden.