# invisible
import pandas as pd
pd.set_option('display.max_colwidth', 65)
pd.set_option('display.max_columns', 65)
import numpy as np
np.core.arrayprint._line_width = 65

Mehrstufige Indizierung

Einführung

Multi Level Indizierung

Die Basiskonzepte von Pandas haben wir im vorherigen Kapitel gelernt. Dabei haben wir uns die Daten-Strukturen

  • Series und
  • DataFrame

angeschaut.

Ebenso haben wir gelernt, wie man Series- und DataFrame-Objekte in numerischen Python-Programmen erstellt und manipuliert.

Jetzt wollen wir weitere Aspekte dieser Datenstrukturen betrachen. Wir beginnen mit den fortgeschrittenen Indizierungsmöglichkeiten in Pandas.

Mehrstufig indizierte Series

Mehrstufige Indizierung ist sowohl für Series als auch für DataFrame verfügbar. Es ist eine faszinierende Möglichkeit, in höheren Daten-Dimensionen mit den Pandas-Datenstrukturen zu arbeiten. Ein effizienter Weg, um beliebig hoch dimensionierte Daten zu speichern und zu manipulieren und damit in 1-dimensionalen (Series) oder 2-dimensionalen (DataFrames) Strukturen zu arbeiten. Mit anderen Worten können wir mit höher-dimensionierten Daten in niedrigeren Dimensionen arbeiten. Es ist Zeit für ein Beispiel in Python:

import pandas as pd
cities = ["Vienna", "Vienna", "Vienna",
          "Hamburg", "Hamburg", "Hamburg",
          "Berlin", "Berlin", "Berlin",
          "Zürich", "Zürich", "Zürich"]
index = [cities, ["country", "area", "population",
                  "country", "area", "population",
                  "country", "area", "population",
                  "country", "area", "population"]]
data = ["Austria", 414.60,    1805681,
        "Germany", 755.00,    1760433,
        "Germany", 891.85,    3562166,
        "Switzerland", 87.88, 378884]
city_series = pd.Series(data, index=index)
print(city_series)
Vienna   country           Austria
         area                414.6
         population        1805681
Hamburg  country           Germany
         area                755.0
         population        1760433
Berlin   country           Germany
         area               891.85
         population        3562166
Zürich   country       Switzerland
         area                87.88
         population         378884
dtype: object
cities_data = { ("Vienna", "country"): "Austria",
                ("Vienna", "area"): 414.6,
                ("Vienna", "population"): 1805681,
                ("Hamburg", "country"): "Germany",
                ("Hamburg", "area"): 755,
                ("Hamburg", "population"): 1760433,
                ("Berlin", "country"): "Germany",
                ("Berlin", "area"): 891.85,
                ("Berlin", "population"): 3562166,
                ("Zürich", "country"): "Switzerland",
                ("Zürich", "area"): 87.88,
                ("Zürich", "population"): 378884 }
city_series = pd.Series(cities_data)
city_series
Ausgabe: :

Vienna   country           Austria
         area                414.6
         population        1805681
Hamburg  country           Germany
         area                  755
         population        1760433
Berlin   country           Germany
         area               891.85
         population        3562166
Zürich   country       Switzerland
         area                87.88
         population         378884
dtype: object

Zugriffsmöglichkeiten

Wir können über folgenden Weg auf die Daten, die mit dem ersten Index bezeichnet sind, zugreifen:

print(city_series["Vienna"])
country       Austria
area            414.6
population    1805681
dtype: object

Ebenso kann auf die Information über das Land (country), Gebiet (area) oder Bevölkerung (population) einer Stadt zugegriffen werden. Dazu gibt es zwei Möglichkeiten:

print(city_series["Vienna"]["area"])
414.6

Zur Vervollständigung der zweite Weg:

print(city_series["Vienna", "area"])
414.6

Wenn der Index geordnet ist, kann auch die Slicing-Operation angewendet werden:

city_series = city_series.sort_index()
print("city_series with sorted index:")
print(city_series)
print("\nSlicing the city_series:")
print(city_series["Berlin":"Vienna"])
city_series with sorted index:
Berlin   area               891.85
         country           Germany
         population        3562166
Hamburg  area                  755
         country           Germany
         population        1760433
Vienna   area                414.6
         country           Austria
         population        1805681
Zürich   area                87.88
         country       Switzerland
         population         378884
dtype: object
Slicing the city_series:
Berlin   area           891.85
         country       Germany
         population    3562166
Hamburg  area              755
         country       Germany
         population    1760433
Vienna   area            414.6
         country       Austria
         population    1805681
dtype: object

Ebenso können dann auch die Inhalte mehrerer Städte selektiv ausgegeben werden, indem man eine Liste der Stadtnamen als Schlüssel verwendet:

city_series[["Vienna", "Berlin"]]
Ausgabe: :

Vienna  area            414.6
        country       Austria
        population    1805681
Berlin  area           891.85
        country       Germany
        population    3562166
dtype: object

Im nächsten Beispiel zeigen wir, wie mittels Slicing auf die inneren Schlüssel zugegriffen werden kann:

print(city_series[:, "area"])
Berlin     891.85
Hamburg       755
Vienna      414.6
Zürich      87.88
dtype: object

Mit city_series.index.levels kann man auf die einzelnen Stufen des mehrstufigen Indexes zugreifen. Bei diesem Objekt handelt es sich um eine FrozenList, über die wir hier iterieren:

for i in range(len(city_series.index.levels)):
    if i == 0:
        print("Oberste Hierarchiestufe:")
    elif i == 1:
        print("Untere Hierarchiestufe:")
    print(city_series.index.levels[i])
Oberste Hierarchiestufe:
Index(['Berlin', 'Hamburg', 'Vienna', 'Zürich'], dtype='object')
Untere Hierarchiestufe:
Index(['area', 'country', 'population'], dtype='object')

Zusammenhang zu DataFrames

Einige werden sicherlich bemerkt haben, dass man obige mehrstufige Series auch als DataFrame-Objekte darstellen könnte. Ein DataFrame ist ja bereits zweidimensional, während eine Series nur eindimensional ist, sofern man keinen mehrstufigen Index verwendet. Nun stellt sich die Frage, wie man aus der Series city_series ein DataFrame erzeigen kann. Man kann dies zwar mit folgendem Code erreichen, aber wir werden danach einen direkteren Weg zeigen.

city_df = pd.DataFrame([], index=index[0][::3])
for key in index[1][:3]:
    city_df = pd.concat([city_df,
                        city_series[:, key]], 
                        axis=1,
                        sort=False)
    
city_df.columns = ["country", "population", "area"]
print(city_df)
             country population     area
Vienna       Austria      414.6  1805681
Hamburg      Germany        755  1760433
Berlin       Germany     891.85  3562166
Zürich   Switzerland      87.88   378884

Setzt man sort auf False, erhält man einen unsortierten Index, in unserem Fall:

city_df = pd.DataFrame([], index=index[0][::3])
for key in index[1][:3]:
    city_df = pd.concat([city_df,
                        city_series[:, key]], 
                        axis=1,
                        sort=False)
    
city_df.columns = ["country", "population", "area"]
print(city_df)
             country population     area
Vienna       Austria      414.6  1805681
Hamburg      Germany        755  1760433
Berlin       Germany     891.85  3562166
Zürich   Switzerland      87.88   378884

Obiges können wir einfacher haben, indem wir die von der Series-Klasse zur Verfügung gestellte Methode unstack benutzen. unstack bietet zwei optionale Parameter:

  • level, der per Default auf -1 gesetzt ist, bestimmt, welcher Teil des mehrstufigen Indexes als Spaltenbezeichner verwendet wird. -1 bedeutet, dass der innere Index verwendet wird. Das entspricht in unserem Beispiel city_series.index.levels[-1], also die Städtenamen. Setzen wir level auf 0, so werden die Städtenamen zum Index des DataFrame.
  • fill_value ist per Default auf None gesetzt. Mit diesem Parameter kann man den Wert bestimmen, auf den NaN-Werte umgesetzt werden, falls diese sich in den Daten befinden.
city_df = city_series.unstack()
print("Für level wurde der Default-Wert -1 genutzt:")
print(city_df)
city_df = city_series.unstack(level=0)
print("\nErgebnis für level=0:")
print(city_df)
Für level wurde der Default-Wert -1 genutzt:
           area      country population
Berlin   891.85      Germany    3562166
Hamburg     755      Germany    1760433
Vienna    414.6      Austria    1805681
Zürich    87.88  Switzerland     378884
Ergebnis für level=0:
             Berlin  Hamburg   Vienna       Zürich
area         891.85      755    414.6        87.88
country     Germany  Germany  Austria  Switzerland
population  3562166  1760433  1805681       378884

Die DataFrame-Methode stack entspricht der Umkehrfunktion, d.h. aus einem DataFrame-Objekt erzeugt sie ein Series-Objekt mit mehrstufigem Index:

city_df.stack()
Ausgabe: :

area        Berlin          891.85
            Hamburg            755
            Vienna           414.6
            Zürich           87.88
country     Berlin         Germany
            Hamburg        Germany
            Vienna         Austria
            Zürich     Switzerland
population  Berlin         3562166
            Hamburg        1760433
            Vienna         1805681
            Zürich          378884
dtype: object

Dreistufige Indizes

Zu Anfang dieses Kapitels haben wir gesehen, wie wir eine Series mit einem mehrstufigen Index direkt durch die Angabe einer Liste mit zwei oder mehr Index-Arrays oder Listen erzeugen können. Wir hatten ein Dictionary cities und die verschachtelte Liste index zu einer mehrstufigen Series gewandelt. Genaugenommen erhielten wir eine zweistufige Series. Im folgenden Beispiel zeigen wir ein Beispiel mit einem dreistufigen Index:

import pandas as pd
index = [ ["hot"] * 6 + ["cold"] * 6,  
         (["red"] * 2  + ["green"] * 2 + ["blue"] * 2) * 2, 
         ["right", "wrong"] * 6]
data = np.random.randint(100, 100000, size=(12,))
S3_series = pd.Series(data, index=index)
print(S3_series)
hot   red    right    16031
             wrong    71724
      green  right    51514
             wrong    56067
      blue   right    51977
             wrong    28134
cold  red    right     8368
             wrong    54434
      green  right     6894
             wrong    82765
      blue   right    34937
             wrong    58604
dtype: int64

Auch im Falle von dreistufigen Indizes können wir mit Hilfe der Methode unstack ein DataFrame erzeugen. Wir zeigen die verschiedenen Möglichkeiten für den Parameter level:

print(S3_series.unstack(level=-1))   # entspricht 'level=2'
            right  wrong
cold blue   34937  58604
     green   6894  82765
     red     8368  54434
hot  blue   51977  28134
     green  51514  56067
     red    16031  71724
print(S3_series.unstack(level=-0))
              cold    hot
blue  right  34937  51977
      wrong  58604  28134
green right   6894  51514
      wrong  82765  56067
red   right   8368  16031
      wrong  54434  71724
x = S3_series.unstack(level=[1, 2])
print(x)
        red         green          blue       
      right  wrong  right  wrong  right  wrong
cold   8368  54434   6894  82765  34937  58604
hot   16031  71724  51514  56067  51977  28134
print(x["red", "right"])
cold     8368
hot     16031
Name: (red, right), dtype: int64
x = S3_series.unstack(level=[2, 1])
print(x)
      right  wrong  right  wrong  right  wrong
        red    red  green  green   blue   blue
cold   8368  54434   6894  82765  34937  58604
hot   16031  71724  51514  56067  51977  28134
x["right"]
Ausgabe: :

red green blue
cold 8368 6894 34937
hot 16031 51514 51977

Die Daten hätten aber auch wie in folgendem Dictionary organisiert gewesen sein können. Auch dann können wir diese direkt in ein mehrstufiges Dictionary wandeln:

Vertauschen mehrstufiger Indizes

Es ist möglich, die Ebenen eines mehrstufigen Index mit der Methode swaplevel zu vertauschen:

S3_swapped = S3_series.swaplevel()
S3_swapped.sort_index(inplace=True)
S3_swapped
Ausgabe: :

cold  right  blue     34937
             green     6894
             red       8368
      wrong  blue     58604
             green    82765
             red      54434
hot   right  blue     51977
             green    51514
             red      16031
      wrong  blue     28134
             green    56067
             red      71724
dtype: int64
print(city_series)
city_series = city_series.swaplevel()
city_series.sort_index(inplace=True)
print("\n--- vertauscht ---")
city_series
Berlin   area               891.85
         country           Germany
         population        3562166
Hamburg  area                  755
         country           Germany
         population        1760433
Vienna   area                414.6
         country           Austria
         population        1805681
Zürich   area                87.88
         country       Switzerland
         population         378884
dtype: object
--- vertauscht ---
Ausgabe: :

area        Berlin          891.85
            Hamburg            755
            Vienna           414.6
            Zürich           87.88
country     Berlin         Germany
            Hamburg        Germany
            Vienna         Austria
            Zürich     Switzerland
population  Berlin         3562166
            Hamburg        1760433
            Vienna         1805681
            Zürich          378884
dtype: object
growth_rates = {("Afghanistan", 2015): 1.31, 
                ("Afghanistan", 2016): 2.37,
                ("Afghanistan", 2017): 2.60, 
                ("Ägypten", 2015): 4.37,
                ("Ägypten", 2016): 4.35,
                ("Ägypten", 2017): 4.18,
                ("Albanien", 2015): 2.22,
                ("Albanien", 2016): 3.35,
                ("Albanien", 2017): 3.84}
growth_rates_series = pd.Series(growth_rates)
growth_rates_series
Ausgabe: :

Afghanistan  2015    1.31
             2016    2.37
             2017    2.60
Ägypten      2015    4.37
             2016    4.35
             2017    4.18
Albanien     2015    2.22
             2016    3.35
             2017    3.84
dtype: float64
growth_rates_series = growth_rates_series.swaplevel()
growth_rates_series.sort_index(inplace=True)
growth_rates_series
Ausgabe: :

2015  Afghanistan    1.31
      Albanien       2.22
      Ägypten        4.37
2016  Afghanistan    2.37
      Albanien       3.35
      Ägypten        4.35
2017  Afghanistan    2.60
      Albanien       3.84
      Ägypten        4.18
dtype: float64