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



Binning in Python und Pandas

Einführung

Binning

Binning ist eine Technik, die in der Datenverarbeitung und Statistik verwendet wird. Unter Binning versteht man eine Klassenbildung in der Vorverarbeitung bei der Datenanalyse. Eine gegebene Menge von Werten, die sortiert sind, werden in Intervalle aufgeteilt. Diese Intervalle bezeichnet man im Englischen als "bins" (deutsch "Behälter"). Jedes dieser Intervalle (Bins) wird dann durch einen Repräsentanten bezeichnet. Man bezeichnet diese auch als Intervallabels. Binning wird häufig angewendet, wenn es mehr mögliche Daten gibt, als man eigentlich braucht. So können bspw. Körpergrössen von Menschen in Intervallen oder Kategorien eingeteilt werden.



Nehmen wir an, wir messen die Körpergrößen von 30 Menschen. Die Größenwerte können, grob geschätzt, zwischen 1,30 Meter und 2,50 Meter liegen. Theoretisch gibt es nun 120 verschiedene cm-Werte, jedoch werden wir meistens nur 30 verschiedene Werte aus der Test-Gruppe beobachten.

Eine Möglichkeit um sie zu gruppieren wäre die Einteilung in die Bereiche von 1,30 - 1,50 Meter, 1,50 - 1,70 Meter, 1,70 - 1,90 Meter, usw. Die Original-Daten werden also zu den passenden "Eimern" (Buckets) zugeordnet. Weiterhin werden die Originaldaten durch die korrespondierenden Intervalle ersetzt. Binning ist also eine Form der Quantifizierung.

Einteilungen (Bins) müssen nicht unbedingt numerisch sein. Die Kategorisierung kann beispielsweise von der Art "Hunde", "Katzen", "Hamster" usw. sein, also von jeder erdenklichen Art.

Binning wird auch in der Bildverarbeitung verwendet, um die Datenmengen zu reduzieren, indem benachbarte Pixel zu einzelnen Pixeln kombiniert werden. Man nennt das Verfahren auch kxk-binning, weil Bereiche von k x k Pixel auf einen Pixel reduziert werden.

Pandas bietet einfache Wege, um Bins zu erstellen und so Daten einzuteilen. Bevor wir auf die Pandas-Funktionalität eingehen, möchten wir noch die Basisfunktionen von Python vorstellen, um Bins zu verwenden. Im folgenden Beispiel kommen Listen und Tupel zum Einsatz:

def create_bins(lower_bound, width, quantity):
    """ create_bins gibt eine Partitionierung mit gleichen Abständen 
        zurück. Es handelt sich um eine aufsteigende Liste von Tupeln, 
        die die Werte der Intervallgrenzen darstellen.
        A tuple bins[i], i.e. (bins[i][0], bins[i][1])  with i > 0 
        and i < quantity, satisfies the following conditions:
            (1) bins[i][0] + width == bins[i][1]
            (2) bins[i-1][0] + width == bins[i][0] and
                bins[i-1][1] + width == bins[i][1]
    """
    
    bins = []
    for low in range(lower_bound, 
                     lower_bound + quantity * width + 1, width):
        bins.append((low, low+width))
    return bins

Wir erstellen nun 5 bins (quantity=5) mit einer Breite von 10 (width=10) und beginnen bei 10 (lower_bound=10):

bins = create_bins(lower_bound=10,
                   width=10,
                   quantity=5)
print(bins)
[(10, 20), (20, 30), (30, 40), (40, 50), (50, 60), (60, 70)]

Die nächste Funktion find_bin() wird mit einer Liste oder einem Tupel bins aufgerufen, welches 2er-Tupel oder Listen mit zwei Elementen enthalten muss. Die Funktion ermittelt den Index des Intervalls, der zu dem Wert value gehört:

def find_bin(value, bins):
    """ 'bins' ist ist eine List von Tupeln, wie beispielsweise
        [(0,20), (20, 40), (40, 60)]
        binning gibt den kleinsten Index i von 'bins' zurück, 
        sodass gilt:
        bin[i][0] <= value < bin[i][1]
    """
    
    for i in range(0, len(bins)):
        if bins[i][0] <= value < bins[i][1]:
            return i
    return -1
from collections import Counter
bins = create_bins(lower_bound=50,
                   width=4,
                   quantity=10)
print(bins)
weights_of_persons = [73.4, 69.3, 64.9, 75.6, 74.9, 80.3, 
                      78.6, 84.1, 88.9, 90.3, 83.4, 69.3, 
                      52.4, 58.3, 67.4, 74.0, 89.3, 63.4]
binned_weights = []
for value in weights_of_persons:
    bin_index = find_bin(value, bins)
    print(value, bin_index, bins[bin_index])
    binned_weights.append(bin_index)
    
frequencies = Counter(binned_weights)
print(frequencies)
[(50, 54), (54, 58), (58, 62), (62, 66), (66, 70), (70, 74), (74, 78), (78, 82), (82, 86), (86, 90), (90, 94)]
73.4 5 (70, 74)
69.3 4 (66, 70)
64.9 3 (62, 66)
75.6 6 (74, 78)
74.9 6 (74, 78)
80.3 7 (78, 82)
78.6 7 (78, 82)
84.1 8 (82, 86)
88.9 9 (86, 90)
90.3 10 (90, 94)
83.4 8 (82, 86)
69.3 4 (66, 70)
52.4 0 (50, 54)
58.3 2 (58, 62)
67.4 4 (66, 70)
74.0 6 (74, 78)
89.3 9 (86, 90)
63.4 3 (62, 66)
Counter({4: 3, 6: 3, 3: 2, 7: 2, 8: 2, 9: 2, 5: 1, 10: 1, 0: 1, 2: 1})

Binning mit Pandas

Das Pandas-Modul bietet starke Funktionalitäten für die Einteilung von Daten. Wir demonstrieren dies anhand der vorigen Daten.

Von Pandas verwendete Bins

Als Bins haben wir im vorigen Beispiel eine Liste von Tupeln verwendet. Diese Liste müssen wir nun in eine Datenstruktur wandeln, welche von der Pandas-Funktion cut() verwendet werden kann. Diese Datenstruktur ist ein IntervalIndex. Wir können das mit dem Befehl pd.IntervalIndex.from_tuples() tun:

import pandas as pd
bins2 = pd.IntervalIndex.from_tuples(bins)

cut ist der Name der Pandas-Funktion, die wir brauchen, um Daten in Bins einzuteilen. cut erwartet einige Parameter. Die wichtigsten sind aber x für die aktuellen Werte und bins, welcher den IntervalIndex definiert. x kann jede eindimensionale Array-ähnliche Sruktur sein, wie z.B. Listen, Tupel, nd-arrays usw.:

categorical_object = pd.cut(weights_of_persons, bins2)
print(categorical_object)
[(70, 74], (66, 70], (62, 66], (74, 78], (74, 78], ..., (58, 62], (66, 70], (70, 74], (86, 90], (62, 66]]
Length: 18
Categories (11, interval[int64]): [(50, 54] < (54, 58] < (58, 62] < (62, 66] ... (78, 82] < (82, 86] < (86, 90] < (90, 94]]

Das Ergebnis der Funktion cut() ist ein sogenanntes "Kategorisches Objekt" (Categorical object). Jedes "bin" entspricht einer Kategorie. Die Kategorien sind in einer mathematischen Notation beschrieben. (70, 74] bedeutet, dass dieses "bin" Werte beinhaltet zwischen 70 (exklusive) und 74 (inklusive). Mathematisch handelt es sich dabei um ein halb-offenes Intervall. Das heißt, dass ein Endpunkt des Intervalls inklusive ist, der anderes Endpunkt dagegen nicht. Manchmal wird es auch halb-geschlossenes Intervall genannt.

In unserem vorherigen Kapitel haben wir auch ein halb-offenes Intervall definiert, jedoch anders herum, d.h. dass die linke Seite offen und die rechte geschlossen war. Wenn wir pd.IntervalIndex.from_tuples verwenden, können wir die Öffnung der "bins" definieren, indem wir den Parameter closed auf einen der folgenden Werte setzen:

  • 'left': linke Seite geschlossen und rechte Seite geöffnet
  • 'right': (Default) linke Seite geöffnet und rechte Seite geschlossen
  • 'both': beide Seiten geschlossen
  • 'neither': beide Seiten geöffnet

Für das gleiche Verhalten wie im vorherigen Kapitel, setzen wir den Parameter closed = 'left':

bins2 = pd.IntervalIndex.from_tuples(bins, closed="left")
categorical_object = pd.cut(weights_of_persons, bins2)
print(categorical_object)
[[70, 74), [66, 70), [62, 66), [74, 78), [74, 78), ..., [58, 62), [66, 70), [74, 78), [86, 90), [62, 66)]
Length: 18
Categories (11, interval[int64]): [[50, 54) < [54, 58) < [58, 62) < [62, 66) ... [78, 82) < [82, 86) < [86, 90) < [90, 94)]

Andere Wege um Bins zu definieren

Wir haben IntervalIndex verwendet, um die Gewichtsdaten in "bins" einzuteilen. Die Funktion cut() kann noch mit zwei weiteren Arten der bin-Repräsentation umgehen:

  • Integer-Werte:
    Angabe eines Integer-Wertes für die gleiche Breite aller "Bins" im Bereich der Werte x. Die Range von x wird auf jeder Seite um 0.1% erweitert, um die Minimum- und Maximum-Werte zu inkludieren.
  • Skalare Sequenzen:
    Definiert die "bin"-Kanten, was eine ungleiche Breite der "bins" erlaubt. Die Range von x wird nicht erweitert.
categorical_object = pd.cut(weights_of_persons, 18)
print(categorical_object[:5])
[(71.35, 73.456], (69.244, 71.35], (62.928, 65.033], (75.561, 77.667], (73.456, 75.561]]
Categories (18, interval[float64]): [(52.362, 54.506] < (54.506, 56.611] < (56.611, 58.717] < (58.717, 60.822] ... (81.878, 83.983] < (83.983, 86.089] < (86.089, 88.194] < (88.194, 90.3]]
sequence_of_scalars = [ x[0] for x in bins]
sequence_of_scalars.append(bins[-1][1])
print(sequence_of_scalars)
categorical_object = pd.cut(weights_of_persons, 
                            sequence_of_scalars,
                            right=False)
for i in range(len(categorical_object)):
    print(categorical_object[i])
[50, 54, 58, 62, 66, 70, 74, 78, 82, 86, 90, 94]
[70, 74)
[66, 70)
[62, 66)
[74, 78)
[74, 78)
[78, 82)
[78, 82)
[82, 86)
[86, 90)
[90, 94)
[82, 86)
[66, 70)
[50, 54)
[58, 62)
[66, 70)
[74, 78)
[86, 90)
[62, 66)

Bins und Werte zählen

Die nächste und interessanteste Frage ist, wie wir denn die aktuellen Werte eines Bins sehen können. Das können wir mit der Funktion value_counts() erreichen:

print(pd.value_counts(categorical_object))
[74, 78)    3
[66, 70)    3
[86, 90)    2
[82, 86)    2
[78, 82)    2
[62, 66)    2
[90, 94)    1
[70, 74)    1
[58, 62)    1
[50, 54)    1
[54, 58)    0
dtype: int64

categorical_object.codes bietet eine Beschriftung der Eingangswerte in die "binning"-Kategorien:

labels = categorical_object.codes
print(labels)
[ 5  4  3  6  6  7  7  8  9 10  8  4  0  2  4  6  9  3]

categories ist der IntervalIndex der Kategorien der Label-Indizes:

categories = categorical_object.categories
categories
Ausgabe: :

IntervalIndex([[50, 54), [54, 58), [58, 62), [62, 66), [66, 70) ... [74, 78), [78, 82), [82, 86), [86, 90), [90, 94)]
              closed='left',
              dtype='interval[int64]')

Zusammenhang zwischen Gewichtsdaten und "bins":

for index in range(len(weights_of_persons)):
    label_index = labels[index]
    print(weights_of_persons[index], 
          label_index, 
          categories[label_index] )
73.4 5 [70, 74)
69.3 4 [66, 70)
64.9 3 [62, 66)
75.6 6 [74, 78)
74.9 6 [74, 78)
80.3 7 [78, 82)
78.6 7 [78, 82)
84.1 8 [82, 86)
88.9 9 [86, 90)
90.3 10 [90, 94)
83.4 8 [82, 86)
69.3 4 [66, 70)
52.4 0 [50, 54)
58.3 2 [58, 62)
67.4 4 [66, 70)
74.0 6 [74, 78)
89.3 9 [86, 90)
63.4 3 [62, 66)

Bins benennen

Stellen wir uns vor, wir hätten eine Universität, die in Abhängigkeit der Durchschnittsnote (GPA - Grade point average) drei Stufen der "Latin honors" verleiht:

  • "summa cum laude" setzt einen GPA über 3.9 voraus
  • "magna cum laude", wenn der GPA über 3.8 ist
  • "cum laude", wenn der GPA 3.6 oder höher ist
degrees = ["none", 
           "cum laude", 
           "magna cum laude", 
           "summa cum laude"]
student_results = [3.93, 3.24, 2.80, 
                   2.83, 3.91, 3.698, 
                   3.731, 3.25, 3.24, 
                   3.82, 3.22]
student_results_degrees = pd.cut(student_results, 
                                 [0, 3.6, 3.8, 3.9, 4.0], 
                                 labels=degrees)
print(pd.value_counts(student_results_degrees))
none               6
summa cum laude    2
cum laude          2
magna cum laude    1
dtype: int64

Schauen wir uns die einzelnen Bewertungen/Benotungen der Studenten an:

labels = student_results_degrees.codes
categories = student_results_degrees.categories
for index in range(len(student_results)):
    label_index = labels[index]
    print(student_results[index], 
          label_index, 
          categories[label_index] )
3.93 3 summa cum laude
3.24 0 none
2.8 0 none
2.83 0 none
3.91 3 summa cum laude
3.698 1 cum laude
3.731 1 cum laude
3.25 0 none
3.24 0 none
3.82 2 magna cum laude
3.22 0 none