web-dev-qa-db-fra.com

Pandas - Comment aplatir un index hiérarchique dans les colonnes

J'ai un bloc de données avec un index hiérarchique dans l'axe 1 (colonnes) (à partir d'une opération 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

Je veux l'aplatir, pour qu'il ressemble à ceci (les noms ne sont pas critiques - je pourrais renommer):

     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

Comment puis-je faire cela? (J'ai essayé beaucoup, en vain.)

Par suggestion, voici la tête sous forme 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}}
246
Ross R

Je pense que le moyen le plus simple de procéder est de définir les colonnes au niveau supérieur:

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

Remarque: si le niveau de destination a un nom, vous pouvez également y accéder par cette adresse plutôt que par 0.

.

Si vous souhaitez combiner/ join votre MultiIndex dans un seul index (en supposant que vous n'ayez que des entrées de chaîne dans vos colonnes) , vous pourriez :

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

Remarque: nous devons strip les espaces pour l'absence d'un deuxième index.

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']
362
Andy Hayden
pd.DataFrame(df.to_records()) # multiindex become columns and new index is integers only
73
Gleb Yarnykh

La réponse d'Andy Hayden est certainement la solution la plus simple. Si vous souhaitez éviter les libellés de colonnes en double, vous devez modifier un peu

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
35
Theodros Zelleke

Toutes les réponses actuelles sur ce fil doivent être un peu datées. A partir de pandas version 0.24.0, la .to_flat_index() fait ce dont vous avez besoin.

De panda's propre documentation :

MultiIndex.to_flat_index ()

Convertir un MultiIndex en un Index de nuplets contenant les valeurs de niveau.

Un exemple simple tiré de sa documentation:

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

Appliquer to_flat_index():

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

Utilisation de celle-ci pour remplacer la colonne pandas existante

Un exemple d'utilisation de celui-ci sur dat, qui est un DataFrame avec une colonne MultiIndex:

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')
19
onlyphantom
df.columns = ['_'.join(tup).rstrip('_') for tup in df.columns.values]
14
tvt173

Et si vous souhaitez conserver les informations d'agrégation du deuxième niveau du multi-index, vous pouvez essayer ceci:

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
12
Zelazny7

La façon la plus pythonique de faire cela consiste à utiliser la fonction map.

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

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

Mise à jour à l'aide de Python 3.6+ avec la chaîne f:

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

print(df.columns)

Sortie:

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

Après avoir lu toutes les réponses, j'ai trouvé ceci:

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

Usage:

Étant donné un bloc de données:

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éthode d'agrégation unique : variables résultantes nommées identiques à la source :

    df.groupby(by="grouper").agg("min").my_flatten_cols()
    
    • Identique à df.groupby(by="grouper", as_index = False ) ou .agg(...). Reset_index ()
    • ----- before -----
                 val1  2
        grouper         
      
      ------ after -----
        grouper  val1  2
      0       x     0  1
      1       y     4  5
      
  • Variable source unique, agrégations multiples : variables résultantes nommées d'après des statistiques :

    df.groupby(by="grouper").agg({"val1": [min,max]}).my_flatten_cols("last")
    
    • Identique à 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
      
  • Plusieurs variables, plusieurs agrégations : variables résultantes nommées (nomvar) _ (nomstat) :

    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)
    
    • Fonctionne a.columns = ["_".join(filter(None, map(str, levels))) for levels in a.columns.values] sous le capot (étant donné que cette forme de agg() aboutit à MultiIndex sur des colonnes).
    • Si vous ne disposez pas de l’aide my_flatten_cols, il sera peut-être plus facile de taper la solution suggérée par @ Seigi : a.columns = ["_".join(t).rstrip("_") for t in a.columns.values], qui fonctionne de la même manière dans ce cas (mais échoue). si vous avez des étiquettes numériques sur les colonnes)
    • Pour gérer les étiquettes numériques sur les colonnes, vous pouvez utiliser la solution suggérée par @ jxstanford et @Nolan Conaway (a.columns = ["_".join(Tuple(map(str, t))).rstrip("_") for t in a.columns.values]), mais je ne comprends pas pourquoi l'appel Tuple() est nécessaire, et je crois que rstrip() n'est requis que si certaines colonnes ont un descripteur tel que ("colname", "") (ce qui peut arriver si vous reset_index() avant d'essayer de corriger .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
      
  • Vous souhaitez nommer les variables résultantes manuellement: (c'est obsolète depuis pandas 0.20. avec pas d'alternative adéquate à partir de 0.2 )

    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")
    
    • Other suggestions include : définition manuelle des colonnes: res.columns = ['A_sum', 'B_sum', 'count'] ou .join()ing plusieurs groupby instructions.
    • ----- 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
      

Cas traités par la fonction d'assistance

  • les noms de niveau peuvent être non-chaîne, par exemple. Index pandas DataFrame par numéros de colonne, lorsque les noms de colonne sont des entiers , nous devons donc convertir avec map(str, ..)
  • ils peuvent aussi être vides, nous devons donc filter(None, ..)
  • pour les colonnes à un seul niveau (c'est-à-dire n'importe quoi sauf MultiIndex), columns.values renvoie les noms (str, pas les tuples)
  • en fonction de votre utilisation de .agg(), vous devrez peut-être conserver l'étiquette la plus basse pour une colonne ou concaténer plusieurs étiquettes.
  • (depuis que je suis nouveau sur les pandas?) le plus souvent, je veux que reset_index() soit capable de travailler avec les colonnes group-by de la manière habituelle, donc il le fait par défaut
4
Nickolay

Un peu en retard peut-être, mais si vous n'êtes pas inquiet au sujet des noms de colonnes en double:

df.columns = df.columns.tolist()
4
Niels

Une solution générale qui gère plusieurs niveaux et types mélangés:

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

Si vous voulez avoir un séparateur dans le nom entre les niveaux, cette fonction fonctionne 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)
3
agartland

Après @jxstanford et @ tvt173, j’ai écrit une fonction rapide qui devrait faire l'affaire, quels que soient les noms de colonne string/int:

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

Je vais partager un moyen simple qui a fonctionné pour moi.

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

Vous pouvez également faire comme ci-dessous. Considérez df comme votre cadre de données et supposez un index à deux niveaux (comme dans votre exemple)

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

Pour aplatir un MultiIndex dans une chaîne d'autres méthodes DataFrame, définissez une fonction comme celle-ci:

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

Utilisez ensuite la méthode pipe pour appliquer cette fonction dans la chaîne de méthodes DataFrame, après groupby et agg mais avant toute autre méthode de la chaîne:

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

La solution la plus simple et la plus intuitive pour moi consistait à combiner les noms de colonne à l'aide de get_level_values . Cela évite les noms de colonnes en double lorsque vous effectuez plusieurs agrégations sur la même colonne:

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 vous voulez un séparateur entre les colonnes, vous pouvez le faire. Cela retournera la même chose que le commentaire de Seiji Armstrong sur la réponse acceptée qui n'inclut que les traits de soulignement pour les colonnes avec des valeurs dans les deux niveaux d'index:

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

Je sais que cela fait la même chose que l'excellente réponse d'Andy Hayden ci-dessus, mais je pense que c'est un peu plus intuitif de cette façon et qu'il est plus facile à retenir (donc je n'ai pas à continuer à faire référence à ce fil), en particulier pour les novices pandas utilisateurs.

Cette méthode est également plus extensible dans le cas où vous pouvez avoir 3 niveaux de colonne.

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
0
bodily11