pandasのcut, qcut関数でビニング処理(ビン分割)
ビニング処理(ビン分割)とは、連続値を任意の境界値で区切りカテゴリ分けして離散値に変換する処理のこと。機械学習の前処理などで行われる。
例えば、年齢のデータを10代、20代の層(水準)ごとに分けるといった処理などがある。
pandasでビニング処理(ビン分割)を行うにはpandas.cut()またはpandas.qcut()を使う。
それぞれ、
- 等間隔または任意の境界値でビン分割:
cut() - 要素数が等しくなるようにビン分割:
qcut()
という違いがある。
ここでは、pandas.cut()およびpandas.qcut()の使い方として、以下の内容を説明する。
- 等間隔または任意の境界値でビニング処理:
cut()- 最大値と最小値の間を等間隔で分割
- 境界値を指定して分割
- 境界値のリストを取得: 引数
retbins - 左右どちらのエッジを含めるか指定: 引数
right - ラベルを指定: 引数
labels - 境界値の精度(小数点以下の桁数)を指定: 引数
precision
- ビンに含まれる要素数を等しくビニング処理:
qcut()- 分割数を指定して分割
- 値が重複している場合の注意
- ビンに含まれる要素数をカウント:
value_counts() - Pythonのリスト、NumPy配列ndarrayをビニング処理
- 具体例: タイタニック生存情報の年齢をビニング
例として以下のpandas.Seriesを使う。
importpandasaspds=pd.Series(data=[x**2forxinrange(11)],index=list('abcdefghijk'))print(s)# a 0# b 1# c 4# d 9# e 16# f 25# g 36# h 49# i 64# j 81# k 100# dtype: int64等間隔または任意の境界値でビニング処理: cut()
pandas.cut()関数では、第一引数xに元データとなる一次元配列(Pythonのリストやnumpy.ndarray,pandas.Series)、第二引数binsにビン分割設定を指定する。
最大値と最小値の間を等間隔で分割
第二引数binsに整数値を指定すると分割数(ビン数)の指定になる。最大値と最小値の間を等間隔で分割する。pandas.Seriesを元データとした場合、pandas.Seriesが返る。
s_cut=pd.cut(s,4)print(s_cut)# a (-0.1, 25.0]# b (-0.1, 25.0]# c (-0.1, 25.0]# d (-0.1, 25.0]# e (-0.1, 25.0]# f (-0.1, 25.0]# g (25.0, 50.0]# h (25.0, 50.0]# i (50.0, 75.0]# j (75.0, 100.0]# k (75.0, 100.0]# dtype: category# Categories (4, interval[float64]): [(-0.1, 25.0] < (25.0, 50.0] < (50.0, 75.0] < (75.0, 100.0]]print(type(s_cut))# <class 'pandas.core.series.Series'>(a, b]はa < x <=bの意味。デフォルトでは左側(小さい方)のエッジの値は含まれず、最左端(最小の境界値)は最大値の0.1%分小さい値になる。
境界値を指定して分割
第二引数binsにリストを指定すると、リストの要素を境界値として分割される。範囲外の値はNaNとなる。
print(pd.cut(s,[0,10,50,100]))# a NaN# b (0, 10]# c (0, 10]# d (0, 10]# e (10, 50]# f (10, 50]# g (10, 50]# h (10, 50]# i (50, 100]# j (50, 100]# k (50, 100]# dtype: category# Categories (3, interval[int64]): [(0, 10] < (10, 50] < (50, 100]]境界値のリストを取得: 引数retbins
引数retbins=Trueとすると、ビン分割されたデータと境界値のリストを同時に取得できる。境界値のリストはnumpy.ndarray。
s_cut,bins=pd.cut(s,4,retbins=True)print(s_cut)# a (-0.1, 25.0]# b (-0.1, 25.0]# c (-0.1, 25.0]# d (-0.1, 25.0]# e (-0.1, 25.0]# f (-0.1, 25.0]# g (25.0, 50.0]# h (25.0, 50.0]# i (50.0, 75.0]# j (75.0, 100.0]# k (75.0, 100.0]# dtype: category# Categories (4, interval[float64]): [(-0.1, 25.0] < (25.0, 50.0] < (50.0, 75.0] < (75.0, 100.0]]print(bins)print(type(bins))# [ -0.1 25. 50. 75. 100. ]# <class 'numpy.ndarray'>左右どちらのエッジを含めるか指定: 引数right
上述のように、デフォルトでは右のエッジがビンに含まれ左のエッジがビンに含まれないが、引数right=Falseとすると、逆に右のエッジがビンに含まれなくなる。
print(pd.cut(s,4,right=False))# a [0.0, 25.0)# b [0.0, 25.0)# c [0.0, 25.0)# d [0.0, 25.0)# e [0.0, 25.0)# f [25.0, 50.0)# g [25.0, 50.0)# h [25.0, 50.0)# i [50.0, 75.0)# j [75.0, 100.1)# k [75.0, 100.1)# dtype: category# Categories (4, interval[float64]): [[0.0, 25.0) < [25.0, 50.0) < [50.0, 75.0) < [75.0, 100.1)]最右端(最大の境界値)は最大値の0.1%分大きい値になる。
ラベルを指定: 引数labels
引数labelsでラベルを指定できる。デフォルトはlabels=Noneで、これまでの例の通り(a, b]。
labels=Falseとすると整数値のインデックス(0始まりの連番)になる。
print(pd.cut(s,4,labels=False))# a 0# b 0# c 0# d 0# e 0# f 0# g 1# h 1# i 2# j 3# k 3# dtype: int64リストで任意のラベルを指定することもできる。この場合、ビンの数とリストの要素数が一致していないとエラーになる。
print(pd.cut(s,4,labels=['small','medium','large','x-large']))# a small# b small# c small# d small# e small# f small# g medium# h medium# i large# j x-large# k x-large# dtype: category# Categories (4, object): [small < medium < large < x-large]境界値の精度(小数点以下の桁数)を指定: 引数precision
引数precisionで境界値の精度(小数点以下の桁数)を指定できる。
print(pd.cut(s,3))# a (-0.1, 33.333]# b (-0.1, 33.333]# c (-0.1, 33.333]# d (-0.1, 33.333]# e (-0.1, 33.333]# f (-0.1, 33.333]# g (33.333, 66.667]# h (33.333, 66.667]# i (33.333, 66.667]# j (66.667, 100.0]# k (66.667, 100.0]# dtype: category# Categories (3, interval[float64]): [(-0.1, 33.333] < (33.333, 66.667] < (66.667, 100.0]]print(pd.cut(s,3,precision=1))# a (-0.1, 33.3]# b (-0.1, 33.3]# c (-0.1, 33.3]# d (-0.1, 33.3]# e (-0.1, 33.3]# f (-0.1, 33.3]# g (33.3, 66.7]# h (33.3, 66.7]# i (33.3, 66.7]# j (66.7, 100.0]# k (66.7, 100.0]# dtype: category# Categories (3, interval[float64]): [(-0.1, 33.3] < (33.3, 66.7] < (66.7, 100.0]]ビンに含まれる個数(要素数)を等しくビニング処理: qcut()
qcut()はcut()のように値に対して等分割したり境界値を指定するのではなく、各ビンに含まれる個数(要素数)が出来る限り等しくなるようにビニング処理(ビン分割)する関数。
第一引数xに元データとなる一次元配列(Pythonのリストやnumpy.ndarray,pandas.Series)、第二引数qに分割数を指定する。
cut()と同じ引数としてlabels,retbinsがある。
分割数を指定して分割
第二引数qに分割数を指定する。
q=2とすると中央値で分割される。
print(pd.qcut(s,2))# a (-0.001, 25.0]# b (-0.001, 25.0]# c (-0.001, 25.0]# d (-0.001, 25.0]# e (-0.001, 25.0]# f (-0.001, 25.0]# g (25.0, 100.0]# h (25.0, 100.0]# i (25.0, 100.0]# j (25.0, 100.0]# k (25.0, 100.0]# dtype: category# Categories (2, interval[float64]): [(-0.001, 25.0] < (25.0, 100.0]]q=4とすると四分位数ごとに分割される。上述のようにcut()と同じ引数としてlabels,retbinsが使える。
s_qcut,bins=pd.qcut(s,4,labels=['Q1','Q2','Q3','Q4'],retbins=True)print(s_qcut)# a Q1# b Q1# c Q1# d Q2# e Q2# f Q2# g Q3# h Q3# i Q4# j Q4# k Q4# dtype: category# Categories (4, object): [Q1 < Q2 < Q3 < Q4]print(bins)# [ 0. 6.5 25. 56.5 100. ]値が重複している場合の注意
元データの要素の値が重複している場合は注意が必要。
例えば中央値までが重複した値である場合。
s_duplicate=pd.Series(data=[0,0,0,0,0,1,2,3,4,5,6],index=list('abcdefghijk'))print(s_duplicate)# a 0# b 0# c 0# d 0# e 0# f 1# g 2# h 3# i 4# j 5# k 6# dtype: int64q=2として中央値で2分割することは可能だが、それより大きい分割数ではエラーとなる。
print(pd.qcut(s_duplicate,2))# a (-0.001, 1.0]# b (-0.001, 1.0]# c (-0.001, 1.0]# d (-0.001, 1.0]# e (-0.001, 1.0]# f (-0.001, 1.0]# g (1.0, 6.0]# h (1.0, 6.0]# i (1.0, 6.0]# j (1.0, 6.0]# k (1.0, 6.0]# dtype: category# Categories (2, interval[float64]): [(-0.001, 1.0] < (1.0, 6.0]]# print(pd.qcut(s_duplicate, 4))# ValueError: Bin edges must be unique: array([0. , 0. , 1. , 3.5, 6. ]).# You can drop duplicate edges by setting the 'duplicates' kwarg例えば4分割の場合、最小値、1/4分位数(25%)、中央値(50%)、3/4分位数(75%)、最大値が境界値として設定されるが、例のように重複した要素が多いと、最小値と1/4分位数が同じ値になってしまうのがエラーの原因。
引数duplicates='drop'とすると重複した境界値は除外して分割される。当然ながら、この場合、ビンに含まれる要素の数は異なる。
print(pd.qcut(s_duplicate,4,duplicates='drop'))# a (-0.001, 1.0]# b (-0.001, 1.0]# c (-0.001, 1.0]# d (-0.001, 1.0]# e (-0.001, 1.0]# f (-0.001, 1.0]# g (1.0, 3.5]# h (1.0, 3.5]# i (3.5, 6.0]# j (3.5, 6.0]# k (3.5, 6.0]# dtype: category# Categories (3, interval[float64]): [(-0.001, 1.0] < (1.0, 3.5] < (3.5, 6.0]]ビンに含まれる個数(要素数)をカウント: value_counts()
cut()やqcut()で取得できるビン分割してラベル付けされたpandas.Seriesからvalue_counts()メソッドを呼ぶと、ビンに含まれる個数(要素数)が得られる。
counts=pd.cut(s,3,labels=['S','M','L']).value_counts()print(counts)# S 6# M 3# L 2# dtype: int64print(type(counts))# <class 'pandas.core.series.Series'>print(counts['M'])# 3value_counts()についての詳細は以下の記事を参照。
value_counts()はpandas.Seriesのメソッドだけでなく関数pandas.value_counts()としても用意されている。その引数にcut()やqcut()で取得できるpandas.Seriesを渡してもOK。
print(pd.value_counts(pd.cut(s,3,labels=['S','M','L'])))# S 6# M 3# L 2# dtype: int64Pythonのリスト、NumPy配列ndarrayをビニング処理
これまでの例はpandas.Seriesを元データとしていたが、cut()やqcut()の第一引数xには一次元配列であればPythonのリスト、NumPy配列ndarrayを指定することも可能。
l=[x**2forxinrange(11)]print(l)# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]l_cut=pd.cut(l,3,labels=['S','M','L'])print(l_cut)# [S, S, S, S, S, ..., M, M, M, L, L]# Length: 11# Categories (3, object): [S < M < L]print(type(l_cut))# <class 'pandas.core.categorical.Categorical'>pandas.Categoricalという型が返る。インデックス(添字)で要素を取得したり、list()でPythonのリスト型に変換したりできる。
print(l_cut[0])# Sprint(list(l_cut))# ['S', 'S', 'S', 'S', 'S', 'S', 'M', 'M', 'M', 'L', 'L']ビンに含まれる個数(要素数)をカウントしたい場合は関数pandas.value_counts()を使う。
print(pd.value_counts(l_cut))# S 6# M 3# L 2# dtype: int64具体例: タイタニック生存情報の年齢をビニング
具体的な例としてタイタニックの生存情報のデータを使用する。Kaggleの問題からダウンロードできる。
こちらにも置いてある。
適当に列を除外している。
df_titanic=pd.read_csv('data/src/titanic_train.csv').drop(['Name','Ticket','Cabin','Embarked'],axis=1)print(df_titanic.head())# PassengerId Survived Pclass Sex Age SibSp Parch Fare# 0 1 0 3 male 22.0 1 0 7.2500# 1 2 1 1 female 38.0 1 0 71.2833# 2 3 1 3 female 26.0 0 0 7.9250# 3 4 1 1 female 35.0 1 0 53.1000# 4 5 0 3 male 35.0 0 0 8.0500年齢'Age'の列に対してcut()関数を用いてビニング処理を行う。
print(df_titanic['Age'].describe())# count 714.000000# mean 29.699118# std 14.526497# min 0.420000# 25% 20.125000# 50% 28.000000# 75% 38.000000# max 80.000000# Name: Age, dtype: float64print(pd.cut(df_titanic['Age'],5,precision=0).value_counts(sort=False,dropna=False))# (0.0, 16.0] 100# (16.0, 32.0] 346# (32.0, 48.0] 188# (48.0, 64.0] 69# (64.0, 80.0] 11# NaN 177# Name: Age, dtype: int64結果を新たな列として元のDataFrameに追加する場合は以下の通り。既存の列に上書き(代入)する場合は左辺の列名を既存の列名にすればOK。
df_titanic['Age_bin']=pd.cut(df_titanic['Age'],5,labels=False)print(df_titanic.head())# PassengerId Survived Pclass Sex Age SibSp Parch Fare Age_bin# 0 1 0 3 male 22.0 1 0 7.2500 1.0# 1 2 1 1 female 38.0 1 0 71.2833 2.0# 2 3 1 3 female 26.0 0 0 7.9250 1.0# 3 4 1 1 female 35.0 1 0 53.1000 2.0# 4 5 0 3 male 35.0 0 0 8.0500 2.0なお、ここでは説明のためすぐにビニング処理を行っているが、本来は先に何らかの方法で欠損値NaNを補完してからビニングする。
関連カテゴリー
関連記事
- pandas.DataFrame, Seriesをpickleで保存、読み込み(to_pickle, read_pickle)
- pandasで要素・行・列に関数を適用するmap, apply, applymap
- pandas.DataFrame, Seriesの重複した行を抽出・削除
- pandasのjson_normalizeで辞書のリストをDataFrameに変換
- pandasでCSV/TSVファイル読み込み(read_csv, read_table)
- pandas, Matplotlib(mplfinance)でローソク足チャートを作成
- pandas.DataFrameのforループ処理(イテレーション)
- pandasで欠損値NaNを前後の値から補間するinterpolate
- pandas.Seriesのmapメソッドで列の要素を置換
- pandas.DataFrame, SeriesとPythonのリストを相互に変換
- pandasで中央値を取得するmedian
- Python, pandasで任意の順番にソート(ソート順を指定)
- pandasのデータ型dtype一覧とastypeによる変換(キャスト)
- pandasで特定の条件を満たす要素数をカウント(全体、行・列ごと)
- pandasでCSVファイルの書き込み・追記(to_csv)