StatsFragments

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

Python pandas データ選択処理をちょっと詳しく <後編>

概要

こちらの続き。これで pandas でのデータ選択についてはひとまず終わり。

Python pandas データ選択処理をちょっと詳しく <前編> - StatsFragments

Python pandas データ選択処理をちょっと詳しく <中編> - StatsFragments

サンプルデータの準備

データは 前編と同じものを使う。ただし変数名は変えた。

import pandas as pd

s1 = pd.Series([1, 2, 3], index = ['I1', 'I2', 'I3'])

df1 = pd.DataFrame({'C1': [11, 21, 31],
                    'C2': [12, 22, 32],
                    'C3': [13, 23, 33]},
                   index = ['I1', 'I2', 'I3'])

s1
# I1    1
# I2    2
# I3    3
# dtype: int64

df1
#     C1  C2  C3
# I1  11  12  13
# I2  21  22  23
# I3  31  32  33

where でのデータ選択

これまでみたとおり、Series から __getitem__ すると、条件に該当する要素のみが返ってくる。

s1[s1 > 2]
# I3    3
# dtype: int64

が、ときには元データと同じ長さのデータがほしい場合がある。Series.where を使うと 条件に該当しない ラベルは NaN でパディングし、元データと同じ長さの結果を返してくれる。

where の引数はデータと同じ長さの bool 型の numpy.array もしくは Series である必要がある。まあ Series への論理演算では長さは変わらないので、以下のような使い方なら特別 意識する必要はないかも。

s1.where(s1 > 2)
# I1   NaN
# I2   NaN
# I3     3
# dtype: float64

NaN 以外でパディングしたいんだよ、ってときは 第二引数に パディングに使う値を渡す。NaN でなく 0 でパディングしたければ、

s1.where(s1 > 2, 0)
# I1    0
# I2    0
# I3    3
# dtype: int64

また、第二引数にはデータと同じ長さの numpy.arraySeries も渡せる。このとき、パディングはそれぞれ対応する位置にある値で行われ、第一引数の条件に該当しないデータを 第二引数で置換するような動きになる。つまり if - else のような表現だと考えていい。

# 第一引数の条件に該当しない s1 の 1, 2番目の要素が array の 1, 2 番目の要素で置換される
s1.where(s1 > 2, np.array([4, 5, 6]))
# I1    4
# I2    5
# I3    3
# dtype: int64

# 置換用の Series を作る
s2 = pd.Series([4, 5, 6], index = ['I1', 'I2', 'I3'])
s2
# I1    4
# I2    5
# I3    6
# dtype: int64

# 第一引数の条件に該当しない s1 の 1, 2番目の要素が Series s2 の 1, 2 番目の要素で置換される
s1.where(s1 > 2, s2)
# I1    4
# I2    5
# I3    3
# dtype: int64

DataFrame でも同様。

df1.where(df1 > 22)
#     C1  C2  C3
# I1 NaN NaN NaN
# I2 NaN NaN  23
# I3  31  32  33

# 0 でパディング
df1.where(df1 > 22, 0)
#     C1  C2  C3
# I1   0   0   0
# I2   0   0  23
# I3  31  32  33

# 置換用の DataFrame を作る
df2 = pd.DataFrame({'C1': [44, 54, 64],
                    'C2': [45, 55, 65],
                    'C3': [46, 56, 66]},
                   index = ['I1', 'I2', 'I3'])
df2
#     C1  C2  C3
# I1  44  45  46
# I2  54  55  56
# I3  64  65  66

# df1 のうち、22以下の値を df2 の値で置換
df1.where(df1 > 22, df2)
#     C1  C2  C3
# I1  44  45  46
# I2  54  55  23
# I3  31  32  33

where がことさら便利なのは以下のようなケース。

  • DataFrame に新しいカラムを作りたい。 (あるいは既存の列の値を置き換えたい)
  • 新しいカラムは、"C2" 列の値が 30を超える場合は "C1" 列の値を使う。
  • それ以外は "C3" 列の値を使う。

これが一行でかける。

df1['C4'] = df1['C1'].where(df1['C2'] > 30, df1['C3'])

df1
#     C1  C2  C3  C4
# I1  11  12  13  13
# I2  21  22  23  23
# I3  31  32  33  31

mask でのデータ選択

where の逆の操作として mask がある。こちらは第一引数の条件に該当するセルを NaN でマスクする。ただし第二引数の指定はできないので 使いどころは限られる。

2015/05/06追記 v0.16.1以降で、DataFrame.maskwhere と同じ引数を処理できるようになった。

df1.mask(df1 > 22)
#     C1  C2  C3  C4
# I1  11  12  13  13
# I2  21  22 NaN NaN
# I3 NaN NaN NaN NaN

# v0.16.1以降
df1.mask(df1 > 22, 0)
#     C1  C2  C3
# I1  11  12  13
# I2  21  22   0
# I3   0   0   0

query でのデータ選択

最後にqueryquery で何か新しい処理ができるようになるわけではないが、__getitem__ と同じ操作がよりシンプル な表現で書ける。

numexpr のインストール

query を使うには numexpr パッケージが必要なので、入っていなければインストールする。

pip install numexpr

サンプルデータの準備

また、さきほどの例で列追加したのでサンプルデータを作り直す。

df1 = pd.DataFrame({'C1': [11, 21, 31],
                    'C2': [12, 22, 32],
                    'C3': [13, 23, 33]},
                   index = ['I1', 'I2', 'I3'])
df1
#     C1  C2  C3
# I1  11  12  13
# I2  21  22  23
# I3  31  32  33

query の利用

__getitem__ を利用したデータ選択では、論理演算の組み合わせで boolSeries を作ってやる必要がある。そのため、[] 内で元データへの参照 ( 下の例では df1 )が繰り返しでてくるし、複数条件の組み合わせの際は 演算順序の都合上 () がでてきたりと式が複雑になりがち。これはわかりにくい。

df1[df1['C1'] > 20]
#     C1  C2  C3
# I2  21  22  23
# I3  31  32  33

df1[df1['C2'] < 30]
#     C1  C2  C3
# I1  11  12  13
# I2  21  22  23

df1[(df1['C1'] > 20) & (df1['C2'] < 30)]
#     C1  C2  C3
# I2  21  22  23

同じ処理は query を使うとすっきり書ける。query の引数にはデータ選択に使う条件式を文字列で渡す。この式が評価される名前空間 = query 名前空間の中では、query を呼び出したデータの列名が あたかも変数のように参照できる。

df1.query('C1 > 20 & C2 < 30')
#     C1  C2  C3
# I2  21  22  23

ただし、query 名前空間の中で使える表現は限られるので注意。例えば以下のような メソッド呼び出しはできない。

# NG!
df1.query('C1.isin([11, 21])')
# NotImplementedError: 'Call' nodes are not implemented

同じ処理を行う場合は in 演算子を使う。

# in を使えば OK
df1.query('C1 in [11, 21]')
#     C1  C2  C3
# I1  11  12  13
# I2  21  22  23

また、numexpr で利用できる関数 の呼び出しは query 名前空間上では使えないっぽい。

df1.query('C1 > sqrt(400)')
# NotImplementedError: 'Call' nodes are not implemented

そのため、query に渡す式表現では論理表現 以外を含めないほうがよい。条件によって式表現を変えたい、なんて場合は 式表現を都度 文字列として連結 + 生成するか、ローカル変数に計算結果を入れて式表現に渡す。

ローカル変数を式表現中に含める際は、変数名を @ ではじめる。

x = 20

# NG!
df1.query('C1 > x')
# UndefinedVariableError: name 'x' is not defined

# OK!
df1.query('C1 > @x')
#     C1  C2  C3
# I2  21  22  23
# I3  31  32  33

query 名前空間上で index の値を参照する場合は、式表現中で index と指定する。

df1.query('index in ["I1", "I2"]')
#     C1  C2  C3
# I1  11  12  13
# I2  21  22  23

index が列名と重複した場合は 列名が優先。

df_idx = pd.DataFrame({'index': [1, 2, 3]}, index=[3, 2, 1])
df_idx
#    index
# 3      1
# 2      2
# 1      3

df_idx.query('index >= 2')
#    index
# 2      2
# 1      3

まとめ

  • where を使うと 元データと同じ形のデータが取得できる。また、if - else 的表現が一行で書ける。
  • 複数の条件式を組み合わせる場合は query を使うとシンプルに書ける。

全三回で pandas でのデータ選択処理をまとめた。公式ドキュメント、またAPIガイドには ここで記載しなかったメソッド / 例もあるので、より尖った使い方をしたい人は読んでおくといいと思う。

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

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