3. NumPy#

NumPy ist die Basis für die performante Arbeit mit Daten in Python. Es stellt mehrdimensionale Arrays zur Verfügung, die sehr effizient sind und viele mächtige Operationen unterstützen. Wir beschränken uns im Folgenden auf Anwendungen zur Datenanalyse. NumPy bietet Performance auf dem Level von optimierten C/Fortran-Code mit der Einfachheit von Python.

INFO

Die Übungen und Beispiele basieren auf Daten über weltweite Systeme des öffentlichen Nahverkehrs von https://www.citylines.co

Im Folgenden erstellen wir ein zweidimensionales NumPy-Array. In den meisten Anwendungen werden die Daten aus einer Datei (z.B. csv oder Excel) oder Datenbank geladen. Die direkte Definition der Daten im Code ist jedoch häufig bei Programmierbeispielen zu finden, damit diese in sich abgeschlossen sind und keine weiteren Dateien benötigen.

import numpy as np

# Some data about cities from citylines.co
# Columns: name, long, lat, start_year, country, length
# length is the length of public transportation network in meters
cities = np.array([
          ["Stockholm", 18.05, 59.28333333, 1919, "Sweden", 132897],
          ["Sydney", 151.2, -33.866667, 1858, "Australia", 355001],
          ["Singapore", 103.833333, 1.283333, 1932, "Singapore", 270996],
          ["Vienna", 16.33333333, 48.23333333, 1978, "Austria", 84970],
          ["Bilbao", -2.953333, 43.262222, 1887, "Spain", 37105],
          ["Montpellier", 3.869985716, 43.61039878, 1996, "France", 98675],
          ["Istanbul", 28.955, 41.013611, 1989, "Turkey", 38096],
          ["Seoul", 126.977966, 37.566536, 1971, "South Korea", 1418234],
          ["Chicago", -87.61666667, 41.83333333, 1892, "United States", 176958],
          ["Cincinnati", -84.5, 39.13333333, 1920, "United States", 31236],
          ["Dallas", -96.76666667, 32.76666667, 1989, "United States", 125699],
          ["Denver", -105, 39.75, 1994, "United States", 174131],
          ["Los Angeles", -118.25, 34.05, 2017, "United States", 140077],
          ["Mexico City", -99.11666667, 19.43333333, 1967, "Mexico", 446306],
          ["Munich", 11.58333333, 48.13333333, 2017, "Germany", 176735],
          ["Naples", 14.25, 40.83333333, 1976, "Italy", 22320],
          ["Madrid", -3.7, 40.43333333, 1869, "Spain", 577574],
          ["London", -0.08333333333, 51.53333333, 1833, "England", 1663529],
          ["Tokyo", 139.75, 35.66666667, 1872, "Japan", 4770864]])

Die Property shape gibt uns die Größe für alle Dimensionen (genannt axes in NumPy und Pandas):

cities.shape
(19, 6)

Das heißt wir haben 19 rows und 6 columns entsprechend 19 Beobachtungen mit je 6 Features. Dies entspricht 19 * 6 Einträgen - diesen Wert bekommen wir mit Hilfe von cities.size

cities.size
114

Wir werden uns hauptsächlich mit zweidimensionalen und eindimensionalen Arrays beschäftigen, es sei erwähnt, dass n-dimensionale Arrays unterstützt werden, daher auch der Klassenname:

type(cities)
numpy.ndarray

3.1. Zugriff auf Elemente in NumPy-Arrays#

Auf einzelne Werte im Array können mit [] zugreifen. Dabei stehen in den Klammern die Indizes für die einzelnen Dimensionen per Komma separiert. Es gibt verschiedene Möglichkeiten die Indizes anzugeben, z.B.:

# Zwei Integer, die die Position bestimmen
print(cities[0,0])
print(cities[1,1])

# Negative Integer, die die Position vom Ende her bestimmen
print(cities[-1,0])
Stockholm
151.2
Tokyo
# Nutzen von : als Platzhalter für alle Einträge entlang einer Dimension
print(cities[0,:])
print(cities[:,0])
['Stockholm' '18.05' '59.28333333' '1919' 'Sweden' '132897']
['Stockholm' 'Sydney' 'Singapore' 'Vienna' 'Bilbao' 'Montpellier'
 'Istanbul' 'Seoul' 'Chicago' 'Cincinnati' 'Dallas' 'Denver' 'Los Angeles'
 'Mexico City' 'Munich' 'Naples' 'Madrid' 'London' 'Tokyo']
# Nutzen von START:END, um Scheiben aus einer Dimension zu schneiden
# Der START-Index ist inklusiv, der END-Index ist exklusiv:
print(cities[0:2, :])
[['Stockholm' '18.05' '59.28333333' '1919' 'Sweden' '132897']
 ['Sydney' '151.2' '-33.866667' '1858' 'Australia' '355001']]
# Nutzen von Listen, die die gewünschten Indizes spezifieren:
print(cities[:,[0,-2]])
[['Stockholm' 'Sweden']
 ['Sydney' 'Australia']
 ['Singapore' 'Singapore']
 ['Vienna' 'Austria']
 ['Bilbao' 'Spain']
 ['Montpellier' 'France']
 ['Istanbul' 'Turkey']
 ['Seoul' 'South Korea']
 ['Chicago' 'United States']
 ['Cincinnati' 'United States']
 ['Dallas' 'United States']
 ['Denver' 'United States']
 ['Los Angeles' 'United States']
 ['Mexico City' 'Mexico']
 ['Munich' 'Germany']
 ['Naples' 'Italy']
 ['Madrid' 'Spain']
 ['London' 'England']
 ['Tokyo' 'Japan']]

Aufgabe

Selektieren Sie ein zweidimensionales Teilarray von cities, das die geographischen Koordinaten von allen Städten enthält.

# 

Hinweis

Rechts finden Sie einen Button, der die Lösung anzeigt

Hide code cell content
cities[:,1:3]
array([['18.05', '59.28333333'],
       ['151.2', '-33.866667'],
       ['103.833333', '1.283333'],
       ['16.33333333', '48.23333333'],
       ['-2.953333', '43.262222'],
       ['3.869985716', '43.61039878'],
       ['28.955', '41.013611'],
       ['126.977966', '37.566536'],
       ['-87.61666667', '41.83333333'],
       ['-84.5', '39.13333333'],
       ['-96.76666667', '32.76666667'],
       ['-105', '39.75'],
       ['-118.25', '34.05'],
       ['-99.11666667', '19.43333333'],
       ['11.58333333', '48.13333333'],
       ['14.25', '40.83333333'],
       ['-3.7', '40.43333333'],
       ['-0.08333333333', '51.53333333'],
       ['139.75', '35.66666667']], dtype='<U32')

3.2. Datentypen in NumPy-Arrays#

Bei der Ausgabe des Arrays fällt auf, dass Zahlen als Strings abgespeichert werden. Das liegt an einer Einschränkung von NumPy-Arrays: sie können nur einen Datentyp enthalten - dabei muss also einer gewählt werden, der alle Daten repräsentieren kann. Die Einschränkung hat technische Gründe, die mit der optimierten Speicherung und parallelen Verarbeitung von Daten durch NumPy zusammenhängt. Mit .astype(np.float) können wir ein Array z.B. in ein Array von Floating Point Zahlen umwandeln:

print('Informationen über ' + cities[0,0] + ':')
print('- Länge des Netzes in Meter: ' + cities[0, -1])
print('- Länge des Netzes in Kilometer: ' + str(cities[0, -1].astype(np.float64) / 1000.0))
Informationen über Stockholm:
- Länge des Netzes in Meter: 132897
- Länge des Netzes in Kilometer: 132.897

Mit Pandas werden wir eine Bibliothek kennenlernen, die das verwalten von mehreren NumPy-Arrays mit unterschiedlichen Typen erleichert. Für jetzt werden wir einfach mehrere Arrays anlegen:

names = cities[:,0]
coords = cities[:, 1:3].astype(np.float64)
start_years = cities[:,3].astype(np.int16)
countries = cities[:,4]
lengths = cities[:,5].astype(np.float64)

So lange wir an den einzelnen Arrays nichts ändern und sie nicht umsortieren, können wir sicher sein, dass ein Index uns zusammengehörende Daten in den verschiedenen Arrays liefert:

print(names[14])
print(countries[14])
Munich
Germany

3.3. Array-Operationen#

Die Stärke von NumPy liegt in vektorisierten Operationen. Diese arbeiten auf einem kompletten Array (oder einem ausgewählten Unterarray) statt auf einzelnen Einträgen. Die Verwendung dieser Operationen ist für imperative Programmier oft ungewohnt aber bietet zwei Vorteile:

  • Der Code ist oftmals kürzer und einfacher verständlich

  • NumPy kann die Ausführung optimieren, z.B. durch Multithreading oder Vektorbefehlen in der CPU

Dies umfasst die typischen Rechnenbefehle, z.B.:

# Zahlen-Array mit Skalar verbinden
a = np.array([1,2,3,4])
a * 2
array([2, 4, 6, 8])
a - 1
array([0, 1, 2, 3])
# Zwei Zahlen-Arrays kombinieren
b = np.array([11, 12, 13, 14])
b - a
array([10, 10, 10, 10])
# Kompexere Berechnungen:
temp_celcius = np.array([0, 20, 40, 100])
temp_fahrenheit = temp_celcius * 9.0/5.0 + 32.0
temp_fahrenheit
array([ 32.,  68., 104., 212.])

NumPy bietet auch eine große Anzahl von Funktionen, die über ein gesamtes Array berechnet werden, z.B.:

a.sum()
10
a.mean()
2.5

Der Performance-Vorteil zeigt sich vor allem bei großen Datenmengen:

# Erstelle Array mit 10 Mio zufälliger Zahlen
r = np.random.rand(10000000)
%%timeit
# Bilde die Summe über alle Zahlen mit klassischer Programmierung:
total = 0.0
for number in r:
    total += number
total
634 ms ± 12.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
# Summe mit Array-basierter Operation
r.sum()
5.42 ms ± 120 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Je nach Hardware zeigt sich eine unterschiedliche Performance - in meinem Beispiel ein Speedup um Faktor >200.

Aufgabe

Nutzen Sie vektorisierte Array-Operationen für die folgenden Aufgaben:

  • Konvertieren Sie die Länge der Streckennetze von Metern in Kilometer

  • Berechnen Sie das Alter der Liniennetze in Jahren für jede Stadt

  • Finden Sie das längste und älteste Liniennetz

ÜBUNG: Array-Operationen

Nutzen Sie vektorisierte Array-Operationen für die folgenden Aufgaben:

  • Konvertieren Sie die Länge der Streckennetze von Metern in Kilometer
  • Berechnen Sie das Alter der Liniennetze in Jahren für jede Stadt
  • Finden Sie das längste und älteste Liniennetz
# 

Rechts finden Sie einen Button, der die Lösung anzeigt (nicht im Übungsblatt).

Hide code cell content
lengths_km = lengths / 1000.0
print('Längen in Kilometern:')
print(lengths_km)

from datetime import date
year = date.today().year
ages = year - start_years 
print('Alter der Liniennetze in Jahren:')
print(ages)

print(f'Längstes Liniennetz ist {lengths_km.max()}km lang')
# Optional - siehe unten
print('... und ist in ' + names[np.argmax(lengths_km)])

print(f'Ältestes Liniennetz ist {ages.max()} Jahre alt')
# Optional - siehe unten
print('... und ist in ' + names[np.argmax(ages)])
Längen in Kilometern:
[ 132.897  355.001  270.996   84.97    37.105   98.675   38.096 1418.234
  176.958   31.236  125.699  174.131  140.077  446.306  176.735   22.32
  577.574 1663.529 4770.864]
Alter der Liniennetze in Jahren:
[105 166  92  46 137  28  35  53 132 104  35  30   7  57   7  48 155 191
 152]
Längstes Liniennetz ist 4770.864km lang
... und ist in Tokyo
Ältestes Liniennetz ist 191 Jahre alt
... und ist in London

3.4. Suchen und Filtern#

Um zu suchen stehen die Funktionen np.argmax, np.argmin, np.argwhere zur Verfügung, die den Index/die Indizes liefern, wo der größte, kleinste, bzw. einer Bedingung entsprechende Wert im Array steht. Beachten Sie, dass wir alle Arrays so angelegt haben, dass sie die gleiche Reihenfolge haben. Das heißt wir können mit den Indizes aus einem Array auf ein anderes zugreifen.

np.argmin(start_years)
17
names[17]
'London'
countries[np.argwhere(names == 'Munich')]
array([['Germany']], dtype='<U32')

Das letzte Beispiel mag naheliegend sein, weil wir die Positionen suchen, wo Name gleich “Munich” ist und dann mit diesen Positionen auf das Ländernamen-Array zugreifen. Es ist aber umständlich. Betrachten wir zunächsten den verwendeten Ausdruck

names == 'Munich'
array([False, False, False, False, False, False, False, False, False,
       False, False, False, False, False,  True, False, False, False,
       False])

Wie wir sehen liefert dieser Ausdruck ein NumPy-Array zurück mit folgenden Eigenschaften:

  • Gleiche Länge, wie das names-Array (und damit wie alle anderen Daten-Arrays, die wir erzeugt haben)

  • Datentyp der Werte ist Boolean

  • Die Werte sind überall False außer an den Indizes, wo die Bedingung, die wir auf das gesamte Array angewandt haben (names == 'Munich') auf den entsprechenden Wert im Array zutreffen, d.h. dort wo names[i] == 'Munich'

Das können wir fürs Filtern verwenden, da NumPy eine weitere Indizierung erlaubt: mit Boolean-Arrays. Es wird ein Array zurückgeliefert, das nur die Werte enthält, wo das für die Indizierung verwendete Boolean-Array True ist.

Beispiele:

filt_munich = names == 'Munich'
countries[filt_munich]
array(['Germany'], dtype='<U32')
names[start_years < 1950]
array(['Stockholm', 'Sydney', 'Singapore', 'Bilbao', 'Chicago',
       'Cincinnati', 'Madrid', 'London', 'Tokyo'], dtype='<U32')

Filter-Bedingungen sind somit nur Boolean-Arrays, die mit ensprechenden Boolean-Operatoren transformiert werden können:

  • a & b ergibt ein logisches Und, d.h. das Ergebnis ist ein Boolean-Array, das nur dort True ist, wo sowohl a als auch b True sind

  • a | b ergibt ein logisches Oder

  • ~a ist ein Boolean-Array mit den logische negierten Werten aus a

# Alle Städte außer München:
names[~filt_munich]
array(['Stockholm', 'Sydney', 'Singapore', 'Vienna', 'Bilbao',
       'Montpellier', 'Istanbul', 'Seoul', 'Chicago', 'Cincinnati',
       'Dallas', 'Denver', 'Los Angeles', 'Mexico City', 'Naples',
       'Madrid', 'London', 'Tokyo'], dtype='<U32')

Neben der Gleichheit per == können noch weitere Bedingungen z.B. mit <, <=, >, >= geprüft werden.

ÜBUNG: Selektion

Geben Sie alle Städte aus, die (a) eine Netzlänge von über 500km haben und (b) nicht in Japan sind.

#
Hide code cell content
names[(lengths_km > 500) & (countries != 'Japan')]

# Gleiches Ergebnis ausführlicherer Code
filt_len500 = lengths_km > 500
filt_notJPN = countries != 'Japan'
names[filt_len500 & filt_notJPN]
array(['Seoul', 'Madrid', 'London'], dtype='<U32')

3.5. Abschluss#

Wir werden NumPy vor allem als Grundlage für weitergehende Bibliotheken (Pandas, Matplotlib, scikit-learn) nutzen. Pandas gibt uns unter anderem eine Möglichkeit mehrere NumPy-Arrays unterschiedlicher Datentypen in einer Datenstruktur (DataFrame) zu verwalten. Dies ist insbesondere bei Sortierungen und Filterungen hilfreich.

NumPy bietet darüber hinaus noch weitere Möglichkeiten und ist die Basis für viele weitere Anwendungen, wenn es um hochperformante Berechnungen geht.

Das NumPy-Projekt bietet eine gute weiterführende Dokumentation: