【Pandas】複数行ヘッダーのデータを読み込む
ExcelやCSVなどで複数行ヘッダーのデータを読み込む際は、列をマルチインデックスとして扱う必要がある。
元データがエクセルの場合、セル結合の有無で読み込まれ方が異なる。
また、マルチインデックスを解除する方法についても紹介する。(アンダースコアで繋げるだけ)
個人的には、ヘッダーが複数行の場合、セルは結合されている方がやりやすい気がする。
ヘッダーが複数行データの基本的な読み込み
pandasのread_csvやread_excel引数でheaderを指定できるため、複数行のヘッダーを指定できる。
1# Excelファイルの1行目と2行目をヘッダーとして読み込む
2df = pd.read_excel("../data/df_multi_columns.xlsx", header=[0,1])
3print(df)
4# A C B
5# AA CC BB AA CC BB AA CC CC.1 AA.1 AA.2 AA.3
6# 0 1 6 11 21 26 31 41 46 51 61 66 71
7# 1 2 7 12 22 27 32 42 47 52 62 67 72
8# 2 3 8 13 23 28 33 43 48 53 63 68 73
9# 3 4 9 14 23 29 34 44 49 54 64 69 74
10# 4 5 10 15 25 30 35 45 50 55 65 70 75
セル結合がある場合とない場合で、列名が変わってくる。
セル結合のあるデータの読み込み
以下のようなデータを読み込む。
セル結合があり、空白のセルも含まれるデータ。
1df1 = pd.read_excel("../data/df_multi_columns.xlsx", header=[0,1])
2print(df1)
3# A C B
4# AA CC BB AA CC BB AA CC CC.1 AA.1 AA.2 AA.3
5# 0 1 6 11 21 26 31 41 46 51 61 66 71
6# 1 2 7 12 22 27 32 42 47 52 62 67 72
7# 2 3 8 13 23 28 33 43 48 53 63 68 73
8# 3 4 9 14 23 29 34 44 49 54 64 69 74
9# 4 5 10 15 25 30 35 45 50 55 65 70 75
ヘッダー1行目は空白は空白のまま扱われる。
ヘッダー2行目は空白の場合、「左側の列名.N」のような形で列名が作成される。
1行目と2行目を合わせるとユニークになるように設定される。
列をMultiIndexとして取得
各列は以下のように設定される。
1df1.columns
2# MultiIndex([('A', 'AA'),
3# ('A', 'CC'),
4# ('A', 'BB'),
5# ('C', 'AA'),
6# ('C', 'CC'),
7# ('C', 'BB'),
8# ('B', 'AA'),
9# ('B', 'CC'),
10# ('B', 'CC.1'),
11# ('B', 'AA.1'),
12# ('B', 'AA.2'),
13# ('B', 'AA.3')],
14# )
15
16df1.columns[0]
17# ('A', 'AA')
18
19df1.columns[0][1]
20# 'AA'
タプルのリストが返ってくる。
レベル別に列名を取得
1df1.columns.levels
2# FrozenList([['A', 'B', 'C'], ['AA', 'AA.1', 'AA.2', 'AA.3', 'BB', 'CC', 'CC.1']])
3
4df1.columns.levels[0]
5# Index(['A', 'B', 'C'], dtype='object')
6
7df1.columns.levels[0][2]
8# 'C'
各レベルで重複が省かれ、かつ昇順に並び替えられた値を取得可能。
元の列の並び順が重要でない場合や、重複を排除して何か処理をしたい場合に便利かもしれない。
列指定で値を取得
1# 1列指定(Seriesとして取得)
2df1.loc[:, ("A", "BB")]
3
4# 0 6
5# 1 7
6# 2 8
7# 3 9
8# 4 10
9# Name: (A, BB), dtype: int64
10
11# 複数列指定
12df1.loc[:, [("A", "BB"), ("B", "AA")]]
13# A B
14# BB AA
15# 0 11 41
16# 1 12 42
17# 2 13 43
18# 3 14 44
19# 4 15 45
列名部分をタプルのリストとして指定することで指定した列を取得できる。
1列のみの場合はリストにしないとSeiresとして取得できる。
なお、複数列をスライスを使用して取得したい場合、列名が昇順でソートされている必要があるため、列を並び替えてもいい場合のみ使える(しかも並び替えた後の列順を考慮して指定する必要がある)
以下の記事を参照。
列のマルチインデックスを解除する
マルチインデックスを「_(アンダースコア)」でつなげて、マルチインデックスを解除する。
1_df1 = df1.copy()
2# 列名をアンダースコアで繋げる
3_df1.columns = ["_".join(cols) for cols in df1.columns]
4print(_df1)
5# A_AA A_CC A_BB C_AA C_CC C_BB B_AA B_CC B_CC.1 B_AA.1 B_AA.2 B_AA.3
6# 0 1 6 11 21 26 31 41 46 51 61 66 71
7# 1 2 7 12 22 27 32 42 47 52 62 67 72
8# 2 3 8 13 23 28 33 43 48 53 63 68 73
9# 3 4 9 14 23 29 34 44 49 54 64 69 74
10# 4 5 10 15 25 30 35 45 50 55 65 70 75
マルチインデックスのまま扱った方がいい場合もあれば、マルチインデックスを解除した方がいい場合もあるため、その都度、扱いやすい方法で扱うといい。
セル結合のないデータの読み込み
以下のようなデータを読み込む。
CSVファイルだとセル結合がないため、ヘッダーが複数行のデータは、このような形になる。
1df2 = pd.read_csv("../data/df_multi_columns.csv", header=[0,1])
2print(df2)
3
4# A Unnamed: 1_level_0 Unnamed: 2_level_0 C Unnamed: 4_level_0 \
5# AA CC BB AA CC
6# 0 1 6 11 21 26
7# 1 2 7 12 22 27
8# 2 3 8 13 23 28
9# 3 4 9 14 23 29
10# 4 5 10 15 25 30
11
12# Unnamed: 5_level_0 B Unnamed: 7_level_0 Unnamed: 8_level_0 \
13# BB AA CC BB
14# 0 31 41 46 51
15# 1 32 42 47 52
16# 2 33 43 48 53
17# 3 34 44 49 54
18# 4 35 45 50 55
19
20# D
21# Unnamed: 9_level_1
22# 0 100
23# 1 200
24# 2 300
25# 3 400
26# 4 500
列名が空白の場合、「Unnamed: N_level_M」という形式で設定される。
Nはそのレベルの自動設定された列名の通し番号。
Mはマルチインデックスのレベル。
列をMultiIndexとして取得
各列は以下のように設定されている。
1df2.columns
2# MultiIndex([( 'A', 'AA'),
3# ('Unnamed: 1_level_0', 'CC'),
4# ('Unnamed: 2_level_0', 'BB'),
5# ( 'C', 'AA'),
6# ('Unnamed: 4_level_0', 'CC'),
7# ('Unnamed: 5_level_0', 'BB'),
8# ( 'B', 'AA'),
9# ('Unnamed: 7_level_0', 'CC'),
10# ('Unnamed: 8_level_0', 'BB'),
11# ( 'D', 'Unnamed: 9_level_1')],
12# )
空白の列名がUnnamed〜に自動で設定されるため、このままでは扱いづらい。
列名を置換する
Unnamedから始まる列名では扱いにくいため、すぐ左側の列名で置換する。
この列名の置換は今回のようなデータ構造の場合の例であるため、データ構造に応じて、その都度置換方法を考えていく必要がある。
なお、そもそもデータ作成(データ読み込み)の時点でこのような状態にならない方が望ましい。
置換した列名の作成
1# 新しいマルチインデックスの列名を格納するためのリスト
2new_columns = []
3# マルチインデックスの列をループ
4for cols in df2.columns:
5 # 各列の列名を格納するためのリスト
6 col_values = []
7 # 各列の列名毎にループ
8 for idx, col in enumerate(cols):
9 # Unnamedから始まる場合は、左側(1つ前)の値に置き換える
10 if re.search("Unnamed", col):
11 col_values.append(new_columns[-1][idx])
12 else:
13 col_values.append(col)
14 new_columns.append(col_values)
15print(new_columns)
16# [['A', 'AA'],
17# ['A', 'CC'],
18# ['A', 'BB'],
19# ['C', 'AA'],
20# ['C', 'CC'],
21# ['C', 'BB'],
22# ['B', 'AA'],
23# ['B', 'CC'],
24# ['B', 'BB'],
25# ['D', 'BB']]
ぱっと見で何してるのかよく分からないので、やはり、元データの構造を修正できるなら、その方がいい。
DataFrameの列名を再設定
1_df2 = df2.copy()
2_df2.columns = pd.MultiIndex.from_tuples(new_columns)
3print(_df2)
4# A C B D
5# AA CC BB AA CC BB AA CC BB BB
6# 0 1 6 11 21 26 31 41 46 51 100
7# 1 2 7 12 22 27 32 42 47 52 200
8# 2 3 8 13 23 28 33 43 48 53 300
9# 3 4 9 14 23 29 34 44 49 54 400
10# 4 5 10 15 25 30 35 45 50 55 500
一応、それっぽい列名にはなる。