前回は行の番号や列のラベルでデータを抽出していました。
今回はセルの値を条件にして抽出する方法を調べて見ます。
下記記事の続きになります。
Boolean Indexing (ブール索引) によるデータ抽出
ある行や列を特定の値で絞り込みたい場合、boolean vectors (ブールベクトル)と呼ばれるTrue/Falseのベクトルを利用する方法がよく使われます。
Another common operation is the use of boolean vectors to filter the data. The operators are: | for or, & for and, and ~ for not. These must be grouped by using parentheses
引用: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#boolean-indexing
SeriesやDataFrameにbool型のベクトルをseries[boolean vector]、df[boolean vector]のように書くと対象の行(Trueの行)のみ抽出されます。
また、前回の記事で推奨されていたlocメソッドではbooleanの配列も利用可能なので、series.loc[boolean vector]やdf.loc[boolean vector]という抽出方法も出来そうです。
.loc[] is primarily label based, but may also be used with a boolean array.
引用: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html
それでは使い方を色々見てみます。
ますデータフレームをボストンの住宅価格のデータセットを使って作成します。
df = pd.read_csv("http://lib.stat.cmu.edu/datasets/boston_corrected.txt", encoding='Windows-1252',skiprows=9,sep="\t")
df.head()
OBS. TOWN TOWN# TRACT LON LAT MEDV CMEDV CRIM ZN ... CHAS NOX RM AGE DIS RAD TAX PTRATIO B LSTAT 0 1 Nahant 0 2011 -70.955 42.2550 24.0 24.0 0.00632 18.0 ... 0 0.538 6.575 65.2 4.0900 1 296 15.3 396.90 4.98 1 2 Swampscott 1 2021 -70.950 42.2875 21.6 21.6 0.02731 0.0 ... 0 0.469 6.421 78.9 4.9671 2 242 17.8 396.90 9.14 2 3 Swampscott 1 2022 -70.936 42.2830 34.7 34.7 0.02729 0.0 ... 0 0.469 7.185 61.1 4.9671 2 242 17.8 392.83 4.03 3 4 Marblehead 2 2031 -70.928 42.2930 33.4 33.4 0.03237 0.0 ... 0 0.458 6.998 45.8 6.0622 3 222 18.7 394.63 2.94 4 5 Marblehead 2 2032 -70.922 42.2980 36.2 36.2 0.06905 0.0 ... 0 0.458 7.147 54.2 6.0622 3 222 18.7 396.90 5.33 5 rows × 21 columns
df.columns
Index(['OBS.', 'TOWN', 'TOWN#', 'TRACT', 'LON', 'LAT', 'MEDV', 'CMEDV', 'CRIM', 'ZN', 'INDUS', 'CHAS', 'NOX', 'RM', 'AGE', 'DIS', 'RAD', 'TAX', 'PTRATIO', 'B', 'LSTAT'], dtype='object')
今回はTOWNカラムとRMカラムを利用するので、どんなデータか確認しておきます。
df.pivot_table(columns=["TOWN"],aggfunc="size")
TOWN Arlington 7 Ashland 2 Bedford 2 Belmont 8 Beverly 6 .. Weymouth 8 Wilmington 3 Winchester 5 Winthrop 5 Woburn 6 Length: 92, dtype: int64
pivot_tableメソッドでExcelのpivot_tableみたいなことが出来るようです。今回は町名カラムのレコード数を調べるのに使いました。
ヒストグラムとかでみるとなお良かったかもしれません。
df.loc[:,"RM"].quantile([0,0.1,0.25,0.5,0.75,0.9,0.95,0.99,1])
0.00 3.5610 0.10 5.5935 0.25 5.8855 0.50 6.2085 0.75 6.6235 0.90 7.1515 0.95 7.5875 0.99 8.3350 1.00 8.7800 Name: RM, dtype: float64
数値型のRMカラムの中身を俯瞰します。今回はパーセンタイルという指標で確認しました。
最小3.5、中央値6.2、最大値8.7のようです。
Series (文字列) から特定条件を含む行の抽出
町名のカラムだけ取り出し、Seriesを作成します。
s = df.loc[:, "TOWN"]
s.head()
0 Nahant 1 Swampscott 2 Swampscott 3 Marblehead 4 Marblehead Name: TOWN, dtype: object
s == "Swampscott"
0 False 1 True 2 True 3 False 4 False ... 501 False 502 False 503 False 504 False 505 False Name: TOWN, Length: 506, dtype: bool
町名がSwampscottのインデックスのみTrueになるようです。
s[s == "Swampscott"] #or s.loc[s == "Swampscott"]
1 Swampscott 2 Swampscott Name: TOWN, dtype: object
2行だけ抽出されました。
個人的な好みですが、s.locの方が見やすいですね。
s[(s == "Swampscott") | (s == "Ashland")]
# or s.loc[(s == "Swampscott") | (s == "Ashland")]
1 Swampscott 2 Swampscott 254 Ashland 255 Ashland Name: TOWN, dtype: object
町名がSwampscottとAshlandの行を抽出しました。
ただしこのままだとインデックスが1,2,254,255と飛び飛びになっています。使い勝手が悪いのでインデックスを抽出したデータに合わせて振り直すということをしたいと思います。
s[(s == "Swampscott") | (s == "Ashland")].reset_index(drop=True)
0 Swampscott 1 Swampscott 2 Ashland 3 Ashland Name: TOWN, dtype: object
インデックスが振り直されました。
上から3つの情報を取得したい場合はs.loc[0:2]で取得することができます。
Series (数値) から特定条件を含む行の抽出
数値の扱いも確認したいため、RM (平均部屋数) のみ抽出してseriesを作成します。
sn = df.loc[:, "RM"]
sn.head()
0 6.575 1 6.421 2 7.185 3 6.998 4 7.147 Name: RM, dtype: float64
sn >= 7
0 False 1 False 2 True 3 False 4 True ... 501 False 502 False 503 False 504 False 505 False Name: RM, Length: 506, dtype: bool
sn[sn >= 7] # or sn.loc[sn >= 7]
2 7.185 4 7.147 40 7.024 55 7.249 64 7.104 ... 364 8.780 370 7.016 375 7.313 453 7.393 482 7.061 Name: RM, Length: 64, dtype: float64
複数条件を適用したい場合、条件式を1つずつ括弧で囲む必要があるようです。
sn[(sn >= 7.5) & (sn <= 8)]
# or sn.loc[(sn >= 7.5) & (sn <= 8)]
98 7.820 162 7.802 166 7.929 180 7.765 186 7.831 195 7.875 202 7.610 203 7.853 228 7.686 261 7.520 273 7.691 280 7.820 282 7.645 283 7.923 Name: RM, dtype: float64
平均部屋数が7.5以上8以下の行を抽出できたようです。
sn[(sn >= 7.5) & (sn <= 8)].reset_index(drop=True)
0 7.820 1 7.802 2 7.929 3 7.765 4 7.831 5 7.875 6 7.610 7 7.853 8 7.686 9 7.520 10 7.691 11 7.820 12 7.645 13 7.923 Name: RM, dtype: float64
DataFrameから特定条件を含む行の抽出 (文字列カラムが対象)
## listを渡すとdataframe
df2 = df.loc[:, ["TOWN","RM"]]
df2
TOWN RM 0 Nahant 6.575 1 Swampscott 6.421 2 Swampscott 7.185 3 Marblehead 6.998 4 Marblehead 7.147 ... ... ... 501 Winthrop 6.593 502 Winthrop 6.120 503 Winthrop 6.976 504 Winthrop 6.794 505 Winthrop 6.030 506 rows × 2 columns
df2["TOWN"] == "Swampscott"
0 False 1 True 2 True 3 False 4 False ... 501 False 502 False 503 False 504 False 505 False Name: TOWN, Length: 506, dtype: bool
町名カラムがSwampscottの行がTrueになり、それ以外がFalseになる。
df2[df2["TOWN"] == "Swampscott"]
# or df2.loc[df2["TOWN"] == "Swampscott"]
TOWN RM 1 Swampscott 6.421 2 Swampscott 7.185
DataFrameから特定条件を含む行の抽出 (数値カラムが対象)
まだ作成しなければdf2を下記コマンドで作成する。
## listを渡すとdataframe
df2 = df.loc[:, ["TOWN","RM"]]
df2["RM"] >= 7
部屋数が7以上という条件でBoolean vectorを作成する
0 False 1 False 2 True 3 False 4 True ... 501 False 502 False 503 False 504 False 505 False Name: RM, Length: 506, dtype: bool
df2[df2["RM"] >= 7]
# or df2.loc[df2["RM"] >= 7]
TOWN RM 2 Swampscott 7.185 4 Marblehead 7.147 40 Lynnfield 7.024 55 Topsfield 7.249 64 Manchester 7.104 ... ... ... 364 Boston Back Bay 8.780 370 Boston Beacon Hill 7.016 375 Boston Charlestown 7.313 453 Boston Savin Hill 7.393 482 Boston West Roxbury 7.061 64 rows × 2 columns
DataFrame全体に対して特定条件で検索したらどうなるか
下記のようなコードでBoolean vectorsを作成するとどうなるか確認して見ました。
df2 == "Swampscott"
TOWN RM 0 False False 1 True False 2 True False 3 False False 4 False False ... ... ... 501 False False 502 False False 503 False False 504 False False 505 False False 506 rows × 2 columns
type(df2 == "Swampscott")
pandas.core.frame.DataFrame
TOWNとRM両方のカラムに対して、値がSwampscottと一致するかどうかTrue/Falseを返すようです。TOWN列の該当行だけTrueになっているようです。最終的にSeriesではなく、DataFrameとして返ってきました。
df2[df2 == "Swampscott"]
# or df2.loc[df2 == "Swampscott"]
TOWN RM 0 NaN NaN 1 Swampscott NaN 2 Swampscott NaN 3 NaN NaN 4 NaN NaN ... ... ... 501 NaN NaN 502 NaN NaN 503 NaN NaN 504 NaN NaN 505 NaN NaN 506 rows × 2 columns
trueの場所以外はNaNになるようです。
次に比較演算子を試して見ます。
df2 >= 7
TypeError Traceback (most recent call last) Input In [166], in----> 1 df2 >= 7 ・・・省略・・・ TypeError: '>=' not supported between instances of 'str' and 'int'
なんとエラーになりました。文字列に対して比較演算子を使用しているためのようです。
DataFrameの各カラムを使用して複合条件を作成するのが間違いなさそうです。
# 下記はOK
(df2["TOWN"] == "Swampscott") & (df2["RM"] >= 7)
0 False 1 False 2 True 3 False 4 False ... 501 False 502 False 503 False 504 False 505 False Length: 506, dtype: bool
TOWN列がSwampscottでかつ、RMが7以上の行を抽出する条件を作成しました。
df2[(df2["TOWN"] == "Swampscott") & (df2["RM"] >= 7)]
# or df2.loc[(df2["TOWN"] == "Swampscott") & (df2["RM"] >= 7)]
TOWN RM 2 Swampscott 7.185
1行のみが抽出されました。
まとめ
今回の記事でBoolean Indexingについて学べたと思います。
前回までの記事と合わせると、データをDataFrameに格納して行列で抽出したり、抽出条件を作成して特定行のみを選択することも可能になりました。
次回は queryやevalメソッドでの抽出方法をまとめたいと思います。
queryやevalメソッドはnumexprというライブラリを内部的に利用して数値計算を高速処理することが可能になるようです。(データ量が少ない場合は逆に遅くなるようですので注意が必要です。)
その後は、欠損値処理やDataFrameへカラムを追加する方法などをまとめることができれば、仕事で使うデータ操作の知識はほとんど網羅出来るのかなと思っています。
参考
https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#boolean-indexing
https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.loc.html
https://pandas.pydata.org/docs/reference/api/pandas.Series.reset_index.html