Pandas - Comment aplatir un index hiérarchique en colonnes

325

J'ai un bloc de données avec un index hiérarchique dans l'axe 1 (colonnes) (à partir d'une groupby.aggopération):

     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 beaucoup essayé, en vain.)

Selon une 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}}
Ross R
la source
5
pouvez-vous ajouter la sortie de df[:5].to_dict()comme exemple pour que d'autres puissent la lire dans votre jeu de données?
Zelazny7
Bonne idée. Je l'ai fait ci-dessus car il était trop long pour le commentaire.
Ross R
Il existe une suggestion sur l'pandas outil de suivi des problèmes pour implémenter une méthode dédiée à cet effet.
joelostblom
2
@joelostblom et il a en fait été implémenté (pandas 0.24.0 et supérieur). J'ai posté une réponse mais essentiellement maintenant vous pouvez le faire dat.columns = dat.columns.to_flat_index(). Fonction pandas intégrée.
onlyphantom

Réponses:

472

Je pense que la façon la plus simple de le faire serait de définir les colonnes au niveau supérieur:

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

Remarque: si le niveau to a un nom, vous pouvez également y accéder par ce biais, plutôt que par 0.

.

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

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

Remarque: nous devons striplaisser un espace pour quand il n'y a pas de second 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']
Andy Hayden
la source
14
df.reset_index (inplace = True) pourrait être une solution alternative.
Tobias
8
un commentaire mineur ... si vous souhaitez utiliser _ pour les niveaux de colonne combinés .. vous pouvez utiliser ceci ... df.columns = ['_'. join (col) .strip () pour col dans df.columns. valeurs]
ihightower
30
modification mineure pour maintenir le trait de soulignement pour les cols joints uniquement:['_'.join(col).rstrip('_') for col in df.columns.values]
Seiji Armstrong
Cela a très bien fonctionné, si vous souhaitez utiliser uniquement la deuxième colonne: df.columns = [col [1] pour col dans df.columns.values]
user3078500
1
Si vous souhaitez utiliser à la sum s_CDplace de s_CD sum, on peut le faire 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
la source
3
Cela fonctionne, mais laisse derrière les noms de colonnes qui sont difficiles d'accès par programme et ne sont pas interrogeables
dmeu
1
Cela ne fonctionnera pas avec la dernière version de pandas. Il fonctionne avec 0,18 mais pas avec 0,20 (au plus tard)
TH22
1
@dmeu pour conserver les noms des colonnes pd.DataFrame(df.to_records(), columns=df.index.names + list(df.columns))
Teoretic
1
Il conserve pour moi les noms de colonnes sous forme de tuples, et pour conserver l'index que j'utilise:pd.DataFrame(df_volume.to_records(), index=df_volume.index).drop('index', axis=1)
Jayen
54

Toutes les réponses actuelles sur ce sujet doivent avoir été un peu datées. Depuis la pandasversion 0.24.0, le.to_flat_index() fait ce dont vous avez besoin.

De la propre documentation de panda :

MultiIndex.to_flat_index ()

Convertissez un MultiIndex en un index de tuples contenant les valeurs de niveau.

Un exemple simple 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')

L'utiliser pour remplacer l'existant pandas colonne

Un exemple de la façon dont vous l'utiliseriez dat, qui est un DataFrame avec une MultiIndexcolonne:

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')
fantôme
la source
42

La réponse d'Andy Hayden est certainement le moyen le plus simple - si vous voulez éviter les étiquettes 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
Theodros Zelleke
la source
2
merci Theodros! C'est la seule solution correcte qui gère tous les cas!
CanCeylan
17
df.columns = ['_'.join(tup).rstrip('_') for tup in df.columns.values]
tvt173
la source
14

Et si vous souhaitez conserver les informations d'agrégation du deuxième niveau du multiindex, 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
Zelazny7
la source
new_colsn'est pas défini.
samthebrand
11

La façon la plus pythonique de le faire pour utiliser la mapfonction.

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

Sortie 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 chaîne f:

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

print(df.columns)

Production:

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
la source
9

La solution la plus simple et la plus intuitive pour moi était de combiner les noms de colonnes à l'aide de get_level_values . Cela empêche les noms de colonne 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 renverra la même chose que le commentaire de Seiji Armstrong sur la réponse acceptée qui ne comprend que des 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 la grande réponse d'Andy Hayden ci-dessus, mais je pense que c'est un peu plus intuitif de cette façon et est plus facile à retenir (donc je n'ai pas à continuer de faire référence à ce fil), en particulier pour les utilisateurs novices de pandas .

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
bodily11
la source
6

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é une trame 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 de la même manière que 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 les 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
  • Variables multiples, agrégations multiples : variables résultantes nommées (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)
    • Fonctionne a.columns = ["_".join(filter(None, map(str, levels))) for levels in a.columns.values]sous le capot (puisque cette forme de agg()résultats enMultiIndex les colonnes).
    • Si vous n'avez pas d' my_flatten_colsaide, il pourrait ê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 façon similaire dans ce cas (mais échoue si vous avez des étiquettes numériques sur des 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' tuple()appel est nécessaire, et je crois rstrip()que ce n'est nécessaire que si certaines colonnes ont un descripteur comme ("colname", "")( ce qui peut arriver si vous reset_index()avant d'essayer de réparer .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 obtenues manuellement: (ce qui est dépréciée depuis pandas géants 0.20.0 avec aucune alternative adéquate à 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")
    • Les autres suggestions incluent : définir les colonnes manuellement: res.columns = ['A_sum', 'B_sum', 'count']ou .join()ing plusieurs groupbyinstructions.
    • ----- 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 avecmap(str, ..)
  • ils peuvent également être vides, nous devons donc filter(None, ..)
  • pour les colonnes à niveau unique (c'est-à-dire tout sauf MultiIndex), columns.valuesretourne les noms ( stret non les tuples)
  • selon la façon dont vous l'avez utilisé, .agg()vous devrez peut-être conserver l'étiquette la plus basse d'une colonne ou concaténer plusieurs étiquettes
  • (puisque je suis nouveau dans les pandas?) le plus souvent, je veux reset_index()pouvoir travailler avec les colonnes de regroupement de manière régulière, donc il le fait par défaut
Nickolay
la source
très bonne réponse, pouvez-vous s'il vous plaît expliquer le fonctionnement de '[" " .join (tuple (map (str, t))). rstrip (" ") for t in a.columns.values]', merci d'avance
Vineet
@Vineet J'ai mis à jour mon message pour indiquer que j'ai mentionné cet extrait pour suggérer qu'il a un effet similaire à ma solution. Si vous voulez des détails sur les raisons pour lesquelles tuple()vous en avez besoin, vous voudrez peut-être commenter le message de jxstanford. Dans le cas contraire, il pourrait être utile d'inspecter le .columns.valuesdans l'exemple fourni: [('val1', 'min'), (2, 'sum'), (2, 'size')]. 1) for t in a.columns.valuesboucles sur les colonnes, pour la deuxième colonne t == (2, 'sum'); 2) map(str, t)s'applique str()à chaque "niveau", résultant en ('2', 'sum'); 3)"_".join(('2','sum')) résulte en "2_sum",
Nickolay
5

Une solution générale qui gère plusieurs niveaux et types mixtes:

df.columns = ['_'.join(tuple(map(str, t))) for t in df.columns.values]
jxstanford
la source
1
Dans le cas où il y a aussi des colonnes non hiérarchiques:df.columns = ['_'.join(tuple(map(str, t))).rstrip('_') for t in df.columns.values]
Nolan Conaway
Merci. Je cherchais depuis longtemps. Puisque mon index multiniveau contenait des valeurs entières. Cela a résolu mon problème :)
AnksG
4

Peut-être un peu en retard, mais si vous n'êtes pas inquiet des noms de colonnes en double:

df.columns = df.columns.tolist()
Niels
la source
Pour moi, cela change les noms des colonnes pour ressembler à des (year, )(tempf, amax)
tuples
3

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)
agartland
la source
1
Je l'aime. Laissant de côté le cas où les colonnes ne sont pas hiérarchiques, cela peut être beaucoup simplifié:df.columns = ["_".join(filter(None, c)) for c in df.columns]
Gigo
3

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

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

Vous pouvez également faire comme ci-dessous. Considérez dfêtre votre dataframe et supposez un index à deux niveaux (comme c'est le cas dans votre exemple)

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

Je vais partager une méthode 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
Lean Bravo
la source
0

Pour aplatir un MultiIndex à l'intérieur d'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 pipeméthode pour appliquer cette fonction dans la chaîne de méthodes DataFrame, après groupbyet aggmais avant toute autre méthode de la chaîne:

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

Une autre routine 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
la source