Pandas - So reduzieren Sie einen hierarchischen Index in Spalten

324

Ich habe einen Datenrahmen mit einem hierarchischen Index in Achse 1 (Spalten) (aus einer groupby.aggOperation):

     USAF   WBAN  year  month  day  s_PC  s_CL  s_CD  s_CNT  tempf       
                                     sum   sum   sum    sum   amax   amin
0  702730  26451  1993      1    1     1     0    12     13  30.92  24.98
1  702730  26451  1993      1    2     0     0    13     13  32.00  24.98
2  702730  26451  1993      1    3     1    10     2     13  23.00   6.98
3  702730  26451  1993      1    4     1     0    12     13  10.04   3.92
4  702730  26451  1993      1    5     3     0    10     13  19.94  10.94

Ich möchte es reduzieren, damit es so aussieht (Namen sind nicht kritisch - ich könnte sie umbenennen):

     USAF   WBAN  year  month  day  s_PC  s_CL  s_CD  s_CNT  tempf_amax  tmpf_amin   
0  702730  26451  1993      1    1     1     0    12     13  30.92          24.98
1  702730  26451  1993      1    2     0     0    13     13  32.00          24.98
2  702730  26451  1993      1    3     1    10     2     13  23.00          6.98
3  702730  26451  1993      1    4     1     0    12     13  10.04          3.92
4  702730  26451  1993      1    5     3     0    10     13  19.94          10.94

Wie mache ich das? (Ich habe viel versucht, ohne Erfolg.)

Laut einem Vorschlag ist hier der Kopf in diktierter Form

{('USAF', ''): {0: '702730',
  1: '702730',
  2: '702730',
  3: '702730',
  4: '702730'},
 ('WBAN', ''): {0: '26451', 1: '26451', 2: '26451', 3: '26451', 4: '26451'},
 ('day', ''): {0: 1, 1: 2, 2: 3, 3: 4, 4: 5},
 ('month', ''): {0: 1, 1: 1, 2: 1, 3: 1, 4: 1},
 ('s_CD', 'sum'): {0: 12.0, 1: 13.0, 2: 2.0, 3: 12.0, 4: 10.0},
 ('s_CL', 'sum'): {0: 0.0, 1: 0.0, 2: 10.0, 3: 0.0, 4: 0.0},
 ('s_CNT', 'sum'): {0: 13.0, 1: 13.0, 2: 13.0, 3: 13.0, 4: 13.0},
 ('s_PC', 'sum'): {0: 1.0, 1: 0.0, 2: 1.0, 3: 1.0, 4: 3.0},
 ('tempf', 'amax'): {0: 30.920000000000002,
  1: 32.0,
  2: 23.0,
  3: 10.039999999999999,
  4: 19.939999999999998},
 ('tempf', 'amin'): {0: 24.98,
  1: 24.98,
  2: 6.9799999999999969,
  3: 3.9199999999999982,
  4: 10.940000000000001},
 ('year', ''): {0: 1993, 1: 1993, 2: 1993, 3: 1993, 4: 1993}}
Ross R.
quelle
5
Können Sie die Ausgabe von df[:5].to_dict()als Beispiel hinzufügen, damit andere sie in Ihrem Datensatz lesen können?
Zelazny7
Gute Idee. Habe es oben gemacht, da es zu lang für den Kommentar war.
Ross R
Es gibt einen Vorschlag zum pandasIssue-Tracker , eine spezielle Methode dafür zu implementieren.
Joelostblom
2
@joelostblom und es wurde tatsächlich implementiert (Pandas 0.24.0 und höher). Ich habe eine Antwort gepostet , aber jetzt können Sie es einfach tun dat.columns = dat.columns.to_flat_index(). Eingebaute Pandas-Funktion.
onlyphantom

Antworten:

470

Ich denke, der einfachste Weg, dies zu tun, wäre, die Spalten auf die oberste Ebene zu setzen:

df.columns = df.columns.get_level_values(0)

Hinweis: Wenn die to-Ebene einen Namen hat, können Sie auch über diesen Namen anstatt über 0 darauf zugreifen.

.

Wenn Sie / joinIhren MultiIndex in einem Index kombinieren möchten (vorausgesetzt, Sie haben nur Zeichenfolgeneinträge in Ihren Spalten), können Sie:

df.columns = [' '.join(col).strip() for col in df.columns.values]

Hinweis: Wir müssen stripdas Leerzeichen verwenden, wenn es keinen zweiten Index gibt.

In [11]: [' '.join(col).strip() for col in df.columns.values]
Out[11]: 
['USAF',
 'WBAN',
 'day',
 'month',
 's_CD sum',
 's_CL sum',
 's_CNT sum',
 's_PC sum',
 'tempf amax',
 'tempf amin',
 'year']
Andy Hayden
quelle
14
df.reset_index (inplace = True) könnte eine alternative Lösung sein.
Tobias
8
ein kleiner Kommentar ... wenn Sie _ für die Mehrebenenspalten-Mehrebenen verwenden möchten ... können Sie dies verwenden ... df.columns = ['_'. join (col) .strip () für col in df.columns. Werte]
ihightower
30
geringfügige Änderung, um den Unterstrich nur für verbundene Cols beizubehalten:['_'.join(col).rstrip('_') for col in df.columns.values]
Seiji Armstrong
Dies hat großartig funktioniert, wenn Sie nur die zweite Spalte verwenden möchten: df.columns = [col [1] für col in df.columns.values]
user3078500
1
Wenn Sie sum s_CDanstelle von verwenden möchten s_CD sum, kann man tun df.columns = ['_'.join(col).rstrip('_') for col in [c[::-1] for c in df.columns.values]].
Irene
82
pd.DataFrame(df.to_records()) # multiindex become columns and new index is integers only
Gleb Yarnykh
quelle
3
Dies funktioniert, hinterlässt jedoch Spaltennamen, auf die programmgesteuert nur schwer zugegriffen werden kann und die nicht abfragbar sind
dmeu
1
Dies funktioniert nicht mit der neuesten Version von Pandas. Es funktioniert mit 0,18, aber nicht mit 0,20 (aktuell ab sofort)
TH22
1
@dmeu , um Spaltennamen zu erhaltenpd.DataFrame(df.to_records(), columns=df.index.names + list(df.columns))
Teoretic
1
Es bewahrt Spaltennamen als Tupel für mich und um den Index zu behalten, den ich benutze:pd.DataFrame(df_volume.to_records(), index=df_volume.index).drop('index', axis=1)
Jayen
53

Alle aktuellen Antworten in diesem Thread müssen etwas veraltet sein. Ab pandasVersion 0.24.0 .to_flat_index()macht das, was Sie brauchen.

Aus Pandas eigener Dokumentation :

MultiIndex.to_flat_index ()

Konvertieren Sie einen MultiIndex in einen Index von Tupeln, der die Ebenenwerte enthält.

Ein einfaches Beispiel aus der Dokumentation:

import pandas as pd
print(pd.__version__) # '0.23.4'
index = pd.MultiIndex.from_product(
        [['foo', 'bar'], ['baz', 'qux']],
        names=['a', 'b'])

print(index)
# MultiIndex(levels=[['bar', 'foo'], ['baz', 'qux']],
#           codes=[[1, 1, 0, 0], [0, 1, 0, 1]],
#           names=['a', 'b'])

Bewerbung to_flat_index():

index.to_flat_index()
# Index([('foo', 'baz'), ('foo', 'qux'), ('bar', 'baz'), ('bar', 'qux')], dtype='object')

Verwenden Sie diese Option, um vorhandene pandasSpalten zu ersetzen

Ein Beispiel für die Verwendung dat, bei dem es sich um einen DataFrame mit einer MultiIndexSpalte handelt:

dat = df.loc[:,['name','workshop_period','class_size']].groupby(['name','workshop_period']).describe()
print(dat.columns)
# MultiIndex(levels=[['class_size'], ['count', 'mean', 'std', 'min', '25%', '50%', '75%', 'max']],
#            codes=[[0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 2, 3, 4, 5, 6, 7]])

dat.columns = dat.columns.to_flat_index()
print(dat.columns)
# Index([('class_size', 'count'),  ('class_size', 'mean'),
#     ('class_size', 'std'),   ('class_size', 'min'),
#     ('class_size', '25%'),   ('class_size', '50%'),
#     ('class_size', '75%'),   ('class_size', 'max')],
#  dtype='object')
einziges Phantom
quelle
42

Die Antwort von Andy Hayden ist sicherlich der einfachste Weg - wenn Sie doppelte Spaltenbeschriftungen vermeiden möchten, müssen Sie einige Anpassungen vornehmen

In [34]: df
Out[34]: 
     USAF   WBAN  day  month  s_CD  s_CL  s_CNT  s_PC  tempf         year
                               sum   sum    sum   sum   amax   amin      
0  702730  26451    1      1    12     0     13     1  30.92  24.98  1993
1  702730  26451    2      1    13     0     13     0  32.00  24.98  1993
2  702730  26451    3      1     2    10     13     1  23.00   6.98  1993
3  702730  26451    4      1    12     0     13     1  10.04   3.92  1993
4  702730  26451    5      1    10     0     13     3  19.94  10.94  1993


In [35]: mi = df.columns

In [36]: mi
Out[36]: 
MultiIndex
[(USAF, ), (WBAN, ), (day, ), (month, ), (s_CD, sum), (s_CL, sum), (s_CNT, sum), (s_PC, sum), (tempf, amax), (tempf, amin), (year, )]


In [37]: mi.tolist()
Out[37]: 
[('USAF', ''),
 ('WBAN', ''),
 ('day', ''),
 ('month', ''),
 ('s_CD', 'sum'),
 ('s_CL', 'sum'),
 ('s_CNT', 'sum'),
 ('s_PC', 'sum'),
 ('tempf', 'amax'),
 ('tempf', 'amin'),
 ('year', '')]

In [38]: ind = pd.Index([e[0] + e[1] for e in mi.tolist()])

In [39]: ind
Out[39]: Index([USAF, WBAN, day, month, s_CDsum, s_CLsum, s_CNTsum, s_PCsum, tempfamax, tempfamin, year], dtype=object)

In [40]: df.columns = ind




In [46]: df
Out[46]: 
     USAF   WBAN  day  month  s_CDsum  s_CLsum  s_CNTsum  s_PCsum  tempfamax  tempfamin  \
0  702730  26451    1      1       12        0        13        1      30.92      24.98   
1  702730  26451    2      1       13        0        13        0      32.00      24.98   
2  702730  26451    3      1        2       10        13        1      23.00       6.98   
3  702730  26451    4      1       12        0        13        1      10.04       3.92   
4  702730  26451    5      1       10        0        13        3      19.94      10.94   




   year  
0  1993  
1  1993  
2  1993  
3  1993  
4  1993
Theodros Zelleke
quelle
2
danke Theodros! Dies ist die einzig richtige Lösung, die alle Fälle behandelt!
CanCeylan
17
df.columns = ['_'.join(tup).rstrip('_') for tup in df.columns.values]
tvt173
quelle
14

Wenn Sie die Aggregationsinformationen aus der zweiten Ebene des Multiindex beibehalten möchten, können Sie Folgendes versuchen:

In [1]: new_cols = [''.join(t) for t in df.columns]
Out[1]:
['USAF',
 'WBAN',
 'day',
 'month',
 's_CDsum',
 's_CLsum',
 's_CNTsum',
 's_PCsum',
 'tempfamax',
 'tempfamin',
 'year']

In [2]: df.columns = new_cols
Zelazny7
quelle
new_colsist nicht definiert.
Samthebrand
11

Der pythonischste Weg, dies zu tun, um die mapFunktion zu nutzen .

df.columns = df.columns.map(' '.join).str.strip()

Ausgabe print(df.columns):

Index(['USAF', 'WBAN', 'day', 'month', 's_CD sum', 's_CL sum', 's_CNT sum',
       's_PC sum', 'tempf amax', 'tempf amin', 'year'],
      dtype='object')

Update mit Python 3.6+ mit f string:

df.columns = [f'{f} {s}' if s != '' else f'{f}' 
              for f, s in df.columns]

print(df.columns)

Ausgabe:

Index(['USAF', 'WBAN', 'day', 'month', 's_CD sum', 's_CL sum', 's_CNT sum',
       's_PC sum', 'tempf amax', 'tempf amin', 'year'],
      dtype='object')
Scott Boston
quelle
9

Die einfachste und intuitivste Lösung für mich war, die Spaltennamen mit get_level_values zu kombinieren . Dies verhindert doppelte Spaltennamen, wenn Sie mehr als eine Aggregation in derselben Spalte durchführen:

level_one = df.columns.get_level_values(0).astype(str)
level_two = df.columns.get_level_values(1).astype(str)
df.columns = level_one + level_two

Wenn Sie ein Trennzeichen zwischen Spalten wünschen, können Sie dies tun. Dies gibt dasselbe zurück wie Seiji Armstrongs Kommentar zur akzeptierten Antwort, der nur Unterstriche für Spalten mit Werten in beiden Indexstufen enthält:

level_one = df.columns.get_level_values(0).astype(str)
level_two = df.columns.get_level_values(1).astype(str)
column_separator = ['_' if x != '' else '' for x in level_two]
df.columns = level_one + column_separator + level_two

Ich weiß, dass dies dasselbe tut wie Andy Haydens großartige Antwort oben, aber ich denke, dass es auf diese Weise etwas intuitiver und leichter zu merken ist (ich muss mich also nicht weiter auf diesen Thread beziehen), insbesondere für Anfänger von Pandas .

Diese Methode ist auch erweiterbarer, wenn Sie möglicherweise 3 Spaltenebenen haben.

level_one = df.columns.get_level_values(0).astype(str)
level_two = df.columns.get_level_values(1).astype(str)
level_three = df.columns.get_level_values(2).astype(str)
df.columns = level_one + level_two + level_three
körperlich11
quelle
6

Nachdem ich alle Antworten durchgelesen hatte, kam ich auf Folgendes:

def __my_flatten_cols(self, how="_".join, reset_index=True):
    how = (lambda iter: list(iter)[-1]) if how == "last" else how
    self.columns = [how(filter(None, map(str, levels))) for levels in self.columns.values] \
                    if isinstance(self.columns, pd.MultiIndex) else self.columns
    return self.reset_index() if reset_index else self
pd.DataFrame.my_flatten_cols = __my_flatten_cols

Verwendungszweck:

Gegeben ein Datenrahmen:

df = pd.DataFrame({"grouper": ["x","x","y","y"], "val1": [0,2,4,6], 2: [1,3,5,7]}, columns=["grouper", "val1", 2])

  grouper  val1  2
0       x     0  1
1       x     2  3
2       y     4  5
3       y     6  7
  • Einzelaggregationsmethode : resultierende Variablen mit dem gleichen Namen wie source :

    df.groupby(by="grouper").agg("min").my_flatten_cols()
    • Gleich wie df.groupby(by="grouper", as_index = False) oder .agg(...).reset_index ()
    • ----- before -----
                 val1  2
        grouper         
      
      ------ after -----
        grouper  val1  2
      0       x     0  1
      1       y     4  5
  • Einzelne Quellvariable, mehrere Aggregationen : resultierende Variablen, benannt nach Statistiken :

    df.groupby(by="grouper").agg({"val1": [min,max]}).my_flatten_cols("last")
    • Gleich wie a = df.groupby(..).agg(..); a.columns = a.columns.droplevel(0); a.reset_index().
    • ----- before -----
                  val1    
                 min max
        grouper         
      
      ------ after -----
        grouper  min  max
      0       x    0    2
      1       y    4    6
  • Mehrere Variablen, mehrere Aggregationen : resultierende Variablen mit dem Namen (varname) _ (statname) :

    df.groupby(by="grouper").agg({"val1": min, 2:[sum, "size"]}).my_flatten_cols()
    # you can combine the names in other ways too, e.g. use a different delimiter:
    #df.groupby(by="grouper").agg({"val1": min, 2:[sum, "size"]}).my_flatten_cols(" ".join)
    • Läuft a.columns = ["_".join(filter(None, map(str, levels))) for levels in a.columns.values]unter der Haube (da diese Form der agg()Ergebnisse in MultiIndexden Spalten).
    • Wenn Sie den my_flatten_colsHelfer nicht haben , ist es möglicherweise einfacher, die von @Seigi : vorgeschlagene Lösung einzugeben. Diesa.columns = ["_".join(t).rstrip("_") for t in a.columns.values] funktioniert in diesem Fall ähnlich (schlägt jedoch fehl, wenn Sie numerische Beschriftungen in Spalten haben).
    • Um die numerischen Beschriftungen in Spalten zu verarbeiten, könnten Sie die von @jxstanford und @Nolan Conaway ( a.columns = ["_".join(tuple(map(str, t))).rstrip("_") for t in a.columns.values]) vorgeschlagene Lösung verwenden , aber ich verstehe nicht, warum der tuple()Aufruf erforderlich ist, und ich glaube, dass dies rstrip()nur erforderlich ist, wenn einige Spalten einen Deskriptor wie ("colname", "")( was passieren kann, wenn Sie reset_index()vor dem Versuch zu reparieren .columns)
    • ----- before -----
                 val1           2     
                 min       sum    size
        grouper              
      
      ------ after -----
        grouper  val1_min  2_sum  2_size
      0       x         0      4       2
      1       y         4     12       2
  • Sie wollen manuell die resultierenden Variablen nennen: (dies ist seit Pandas 0.20.0 veraltet mit keiner adäquaten Alternative als 0,23 )

    df.groupby(by="grouper").agg({"val1": {"sum_of_val1": "sum", "count_of_val1": "count"},
                                       2: {"sum_of_2":    "sum", "count_of_2":    "count"}}).my_flatten_cols("last")
    • Weitere Vorschläge sind : manuelles Setzen der Spalten: res.columns = ['A_sum', 'B_sum', 'count']oder .join()mehrere groupbyAnweisungen.
    • ----- before -----
                         val1                      2         
                count_of_val1 sum_of_val1 count_of_2 sum_of_2
        grouper                                              
      
      ------ after -----
        grouper  count_of_val1  sum_of_val1  count_of_2  sum_of_2
      0       x              2            2           2         4
      1       y              2           10           2        12

Fälle, die von der Hilfsfunktion behandelt werden

  • Ebenennamen können keine Zeichenfolgen sein, z. B. Indexpandas DataFrame nach Spaltennummern, wenn Spaltennamen Ganzzahlen sind , daher müssen wir mit konvertierenmap(str, ..)
  • Sie können auch leer sein, also müssen wir filter(None, ..)
  • Gibt für einstufige Spalten (dh alles außer MultiIndex) columns.valuesdie Namen ( strnicht Tupel) zurück.
  • Je nachdem, wie Sie es verwendet haben, müssen .agg()Sie möglicherweise die unterste Beschriftung für eine Spalte beibehalten oder mehrere Beschriftungen verketten
  • (da ich neu bei Pandas bin?) Meistens möchte ich reset_index()in der Lage sein, auf normale Weise mit den Gruppenspalten zu arbeiten, also macht es das standardmäßig
Nickolay
quelle
wirklich gute Antwort, können Sie bitte die Arbeit an '[" " .join (tuple (map (str, t))). rstrip (" ") für t in a.columns.values]' erklären, danke im Voraus
Vineet
@Vineet Ich habe meinen Beitrag aktualisiert, um anzuzeigen, dass ich dieses Snippet erwähnt habe, um darauf hinzuweisen, dass es einen ähnlichen Effekt wie meine Lösung hat. Wenn Sie tuple()wissen möchten, warum dies erforderlich ist, können Sie den Beitrag von jxstanford kommentieren. Andernfalls kann es hilfreich sein, die .columns.valuesim angegebenen Beispiel zu überprüfen : [('val1', 'min'), (2, 'sum'), (2, 'size')]. 1) for t in a.columns.valuesSchleifen über die Spalten für die zweite Spalte t == (2, 'sum'); 2) map(str, t)gilt str()für jede "Ebene", was zu ('2', 'sum'); 3) "_".join(('2','sum'))ergibt "2_sum",
Nickolay
5

Eine allgemeine Lösung für mehrere Ebenen und gemischte Typen:

df.columns = ['_'.join(tuple(map(str, t))) for t in df.columns.values]
jxstanford
quelle
1
Falls es auch nicht hierarchische Spalten gibt:df.columns = ['_'.join(tuple(map(str, t))).rstrip('_') for t in df.columns.values]
Nolan Conaway
Vielen Dank. War lange auf der Suche. Da mein Multilevel-Index ganzzahlige Werte enthielt. Es hat mein Problem gelöst :)
AnksG
4

Vielleicht etwas spät, aber wenn Sie sich keine Sorgen über doppelte Spaltennamen machen:

df.columns = df.columns.tolist()
Niels
quelle
Für mich ändert dies die Namen der Spalten, um tupelartig zu sein: (year, )und(tempf, amax)
Nickolay
3

Wenn Sie ein Trennzeichen im Namen zwischen den Ebenen haben möchten, funktioniert diese Funktion gut.

def flattenHierarchicalCol(col,sep = '_'):
    if not type(col) is tuple:
        return col
    else:
        new_col = ''
        for leveli,level in enumerate(col):
            if not level == '':
                if not leveli == 0:
                    new_col += sep
                new_col += level
        return new_col

df.columns = df.columns.map(flattenHierarchicalCol)
Agartland
quelle
1
Ich mag das. Wenn der Fall df.columns = ["_".join(filter(None, c)) for c in df.columns]
Gigo
3

Nach @jxstanford und @ tvt173 habe ich eine Schnellfunktion geschrieben, die den Trick ausführen sollte, unabhängig von den Spaltennamen von string / int:

def flatten_cols(df):
    df.columns = [
        '_'.join(tuple(map(str, t))).rstrip('_') 
        for t in df.columns.values
        ]
    return df
Nolan Conaway
quelle
1

Sie können auch wie folgt vorgehen. Betrachten Sie dfals Ihren Datenrahmen und nehmen Sie einen zweistufigen Index an (wie in Ihrem Beispiel).

df.columns = [(df.columns[i][0])+'_'+(datadf_pos4.columns[i][1]) for i in range(len(df.columns))]
Heiliger Bimbam
quelle
1

Ich werde einen direkten Weg teilen, der für mich funktioniert hat.

[" ".join([str(elem) for elem in tup]) for tup in df.columns.tolist()]
#df = df.reset_index() if needed
Lean Bravo
quelle
0

Definieren Sie eine Funktion wie die folgende, um einen MultiIndex innerhalb einer Kette anderer DataFrame-Methoden zu reduzieren:

def flatten_index(df):
  df_copy = df.copy()
  df_copy.columns = ['_'.join(col).rstrip('_') for col in df_copy.columns.values]
  return df_copy.reset_index()

Verwenden Sie dann die pipeMethode , um diese Funktion in der Kette der DataFrame-Methoden nach groupbyund aggvor allen anderen Methoden in der Kette anzuwenden:

my_df \
  .groupby('group') \
  .agg({'value': ['count']}) \
  .pipe(flatten_index) \
  .sort_values('value_count')
ianmcook
quelle
0

Eine weitere einfache Routine.

def flatten_columns(df, sep='.'):
    def _remove_empty(column_name):
        return tuple(element for element in column_name if element)
    def _join(column_name):
        return sep.join(column_name)

    new_columns = [_join(_remove_empty(column)) for column in df.columns.values]
    df.columns = new_columns
Ufos
quelle