Pandas – Cómo aplanar un índice jerárquico en columnas

Tengo un dataframe con un índice jerárquico en el eje 1 (columnas) (de una operación groupby.agg ):

  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? (He intentado mucho, en vano.)

Por una sugerencia, aquí está la cabeza en forma de 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}} 

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 tiene un nombre, también puede acceder a él por este, en lugar de 0.

.

Si desea combinar / join su índice múltiple en un índice (asumiendo que solo tiene entradas de cadena en sus columnas) , podría:

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

Nota: debemos strip el espacio en blanco para cuando no haya 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'] 
 pd.DataFrame(df.to_records()) # multiindex become columns and new index is integers only 

La respuesta de Andy Hayden es sin duda la forma más fácil: si desea evitar las tags de columnas duplicadas, debe ajustar 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 
 df.columns = ['_'.join(tup).rstrip('_') for tup in df.columns.values] 

Y si desea conservar cualquier información de agregación del segundo nivel del índice múltiple, puede intentar 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 

Quizás un poco tarde, pero si no te preocupan los nombres de columna duplicados:

 df.columns = df.columns.tolist() 

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 dataframe:

 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 individual : las variables resultantes se denominan igual 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 origen único, agregaciones múltiples : variables resultantes nombradas después de las 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() a = df.groupby(..).agg(..); a.columns = a.columns.droplevel(0); a.reset_index() 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, eg use a different delimiter: #df.groupby(by="grouper").agg({"val1": min, 2:[sum, "size"]}).my_flatten_cols(" ".join) 
    • Ejecuta a.columns = ["_".join(filter(None, map(str, levels))) for levels in a.columns.values] bajo el capó (ya que esta forma de agg() da como resultado MultiIndex en columnas) .
    • Si no tiene el ayudante my_flatten_cols , puede 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 tags numéricas en las columnas)
    • Para manejar las tags 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é es necesaria la llamada tuple() , y creo que rstrip() solo es necesario si algunas columnas tienen un descriptor como ("colname", "") (lo que puede suceder si reset_index() antes de intentar arreglar .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 manualmente: (está en desuso desde pandas 0.20.0 sin una 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 de forma manual: res.columns = ['A_sum', 'B_sum', 'count'] o .join() ing varias declaraciones groupby .
    •  ----- 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 no de cadena, por ejemplo, Índice de datos de pandas por números de columna, cuando los nombres de columna son enteros , por lo que tenemos que convertir con map(str, ..)
  • También pueden estar vacíos, así que tenemos que filter(None, ..)
  • para columnas de un solo nivel (es decir, cualquier cosa excepto MultiIndex), columns.values devuelve los nombres ( str , no tuplas)
  • Dependiendo de cómo usó .agg() es posible que deba mantener la etiqueta de la parte inferior de la columna o concatenar varias tags.
  • (¿Ya que soy nuevo en pandas?) La mayoría de las veces, quiero que reset_index() pueda trabajar con las columnas reset_index() forma regular, por lo que lo hace de forma predeterminada

En caso de que quiera 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) 

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] 

La forma más pythonica de hacer esto para usar la función de map .

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

print(df.columns) 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') 

Actualiza usando Python 3.6+ con la 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') 

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

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

También puedes hacer lo siguiente. Considere df como su dataframe y 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))] 

Voy a compartir una manera directa que funcionó para mí.

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

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 método pipe para aplicar esta función en la cadena de métodos DataFrame, después de groupby y agg pero antes de cualquier otro método en la cadena:

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