Dateneinteilungen in Python und Pandas



Einführung

Binning

Dateneinteilung (Data Binning), auch bekannt als "Bucketing", ist eine Technik die in der Datenverarbeitung und Statistik verwendet wird. Binning kann bspw. verwendet werden, 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össen von 30 Menschen. Die Grössen-Werte 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 Original-Daten durch die korrespondierenden Intervalle ersetzt. Binning ist also eine Form der Quantifizierung.

Einteilungen (Bins) müssen nicht unbedingt numerisch sein. Die Kategorisierung kann bspw. 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 Basis-Funktionen 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 returns an equal-width (distance) partitioning. 
        It returns an ascending list of tuples, representing the intervals.
        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_bind() wird mit einer Liste oder einem Tupel bins aufgerufen, welches 2er-Tupel oder Listen mit 2 Elementen enthalten muss. Die Funktion ermitteln den Index des Intervalls, der zu dem Wert value entspricht:

def find_bin(value, bins):
    """ bins is a list of tuples, like [(0,20), (20, 40), (40, 60)],
        binning returns the smallest index i of bins so that
        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 wichtigesten 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 s.g. "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. D.h. 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:

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 in IntervalIndex verwendet um die Gewichtsdaten in "bins" einzuteilen. Die Funktion cut() kann noch mit zwei weiteren Arten der bin-Repräsentation umgehen:

categorical_object = pd.cut(weights_of_persons, 18)
print(categorical_object)
[(71.35, 73.456], (69.244, 71.35], (62.928, 65.033], (75.561, 77.667], (73.456, 75.561], ..., (56.611, 58.717], (67.139, 69.244], (73.456, 75.561], (88.194, 90.3], (62.928, 65.033]]
Length: 18
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)
print(categorical_object)
[50, 54, 58, 62, 66, 70, 74, 78, 82, 86, 90, 94]
[[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)]

Bins und Werte zählen

Die nächste und interessanteste Frage ist, wie wir denn die aktuelle "bin"-Zahlen sehen können. Das können wir mit der Funktion value_counts() erreichen:

pd.value_counts(categorical_object)
Der obige Python-Code führt zu folgender Ausgabe:
[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 Eingangs-Werte in die "binning"-Kategorien:

labels = categorical_object.codes
labels
Der obige Python-Code liefert folgendes Ergebnis:
array([ 5,  4,  3,  6,  6,  7,  7,  8,  9, 10,  8,  4,  0,  2,  4,  6,  9,
        3], dtype=int8)

categories ist der IntervalIndex der Kategorien der label-Indizes:

categories = categorical_object.categories
categories
Der obige Code liefert folgendes Ergebnis:
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)
categorical_object.categories
Führt man obigen Code aus, erhält man folgendes Ergebnis:
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]')

bins benennen

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

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)
pd.value_counts(student_results_degrees)
Der obige Python-Code führt zu folgender Ausgabe:
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