wake-up-neo.com

Pandas gruppieren sich mit Kategorien mit redundanten nan

Ich habe Probleme bei der Verwendung von pandas groupby mit kategorialen Daten. Theoretisch sollte es äußerst effizient sein: Sie gruppieren und indizieren nicht über Strings, sondern über Ganzzahlen. Es besteht jedoch darauf, dass bei der Gruppierung nach mehreren Kategorien jede Kombination von Kategorien berücksichtigt werden muss.

Ich verwende manchmal Kategorien, selbst wenn die Dichte der üblichen Zeichenfolgen gering ist, einfach weil diese Zeichenfolgen lang sind und Speicherplatz spart/die Leistung verbessert. Manchmal gibt es Tausende von Kategorien in jeder Spalte. Bei der Gruppierung nach 3 Spalten zwingt pandas uns, Ergebnisse für 1000 ^ 3 Gruppen zu speichern.

Meine Frage: Gibt es eine praktische Möglichkeit, groupby mit Kategorien zu verwenden, während Sie dieses ungünstige Verhalten vermeiden? Ich suche keine dieser Lösungen:

  • Alle Funktionen über numpy wiederherstellen.
  • Konvertieren Sie fortlaufend in Strings/Codes vor groupby und kehren Sie später zu Kategorien zurück.
  • Eine Tupel-Spalte aus Gruppenspalten erstellen und dann nach der Tuple-Spalte gruppieren.

Ich hoffe, dass es eine Möglichkeit gibt, nur diese bestimmte pandas Idiosyncrasy zu ändern. Ein einfaches Beispiel ist unten. Anstelle von 4 Kategorien, die ich in der Ausgabe haben möchte, ende ich mit 12.

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))

for col in group_cols:
    df[col] = df[col].astype('category')

df.groupby(group_cols, as_index=False).sum()

Group1  Group2  Group3  Value
#   A   A   A   NaN
#   A   A   C   NaN
#   A   A   D   NaN
#   A   B   A   NaN
#   A   B   C   54.34
#   A   B   D   826.74
#   B   A   A   765.40
#   B   A   C   514.50
#   B   A   D   NaN
#   B   B   A   NaN
#   B   B   C   NaN
#   B   B   D   NaN

Kopfgeld-Update

Das Problem wird vom Pandas-Entwicklungsteam (cf github.com/pandas-dev/pandas/issues/17594 ) nur unzureichend angesprochen. Daher suche ich nach Antworten, die sich mit den folgenden Punkten befassen:

  1. Warum werden kategoriale Daten in Groupby-Operationen mit Bezug auf Pandas-Quellcode unterschiedlich behandelt?
  2. Warum sollte die aktuelle Implementierung bevorzugt werden? Ich weiß, dass dies subjektiv ist, aber ich habe Schwierigkeiten, eine Antwort auf diese Frage zu finden. Das gegenwärtige Verhalten ist in vielen Situationen unangemessen, ohne umständliche, möglicherweise kostspielige Problemumgehungen.
  3. Gibt es eine saubere Lösung, um die Pandas-Behandlung kategorialer Daten in Groupby-Operationen außer Kraft zu setzen? Beachten Sie die 3 No-Go-Routen (Herunterfahren nach Numpy; Konvertierungen in/aus Codes; Erstellen und Gruppieren nach Tupel-Spalten). Ich würde eine Lösung bevorzugen, die "Pandas-kompatibel" ist, um den Verlust anderer kategorialer Pandas-Funktionen zu minimieren/zu vermeiden.
  4. Eine Antwort des Pandas-Entwicklungsteams zur Unterstützung und Klärung der bestehenden Behandlung. Warum sollten auch nicht alle Kategorienkombinationen als boolesche Parameter konfiguriert werden?

Kopfgeld-Update # 2

Um ehrlich zu sein, erwarte ich nicht die Antwort auf alle 4 Fragen. Die Hauptfrage, die ich frage, ist, ob es möglich oder ratsam ist, pandas-Bibliotheksmethoden zu überschreiben, damit Kategorien so behandelt werden, dass groupby/set_index-Vorgänge erleichtert werden.

12
jpp

Seit Pandas 0.23.0 kann die groupby-Methode jetzt einen Parameter observed annehmen, der dieses Problem behebt, wenn True (Standardeinstellung False) ..__ eingestellt ist. Unten ist derselbe Code wie in der Frage mit genau angegeben observed=True hinzugefügt:

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))

for col in group_cols:
    df[col] = df[col].astype('category')

df.groupby(group_cols, as_index=False, observed=True).sum()

 enter image description here

6
ismax

Ich fand das Verhalten ähnlich dem, was im Abschnitt "Operations" von Categorical Data dokumentiert ist. 

Insbesondere ähnlich

In [121]: cats2 = pd.Categorical(["a","a","b","b"], categories=["a","b","c"])

In [122]: df2 = pd.DataFrame({"cats":cats2,"B":["c","d","c","d"], "values":[1,2,3,4]})

In [123]: df2.groupby(["cats","B"]).mean()
Out[123]: 
        values
cats B        
a    c     1.0
     d     2.0
b    c     3.0
     d     4.0
c    c     NaN
     d     NaN

Einige andere Wörter, die das verwandte Verhalten in Series und groupby beschreiben. Am Ende des Abschnitts befindet sich auch ein Beispiel für einen Pivot-Tisch.

Abgesehen von Series.min (), Series.max () und Series.mode () werden die folgenden Operationen sind mit kategorialen Daten möglich:

Serienmethoden wie Series.value_counts () verwenden alle Kategorien auch wenn einige Kategorien nicht in den Daten vorhanden sind:

Groupby zeigt auch "nicht verwendete" Kategorien an:

Die Wörter und das Beispiel werden aus Kategoriale Daten zitiert.

4
Tai

Ich konnte eine Lösung bekommen, die wirklich gut funktionieren sollte. Ich werde meinen Beitrag mit einer besseren Erklärung bearbeiten. Aber funktioniert das in der Zwischenzeit gut für Sie?

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))
for col in group_cols:
    df[col] = df[col].astype('category')


result = df.groupby([df[col].values.codes for col in group_cols]).sum()
result = result.reset_index()
level_to_column_name = {f"level_{i}":col for i,col in enumerate(group_cols)}
result = result.rename(columns=level_to_column_name)
for col in group_cols:
    result[col] = pd.Categorical.from_codes(result[col].values, categories=df[col].values.categories)
result

Die Antwort darauf war mehr eine richtige Programmierung als eine normale Pandas-Frage. Unter der Haube sind alle kategorialen Serien nur eine Reihe von Zahlen, die in einen Namen von Kategorien eingehen. Ich habe eine Gruppe mit diesen zugrunde liegenden Zahlen gemacht, weil sie nicht das gleiche Problem haben wie kategoriale Spalten. Danach musste ich die Spalten umbenennen. Ich habe dann den from_codes-Konstruktor verwendet, um die Liste der Ganzzahlen effizient in eine kategoriale Spalte umzuwandeln. 

Group1  Group2  Group3  Value
A       B       C       54.34
A       B       D       826.74
B       A       A       765.40
B       A       C       514.50

Ich verstehe, dass dies nicht genau Ihre Antwort ist, aber ich habe meine Lösung zu einer kleinen Funktion für Menschen gemacht, die dieses Problem in der Zukunft haben.

def categorical_groupby(df,group_cols,agg_fuction="sum"):
    "Does a groupby on a number of categorical columns"
    result = df.groupby([df[col].values.codes for col in group_cols]).agg(agg_fuction)
    result = result.reset_index()
    level_to_column_name = {f"level_{i}":col for i,col in enumerate(group_cols)}
    result = result.rename(columns=level_to_column_name)
    for col in group_cols:
        result[col] = pd.Categorical.from_codes(result[col].values, categories=df[col].values.categories)
    return result

nenne es so:

df.pipe(categorical_groupby,group_cols)
4
Gabriel A

Es gibt eine Menge Fragen, die hier beantwortet werden müssen.
Beginnen wir damit, zu verstehen, was eine "Kategorie" ist ...

Definition von Categorical dtype

Zitat aus pandas docs für "Kategoriale Daten":

Kategorien sind ein pandas Datentyp, die kategorialen Variablen in der Statistik entsprechen: eine Variable, die kann nur eine begrenzte und in der Regel feste Anzahl möglicher Werte annehmen ( Kategorien; Ebenen in R). Beispiele sind Geschlecht, soziale Schicht, Blutgruppe, Länderzugehörigkeit, Beobachtungszeit oder Bewertungen über Likert-Skalen.

Es gibt zwei Punkte, auf die ich mich hier konzentrieren möchte:

  1. Die Definition von Kategorien als statistische Variable:
    Im Grunde bedeutet dies, dass wir sie aus statistischer Sicht betrachten müssen, nicht aus der Sicht der "regulären" Programmierung. d.h. sie sind keine "Aufzählungen". Statistische kategoriale Variablen haben spezifische Operationen und Verwendungszwecke. Weitere Informationen finden Sie in wikipedia .
    Nach dem zweiten Punkt werde ich mehr darüber sprechen.

  2. Kategorien sind Ebenen in R:
    Wir können mehr über Kategorien verstehen, wenn wir über R Ebenen und Faktoren lesen.
    Ich weiß nicht viel über R, aber ich fand diese Quelle einfach und ausreichend. Zitiert ein interessantes Beispiel daraus:

    When a factor is first created, all of its levels are stored along with the factor, and if subsets of the factor are extracted, they will retain all of the original levels. This can create problems when constructing model matrices and may or may not be useful when displaying the data using, say, the table function. As an example, consider a random sample from the letters vector, which is part of the base R distribution.
    
    > lets = sample(letters,size=100,replace=TRUE)
    > lets = factor(lets)
    > table(lets[1:5])
    
    a b c d e f g h i j k l m n o p q r s t u v w x y z
    1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1
    
    Even though only five of the levels were actually represented, the table function shows the frequencies for all of the levels of the original factors. To change this, we can simply use another call to factor
    
    > table(factor(lets[1:5]))
    
    a k q s z
    1 1 1 1 1
    

Grundsätzlich sagt uns dies, dass das Anzeigen/Verwenden aller Kategorien, auch wenn sie nicht benötigt werden, keine Seltenheit ist. Und tatsächlich ist es das Standardverhalten!
Dies liegt an den üblichen Anwendungsfällen von kategorialen Variablen in der Statistik. In fast allen Fällen kümmern Sie sich do um alle Kategorien, auch wenn sie nicht verwendet werden. Nehmen Sie zum Beispiel die Funktion pandas cut .

Ich hoffe zu diesem Zeitpunkt, dass Sie verstanden haben, warum dieses Verhalten bei Pandas vorkommt.

GroupBy für kategoriale Variablen

Wie auch immer, groupby berücksichtigt alle Kombinationen von Kategorien: Ich kann es nicht mit Sicherheit sagen, aber meine beste Vermutung basierend auf einer schnellen Überprüfung des Quellcodes (und des von Ihnen erwähnten Github-Problems) ist, dass sie Betrachten Sie groupby für kategoriale Variablen und Interaktion zwischen ihnen. Daher sollte es alle Paare/Tupel berücksichtigen (wie ein kartesisches Produkt). AFAIK, das hilft sehr, wenn Sie versuchen, etwas wie ANOVA zu tun.
Dies bedeutet auch, dass Sie in diesem Kontext nicht an die übliche SQL-ähnliche Terminologie denken können.

Lösungen?

Ok, aber was ist, wenn Sie dieses Verhalten nicht wollen?
Nach meinem besten Wissen und unter Berücksichtigung der Tatsache, dass ich die letzte Nacht damit verbracht habe, dies im pandas Quellcode zu verfolgen, können Sie es nicht "deaktivieren". Es ist in jedem kritischen Schritt hart codiert.
Aufgrund der Funktionsweise von groupby findet die eigentliche "Erweiterung" jedoch erst statt, wenn sie benötigt wird. Zum Beispiel, wenn Sie sum über die Gruppen aufrufen oder versuchen, sie zu drucken.
Daher können Sie eine der folgenden Aktionen ausführen, um nur die benötigten Gruppen abzurufen:

df.groupby(group_cols).indices
#{('A', 'B', 'C'): array([0]),
# ('A', 'B', 'D'): array([1, 4]),
# ('B', 'A', 'A'): array([3]),
# ('B', 'A', 'C'): array([2])}

df.groupby(group_cols).groups
#{('A', 'B', 'C'): Int64Index([0], dtype='int64'),
# ('A', 'B', 'D'): Int64Index([1, 4], dtype='int64'),
# ('B', 'A', 'A'): Int64Index([3], dtype='int64'),
# ('B', 'A', 'C'): Int64Index([2], dtype='int64')}

# an example
for g in df.groupby(group_cols).groups:
    print(g, grt.get_group(g).sum()[0])
#('A', 'B', 'C') 54.34
#('A', 'B', 'D') 826.74
#('B', 'A', 'A') 765.4
#('B', 'A', 'C') 514.5

Ich weiß, das ist ein No-Go für Sie, aber ich bin mir zu 99% sicher, dass es keinen direkten Weg gibt, dies zu tun.
Ich bin damit einverstanden, dass es eine boolesche Variable geben sollte, um dieses Verhalten zu deaktivieren und die "normale" SQL-ähnliche zu verwenden.

3
Qusai Alothman

Ich habe diesen Beitrag beim Debuggen gefunden. Sehr guter Beitrag, und ich mag die Einbeziehung von Randbedingungen sehr!

Hier ist der Code, der das ursprüngliche Ziel erreicht:

r = df.groupby(group_cols, as_index=False).agg({'Value': 'sum'})

r.columns = ['_'.join(col).strip('_') for col in r.columns]

 enter image description here

Der Nachteil dieser Lösung ist, dass sie zu einem hierarchischen Spaltenindex führt, den Sie möglicherweise reduzieren möchten (insbesondere, wenn Sie mehrere Statistiken haben). Ich habe die Abflachung des Spaltenindex in den obigen Code aufgenommen.

Ich weiß nicht, warum Instanzmethoden:

df.groupby(group_cols).sum() 
df.groupby(group_cols).mean()
df.groupby(group_cols).stdev()

verwenden Sie alle eindeutigen Kombinationen von kategorialen Variablen, während die .agg () -Methode: 

df.groupby(group_cols).agg(['count', 'sum', 'mean', 'std']) 

ignoriert die nicht verwendeten Ebenenkombinationen der Gruppen. Das scheint inkonsistent zu sein. Ich bin einfach froh, dass wir die .agg () - Methode verwenden können und uns keine Sorgen um eine kartesische Kombinationsexplosion machen müssen. 

Ich denke auch, dass es sehr üblich ist, im Vergleich zum kartesischen Produkt eine wesentlich niedrigere Kardinalitätszahl zu haben. Denken Sie an all die Fälle, in denen Daten Spalten wie "State", "County", 'Zip "haben. Dies sind alles geschachtelte Variablen, und viele Datensätze haben Variablen, die einen hohen Verschachtelungsgrad aufweisen. 

In unserem Fall beträgt der Unterschied zwischen dem kartesischen Produkt der Gruppierungsvariablen und den natürlich vorkommenden Kombinationen mehr als das 1000fache (und der Startdatensatz beträgt mehr als 1.000.000 Zeilen). 

Folglich hätte ich dafür gestimmt, das Standardverhalten zu beobachten. 

0
Randall Goodwin