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
を使う。
import pandas as pd
s = pd.Series(data=[x**2 for x in range(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: int64
q=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: int64
print(type(counts))
# <class 'pandas.core.series.Series'>
print(counts['M'])
# 3
value_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: int64
Pythonのリスト、NumPy配列ndarrayをビニング処理
これまでの例はpandas.Series
を元データとしていたが、cut()
やqcut()
の第一引数x
には一次元配列であればPythonのリスト、NumPy配列ndarrayを指定することも可能。
l = [x**2 for x in range(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])
# S
print(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: float64
print(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
を補完してからビニングする。