Panda - Come appiattire un indice gerarchico in colonne


325

Ho un frame di dati con un indice gerarchico nell'asse 1 (colonne) (da groupby.aggun'operazione):

     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

Voglio appiattirlo, in modo che appaia così (i nomi non sono critici - potrei rinominare):

     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

Come faccio a fare questo? (Ho provato molto, inutilmente.)

Per un suggerimento, ecco la testa in forma dettata

{('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}}

5
puoi aggiungere l'output di df[:5].to_dict()come esempio che altri possano leggere nel tuo set di dati?
Zelazny7,

Buona idea. L'ho fatto sopra poiché era troppo lungo per il commento.
Ross R,

C'è un suggerimento sul pandastracker dei problemi per implementare un metodo dedicato per questo.
Joelostblom,

2
@joelostblom ed è stato effettivamente implementato (Panda 0.24.0 e versioni successive). Ho pubblicato una risposta, ma essenzialmente ora puoi semplicemente fare dat.columns = dat.columns.to_flat_index(). Panda integrati.
fantasma

Risposte:


472

Penso che il modo più semplice per farlo sarebbe quello di impostare le colonne al livello più alto:

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

Nota: se il livello a ha un nome, puoi accedervi anche tramite questo, anziché 0.

.

Se vuoi combinare / il jointuo MultiIndex in un indice (supponendo che tu abbia solo voci di stringa nelle tue colonne) potresti:

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

Nota: è necessario striplo spazio bianco per quando non esiste un secondo indice.

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']

14
df.reset_index (inplace = True) potrebbe essere una soluzione alternativa.
Tobias,

8
un commento minore ... se vuoi usare _ per la colonna combinata multilivelli .. puoi usare questo ... df.columns = ['_'. join (col) .strip () per col in df.columns. valori]
ihightower

30
modifiche minori per mantenere la sottolineatura solo per i cols uniti:['_'.join(col).rstrip('_') for col in df.columns.values]
Seiji Armstrong,

Funzionava alla grande, se vuoi usare solo la seconda colonna: df.columns = [col [1] per col in df.columns.values]
user3078500

1
Se si desidera utilizzare sum s_CDinvece di s_CD sum, si può fare 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

3
Funziona, ma lascia i nomi delle colonne che sono difficili da accedere a livello di codice e non sono interrogabili
dmeu,

1
Questo non funzionerà con l'ultima versione di Panda. Funziona con 0,18 ma non con 0,20 (ultimo a partire da ora)
TH22

1
@dmeu per preservare i nomi delle colonne pd.DataFrame(df.to_records(), columns=df.index.names + list(df.columns))
Teoretic

1
Conserva i nomi delle colonne come tuple per me, e per mantenere l'indice che uso:pd.DataFrame(df_volume.to_records(), index=df_volume.index).drop('index', axis=1)
Jayen

54

Tutte le risposte correnti su questo thread devono essere state un po 'datate. A partire dalla pandasversione 0.24.0, .to_flat_index()fa quello che ti serve.

Dalla stessa documentazione del panda :

MultiIndex.to_flat_index ()

Converti un MultiIndex in un indice di tuple contenente i valori di livello.

Un semplice esempio dalla sua documentazione:

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'])

Applicando to_flat_index():

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

Usandolo per sostituire la pandascolonna esistente

Un esempio di come lo datuseresti, che è un DataFrame con una MultiIndexcolonna:

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')

42

La risposta di Andy Hayden è sicuramente il modo più semplice: se vuoi evitare etichette di colonne duplicate devi modificare un po '

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

2
grazie Teodros! Questa è l'unica soluzione corretta che gestisce tutti i casi!
CanCeylan,

17
df.columns = ['_'.join(tup).rstrip('_') for tup in df.columns.values]

14

E se vuoi conservare una qualsiasi delle informazioni di aggregazione dal secondo livello del multiindex puoi provare questo:

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

new_colsnon è definito.
samthebrand,

11

Il modo più pitonico per farlo per usare la mapfunzione.

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

Uscita 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')

Aggiornamento usando Python 3.6+ con stringa f:

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

print(df.columns)

Produzione:

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

9

La soluzione più semplice e intuitiva per me è stata quella di combinare i nomi delle colonne usando get_level_values . Ciò impedisce i nomi di colonna duplicati quando si esegue più di un'aggregazione sulla stessa colonna:

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

Se vuoi un separatore tra le colonne, puoi farlo. Ciò restituirà la stessa cosa del commento di Seiji Armstrong sulla risposta accettata che include solo caratteri di sottolineatura per le colonne con valori in entrambi i livelli di indice:

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

So che fa la stessa cosa della grande risposta di Andy Hayden sopra, ma penso che sia un po 'più intuitivo in questo modo ed è più facile da ricordare (quindi non devo continuare a fare riferimento a questa discussione), specialmente per gli utenti panda principianti .

Questo metodo è anche più estensibile nel caso in cui si possano avere 3 livelli di colonna.

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

6

Dopo aver letto tutte le risposte, ho pensato a questo:

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

Uso:

Dato un frame di dati:

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
  • Metodo di aggregazione singola : le variabili risultanti hanno lo stesso nome della fonte :

    df.groupby(by="grouper").agg("min").my_flatten_cols()
    • Come df.groupby(by="grouper", as_index = False) o .agg(...).reset_index ()
    • ----- before -----
                 val1  2
        grouper         
      
      ------ after -----
        grouper  val1  2
      0       x     0  1
      1       y     4  5
  • Variabile di origine singola, aggregazioni multiple : variabili risultanti che prendono il nome dalle statistiche :

    df.groupby(by="grouper").agg({"val1": [min,max]}).my_flatten_cols("last")
    • Come 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
  • Più variabili, più aggregazioni : variabili risultanti denominate (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)
    • Funziona a.columns = ["_".join(filter(None, map(str, levels))) for levels in a.columns.values]sotto il cofano (poiché questa forma di agg()risultati è nelle MultiIndexcolonne).
    • Se non si dispone del my_flatten_colssoccorritore, potrebbe essere più facile da digitare nella soluzione suggerita da @Seigi : a.columns = ["_".join(t).rstrip("_") for t in a.columns.values], che funziona in modo simile, in questo caso (ma non riesce se si dispone di etichette numeriche su colonne)
    • Per gestire le etichette numeriche sulle colonne, è possibile utilizzare la soluzione suggerita da @jxstanford e @Nolan Conaway ( a.columns = ["_".join(tuple(map(str, t))).rstrip("_") for t in a.columns.values]), ma non capisco perché tuple()sia necessaria la chiamata e credo rstrip()sia necessario solo se alcune colonne hanno un descrittore come ("colname", "")( che può succedere se reset_index()prima di provare a sistemare .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
  • Si vuole dare un nome alle variabili risultanti manualmente: (questo è deprecato dal panda 0.20.0 con senza un'adeguata alternativa come di 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")
    • Altri suggerimenti includono : impostare manualmente le colonne res.columns = ['A_sum', 'B_sum', 'count']o inserire.join() più groupbyistruzioni.
    • ----- 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

Casi gestiti dalla funzione di supporto

  • i nomi dei livelli possono essere senza stringhe, ad esempio Index Panda DataFrame per numero di colonna, quando i nomi di colonna sono numeri interi , quindi dobbiamo convertire conmap(str, ..)
  • possono anche essere vuoti, quindi dobbiamo filter(None, ..)
  • per colonne a livello singolo (ovvero qualsiasi cosa tranne MultiIndex), columns.valuesrestituisce i nomi ( str, non le tuple)
  • a seconda del modo in cui è stato utilizzato .agg(), potrebbe essere necessario mantenere l'etichetta più in basso per una colonna o concatenare più etichette
  • (dato che sono nuovo ai panda?) il più delle volte, voglio reset_index()essere in grado di lavorare con le colonne raggruppate in modo regolare, quindi lo fa per impostazione predefinita

davvero un'ottima risposta, puoi per favore spiegare il lavoro su '[" " .join (tuple (map (str, t))). rstrip (" ") per t in a.columns.values]', grazie in anticipo
Vineet,

@Vineet Ho aggiornato il mio post per indicare che ho menzionato quel frammento per suggerire che ha un effetto simile alla mia soluzione. Se vuoi dettagli sul perché tuple()è necessario, potresti voler commentare il post di jxstanford. In caso contrario, potrebbe essere utile per ispezionare la .columns.valuesnell'esempio fornito: [('val1', 'min'), (2, 'sum'), (2, 'size')]. 1) for t in a.columns.valuespassa sopra le colonne, per la seconda colonna t == (2, 'sum'); 2) si map(str, t)applica str()a ciascun "livello", risultante ('2', 'sum'); 3) "_".join(('2','sum'))risultati in "2_sum",
Nickolay

5

Una soluzione generale che gestisce più livelli e tipi misti:

df.columns = ['_'.join(tuple(map(str, t))) for t in df.columns.values]

1
Nel caso ci siano anche colonne non gerarchiche:df.columns = ['_'.join(tuple(map(str, t))).rstrip('_') for t in df.columns.values]
Nolan Conaway

Grazie. Stavo cercando da molto tempo. Dal momento che il mio indice multilivello conteneva valori interi. Ha risolto il mio problema :)
AnksG il

4

Forse un po 'in ritardo, ma se non sei preoccupato per i nomi di colonne duplicate:

df.columns = df.columns.tolist()

Per me, questo cambia i nomi delle colonne per essere simili a tuple: (year, )e(tempf, amax)
Nickolay,

3

Nel caso in cui si desideri avere un separatore nel nome tra i livelli, questa funzione funziona bene.

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)

1
Mi piace. Tralasciando il caso in cui le colonne non siano gerarchiche, questo può essere semplificato molto:df.columns = ["_".join(filter(None, c)) for c in df.columns]
Gigo,

3

Seguendo @jxstanford e @ tvt173, ho scritto una funzione rapida che dovrebbe fare il trucco, indipendentemente dai nomi delle colonne stringa / int:

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

1

Puoi anche fare come di seguito. Considera dfdi essere il tuo frame di dati e assume un indice a due livelli (come nel tuo esempio)

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

1

Condividerò un modo diretto che ha funzionato per me.

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

0

Per appiattire un MultiIndex all'interno di una catena di altri metodi DataFrame, definire una funzione come questa:

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()

Quindi utilizzare il pipemetodo per applicare questa funzione nella catena di metodi DataFrame, dopo groupbye aggprima di qualsiasi altro metodo nella catena:

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

0

Un'altra semplice 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.