Panda - Cara meratakan indeks hierarkis dalam kolom

325

Saya memiliki bingkai data dengan indeks hierarki di sumbu 1 (kolom) (dari groupby.aggoperasi):

     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

Saya ingin meratakannya, sehingga terlihat seperti ini (nama tidak kritis - saya bisa mengganti nama):

     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

Bagaimana saya melakukan ini? (Saya sudah mencoba banyak, tetapi tidak berhasil.)

Per saran, di sini adalah kepala dalam bentuk dikt

{('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
sumber
5
dapatkah Anda menambahkan output df[:5].to_dict()sebagai contoh untuk dibaca orang lain dalam dataset Anda?
Zelazny7
Ide bagus. Apakah itu di atas karena terlalu lama untuk komentar.
Ross R
Ada saran tentang pandaspelacak masalah untuk menerapkan metode khusus untuk ini.
joelostblom
2
@ joelostblom dan itu sebenarnya telah diterapkan (panda 0.24.0 ke atas). Saya mengirim jawaban tetapi pada dasarnya sekarang Anda bisa melakukannya dat.columns = dat.columns.to_flat_index(). Fungsi panda bawaan.
onlyphantom

Jawaban:

471

Saya pikir cara termudah untuk melakukan ini adalah dengan mengatur kolom ke tingkat atas:

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

Catatan: jika level to memiliki nama Anda juga dapat mengaksesnya dengan ini, daripada 0.

.

Jika Anda ingin menggabungkan / joinMultiIndex Anda menjadi satu Indeks (dengan asumsi Anda hanya memiliki entri string di kolom Anda ) Anda bisa:

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

Catatan: kita harus stripspasi ketika ketika tidak ada indeks kedua.

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
sumber
14
df.reset_index (inplace = True) dapat menjadi solusi alternatif.
Tobias
8
satu komentar kecil ... jika Anda ingin menggunakan _ untuk multilevel kolom gabungan .. Anda bisa menggunakan ini ... df.columns = ['_'. join (col) .strip () untuk col di df.columns. nilai]
ihightower
30
modifikasi kecil untuk mempertahankan garis bawah hanya untuk cols yang bergabung:['_'.join(col).rstrip('_') for col in df.columns.values]
Seiji Armstrong
Ini bekerja dengan baik, jika Anda hanya ingin menggunakan kolom kedua: df.columns = [col [1] untuk col di df.columns.values]
user3078500
1
Jika Anda ingin menggunakan, sum s_CDbukan yang s_CD sumbisa dilakukan 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
sumber
3
Ini berfungsi, tetapi menyisakan nama kolom yang sulit diakses secara terprogram dan tidak dapat
dipertanyakan
1
Ini tidak akan berfungsi dengan versi panda terbaru. Ini bekerja dengan 0,18 tetapi tidak dengan 0,20 (terbaru sekarang)
TH22
1
@dmeu untuk melestarikan nama kolom pd.DataFrame(df.to_records(), columns=df.index.names + list(df.columns))
Teoretik
1
Ini melestarikan nama kolom sebagai tupel untuk saya, dan untuk menjaga indeks saya gunakan:pd.DataFrame(df_volume.to_records(), index=df_volume.index).drop('index', axis=1)
Jayen
54

Semua jawaban saat ini di utas ini pasti sedikit bertanggal. Pada pandasversi 0.24.0, .to_flat_index()lakukan apa yang Anda butuhkan.

Dari dokumentasi panda sendiri :

MultiIndex.to_flat_index ()

Ubah MultiIndex menjadi Indeks Tuples yang berisi nilai level.

Contoh sederhana dari dokumentasinya:

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

Menerapkan to_flat_index():

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

Menggunakannya untuk mengganti pandaskolom yang ada

Contoh cara Anda menggunakannya dat, yaitu DataFrame dengan MultiIndexkolom:

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
sumber
42

Jawaban Andy Hayden tentu saja merupakan cara termudah - jika Anda ingin menghindari label kolom duplikat Anda perlu sedikit mengubah

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
sumber
2
terima kasih Theodros! Ini adalah satu-satunya solusi yang benar yang menangani semua kasus!
CanCeylan
17
df.columns = ['_'.join(tup).rstrip('_') for tup in df.columns.values]
tvt173
sumber
14

Dan jika Anda ingin mempertahankan informasi agregasi dari tingkat kedua multiindex Anda dapat mencoba ini:

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
sumber
new_colstidak ditentukan.
samthebrand
11

Cara paling pythonic untuk melakukan ini menggunakan mapfungsi.

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

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

Perbarui menggunakan Python 3.6+ dengan f string:

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

print(df.columns)

Keluaran:

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

Solusi termudah dan paling intuitif bagi saya adalah menggabungkan nama kolom menggunakan get_level_values . Ini mencegah nama kolom duplikat ketika Anda melakukan lebih dari satu agregasi pada kolom yang sama:

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

Jika Anda ingin pemisah antar kolom, Anda bisa melakukan ini. Ini akan mengembalikan hal yang sama dengan komentar Seiji Armstrong pada jawaban yang diterima yang hanya mencakup garis bawah untuk kolom dengan nilai di kedua level indeks:

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

Saya tahu ini melakukan hal yang sama dengan jawaban hebat Andy Hayden di atas, tapi saya pikir ini sedikit lebih intuitif dengan cara ini dan lebih mudah diingat (jadi saya tidak harus terus merujuk ke utas ini), terutama untuk pengguna panda pemula .

Metode ini juga lebih dapat dikembangkan jika Anda memiliki 3 level kolom.

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
tubuh11
sumber
6

Setelah membaca semua jawaban, saya datang dengan ini:

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

Pemakaian:

Diberi bingkai data:

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
  • Metode agregasi tunggal : variabel yang dihasilkan dinamai sama dengan sumber :

    df.groupby(by="grouper").agg("min").my_flatten_cols()
    • Sama seperti df.groupby(by="grouper", as_index = Salah) atau .agg(...).reset_index ()
    • ----- before -----
                 val1  2
        grouper         
      
      ------ after -----
        grouper  val1  2
      0       x     0  1
      1       y     4  5
  • Variabel sumber tunggal, banyak agregasi : variabel yang dihasilkan dinamai menurut statistik :

    df.groupby(by="grouper").agg({"val1": [min,max]}).my_flatten_cols("last")
    • Sama seperti 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
  • Beberapa variabel, banyak agregasi : variabel yang dihasilkan bernama (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)
    • Berjalan di a.columns = ["_".join(filter(None, map(str, levels))) for levels in a.columns.values]bawah tenda (karena ini agg()menghasilkan MultiIndexkolom-kolom).
    • Jika Anda tidak memiliki my_flatten_colshelper, mungkin lebih mudah untuk mengetikkan solusi yang disarankan oleh @Seigi :a.columns = ["_".join(t).rstrip("_") for t in a.columns.values] , yang bekerja sama dalam hal ini (tapi gagal jika Anda memiliki label numerik pada kolom)
    • Untuk menangani label numerik pada kolom, Anda bisa menggunakan solusi yang disarankan oleh @jxstanford dan @Nolan Conaway ( a.columns = ["_".join(tuple(map(str, t))).rstrip("_") for t in a.columns.values]), tapi saya tidak mengerti mengapa tuple()panggilan itu diperlukan, dan saya percaya rstrip()hanya diperlukan jika beberapa kolom memiliki deskriptor seperti ("colname", "")( yang dapat terjadi jika Anda reset_index()sebelum mencoba memperbaiki .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
  • Anda ingin nama variabel yang dihasilkan secara manual: (ini ditinggalkan karena panda 0.20.0 dengan tidak ada alternatif yang memadai sebagai 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")
    • Saran lain termasuk : mengatur kolom secara manual: res.columns = ['A_sum', 'B_sum', 'count']atau memasukkan.join() banyak groupbypernyataan.
    • ----- 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

Kasus yang ditangani oleh fungsi pembantu

  • nama level dapat berupa non-string, mis. Indeks panda DataFrame dengan nomor kolom, ketika nama kolom bilangan bulat , jadi kita harus mengonversi denganmap(str, ..)
  • mereka juga bisa kosong, jadi kita harus filter(None, ..)
  • untuk kolom tingkat tunggal (mis. apa pun kecuali MultiIndex), columns.valuesmengembalikan nama ( str, bukan tupel)
  • tergantung pada bagaimana Anda menggunakan .agg()Anda mungkin perlu menjaga label terbawah untuk kolom atau menggabungkan beberapa label
  • (Karena saya baru mengenal panda?) lebih sering daripada tidak, saya ingin reset_index()dapat bekerja dengan kolom kelompok-per-cara secara teratur, jadi ia melakukannya secara default
Nickolay
sumber
jawaban yang sangat bagus, bisakah Anda jelaskan bekerja pada '[" " .join (tuple (peta (str, t))). rstrip (" ") untuk t di a.columns.values]', terima kasih sebelumnya
Vineet
@Vineet Saya memperbarui posting saya untuk menunjukkan bahwa saya menyebutkan cuplikan yang menyarankannya memiliki efek yang mirip dengan solusi saya. Jika Anda ingin detail tentang mengapa tuple()diperlukan, Anda mungkin ingin mengomentari posting jxstanford. Jika tidak, mungkin akan membantu untuk memeriksa .columns.valuesdalam contoh yang diberikan: [('val1', 'min'), (2, 'sum'), (2, 'size')]. 1) for t in a.columns.valuesloop di atas kolom, untuk kolom kedua t == (2, 'sum'); 2) map(str, t)berlaku str()untuk setiap "level", menghasilkan ('2', 'sum'); 3) "_".join(('2','sum'))menghasilkan "2_sum",
Nickolay
5

Solusi umum yang menangani beberapa level dan tipe campuran:

df.columns = ['_'.join(tuple(map(str, t))) for t in df.columns.values]
jxstanford
sumber
1
Dalam hal ada kolom non-hierarkis juga:df.columns = ['_'.join(tuple(map(str, t))).rstrip('_') for t in df.columns.values]
Nolan Conaway
Terima kasih. Sedang mencari lama. Karena indeks Multilevel saya mengandung nilai integer. Ini menyelesaikan masalah saya :)
AnksG
4

Agak terlambat mungkin, tetapi jika Anda tidak khawatir tentang duplikat nama kolom:

df.columns = df.columns.tolist()
Niels
sumber
Bagi saya, ini mengubah nama kolom menjadi seperti tuple: (year, )dan(tempf, amax)
Nickolay
3

Jika Anda ingin memiliki pemisah dalam nama antar level, fungsi ini berfungsi dengan baik.

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
sumber
1
Saya suka itu. Meninggalkan kasus di mana kolom tidak hierarkis ini dapat disederhanakan banyak:df.columns = ["_".join(filter(None, c)) for c in df.columns]
Gigo
3

Mengikuti @jxstanford dan @ tvt173, saya menulis fungsi cepat yang seharusnya melakukan trik, terlepas dari nama kolom string / int:

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

Anda juga bisa melakukan seperti di bawah ini. Pertimbangkan dfuntuk menjadi kerangka data Anda dan anggap indeks dua tingkat (seperti halnya dalam contoh Anda)

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

Saya akan berbagi cara langsung yang bekerja untuk saya.

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

Untuk meratakan MultiIndex di dalam rangkaian metode DataFrame lainnya, tentukan fungsi seperti ini:

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

Kemudian gunakan pipemetode untuk menerapkan fungsi ini dalam rantai metode DataFrame, setelah groupbydan aggtetapi sebelum metode lain dalam rantai:

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

Rutinitas sederhana lain.

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
Ufos
sumber