StatsFragments

Python, R, Rust, 統計, 機械学習とか

Python pandas 図でみる データ連結 / 結合処理

なんかぼやぼやしているうちにひさびさの pandas エントリになってしまった。基本的な使い方については網羅したい気持ちはあるので、、、。

今回は データの連結 / 結合まわり。この部分 公式ドキュメント がちょっとわかりにくいので改訂したいなと思っていて、自分の整理もかねて書きたい。

公式の方はもう少し細かい使い方も載っているのだが、特に重要だろうというところだけをまとめる。

連結 / 結合という用語は以下の意味で使っている。まず憶えておいたほうがよい関数、メソッドは以下の 4 つだけ。

  • 連結: データの中身をある方向にそのままつなげる。pd.concat, DataFrame.append
  • 結合: データの中身を何かのキーの値で紐付けてつなげる。pd.merge, DataFrame.join

連結 (concatenate)

柔軟な連結 pd.concat

ふたつの DataFrame の連結は pd.concat で行う ( DataFrame.concat ではない)。元データとする DataFramedf1df2 としてそれぞれ以下のように定義する。

import pandas as pd
df1 = pd.DataFrame({'A': ['A0', 'A1', 'A2', 'A3'],
                    'B': ['B0', 'B1', 'B2', 'B3'],
                    'C': ['C0', 'C1', 'C2', 'C3'],
                    'D': ['D0', 'D1', 'D2', 'D3']},
                   index=[0, 1, 2, 3])

df2 = pd.DataFrame({'A': ['A4', 'A5', 'A6', 'A7'],
                    'B': ['B4', 'B5', 'B6', 'B7'],
                    'C': ['C4', 'C5', 'C6', 'C7'],
                    'D': ['D4', 'D5', 'D6', 'D7']},
                   index=[4, 5, 6, 7])
基本的な連結

この 2 つのデータに対する pd.concat の結果は下図 Result で示すもの = 縦方向の連結になる。pd.concat の引数は連結したい DataFrame のリスト [df1, df2] になる。Result の上半分が df1, 下半分が df2 に対応している。

pd.concat([df1, df2])

f:id:sinhrks:20150125225941p:plain

引数には 3つ以上の DataFrame からなるリストを渡してもよい。結果は 各 DataFrame が順に縦方向に連結されたものになる。

df3 = pd.DataFrame({'A': ['A8', 'A9', 'A10', 'A11'],
                    'B': ['B8', 'B9', 'B10', 'B11'],
                    'C': ['C8', 'C9', 'C10', 'C11'],
                    'D': ['D8', 'D9', 'D10', 'D11']},
                   index=[8, 9, 10, 11])

pd.concat([df1, df2, df3])

f:id:sinhrks:20150125225948p:plain

列名が異なる場合の連結

上ふたつの例では連結する各 DataFrame の 列名 ( columns )はすべて ['A', 'B', 'C', 'D'] で同一だったので気にする必要はなかったが、内部的には 各列は 列名で紐付けされてから連結されている。

以下のように 列名が異なる場合、元データ両方に存在する 列 ['B', 'D'] はそれそれ紐付けられて縦連結され、片方にしか存在しない列は 空の部分が NaN でパディングされる。

df4 = pd.DataFrame({'B': ['B2', 'B3', 'B6', 'B7'],
                    'D': ['D2', 'D3', 'D6', 'D7'],
                    'F': ['F2', 'F3', 'F6', 'F7']},
                   index=[2, 3, 6, 7])

pd.concat([df1, df4])

f:id:sinhrks:20150125230000p:plain

補足 連結方向のラベル ( 縦方向の場合 行名 = index ) 同士は紐付けされず 常にそのまま連結される。出力をみると、 df1index である [0, 1, 2, 3]df2index である [2, 3, 6, 7] は一部重複しているが、紐付けされず元の順序のまま連結されている。連結方向のラベルを重複させたくない場合の処理は後述。

横方向の連結

横方向に連結したい場合は axis=1 を指定。このとき 直前の例とは逆に 紐付けは 連結方向でないラベル = index について行われる。 連結方向のラベルにあたる columns はそのまま維持される。

pd.concat([df1, df4], axis=1)

f:id:sinhrks:20150125230033p:plain

連結処理の指定

上でみたとおり、pd.concat による連結では、データを "連結方向でないラベル" で紐付けしてから 連結していた。この紐付けは元データをすべて残す形 = 完全外部結合のような形で行われている。

引数の各データに共通のラベルのみを残して連結したい場合は join='inner' を指定する。下の例では横連結を指定しているため、共通の index である [2, 3] に対応する行のみが残る。

pd.concat([df1, df4], axis=1, join='inner')

f:id:sinhrks:20150125230043p:plain

また、紐付け時に特定のラベルのみを残したい場合もある。そのときは join_axes で残したいラベルの名前を指定すればよい。

例えば axis=1 横方向に連結するとき、join_axes に ひとつめの DataFrameindex を指定すると、それらだけが残るため 左外部結合のような処理になり、

pd.concat([df1, df4], axis=1, join_axes=[df1.index])

f:id:sinhrks:20150125230051p:plain

ふたつめの DataFrameindex を指定すると 右外部結合のような処理になる。

pd.concat([df1, df4], axis=1, join_axes=[df4.index])

f:id:sinhrks:20150127234220p:plain

連結方向のラベルの指定

pd.concat は既定では 連結方向のラベル ( 縦連結の場合は index、横連結の場合は columns ) に対しては特に変更を行わない。そのため、上の例のように 連結結果に同名のラベルが重複してしまうことがある。

こういう場合、連結元のデータごとに keys キーワードで指定したラベルを追加で付与することができる。このとき、結果は 複数のレベルをもつ MultiIndex になる ( MultiIndex については別途)。これは図だけでもわかりにくいので テキストでの出力も添付。

pd.concat([df1, df4], axis=1, keys=['X', 'Y'])

#      X                   Y          
#      A    B    C    D    B    D    F
# 0   A0   B0   C0   D0  NaN  NaN  NaN
# 1   A1   B1   C1   D1  NaN  NaN  NaN
# 2   A2   B2   C2   D2   B2   D2   F2
# 3   A3   B3   C3   D3   B3   D3   F3
# 6  NaN  NaN  NaN  NaN   B6   D6   F6
# 7  NaN  NaN  NaN  NaN   B7   D7   F7

f:id:sinhrks:20150125230058p:plain

もしくは、ignore_index=True を指定して 連結方向のラベルを 0 から振りなおすことができる。これは縦連結のときに index を連番で振りなおす場合に便利。

pd.concat([df1, df4], ignore_index=True)

f:id:sinhrks:20150128002540p:plain

縦方向のシンプルな連結 DataFrame.append

pd.concat でたいていの連結はできる。うち、よく使う 縦方向の連結については DataFrame.append でよりシンプルに書ける。

df1.append(df2)

f:id:sinhrks:20150125230116p:plain

引数には DataFrame のリストも渡せる。

df1.append([df2, df4])

f:id:sinhrks:20150125230124p:plain

また、データに一行追加したい、なんて場合も DataFrame.append。このとき、引数は 追加する行に対応した Series になる。

このとき、連結対象の DataFramecolumnsSeriesindex どうしが紐付けられて連結される。そのため、行として追加する Series は以下のような形で作る。Seriesname 属性を持っている場合は 連結後の行の indexname で指定されたもの (ここでは 10 ) になる。

s1 = pd.Series(['X0', 'X1', 'X2', 'X3'],
               index=['A', 'B', 'C', 'D'], name=10)
df1.append(s1)

f:id:sinhrks:20150125230133p:plain

name 属性のない Series を連結したい場合は ignore_index=True を指定しないとエラーになる。

s2 = pd.Series(['X0', 'X1', 'X2', 'X3'],
               index=['A', 'B', 'C', 'D'])

# NG!
df1.append(s2)
# TypeError: Can only append a Series if ignore_index=True or if the Series has a name

# OK
df1.append(s2, ignore_index=True)

f:id:sinhrks:20150125230141p:plain

補足 append の際、内部では毎回 元データも含めた全体のコピー処理が走るので、ループで一行ずつ追加するような処理は避けたほうがよい。

結合 (merge)

列の値による結合 pd.merge

ふたつの DataFrame の結合は pd.merge もしくは DataFrame.merge で行う。結合もとの DataFrameleftright としてそれぞれ以下のように定義する。

left = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'],
                     'A': ['A0', 'A1', 'A2', 'A3'],
                     'B': ['B0', 'B1', 'B2', 'B3']})

right = pd.DataFrame({'key': ['K1', 'K3', 'K5', 'K7'],
                      'C': ['C1', 'C3', 'C5', 'C7'],
                      'D': ['D1', 'D3', 'D5', 'D7']},
                     index=[1, 3, 5, 7])

この例では 作成した DataFrame 中の key カラムを結合時のキーとし、この列の値が同じ行どうしを結合したい。結合時のキーとなる列名は on キーワードで指定する。既定では内部結合となり、両方のデータに共通の ['K1', 'K3'] に対応する行どうしが結合されて残る。

pd.merge(left, right, on='key')

f:id:sinhrks:20150125230151p:plain

結合方法は how キーワードで指定する。指定できるのは、

  • inner: 既定。内部結合。両方のデータに含まれるキーだけを残す。
  • left: 左外部結合。ひとつめのデータのキーをすべて残す。
  • right: 右外部結合。ふたつめのデータのキーをすべて残す。
  • outer: 完全外部結合。すべてのキーを残す。

それぞれの出力を順に図示する。

pd.merge(left, right, on='key', how='left')

f:id:sinhrks:20150125230158p:plain

pd.merge(left, right, on='key', how='right')

f:id:sinhrks:20150125230208p:plain

pd.merge(left, right, on='key', how='outer')

f:id:sinhrks:20150125230218p:plain

複数のキーによる結合

キーとして複数の列を指定したい場合は、on キーワードにキーとする列名のリストを渡せばよい。

left = pd.DataFrame({'key1': ['K0', 'K0', 'K1', 'K2'],
                     'key2': ['K0', 'K1', 'K0', 'K1'],
                     'A': ['A0', 'A1', 'A2', 'A3'],
                     'B': ['B0', 'B1', 'B2', 'B3']})

right = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'],
                      'key2': ['K0', 'K0', 'K0', 'K0'],
                      'C': ['C0', 'C1', 'C2', 'C3'],
                      'D': ['D0', 'D1', 'D2', 'D3']})
pd.merge(left, right, on=['key1', 'key2'])

f:id:sinhrks:20150125230229p:plain

index による結合 DataFrame.join

各データの index をキーとして結合したい場合は、DataFrame.join が便利。既定は左外部結合となり、結合方法は how で変更できる。指定できるオプションは pd.merge と同じ。

left = pd.DataFrame({'A': ['A0', 'A1', 'A2'],
                     'B': ['B0', 'B1', 'B2']},
                    index=['K0', 'K1', 'K2'])

right = pd.DataFrame({'C': ['C0', 'C2', 'C3'],
                      'D': ['D0', 'D2', 'D3']},
                     index=['K0', 'K2', 'K3'])
left.join(right)

f:id:sinhrks:20150125230238p:plain

left.join(right, how='inner')

f:id:sinhrks:20150125230257p:plain

left.join(right, how='right')

f:id:sinhrks:20150125230304p:plain

left.join(right, how='outer')

f:id:sinhrks:20150125230248p:plain

補足 pd.merge でも left_index ならびに right_index キーワードによって index をキーとした結合はできる。

まとめ

pandas でのデータ連結 / 結合まわりを整理した。これ以外の データ変形 (行持ち / 列持ち変換とか) は R の {dplyr}{tidyr} との対比でまとめたやつがあるのだが、列名 や行名が複数のレベルを持つ = MultiIndex の場合など pandas 固有のものもあるのでまた別途。

2015/05/12追記

公式ドキュメントに反映した。こちらのほうが網羅的。

Merge, join, and concatenate — pandas 0.16.2 documentation

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理