NumPy

NumPy

Library zur effizienten Datenverarbeitung

Daten sind in mehrdimensionalen Arrays von Zahlen gespeichert, die resourcenschonend umgesetzt sind:

  • kleinerer Speicherverbrauch als z.B. Listen von Zahlen in Python
  • deutlich schnelleres Ausführen von z.B. elementweiser Addition zweier Arrays

Daten können z.B. Bilder, Tondateien, Messwerte und vieles anderes repräsentieren

Importieren von NumPy

oft verkürzt als:

import numpy as np

Arrays

Erstellen eines 1-dimensionalen Arrays:

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

Arrays

Erstellen eines 2-dimensionalen Arrays:

a2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

Ausgabe:

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

Arrays

Erstellen eines 3-dimensionalen Arrays:

a3d = np.array([[[1, 2], [3, 4]], [[5, 6], [7,8]]])

Ausgabe:

array([[[1, 2],
        [3, 4]],

       [[5, 6],
        [7, 8]]])

NumPy Arrays vs Python Listen

Arrays sind im Hintergrund in C implementiert, die numerischen Einträge (z.B. Integer) sind keine Python-Objekte und damit resourcenschonender.

NumPy Arrays vs Python Listen

Python-Liste (referenziert Integer-Objekte):

list_a = [1, 2, 3, 4]

NumPy Array (Daten sind im Array enthalten ohne auf externe Objekte zu verweisen):

array_a = np.array(list_a)

Schnelle elementweise Operation (in C implementiert):

array_a * array_a

NumPy Arrays vs Python Listen

Ãœbung:

Vergleiche die Ausführungszeit einer Operation in reinem Python und in NumPy mittels time.perf_counter()

z.B. berechne die Quadratwurzeln aller Zahlen von 0 bis 1 000 000

Array Shape

Wir können folgendes abfragen:

  • a3d.shape: (2, 2, 2)
  • a3d.ndim: 3
  • a3d.size: 8

Mehr als ein Weg

Mehr als ein Weg

aus dem Zen of Python:

There should be one-- and preferably only one --obvious way to do it.

diese Philosophie wird bei NumPy oft nicht angewendet

Mehr als ein Weg

Beispiel: Transponieren eines Arrays

a2d.T
a2d.transpose()
np.transpose(a2d)

NumPy Funktionen vs Array-Methoden

viele Operationen sind auf zwei Arten verfügbar:

  • Funktionen im numpy-Paket
  • Methoden der array-Klasse

wir werden meist Funktionen verwenden

NumPy Funktionen vs Array-Methoden

verfügbar als Funktionen und Methoden:

np.max(a2d)
a2d.max()
np.round(a2d)
a2d.round()

nur als Funktionen verfügbar:

np.sin(a2d)
np.exp(a2d)
np.expand_dims(a2d, 2)

Arrays erstellen

Arrays erstellen

Ein Array der Größe 2x6, gefüllt mit Nullen:

np.zeros((2, 6))
# or
np.full((2, 6), 0.0)

Arrays erstellen

Zahlenfolgen erstellen:

np.linspace(0, 1.0, 11)
# [0.0, 0.1, ... 1.0]
np.arange(0, 3.14, 0.1)
# [0.0, 0.1, ... 3.1]

Arrays erstellen

Ein 2x2 Array mit Zufallswerten:

# create a random number generator
rng = np.random.default_rng(seed=1)

# floats between 0 and 1:
rng.random((2, 2))
# integers between 1 and 6:
rng.integers(1, 7, (2, 2))

älteres Interface: np.random.random() und np.random.randint()

Auswählen von Array-Einträgen

Auswählen von Array-Einträgen

a1d[0] # 0
a2d[0, 1] # 2
a2d[0, :] # [1, 2, 3]
a2d[:, 0] # [1, 4, 7]

bei 2D-Arrays: [Zeilenindex, Spaltenindex]

im Allgemeinen:

  • letzter Index: zählt richtung rechts
  • vorletzter Index (sofern er existiert): zählt richtung unten

Auswählen von Array-Einträgen

a2d[0, :] # [1, 2, 3]

Kurzform:

a2d[0] # [1, 2, 3]

Slices

a1d[:3] # [0, 1, 2]
a1d[3:6] # [3, 4, 5]
a1d[6:] # [6, 7, 8, 9]
a1d[0:8:2] # [0, 2, 4, 6]
a1d[3:0:-1] # [3, 2, 1]
a1d[::-1] # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
a2d[1:, :] # [[5, 6, 7], [8, 9, 10]]

gleiches funktioniert mit Python-Listen

Operationen auf Arrays

Operatoren

Operatoren werden elementweise angewendet:

a = np.array([0, 1, 2, 3])
b = np.array([2, 2, 2, 2])

-a
# np.array([0, -1, -2, -3])
a + b
# np.array([2, 3, 4, 5])
a * b
# np.array([0, 2, 4, 6])

Vergleiche

Elementweises Vergleichen von Arrays:

a < b
# np.array([True, True, False, False])
a == b
# np.array([False, False, True, False])

Achtung: a == b kann nicht sinnvoll in if-Abfragen verwendet werden - verwende np.array_equal(a, b).

Operatoren

Auch mit einzelnen Zahlen möglich (broadcasting):

print(a + 1)
# np.array([1, 2, 3, 4])

Einige Konstanten sind direkt in NumPy verfügbar:

print(a + np.pi)
print(a + np.e)
print(np.nan)

Elementweise Funktionen

NumPy bietet spezielle Funktionen, die elementweise angewendet werden:

print(np.sin(a))
# [0.0, 0.84147098, 0.9... ]
print(np.sqrt(a))
# [0.0, 1.0, 1.414... ]

Elementweise Funktionen

  • abs
  • sin
  • cos
  • sqrt
  • exp
  • log
  • log10
  • round
  • ...

Aggregationen

Aggregationen berechnen beispielsweise Werte zu jeder Zeile / jeder Spalte oder zu einem ganzen Array

Gesamtsumme:

np.sum(a2d)

Summe entlang Achse 0 ("richtung unten")

np.sum(a2d, axis=0)

Summe entlang Achse 1 ("richtung rechts")

np.sum(a2d, axis=1)

Aggregationen

  • sum
  • min
  • max
  • std
  • percentile

Ãœbungen

(siehe nächste Slides)

  • Preise und Mengen → Gesamtpreis
  • kinetic energy
  • Schwerpunkt eines Dreiecks
  • Sinus- und Kosinusfunktion - Wertetabelle
  • dice rolls

Ãœbungen

Gegeben sind ein Array von Preisen und ein Array von gekauften Mengen. Bestimme den Gesamtpreis:

prices = np.array([3.99, 4.99, 3.99, 12.99])
# buying the first item 3 times and the last item 2 times
quantities = np.array([3, 0, 0, 2])

# solution: 37.95

Ãœbungen

Gegeben sind die Massen und Geschwindigkeiten einiger Körper; bestimme die kinetische Energie aller einzelnen Körper und die gesamte kinetische Energie aller Körper zusammen

masses = np.array([1.2, 2.2, 1.5, 2.0])
velocities = np.array([12.0, 14.0, 14.0, 7.5])

Formel: E = m*v^2 / 2

Ãœbungen

Gegeben sind die Koordinaten von Eckpunkten eines Dreiecks. Bestimme den Schwerpunkt (arithmetisches Mittel der Eckpunkte).

a = np.array([5, 1])
b = np.array([6, 8])
c = np.array([1, 3])

# solution: [4, 4]

Ãœbungen

Erstelle eine Wertetabelle für Sinus- und Kosinusfunktion im Intervall von 0 bis 2*pi.

Resultat:

# x, sin(x), cos(x)
np.array([[0.0, 0.01, 0.02, ...],
          [0.0, 0.0099998, 0.0199999, ...],
          [1.0, 0.99995, 0.99980, ...]])

Überprüfe anhand der Daten, ob näherungsweise gilt: sin(x)^2 + cos(x)^2 = 1

Ãœbungen

Simuliere 1 Million Mal würfeln mit je 10 Würfeln

Boolean Indexing

Boolean Indexing (Langform)

a = np.array([4.1, 2.7, -1, 3.8, -1])

a_valid = a > 0
# array([True, True, False, True, False])
a_filtered = a[a_valid]
# array([4.1, 2.7, 3.8])

a_invalid = a < 0
a_with_nans = np.copy(a)
a_with_nans[a_invalid] = np.nan
# array([4.1, 2.7, nan, 3.8, nan])

Boolean Indexing (Kurzform)

a = np.array([4.1, 2.7, -1, 3.8, -1])

a_filtered = a[a >= 0]

a_with_nans = np.copy(a)
a_with_nans[a_with_nans < 0] = np.nan

Numerische Typen

Numerische Typen

  • int
  • float
  • decimal

Int

ein int8 besteht aus 8 bits und kann 2^8 (256) verschiedene Zahlen darstellen

Anzahl an darstellbaren Zahlen:

  • int8: 256 (-128 bis +127)
  • int16: 65,536 (-32,768 bis +32,767)
  • int32: 4,294,967,296
  • int64: 18,446,744,073,709,551,616

Int

Ein unsigned integer (uint) kann nur nicht-negative Zahlen repräsentieren

z.B. bei uint8: 0 bis 255

Float

Standard für die Repräsentation reeller Zahlen in Computern: IEEE 754

  • binäre Gleitkommazahlen
  • dezimale Gleitkommazahlen

Float

wichtige Gleitkommatypen:

  • float32 (single): exakt für ~7 Dezimalstellen
  • float64 (double): exakt für ~16 Dezimalstellen

Float

Rundungsfehler: manche Zahlen können nicht als Gleitkommazahlen dargestellt werden, sie sind immer Annäherungen

Beispiele im Dezimalsystem: 1/3, 1/7, π

Beispiele im Binärsystem (floats): 1/10, 1/5, 1/3, π

Beispiel: π + π ergibt 6.2, wenn wir Dezimalzahlen mit 2 Stellen verwenden (besseres Ergebnis wäre 6.3)

Beispiel: 0.1 + 0.2 ergibt ~ 0.30000000000000004, wenn wir 64-bit floats verwenden

Float

manche Operationen führen zu einem Verlust von Genauigkeit - z.B. Subtrahieren von nahe beieinanderliegenden Zahlen

Beispiel:

a = 0.001234567 (7 relevante Dezimalstellen)
b = 0.001234321 (7 relevante Dezimalstellen)

c = a - b
c = 0.000000246 (3 relevante Dezimalstellen)

Float

Besondere Werte in IEEE 754:

  • inf und -inf (unendliche Werte)
  • nan (not-a-number: undefinierter / unbekannter Wert)

Floats in IEEE 754

Floats in IEEE 754

Speicherformat:

(-) 2^e * s
  • e ... Exponent
  • s ... Signifikand / Koeffizient

Beispiele

pi als float32:

0 10000000 10010010000111111011011

2*pi als float32:

0 10000001 10010010000111111011011

pi/2 als float32:

0 01111111 10010010000111111011011

Beispiele

Die Zahlen 0.20000000, 0.20000001, ... 0.20000005 als nächstgelegene float32 ausgedrückt:

  • 0 01111100 10011001100110011001101
  • 0 01111100 10011001100110011001101
  • 0 01111100 10011001100110011001110
  • 0 01111100 10011001100110011001111
  • 0 01111100 10011001100110011001111
  • 0 01111100 10011001100110011010000

Beispiele

Avogadro-Konstante (6.02214076 * 10^23):

0 11001101 11111110000110001000001

Planck-Länge (1.61625518 * 10^-35):

0 00001011 01010111101011110110100

Overflow und Underflow

größte float32-Zahl:

0 11111110 11111111111111111111111

~ 2^127.9999 ~ 3.403e38

kleinste positive float32 Zahl mit voller Präzision:

0 00000001 00000000000000000000000

= 2^-126 ~ 1.175e-36

größere Zahlen ergeben inf

kleinere Zahlen verlieren Genauigkeit oder ergeben 0.0

Besondere Werte

inf: 0 11111111 00000000000000000000000

nan: 0 11111111 00000000000000000000001

Array Typen

Array Typen

Jedes NumPy Array kann nur Daten eines Typs enthalten (z.B. nur 64-bit floats oder nur bytes)

Array Typen

Jedes Array hat einen vorgegebenen Datentyp für alle Einträge

a = np.array([1])
a.dtype # int32
b = np.array([1.0])
b.dtype # float64
c = np.array(['abc'])
c.dtype # <U3
d = np.array([b'abc'])
d.dtype # |S3

Array Typen

Typen können explizit angegeben werden:

a = np.array([1, 2, 3, 4], dtype='int64')
b = np.array([1, 2, 3, 4], dtype='uint8')

Typen werden wenn möglich automatisch umgewandelt:

c = a + b
c.dtype # int64

Array Typen

wichtige Typen:

  • bool / bool_ (Speicherverbrauch 8 Bit)
  • int8, int16, int32, int64
  • uint8, uint16, uint32, uint64
  • float16, float32, float64

Float Typen

Genauigkeit für float Typen:

  • flaot16: ~3 Dezimalstellen
  • float32: ~7 Dezimalstellen
  • float64: ~16 Dezimalstellen

Floats haben ebenfalls einen Minimal- und Maximalwert

Float Typen

float16: genau für etwa 3 Dezimalstellen

np.array([2.71828, 0.271828], dtype="float16")
# array([2.719 , 0.2717])

Float Typen

float16: overflow

np.array([65450, 65500, 65550], dtype="float16")
# array([65440, 65500, inf])

float16: underflow

np.array(
    [3.141e-5, 3.141e-6, 3.141e-7, 3.141e-8, 3.141e-9],
    dtype="float16"
)
# array([3.14e-05, 3.16e-06, 2.98e-07, 5.96e-08, 0.00e+00])

NumPy Fortgeschritten

Views

Mehrere Operationen in NumPy erstellen views der Daten - mehrere arrays können im Hintergund auf die gleichen Daten verweisen (für bessere Effizienz)

Views

Beispiel: Erstellen einer Kopie einer Liste, Erstellen eines Views eines Arrays

list = [1, 2, 3]
list_copy = list[:]
list_copy[0] = 10 # does NOT change list

array = np.array([1, 2, 3])
array_view = array[:]
array_view[0] = 10 # DOES change array

Arrays kopieren

Arrays können via np.copy() kopiert werden

Form von Arrays ändern

np.reshape(a3d, (8, )) # 1d array
np.reshape(a3d, (2, 4)) # 2d array

Automatische Größe entlang einer Achse:

np.ravel(a3d) # 1d array
np.reshape(a3d, (-1, )) # 1d array
np.reshape(a3d, (2, -1)) # 2d array

diese Operationen erstellen Views

Transponieren

Umkehren der Achsenreihenfolge:

np.transpose(a2d)

a2d.T

Arrays aneinanderfügen

entlang einer bestehenden Achse aneinanderfügen (standardmäßig Achse 0):

np.concatenate([a1d, a1d])
np.concatenate([a2d, a2d])
np.concatenate([a2d, a2d], axis=1)

entlang einer neuen Achse aneinanderfügen:

np.stack([a1d, a1d])

Lineare Algebra

Lineare Algebra

np.transpose(m)
np.linalg.inv(m)
np.eye(2) # unit matrix

Array-Multiplikation

mittels des binären Operators @

Beispiel: Rotation verschiedener Punkte um 45° bzw 90° (gegen den Uhrzeigersinn):

points = np.array([[0, 0], [0, 1], [1, 1], [1, 0]])

m = np.array([[np.sqrt(0.5), np.sqrt(0.5)],
              [-np.sqrt(0.5), np.sqrt(0.5)]])

print(points @ m)
print(points @ m @ m)

Array-Multiplikation

Beispiel:

bekannt: Preise verschiedener Produkte, derent Bestände in verschiedenen Lagern

prices = np.array([3.99, 12.99, 5.90, 15])
quantities = np.array([[0, 80, 80, 100],
                       [100, 0, 0, 0],
                       [50, 0, 0, 50]])

Gesucht: Warenwert pro Lager