Pandas: cómo aplanar un índice jerárquico en columnas

325

Tengo un marco de datos con un índice jerárquico en el eje 1 (columnas) (de una groupby.aggoperación):

     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

Quiero aplanarlo, para que se vea así (los nombres no son críticos, podría cambiar el nombre):

     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

¿Cómo hago esto? (Lo he intentado mucho, fue en vano).

Por una sugerencia, aquí está la cabeza en forma dict

{('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
fuente
55
¿puede agregar la salida de df[:5].to_dict()como ejemplo para que otros la lean en su conjunto de datos?
Zelazny7
Buena idea. Lo hice arriba ya que era demasiado largo para el comentario.
Ross R
Hay una sugerencia sobre el pandasrastreador de problemas para implementar un método dedicado para esto.
joelostblom
2
@joelostblom y de hecho se ha implementado (pandas 0.24.0 y superior). Publiqué una respuesta, pero esencialmente ahora puedes hacerlo dat.columns = dat.columns.to_flat_index(). Función de pandas incorporada.
onlyphantom

Respuestas:

472

Creo que la forma más fácil de hacer esto sería establecer las columnas en el nivel superior:

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

Nota: si el nivel to tiene un nombre, también puede acceder a él por este, en lugar de 0.

.

Si desea combinar / joinsu MultiIndex en un índice (suponiendo que solo tenga entradas de cadena en sus columnas) puede:

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

Nota: debemos stripel espacio en blanco para cuando no hay un segundo índice.

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
fuente
14
df.reset_index (inplace = True) podría ser una solución alternativa.
Tobias
8
un comentario menor ... si desea usar _ para los niveles múltiples de la columna combinada ... podría usar esto ... df.columns = ['_'. join (col) .strip () para col en df.columns. valores]
ihightower
30
modificación menor para mantener el guión bajo solo para cols unidos:['_'.join(col).rstrip('_') for col in df.columns.values]
Seiji Armstrong
Esto funcionó muy bien, si desea utilizar solo la segunda columna: df.columns = [col [1] para col en df.columns.values]
user3078500
1
Si desea usar en sum s_CDlugar de s_CD sum, uno puede hacerlo 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
fuente
3
Esto funciona, pero deja tras de nombres de columna que son de difícil acceso mediante programación y no son queriable
DMEU
1
Esto no funcionará con la última versión de pandas. Funciona con 0.18 pero no con 0.20 (más reciente a partir de ahora)
TH22
1
@dmeu para preservar los nombres de las columnas pd.DataFrame(df.to_records(), columns=df.index.names + list(df.columns))
Teoretic
1
Está preservando los nombres de las columnas como tuplas para mí, y para mantener el índice que uso:pd.DataFrame(df_volume.to_records(), index=df_volume.index).drop('index', axis=1)
Jayen
54

Todas las respuestas actuales en este hilo deben haber sido un poco anticuadas. A partir de la pandasversión 0.24.0, .to_flat_index()hace lo que necesita.

De la propia documentación de panda :

MultiIndex.to_flat_index ()

Convierta un MultiIndex en un Índice de Tuplas que contenga los valores de nivel.

Un ejemplo simple de su documentación:

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

Aplicando to_flat_index():

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

Usándolo para reemplazar la pandascolumna existente

Un ejemplo de cómo lo usaría dat, que es un DataFrame con una MultiIndexcolumna:

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')
onlyphantom
fuente
42

La respuesta de Andy Hayden es sin duda la forma más fácil: si desea evitar las etiquetas de columna duplicadas, debe modificar un poco

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
fuente
2
gracias Theodros! ¡Esta es la única solución correcta que maneja todos los casos!
CanCeylan
17
df.columns = ['_'.join(tup).rstrip('_') for tup in df.columns.values]
tvt173
fuente
14

Y si desea conservar la información de agregación del segundo nivel del índice múltiple, puede probar esto:

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
fuente
new_colsno está definido
samthebrand
11

La forma más pitónica de hacer esto para usar la mapfunción.

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

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

Actualización usando Python 3.6+ con cadena f:

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

print(df.columns)

Salida:

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
fuente
9

La solución más fácil e intuitiva para mí fue combinar los nombres de columna usando get_level_values . Esto evita nombres de columna duplicados cuando realiza más de una agregación en la misma columna:

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

Si desea un separador entre columnas, puede hacerlo. Esto devolverá lo mismo que el comentario de Seiji Armstrong sobre la respuesta aceptada que solo incluye guiones bajos para columnas con valores en ambos niveles de índice:

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

Sé que esto hace lo mismo que la gran respuesta de Andy Hayden anterior, pero creo que es un poco más intuitivo de esta manera y es más fácil de recordar (por lo que no tengo que seguir refiriéndome a este hilo), especialmente para los usuarios novatos de pandas .

Este método también es más extensible en el caso de que pueda tener 3 niveles de columna.

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
corporal11
fuente
6

Después de leer todas las respuestas, se me ocurrió esto:

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:

Dado un marco de datos:

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
  • Método de agregación única : las variables resultantes reciben el mismo nombre que la fuente :

    df.groupby(by="grouper").agg("min").my_flatten_cols()
    • Igual que 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
  • Variable de fuente única, agregaciones múltiples : variables resultantes con nombres de estadísticas :

    df.groupby(by="grouper").agg({"val1": [min,max]}).my_flatten_cols("last")
    • Igual que 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
  • Múltiples variables, múltiples agregaciones : variables resultantes llamadas (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)
    • Se ejecuta a.columns = ["_".join(filter(None, map(str, levels))) for levels in a.columns.values]bajo el capó (ya que esta forma de agg()resultados en las MultiIndexcolumnas).
    • Si no tiene el my_flatten_colsayudante, podría ser más fácil escribir la solución sugerida por @Seigi : a.columns = ["_".join(t).rstrip("_") for t in a.columns.values]que funciona de manera similar en este caso (pero falla si tiene etiquetas numéricas en las columnas)
    • Para manejar las etiquetas numéricas en las columnas, puede usar la solución sugerida por @jxstanford y @Nolan Conaway ( a.columns = ["_".join(tuple(map(str, t))).rstrip("_") for t in a.columns.values]), pero no entiendo por qué tuple()es necesaria la llamada, y creo rstrip()que solo es necesaria si algunas columnas tienen un descriptor como ("colname", "")( que puede suceder si reset_index()antes de intentar arreglarlo .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
  • Desea nombrar las variables resultantes de forma manual: (esto es obsoleta desde pandas 0.20.0 con otra alternativa adecuada a partir de 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")
    • Otras sugerencias incluyen : configurar las columnas manualmente: res.columns = ['A_sum', 'B_sum', 'count']o .join()ing múltiples groupbydeclaraciones.
    • ----- 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

Casos manejados por la función auxiliar

  • los nombres de nivel pueden ser sin cadenas, por ejemplo, Index pandas DataFrame por números de columna, cuando los nombres de columna son enteros , por lo que debemos convertir conmap(str, ..)
  • también pueden estar vacías, así que tenemos que filter(None, ..)
  • para columnas de un solo nivel (es decir, cualquier cosa excepto MultiIndex), columns.valuesdevuelve los nombres ( strno las tuplas)
  • dependiendo de cómo lo haya usado, .agg()es posible que deba mantener la etiqueta de la parte inferior de una columna o concatenar varias etiquetas
  • (¿dado que soy nuevo en los pandas?) la mayoría de las veces, quiero reset_index()poder trabajar con las columnas de agrupación de forma regular, por lo que lo hace de manera predeterminada
Nickolay
fuente
respuesta realmente genial, ¿podría explicar el funcionamiento de '[" " .join (tuple (map (str, t))). rstrip (" ") para t en a.columns.values]', gracias de antemano
Vineet
@Vineet Actualicé mi publicación para indicar que mencioné ese fragmento para sugerir que tiene un efecto similar a mi solución. Si desea detalles sobre por qué tuple()es necesario, puede comentar la publicación de jxstanford. De lo contrario, podría ser útil para inspeccionar el .columns.valuesen el ejemplo dado: [('val1', 'min'), (2, 'sum'), (2, 'size')]. 1) for t in a.columns.valuesrecorre las columnas, para la segunda columna t == (2, 'sum'); 2) se map(str, t)aplica str()a cada "nivel", lo que resulta en ('2', 'sum'); 3) "_".join(('2','sum'))resulta en "2_sum",
Nickolay
5

Una solución general que maneja múltiples niveles y tipos mixtos:

df.columns = ['_'.join(tuple(map(str, t))) for t in df.columns.values]
jxstanford
fuente
1
En caso de que también haya columnas no jerárquicas:df.columns = ['_'.join(tuple(map(str, t))).rstrip('_') for t in df.columns.values]
Nolan Conaway
Gracias. Estuvo buscando por mucho tiempo. Desde mi índice multinivel contenía valores enteros. Resolvió mi problema :)
AnksG
4

Tal vez un poco tarde, pero si no le preocupan los nombres de columna duplicados:

df.columns = df.columns.tolist()
Niels
fuente
Para mí, esto cambia los nombres de las columnas para que sean como tuplas: (year, )y(tempf, amax)
Nickolay
3

En caso de que desee tener un separador en el nombre entre niveles, esta función funciona bien.

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
fuente
1
Me gusta. Dejando de lado el caso donde las columnas no son jerárquicas, esto se puede simplificar mucho:df.columns = ["_".join(filter(None, c)) for c in df.columns]
Gigo
3

Siguiendo @jxstanford y @ tvt173, escribí una función rápida que debería funcionar, independientemente de los nombres de columna string / int:

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

También puedes hacer lo siguiente. Considere dfser su marco de datos y asuma un índice de dos niveles (como es el caso en su ejemplo)

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

Compartiré una forma directa que funcionó para mí.

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

Para aplanar un MultiIndex dentro de una cadena de otros métodos DataFrame, defina una función como esta:

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

Luego use el pipemétodo para aplicar esta función en la cadena de métodos DataFrame, después groupbyy aggpero antes de cualquier otro método en la cadena:

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

Otra rutina simple.

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
Ovnis
fuente