pandasのcut, qcut関数でビニング処理(ビン分割) | note.nkmk.me

pandasのcut, qcut関数でビニング処理(ビン分割)

Posted: | Tags: Python, pandas

ビニング処理(ビン分割)とは、連続値を任意の境界値で区切りカテゴリ分けして離散値に変換する処理のこと。機械学習の前処理などで行われる。

例えば、年齢のデータを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=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を補完してからビニングする。

関連カテゴリー

関連記事