Pynote

Python、機械学習、画像処理について

pandas - cut() でビン分割を行う方法

ビン分割

ビン分割 (binning) とは、ビン (bins) と呼ばれる互いに素である区間を用意し、数値をその値が属するビンに割り振ることをいう。

cut()

ビン分割は pandas.cut で行える。

pandas.cut(x, bins, right=True, labels=None, retbins=False, precision=3, include_lowest=False, duplicates='raise')

以下の例では、互いに素な5つの区間 (-0.1, 20.0], (20.0, 40.0], (40.0, 60.0], (60.0, 80.0], (80.0, 100.0] を用意し、配列 [0, 1, ..., 100] の各値をそれが属するビンに割り振っている。

import pandas as pd

x = np.arange(101)
print(x)
# [  0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17
#   18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35
#   36  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53
#   54  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71
#   72  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89
#   90  91  92  93  94  95  96  97  98  99 100]

ret = pd.cut(x, bins=5)
print(ret)
# [(-0.1, 20.0], (-0.1, 20.0], (-0.1, 20.0], (-0.1, 20.0], (-0.1, 20.0], ..., (80.0, 100.0], (80.0, 100.0], (80.0, 100.0], (80.0, 100.0], (80.0, 100.0]]
# Length: 101
# Categories (5, interval[float64]): [(-0.1, 20.0] < (20.0, 40.0] < (40.0, 60.0] < (60.0, 80.0] < (80.0, 100.0]]

返り値は、各値が属するビンを表す pandas.Categorical 型の配列となっている。 pandas.Categorical のカテゴリは各ビンとなっている。
上記の例では、返り値の配列は [(-0.1, 20.0], (-0.1, 20.0], ... となっているので、x[0], x[1] はともにビン (-0.1, 20.0] に属することを意味している。

bins: ビンの指定方法

整数の場合

整数 n を指定した場合、x の値の範囲 [\min x, \max x] を等間隔に n 等分したビンを作成して、ビン分割が行われる。
左端のビンの範囲は、最小値を含めるために、0.1%だけ大きくなる。

例えば、[0, 100] を5等分する場合、ビン (-0.1, 20.0], (20.0, 40.0], (40.0, 60.0], (60.0, 80.0], (80.0, 100.0] が作成される。
左端のビンの範囲は本来 (0, 20] となるはずだが、これでは x の最小値0が含まれないため、100 * 0.1% = 0.1 だけ拡張して、(-0.1, 20] となっていることに注意する。

ret = pd.cut(x, bins=5)
print(ret)
# [(-0.1, 20.0], (-0.1, 20.0], (-0.1, 20.0], (-0.1, 20.0], (-0.1, 20.0], ..., (80.0, 100.0], (80.0, 100.0], (80.0, 100.0], (80.0, 100.0], (80.0, 100.0]]
# Length: 101
# Categories (5, interval[float64]): [(-0.1, 20.0] < (20.0, 40.0] < (40.0, 60.0] < (60.0, 80.0] < (80.0, 100.0]]

1次元配列を指定した場合

1次元配列を指定した場合、各値を端点としたビンを作成して、ビン分割が行われる。
例えば、[0, 20, 40, 60, 100] という配列を渡した場合、(0, 20], (20, 40], (40, 60], (60, 100] というビンが作成される。
ビン分割の結果を見ると、0はどのビンにも含まれないため、NaN となっていることに注意する。

ret = pd.cut(x, bins=[0, 20, 40, 60, 100])
print(ret)
# [NaN, (0.0, 20.0], (0.0, 20.0], (0.0, 20.0], (0.0, 20.0], ..., (60, 100], (60, 100], (60, 100], (60, 100], (60, 100]]
# Length: 101
# Categories (4, interval[int64]): [(0, 20] < (20, 40] < (40, 60] < (60, 100]]

IntervalIndex を指定した場合

IntervalIndex を指定した場合、それに基づきビン分割が行われる。

bins = pd.interval_range(0, 100, 5)
print(bins)
# IntervalIndex([(0, 20], (20, 40], (40, 60], (60, 80], (80, 100]],
#               closed='right',
#               dtype='interval[int64]')

ret = pd.cut(x, bins=bins)
print(ret)
# [NaN, (0.0, 20.0], (0.0, 20.0], (0.0, 20.0], (0.0, 20.0], ..., (80, 100], (80, 100], (80, 100], (80, 100], (80, 100]]
# Length: 101
# Categories (5, interval[int64]): [(0, 20] < (20, 40] < (40, 60] < (60, 80] < (80, 100]]

right: ビンの区間を左半開区間とするか、右半開区間とするか

right=True (デフォルト) の場合、各ビンは右半開区間 (例: (0, 20]) となる。 right=False の場合、各ビンは左半開区間 (例: [0, 20)]) となる。

ret = pd.cut(x, bins=5, right=True)
print(ret.categories)
# IntervalIndex([(-0.1, 20.0], (20.0, 40.0], (40.0, 60.0], (60.0, 80.0], (80.0, 100.0]],
#               closed='right',
#               dtype='interval[float64]')

ret = pd.cut(x, bins=5, right=False)
print(ret.categories)
# IntervalIndex([[0.0, 20.0), [20.0, 40.0), [40.0, 60.0), [60.0, 80.0), [80.0, 100.1)],
#               closed='left',
#               dtype='interval[float64]')

labels: 返り値を値が属するビンの代わりにインデックスまたはラベルを返す。

labels=False の場合、その値が属するビンのインデックスを返す。

ret = pd.cut(x, bins=3, labels=False)
print(ret)
# [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1
#  1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2
#  2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2]

labels に作成されるビンの数と同じ長さの配列を指定した場合、返り値をその値が属するビンの代わりに指定したラベルを返すようにできる。

ret = pd.cut(x, bins=3, labels=["bad", "medium", "good"])
print(ret)
# [bad, bad, bad, bad, bad, ..., good, good, good, good, good]
# Length: 101
# Categories (3, object): [bad < medium < good]

retbins: 作成されたビンも返り値で返す。

retbins=True を指定した場合、返り値は (ビン分割の結果, ビンの端点) というタプルになる。

ret, bins = pd.cut(x, bins=3, retbins=True)
print(bins)  # [ -0.1         33.33333333  66.66666667 100.        ]

include_lowest: 左端の区間を拡張するかどうか

bins に1次元配列または IntervalIndex を指定した場合で include_lowest=True の場合、最小値が含まれるように左端の区間を拡張する。

bins = pd.cut(x, bins=[0, 20, 40, 60, 100], include_lowest=False)
print(bins.categories)
# IntervalIndex([(0, 20], (20, 40], (40, 60], (60, 100]],
#               closed='right',
#               dtype='interval[int64]')

bins = pd.cut(x, bins=[0, 20, 40, 60, 100], include_lowest=True)
print(bins.categories)
# IntervalIndex([(-0.001, 20.0], (20.0, 40.0], (40.0, 60.0], (60.0, 100.0]],
#               closed='right',
#               dtype='interval[float64]')

duplicates: bins に重複した値を含む1次元配列を指定した場合の挙動を制御する。

bins に1次元配列を指定した場合に、重複する端点が含まれている場合の挙動を duplicates 引数で制御できる。
duplicates='raise' の場合、ValueError 例外が送出される。
ValueError='drop' の場合、重複する端点は削除される。

bins = pd.cut(x, bins=[0, 20, 40, 60, 60, 100], duplicates='raise')
# ValueError: Bin edges must be unique

bins = pd.cut(x, bins=[0, 20, 40, 60, 60, 100], duplicates='drop')
print(bins.categories)
# IntervalIndex([(0, 20], (20, 40], (40, 60], (60, 100]],
#               closed='right',
#               dtype='interval[int64]')