note.nkmk.me

pandas.DataFrameの行を条件で抽出するquery

Date: 2018-05-22 / Modified: 2019-09-19 / tags: Python, pandas

pandas.DataFrameの列の値に対する条件に応じて行を抽出するにはquery()メソッドを使う。比較演算子や文字列メソッドを使った条件指定、複数条件の組み合わせなどをかなり簡潔に記述できて便利。

ここでは以下の内容について説明する。

  • numexpr
  • 比較演算子で条件指定
  • in演算子で条件指定(isin()での条件指定と同等)
  • 文字列メソッドで条件指定
    • 欠損値NaNがある場合の注意
  • index列に対する条件
  • 変数を使う
  • 複数条件を指定
  • query()メソッドの注意点
  • 引数inplaceで元のオブジェクトを更新

以下のサンプルコードはpandasバージョン0.23.0。以前のバージョンではエラーになる可能性もあるので注意。

ブールインデックスを使ったレガシーな条件指定については以下の記事を参照。query()の場合、選択範囲に新たな値を代入することはできないので、条件で指定した範囲の値を変更したい場合はブールインデックスを使う。またpandas.Seriesにはquery()メソッドはないので、pandas.Seriesの条件抽出もブールインデックスを使う。

以下のpandas.DataFrameを例とする。

import pandas as pd

df = pd.read_csv('data/src/sample_pandas_normal.csv')

print(df)
#       name  age state  point
# 0    Alice   24    NY     64
# 1      Bob   42    CA     92
# 2  Charlie   18    CA     70
# 3     Dave   68    TX     70
# 4    Ellen   24    CA     88
# 5    Frank   30    NY     57

サンプルのcsvファイルのリンクはこちら。

スポンサーリンク

numexpr

query()pandas.eval()を使っており、pandas.eval()では式を評価するエンジンとしてnumexprを使うことができる。

数万行を超えるような大規模なデータを処理する場合numexprを使うと速くなる(らしい)。

numexprはpipでインストールできる。(環境によってはpip3

$ pip install numexpr

query()の引数engine'python''numexpr'かを選択できる。デフォルトはNoneでnumexprがインストールされていればnumexprが、無ければpythonが使われる。

文字列メソッドはengine='python'でないとquery()メソッドで使えない。後述。

比較演算子で条件指定

pandasでは比較演算子を使って以下のように行を抽出できる。

print(df[df['age'] < 25])
#       name  age state  point
# 0    Alice   24    NY     64
# 2  Charlie   18    CA     70
# 4    Ellen   24    CA     88

query()メソッドを使うと文字列で同様の条件を指定できる。列名に対する条件を文字列で指定する。

print(df.query('age < 25'))
#       name  age state  point
# 0    Alice   24    NY     64
# 2  Charlie   18    CA     70
# 4    Ellen   24    CA     88

否定はnot

print(df.query('not age < 25'))
#     name  age state  point
# 1    Bob   42    CA     92
# 3   Dave   68    TX     70
# 5  Frank   30    NY     57

Pythonの条件指定のように2つの比較演算子で範囲を指定可能。

print(df.query('24 <= age < 50'))
#     name  age state  point
# 0  Alice   24    NY     64
# 1    Bob   42    CA     92
# 4  Ellen   24    CA     88
# 5  Frank   30    NY     57

列と列との比較や、算術演算子で計算して比較することもできる。

print(df.query('age < point / 3'))
#       name  age state  point
# 2  Charlie   18    CA     70
# 4    Ellen   24    CA     88

一致、不一致は==, !=。文字列内の文字列も引用符で囲む必要があるので注意。

シングルクォート'で囲んだ文字列リテラル内ではダブルクォート"、ダブルクォート"で囲んだ文字列リテラル内ではシングルクォート'を使用できる。バックスラッシュ\でエスケープすれば同じ記号を使用可能。

print(df.query('state == "CA"'))
#       name  age state  point
# 1      Bob   42    CA     92
# 2  Charlie   18    CA     70
# 4    Ellen   24    CA     88

print(df.query('state != "CA"'))
#     name  age state  point
# 0  Alice   24    NY     64
# 3   Dave   68    TX     70
# 5  Frank   30    NY     57

in演算子で条件指定(isinでの条件指定と同等)

isin()は列の要素が引数に渡したリストの要素に含まれているかをbool値(True, False)で返すメソッド。これを利用して、ある列の要素が特定の値に一致する行のみを抽出できる。

print(df[df['state'].isin(['NY', 'TX'])])
#     name  age state  point
# 0  Alice   24    NY     64
# 3   Dave   68    TX     70
# 5  Frank   30    NY     57

query()メソッドではinを使って同等の処理が可能。

print(df.query('state in ["NY", "TX"]'))
#     name  age state  point
# 0  Alice   24    NY     64
# 3   Dave   68    TX     70
# 5  Frank   30    NY     57

==とリストでもOK。

print(df.query('state == ["NY", "TX"]'))
#     name  age state  point
# 0  Alice   24    NY     64
# 3   Dave   68    TX     70
# 5  Frank   30    NY     57

文字列メソッドで条件指定

文字列が完全一致する条件は==inで指定できるが、部分一致する条件は文字列メソッドstr.xxx()を使う。

  • str.contains(): 特定の文字列を含む
  • str.endswith(): 特定の文字列で終わる
  • str.startswith(): 特定の文字列で始まる
  • str.match(): 正規表現のパターンに一致する

以下の記事を参照。

これらの文字列メソッドもquery()で使用可能。

ただし、numexprのバージョン2.6.5、pandasのバージョン0.23.0では、引数engine='python'としないとエラーになった。numexprがインストールされていなければ問題ないがインストールされている場合はデフォルトでnumexprが使われるので注意。

print(df.query('name.str.endswith("e")', engine='python'))
#       name  age state  point
# 0    Alice   24    NY     64
# 2  Charlie   18    CA     70
# 3     Dave   68    TX     70

print(df.query('name.str.contains("li")', engine='python'))
#       name  age state  point
# 0    Alice   24    NY     64
# 2  Charlie   18    CA     70

print(df.query('name.str.match(".*i.*e")', engine='python'))
#       name  age state  point
# 0    Alice   24    NY     64
# 2  Charlie   18    CA     70

文字列以外の型dtypeの列はastype()で文字列型strに変換することで文字列メソッドを使える。これもquery()で指定可能。

print(df.query('age.astype("str").str.endswith("8")', engine='python'))
#       name  age state  point
# 2  Charlie   18    CA     70
# 3     Dave   68    TX     70

欠損値NaNがある場合の注意

Noneや欠損値NaNがある列に対して文字列メソッドを適用して条件とするとエラーになるので注意。

df.at[0, 'name'] = None
print(df)
#       name  age state  point
# 0     None   24    NY     64
# 1      Bob   42    CA     92
# 2  Charlie   18    CA     70
# 3     Dave   68    TX     70
# 4    Ellen   24    CA     88
# 5    Frank   30    NY     57

# print(df.query('name.str.endswith("e")', engine='python'))
# ValueError: cannot index with vector containing NA / NaN values

文字列メソッドの多くは引数naNoneや欠損値NaNに対する結果を置き換える値を指定できる。これにTrueを指定すると欠損値の行も抽出され、Falseを指定すると欠損値の行は抽出されない。

print(df[df['name'].str.endswith('e', na=False)])
#       name  age state  point
# 2  Charlie   18    CA     70
# 3     Dave   68    TX     70

が、query()では引数を指定できない。(何かやり方があるのかもしれないがドキュメントを見てもよく分からない。)

# print(df.query('name.str.endswith("e", na=False)', engine='python'))
# AttributeError: 'dict' object has no attribute 'append'

query()ではなくブールインデックスを使った方法で条件を指定するか、fillna()で欠損値を埋める必要がある。ここでは以降の例のためにもとの値で置換している。

df['name'].fillna('Alice', inplace=True)
print(df)
#       name  age state  point
# 0    Alice   24    NY     64
# 1      Bob   42    CA     92
# 2  Charlie   18    CA     70
# 3     Dave   68    TX     70
# 4    Ellen   24    CA     88
# 5    Frank   30    NY     57

index列に対する条件

index列(行名)に対する条件はindexで指定可能。

print(df.query('index % 2 == 0'))
#       name  age state  point
# 0    Alice   24    NY     64
# 2  Charlie   18    CA     70
# 4    Ellen   24    CA     88

index列に名前が付いている場合はその名前でもindexでもどちらでもOK。

df_name = df.set_index('name')
print(df_name)
#          age state  point
# name                     
# Alice     24    NY     64
# Bob       42    CA     92
# Charlie   18    CA     70
# Dave      68    TX     70
# Ellen     24    CA     88
# Frank     30    NY     57

print(df_name.query('name.str.endswith("e")', engine='python'))
#          age state  point
# name                     
# Alice     24    NY     64
# Charlie   18    CA     70
# Dave      68    TX     70

print(df_name.query('index.str.endswith("e")', engine='python'))
#          age state  point
# name                     
# Alice     24    NY     64
# Charlie   18    CA     70
# Dave      68    TX     70

columns(列名)に対する条件で列を抽出したい場合は以下の記事を参照。

変数を使う

query()メソッドの条件文字列の中で変数を使用するには変数名の前に@をつける。

val = 80
print(df.query('point > @val'))
#     name  age state  point
# 1    Bob   42    CA     92
# 4  Ellen   24    CA     88

複数条件を指定

query()メソッドを使わずに複数条件を指定する場合は以下のように記述する。

print(df[(df['age'] < 25) & (df['point'] > 65)])
#       name  age state  point
# 2  Charlie   18    CA     70
# 4    Ellen   24    CA     88

query()メソッドだと以下のように書ける。条件ごとの括弧は必要なく、「かつ」は&でもandでもどちらでもOK。

print(df.query('age < 25 & point > 65'))
#       name  age state  point
# 2  Charlie   18    CA     70
# 4    Ellen   24    CA     88

print(df.query('age < 25 and point > 65'))
#       name  age state  point
# 2  Charlie   18    CA     70
# 4    Ellen   24    CA     88

「または」も|でもorでもどちらでもOK。

print(df.query('age < 25 | point > 65'))
#       name  age state  point
# 0    Alice   24    NY     64
# 1      Bob   42    CA     92
# 2  Charlie   18    CA     70
# 3     Dave   68    TX     70
# 4    Ellen   24    CA     88

print(df.query('age < 25 or point > 65'))
#       name  age state  point
# 0    Alice   24    NY     64
# 1      Bob   42    CA     92
# 2  Charlie   18    CA     70
# 3     Dave   68    TX     70
# 4    Ellen   24    CA     88

否定はnot

print(df.query('not age < 25 and not point > 65'))
#     name  age state  point
# 5  Frank   30    NY     57

3つ以上での条件も同様だが、andのほうがorより優先順位が高いなど順番によって結果が異なるので、先に処理したいまとまりを括弧で囲んだほうが無難。

print(df.query('age == 24 | point > 80 & state == "CA"'))
#     name  age state  point
# 0  Alice   24    NY     64
# 1    Bob   42    CA     92
# 4  Ellen   24    CA     88

print(df.query('(age == 24 | point > 80) & state == "CA"'))
#     name  age state  point
# 1    Bob   42    CA     92
# 4  Ellen   24    CA     88

queryメソッドの注意点

query()メソッドを使う上で列名に注意が必要。

以下のように列名を変更する。

df.columns = ['名前', 'age.year', 'state name', 3]
print(df)
#         名前  age.year state name   3
# 0    Alice        24         NY  64
# 1      Bob        42         CA  92
# 2  Charlie        18         CA  70
# 3     Dave        68         TX  70
# 4    Ellen        24         CA  88
# 5    Frank        30         NY  57

日本語はOK。

print(df.query('名前 == ["Alice", "Dave"]'))
#       名前  age.year state name   3
# 0  Alice        24         NY  64
# 3   Dave        68         TX  70

print(df.query('名前.str.contains("li")', engine='python'))
#         名前  age.year state name   3
# 0    Alice        24         NY  64
# 2  Charlie        18         CA  70

.やスペースが含まれていたり、数値だったりするとNG。

# print(df.query('age.year < 25'))
# UndefinedVariableError: name 'age' is not defined

# print(df.query('state name == "CA"'))
# SyntaxError: invalid syntax

# print(df.query('3 > 75'))
# KeyError: False

ブールインデックスを使った条件指定であれば問題ない。

print(df[df['age.year'] < 25])
#         名前  age.year state name   3
# 0    Alice        24         NY  64
# 2  Charlie        18         CA  70
# 4    Ellen        24         CA  88

print(df[df['state name'] == 'CA'])
#         名前  age.year state name   3
# 1      Bob        42         CA  92
# 2  Charlie        18         CA  70
# 4    Ellen        24         CA  88

print(df[df[3] > 75])
#       名前  age.year state name   3
# 1    Bob        42         CA  92
# 4  Ellen        24         CA  88

列名はrename()メソッドで変更可能。

df.rename(columns={3: 'point'}, inplace=True)
print(df)
#         名前  age.year state name  point
# 0    Alice        24         NY     64
# 1      Bob        42         CA     92
# 2  Charlie        18         CA     70
# 3     Dave        68         TX     70
# 4    Ellen        24         CA     88
# 5    Frank        30         NY     57

以下のように列名columns., スペースを一括でアンダースコア_に置き換えることもできる。

df.columns = [str(s).replace(' ', '_').replace('.', '_') for s in df.columns]
print(df)
#         名前  age_year state_name  point
# 0    Alice        24         NY     64
# 1      Bob        42         CA     92
# 2  Charlie        18         CA     70
# 3     Dave        68         TX     70
# 4    Ellen        24         CA     88
# 5    Frank        30         NY     57

.やスペース以外にもなどエラーになる文字があるので注意。

引数inplaceで元のオブジェクトを更新

これまでの例ではquery()で行を抽出した新たなpandas.DataFrameが返され、元のオブジェクトはそのままだったが、引数inplace=Trueとすると元のオブジェクト自体が変更される。

df.query('age_year > 25', inplace=True)
print(df)
#       名前  age_year state_name  point
# 1    Bob        42         CA     92
# 3   Dave        68         TX     70
# 5  Frank        30         NY     57
スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事