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.
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
Show 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
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).
Show 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 wonames[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 dortTrue
ist, wo sowohla
als auchb
True
sinda | b
ergibt ein logisches Oder~a
ist ein Boolean-Array mit den logische negierten Werten ausa
# 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.
Geben Sie alle Städte aus, die (a) eine Netzlänge von über 500km haben und (b) nicht in Japan sind.
#
Show 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:
User Guide: https://numpy.org/doc/stable/user/index.html
API Referenz: https://numpy.org/doc/stable/reference/index.html