わかりやすい pandas qcut 解説:データ分析での実践例を紹介


わかりやすい pandas qcut 関数徹底解説:データ分析での実践例と活用法

データ分析において、連続的な数値を扱うことは非常に一般的です。しかし、そのままの数値で分析を行うよりも、特定の区間に分割(ビン化または離散化)して扱う方が、データの傾向を捉えやすくなったり、特定の分析手法を適用しやすくなったりする場合があります。pandas ライブラリは、このようなビン化を行うための強力な関数として cutqcut を提供しています。

本記事では、特に「分位数」に基づいたビン化を行う pandas.qcut 関数に焦点を当て、その機能、使い方、そしてデータ分析における実践的な活用方法を、豊富なコード例とともに詳細に解説します。約5000語にわたる徹底的な解説を通じて、qcut を完全にマスターし、日々のデータ分析作業に役立てることを目指します。

1. はじめに:なぜ連続値をビン化する必要があるのか?

データセットに含まれる「年齢」「収入」「売上」「試験の点数」のような数値データは、多くの場合連続的な値を取ります。これらの連続値をそのまま分析することも多いですが、いくつかの理由から、特定の区間に分割(ビン化、binnning)してカテゴリカルなデータとして扱う方が都合が良い場合があります。

ビン化の主な目的は以下の通りです。

  • 傾向の把握の容易化: 連続値を少数のカテゴリに集約することで、全体の傾向やパターンをより簡単に把握できるようになります。「若年層」「中年層」「高年層」や「低所得層」「中所得層」「高所得層」といったカテゴリに分けることで、それぞれのグループがどのような特徴を持っているかを比較しやすくなります。
  • ノイズの低減: 細かい数値の変動によるノイズを平滑化し、より安定した分析結果を得られることがあります。
  • 特定の分析手法の適用: 回帰分析におけるダミー変数化、クロス集計、ノンパラメトリック検定など、カテゴリ変数または区間データとして扱うことで初めて適用可能になる分析手法があります。
  • 可視化の補助: 連続値の散布図だけでは見えにくい傾向が、ビン化してカテゴリごとに集計・可視化することで明確になることがあります。
  • データサイズの削減: 連続値を少数のカテゴリに置き換えることで、データのサイズを小さくできる場合があります(ただし、これは主な目的ではないことが多いです)。

pandas には、ビン化を行うための主要な関数として cutqcut があります。

  • pandas.cut: 固定された区間に基づいてビン化を行います。例えば、「0-10歳」「10-20歳」「20-30歳」のように、区間の境界値を明確に指定する場合に使います。
  • pandas.qcut: 分位数に基づいてビン化を行います。データに含まれる値の分布を見て、各ビンにほぼ等しい数のデータが入るように区間を自動的に決定します。例えば、データを「下位25%」「25%-50%」「50%-75%」「上位25%」のように分割したい場合に使います。

本記事では、特にデータの分布に偏りがある場合や、各ビンに均等なデータ数を持たせたい場合に強力なツールとなる qcut 関数に焦点を当て、その詳細な使い方から実践的な応用例までを徹底的に解説します。

2. pandas.qcut 関数の基本

pandas.qcut 関数は、指定したSeriesや配列の値を、分位数に基づいて指定した数の区間に分割します。基本的な使い方を見てみましょう。

2.1. 基本的な構文と引数

pandas.qcut の基本的な構文は以下の通りです。

python
pandas.qcut(x, q, labels=None, retbins=False, precision=3, duplicates='raise', right=True, include_lowest=False, errors='raise')

  • x: ビン化したいデータを持つ Series または NumPy 配列。
  • q: 分割する区間の数(整数)、または分位点のリスト。qcut 関数の中核となる引数です。
  • labels: ビンに付けるラベルのリスト。省略した場合は区間を表すタプルがラベルになります。False を指定すると整数のインデックスが返されます。
  • retbins: True を指定すると、ビン化された Series とともに区間の境界値を持つ配列も返されます。
  • precision: 区間の境界値を文字列として表示する際の小数点以下の精度。
  • duplicates: 重複する境界値が見つかった場合の処理方法。'raise' (デフォルト), 'drop', 'warn' が指定できます。これは qcut を使う上で非常に重要な引数であり、後ほど詳しく解説します。
  • right: 区間の右端を含むかどうか。デフォルトは True です。つまり、区間は (a, b] の形になります。
  • include_lowest: right=True の場合、一番左の区間が [a, b] となるように、左端の値を含むかどうか。デフォルトは False です。
  • errors: エラー処理の方法。'raise' (デフォルト) または 'ignore'

2.2. q 引数の指定方法

qcut 関数で最も重要な引数は q です。これにより、どのようにデータを分割するかが決まります。

方法1: 整数を指定

q に整数を指定すると、データをその数だけ(ほぼ)等しいデータ数のビンに分割します。例えば、q=4 と指定すると、四分位数(0%, 25%, 50%, 75%, 100%)に基づいてデータを4つのビンに分割します。各ビンには、全体の約1/4のデータが含まれることになります(重複する値が多い場合などを除く)。

“`python
import pandas as pd
import numpy as np

サンプルデータの生成 (少し偏った分布)

np.random.seed(42)
data = np.concatenate([np.random.normal(50, 10, 100),
np.random.normal(80, 5, 50),
np.random.normal(20, 15, 30)])
s = pd.Series(data)

データを4つのビンに分割 (四分位数)

quartiles = pd.qcut(s, q=4)

print(“— 分割結果の最初の5件 —“)
print(quartiles.head())

print(“\n— 各ビンのデータ数 —“)
print(quartiles.value_counts().sort_index())
“`

実行結果の例:

“`
— 分割結果の最初の5件 —
0 (34.207, 54.39]
1 (54.39, 67.476]
2 (34.207, 54.39]
3 (34.207, 54.39]
4 (34.207, 54.39]
dtype: category
Categories (4, interval[float64]): [(1.731, 34.207] < (34.207, 54.39] < (54.39, 67.476] < (67.476, 94.161]]

— 各ビンのデータ数 —
(1.731, 34.207] 45
(34.207, 54.39] 45
(54.39, 67.476] 45
(67.476, 94.161] 45
dtype: int64
“`

上記の例では、データを4つのビンに分割した結果、それぞれのビンに45個のデータ(合計180個のデータの約1/4)がほぼ均等に含まれていることがわかります。デフォルトでは、ビンは区間を表すタプル (a, b] の形式でラベル付けされます(左端を含まず右端を含む)。

方法2: 分位点のリストを指定

q に分位点のリスト(0から1までの浮動小数点数のリスト)を指定することもできます。これにより、カスタムのパーセンタイルに基づいてデータを分割できます。リストの最初と最後は通常 01 になります。

“`python

データを 下位10%, 10-50%, 50-90%, 上位10% に分割

custom_quantiles = pd.qcut(s, q=[0, 0.1, 0.5, 0.9, 1])

print(“— カスタム分位点での分割結果の最初の5件 —“)
print(custom_quantiles.head())

print(“\n— 各ビンのデータ数 —“)
print(custom_quantiles.value_counts().sort_index())
“`

実行結果の例:

“`
— カスタム分位点での分割結果の最初の5件 —
0 (34.207, 67.476]
1 (34.207, 67.476]
2 (34.207, 67.476]
3 (34.207, 67.476]
4 (34.207, 67.476]
dtype: category
Categories (4, interval[float64]): [(1.731, 17.022] < (17.022, 67.476] < (67.476, 88.06] < (88.06, 94.161]] # 注意:境界値はデータに依存

— 各ビンのデータ数 —
(1.731, 17.022] 18
(17.022, 67.476] 108
(67.476, 88.06] 36
(88.06, 94.161] 18
dtype: int64
“`

この例では、q=[0, 0.1, 0.5, 0.9, 1] という5つの分位点を指定したため、4つのビンに分割されました。各ビンには、全体の約10%, 40%, 40%, 10% のデータが含まれるように(分位点が正確に計算できれば)分割されます。ただし、実際のデータ分布によっては、指定した割合と完全に一致しない場合もあります。特に、同じ値を持つデータポイントが多い場合やデータ数が少ない場合に、指定通りの分位点やビン数にならないことがあります。

2.3. 戻り値:Categorical 型

qcut 関数は、デフォルトでは pandas の Categorical 型の Series を返します。Categorical 型は、有限かつ固定された数のカテゴリ(ビン)を持つデータに適した型です。

  • メモリ効率: 多数の重複する文字列ラベルを持つデータの場合、Categorical 型は文字列そのものよりも効率的にメモリを使用します。
  • 順序: デフォルトでは、ビンは区間の順序に基づいて順序付けられます。これにより、ソートや比較などの操作が直感的に行えます。

value_counts() を使用すると、各カテゴリ(ビン)に属するデータポイントの数を簡単に確認できます。これは、qcut が意図通りにデータを分割できたかを確認するのに役立ちます。

2.4. labels 引数を使ったラベル付け

デフォルトの区間タプルによるラベルは正確ですが、分析結果を報告したり、他の人と共有したりする際には、より分かりやすいラベルを付けたい場合があります。labels 引数に文字列のリストを指定することで、ビンにカスタムラベルを付けることができます。リストの長さは、作成されるビンの数と同じである必要があります(q が整数の場合はその整数、q がリストの場合は len(q) - 1)。

“`python

データを4つのビンに分割し、カスタムラベルを付ける

quartiles_labeled = pd.qcut(s, q=4, labels=[‘Q1’, ‘Q2’, ‘Q3’, ‘Q4’])

print(“— ラベル付き分割結果の最初の5件 —“)
print(quartiles_labeled.head())

print(“\n— ラベル付き各ビンのデータ数 —“)
print(quartiles_labeled.value_counts().sort_index())
“`

実行結果の例:

“`
— ラベル付き分割結果の最初の5件 —
0 Q2
1 Q3
2 Q2
3 Q2
4 Q2
dtype: category
Categories (4, object): [‘Q1’ < ‘Q2’ < ‘Q3’ < ‘Q4’]

— ラベル付き各ビンのデータ数 —
Q1 45
Q2 45
Q3 45
Q4 45
dtype: int64
“`

カスタムラベルを指定した場合も、Categorical 型が返されます。デフォルトでは、指定したラベルのリストの順序に基づいてカテゴリが順序付けられます。これは、例えば「低」「中」「高」のような順序関係を持つカテゴリを作成する際に便利です。

もし、ラベルを付けずに単に整数のインデックス(0, 1, 2, …)を返したい場合は、labels=False を指定します。

“`python

ラベルを付けずに整数のインデックスを取得

quartiles_indexed = pd.qcut(s, q=4, labels=False)

print(“— インデックス付き分割結果の最初の5件 —“)
print(quartiles_indexed.head())

print(“\n— インデックス付き各ビンのデータ数 —“)
print(quartiles_indexed.value_counts().sort_index())
“`

実行結果の例:

“`
— インデックス付き分割結果の最初の5件 —
0 1
1 2
2 1
3 1
4 1
dtype: int64

— インデックス付き各ビンのデータ数 —
0 45
1 45
2 45
3 45
dtype: int64
“`

labels=False は、機械学習モデルにカテゴリデータを数値として入力する際などに便利です。

2.5. retbins 引数で境界値を取得

retbins=True を指定すると、qcut はビン化された Series とともに、使用された区間の境界値を持つ NumPy 配列をタプルとして返します。これは、どのような区間にデータが分割されたかを正確に知りたい場合や、後続の処理でこの境界値を再利用したい場合に便利です。

“`python

データを4つのビンに分割し、境界値も取得

quartiles_series, bins = pd.qcut(s, q=4, retbins=True)

print(“— 取得した境界値 —“)
print(bins)

quartiles_series は pd.qcut(s, q=4) と同じ結果

print(“\n— ビン化されたSeries (retbins=Trueの場合) —“)
print(quartiles_series.head())
“`

実行結果の例:

“`
— 取得した境界値 —
[ 1.73107971 34.20741751 54.39029994 67.47633481 94.16137463]

— ビン化されたSeries (retbins=Trueの場合) —
0 (34.207, 54.39]
1 (54.39, 67.476]
2 (34.207, 54.39]
3 (34.207, 54.39]
4 (34.207, 54.39]
dtype: category
Categories (4, interval[float64]): [(1.731, 34.207] < (34.207, 54.39] < (54.39, 67.476] < (67.476, 94.161]]
“`

取得された bins 配列には、区間の開始値と終了値を含む、ビンの数 + 1 個の値が含まれます。この例では、5つの値があり、これらが4つのビンの境界線を示しています。

2.6. right および include_lowest 引数

これらの引数は、区間の境界値の扱い方を制御します。デフォルトでは right=Trueinclude_lowest=False となっています。

  • right=True (デフォルト): 各区間は左端を含まず右端を含みます。例えば、境界値が [b0, b1, b2, b3] の場合、区間は (b0, b1], (b1, b2], (b2, b3] となります。
  • right=False: 各区間は左端を含み右端を含みません。例えば、境界値が [b0, b1, b2, b3] の場合、区間は [b0, b1), [b1, b2), [b2, b3) となります。
  • include_lowest=True: right=True の場合(デフォルト)、通常最初の区間は (b0, b1] となりますが、include_lowest=True を指定すると、最初の区間だけは左端の b0 を含む [b0, b1] となります。これは、最小値がどのビンにも含まれないという状況を防ぐために便利です。

これらの引数の組み合わせは、特にデータの最小値や最大値が境界値に等しい場合に影響します。特別な理由がない限り、多くの場合デフォルト設定 (right=True, include_lowest=False) のままで問題ありません。ただし、区間の定義を厳密にコントロールしたい場合は、これらの引数を調整してください。

“`python

right=False の例

quartiles_left_inclusive = pd.qcut(s, q=4, right=False)
print(“— right=False の場合の区間例 —“)
print(quartiles_left_inclusive.head())

include_lowest=True の例 (right=True と組み合わせる)

quartiles_lowest_inclusive = pd.qcut(s, q=4, include_lowest=True)
print(“\n— include_lowest=True の場合の区間例 —“)
print(quartiles_lowest_inclusive.head())
“`

実行結果の例:

“`
— right=False の場合の区間例 —
0 [34.207, 54.39)
1 [54.39, 67.476)
2 [34.207, 54.39)
3 [34.207, 54.39)
4 [34.207, 54.39)
dtype: category
Categories (4, interval[float64]): [[1.731, 34.207) < [34.207, 54.39) < [54.39, 67.476) < [67.476, 94.161)]] # 区間が [a, b) 形式に

— include_lowest=True の場合の区間例 —
0 (34.207, 54.39]
1 (54.39, 67.476]
2 (34.207, 54.39]
3 (34.207, 54.39]
4 (34.207, 54.39]
dtype: category
Categories (4, interval[float64]): [[1.731, 34.207] < (34.207, 54.39] < (54.39, 67.476] < (67.476, 94.161]] # 最初の区間だけ [a, b] 形式に
“`

2.7. duplicates 引数:重複する境界線の扱い

これが qcut を使う上で最も注意が必要な点の一つです。qcut は分位数に基づいて境界線を計算しますが、元のデータに同じ値が多数存在する場合、計算された分位点が重複することがあります。例えば、データの50%が同じ値である場合、その値に対応する分位点(例えば25%点、50%点、75%点など)がすべて同じ値になる可能性があります。

デフォルトでは duplicates='raise' となっており、重複する境界線が見つかった場合にエラーが発生します。これは、ユーザーに問題が発生したことを知らせ、意図しないビン化を防ぐための安全策です。

重複する境界線が発生する主な原因:

  • データに同じ値が多く含まれる: 特に、データが離散的であるか、多くの値が同じ値に集中している場合。
  • データポイントが少ない: データ数が少ないと、ユニークな値の数が q で指定したビンの数より少なくなり、分位点が重複しやすくなります。

重複が発生した場合の duplicates 引数のオプション:

  • 'raise' (デフォルト): 重複が見つかるとエラーが発生します。
  • 'drop': 重複する境界線を削除し、作成されるビンの数が指定した q より少なくなることを許容します。この場合、qcut は可能な限り多くのビンを作成しようとしますが、重複がない範囲での最大ビン数になります。
  • 'warn': 重複が見つかってもエラーは発生せず処理は続行されますが、警告が表示されます。作成されるビンの数は 'drop' と同様に少なくなる可能性があります。

具体的な例で 'raise''drop' の違いを見てみましょう。非常に単純で、かつユニークな値が少ないデータを作成します。

“`python

重複する値が多いシンプルなデータ

simple_data = pd.Series([10, 10, 20, 20, 20, 30, 30, 30, 30, 40])

データを4つのビンに分割しようとする (duplicates=’raise’ の場合)

try:
qcut_simple_raise = pd.qcut(simple_data, q=4)
print(“— duplicates=’raise’ (成功) —“)
print(qcut_simple_raise.value_counts().sort_index())
except ValueError as e:
print(“— duplicates=’raise’ (エラー発生) —“)
print(f”エラーメッセージ: {e}”)

print(“-” * 20)

データを4つのビンに分割しようとする (duplicates=’drop’ の場合)

qcut_simple_drop = pd.qcut(simple_data, q=4, duplicates=’drop’)
print(“— duplicates=’drop’ —“)
print(qcut_simple_drop.value_counts().sort_index())

境界値も確認

_, bins_drop = pd.qcut(simple_data, q=4, duplicates=’drop’, retbins=True)
print(“— duplicates=’drop’ で取得した境界値 —“)
print(bins_drop)
“`

実行結果の例:

“`
— duplicates=’raise’ (エラー発生) —
エラーメッセージ: Bin edges must be unique: array([10., 20., 30., 40., 40.]).
You can drop duplicate edges by setting the ‘duplicates’ kwarg


— duplicates=’drop’ —
(9.999, 20.0] 5
(20.0, 30.0] 4
(30.0, 40.0] 1
dtype: int64

— duplicates=’drop’ で取得した境界値 —
[10. 20. 30. 40.]
“`

この例では、データが [10, 10, 20, 20, 20, 30, 30, 30, 30, 40] であり、分位数(25%点, 50%点, 75%点など)を計算しようとすると、20%点、30%点、40%点あたりがすべて20になり、50%, 60%, 70%点あたりがすべて30になり、80%点、90%点などがすべて40になるなど、分位点が重複します。

duplicates='raise' の場合は、これらの重複する境界線がエラーを引き起こします。
duplicates='drop' の場合は、重複する境界線(この例では複数の分位点に対応する20, 30, 40)が削除され、ユニークな境界線 [10., 20., 30., 40.] のみを使用してビン化が行われます。これにより、区間は (10, 20], (20, 30], (30, 40] の3つになり、指定した q=4 のビン数より少なくなります。

duplicates='drop' を使うとエラーを回避できますが、指定した数より少ないビンが生成される可能性があるということを理解しておく必要があります。これは、各ビンに「ほぼ等しいデータ数」を持たせるという qcut の本来の目的から少し外れる可能性があります。

duplicates='drop' をいつ使うべきか?

  • データ数が少ない場合。
  • データに同じ値が多数含まれており、かつエラーで処理を止めずに可能な範囲で分位数ビン化を行いたい場合。

ただし、duplicates='drop' を使用した場合は、生成されたビンの数と各ビンのデータ数を必ず確認し、意図した結果になっているか、分析の目的に合っているかを評価することが重要です。

3. qcut がなぜ便利なのか? cut との比較

qcut の利点は、その「分位数ベース」の特性にあります。これを理解するために、qcutcut の違いを比較してみましょう。

pandas.cut は、指定された固定の境界値または固定の区間幅でデータをビン化します。

“`python

再度、偏りのあるサンプルデータを使用

s = pd.Series(np.concatenate([np.random.normal(50, 10, 100),

np.random.normal(80, 5, 50),

np.random.normal(20, 15, 30)])) # total 180 data points

cut を使用して、データの最小値から最大値までを4つの固定区間に分割

bins=’auto’ (デフォルト) または 明示的な境界値を指定

min_val = s.min()
max_val = s.max()
cut_fixed_width = pd.cut(s, bins=4) # 4つの等幅区間に分割

print(“— cut (固定幅) での分割結果 —“)
print(cut_fixed_width.value_counts().sort_index())

print(“\n— qcut (分位数) での分割結果 —“)
qcut_quantiles = pd.qcut(s, q=4) # 4つの等データ数ビンに分割
print(qcut_quantiles.value_counts().sort_index())
“`

実行結果の例:

“`
— cut (固定幅) での分割結果 —
(1.671, 25.491] 31
(25.491, 49.251] 50
(49.251, 73.011] 69
(73.011, 96.771] 30
dtype: int64 # 合計 180

— qcut (分位数) での分割結果 —
(1.731, 34.207] 45
(34.207, 54.39] 45
(54.39, 67.476] 45
(67.476, 94.161] 45
dtype: int64 # 合計 180
“`

上記の比較を見ると、cut はデータの最小値から最大値までを等しい「幅」で4つの区間に分割しています。その結果、各区間に含まれるデータ数は [31, 50, 69, 30] とばらつきがあります。これは、元のデータが正規分布から生成されたものではなく、いくつかの山を持つ偏った分布をしているためです。

一方、qcut は各区間に「ほぼ等しいデータ数」を含めるように区間の境界値を自動的に調整しています。そのため、各ビンのデータ数は [45, 45, 45, 45] とほぼ均等になっています。しかし、それぞれの区間の「幅」は異なります。例えば、最初の区間 (1.731, 34.207] の幅は約32.4ですが、次の区間 (34.207, 54.39] の幅は約20.2と狭くなっています。

qcut が特に有効なケース:

  • データの分布が大きく偏っている場合: 所得、ウェブサイトのアクセス数、販売個数など、少数の値が大きな割合を占める(あるいはその逆)といった偏った分布を持つデータでは、cut で固定幅に分割すると、あるビンにはデータが集中し、別のビンにはほとんどデータが入らないという状況になりがちです。qcut なら、このようなデータの偏りに関係なく、各ビンに均等なデータ数を割り振ることができます。
  • データを行動やパフォーマンスのランクに基づいて分割したい場合: 例えば、顧客の購買金額を「上位20%」「次の30%」「下位50%」のように分割して、それぞれの層の特徴や行動を分析したい場合など。qcut はこのような分位点ベースの分割に最適です。
  • 各ビンのデータ数が後の分析で重要になる場合: 例えば、各ビンごとに詳細な分析(例: 平均値の計算、回帰分析など)を行う際に、データ数が極端に少ないビンがあると分析結果が不安定になる可能性があります。qcut なら、各ビンのデータ数を一定に保つことができます。

cut が有効なケース:

  • 区間の定義に明確な基準がある場合: 年齢層(20代、30代、40代など)、試験の評価基準(80点以上はA、70点以上はBなど)のように、分析の前に区間の境界値が定義されている場合。
  • 区間の「幅」に意味がある場合: 例えば、特定の物理量の測定値など、等しい区間幅が等しい物理的な意味を持つ場合。
  • 解釈の容易さ: 固定区間は、人間が直感的に理解しやすいことが多いです。

結論として、どちらの関数を使うかは、分析の目的とデータの性質によって決まります。データの分布に偏りがあり、各ビンに均等なデータ数を割り振りたい場合は qcut が強力な選択肢となります。

4. qcut の実践例:実際のデータを使った分析

ここからは、より実践的なシナリオを想定して qcut の使い方を見ていきます。ここでは、架空の顧客データ(年齢、年収、購入金額などを含む)を想定したサンプルデータを作成し、これを使って様々な例を試します。

“`python

実践例用のサンプルデータ生成

np.random.seed(42)
n_customers = 1000

年齢: 正規分布 (少し若めに偏らせる)

ages = np.random.normal(40, 15, n_customers)
ages = np.clip(ages, 18, 80) # 18歳から80歳にクリップ

年収: 対数正規分布 (高所得者に偏りがある)

incomes = np.random.lognormal(mean=np.log(600), sigma=0.8, size=n_customers) * 1000 # 平均600万くらい

購入金額: ゼロインフレ(購入しない人が多い)かつ偏りがある分布

purchase_amounts_raw = np.random.lognormal(mean=np.log(5000), sigma=1.0, size=n_customers)
purchase_amounts = np.where(np.random.rand(n_customers) < 0.3, 0, purchase_amounts_raw) # 70%が購入

data = pd.DataFrame({
‘Age’: ages,
‘Income’: incomes,
‘PurchaseAmount’: purchase_amounts
})

print(“— サンプルデータの最初の5行 —“)
print(data.head())

print(“\n— 各列の基本統計量 —“)
print(data.describe())
“`

実行結果の例:

“`
— サンプルデータの最初の5行 —
Age Income PurchaseAmount
0 47.355803 232251.643144 232251.643144
1 37.520840 104280.394076 104280.394076
2 50.521021 165411.151665 165411.151665
3 60.170620 79900.243027 79900.243027
4 45.105266 101243.312337 101243.312337

— 各列の基本統計量 —
Age Income PurchaseAmount
count 1000.000000 1000.000000 1000.000000
mean 40.525667 120018.619850 101228.612297
std 14.698866 128874.607753 134398.070407
min 18.000000 12914.239180 0.000000
25% 30.188146 48784.857558 0.000000 # PurchaseAmountの25%点が0
50% 40.184266 83244.141548 50000.000000 # PurchaseAmountの50%点が5万程度
75% 50.738575 148677.151320 150000.000000 # 購入しない人が30%いるため、PurchaseAmountの分位点が低い側に偏る
max 80.000000 895891.451763 895891.451763
“`

PurchaseAmount の基本統計量を見ると、25%点が0になっています。これは、購入しない顧客が多いため、下位のデータがゼロに集中していることを示しています。このようなデータに対して cut で固定幅に分割すると、ゼロを含む最初のビンに大量のデータが集中し、他のビンはまばらになる可能性が高いです。ここで qcut の出番です。

4.1. 例1:年収を四分位数で分割し、カテゴリ別の平均購入金額を分析

年収データは通常、高所得者側に裾野が広がる偏った分布をしています。これを四分位数で分割し、それぞれの年収層における平均購入金額を見てみましょう。

“`python

年収データを四分位数で分割 ( duplicates=’drop’ を使用して念のため重複に対応)

data[‘Income_Quartile’] = pd.qcut(data[‘Income’], q=4, labels=[‘Low’, ‘Medium-Low’, ‘Medium-High’, ‘High’], duplicates=’drop’)

print(“— 年収四分位数別のデータ数 —“)
print(data[‘Income_Quartile’].value_counts().sort_index())

print(“\n— 年収四分位数別の平均購入金額 —“)

購入しない人も含まれているため、まず購入者のみに絞ってから集計する方が分析の目的に合うかもしれない

ここではシンプルに全顧客を含めて集計

average_purchase_by_income = data.groupby(‘Income_Quartile’)[‘PurchaseAmount’].mean()
print(average_purchase_by_income)
“`

実行結果の例:

“`
— 年収四分位数別のデータ数 —
Low 250
Medium-Low 250
Medium-High 250
High 250
dtype: int64

— 年収四分位数別の平均購入金額 —
Income_Quartile
Low 34164.219306
Medium-Low 84848.662745
Medium-High 133728.352905
High 190273.246199
Name: PurchaseAmount, dtype: float64
“`

qcut を使って年収を四つの層に分割した結果、それぞれの層に250人ずつ(合計1000人の1/4)が均等に割り振られていることがわかります。そして、それぞれの年収層の平均購入金額を計算すると、「Low」層から「High」層になるにつれて平均購入金額が上昇している傾向が明確に見て取れます。このように、qcut を使って連続値をカテゴリに変換することで、グループ間の比較分析が容易になります。

4.2. 例2:購入金額を分位数で分割し、「非購入者」と「購入者層」を区別する

PurchaseAmount はゼロが多く含まれるため、単純な四分位数分割ではゼロのビンが大きくなりすぎる可能性があります。しかし、qcut はゼロの重複に対応しつつ、購入者の中でのランク付けを行うのに役立ちます。また、購入金額がゼロの顧客と、購入した顧客を明確に区別したい場合もあります。

ここでは、まず購入金額がゼロかどうかで顧客を分け、購入金額がゼロでない顧客に対して qcut を適用してみましょう。

“`python

購入金額がゼロの顧客とゼロでない顧客に分ける

non_buyers = data[data[‘PurchaseAmount’] == 0].copy()
buyers = data[data[‘PurchaseAmount’] > 0].copy()

print(f”\n— 非購入者数: {len(non_buyers)}人 —“)
print(f”— 購入者数: {len(buyers)}人 —“)

購入者に対してのみ、購入金額を三分位で分割

duplicates=’drop’ は、もし購入金額が同じ値に集中している場合にも対応

buyers[‘PurchaseAmount_Tier’] = pd.qcut(buyers[‘PurchaseAmount’], q=3, labels=[‘Tier1’, ‘Tier2’, ‘Tier3′], duplicates=’drop’)

print(“\n— 購入者層別のデータ数 —“)
print(buyers[‘PurchaseAmount_Tier’].value_counts().sort_index())

非購入者と購入者層を組み合わせる

非購入者には特別なカテゴリを割り当てる

non_buyers[‘PurchaseAmount_Tier’] = ‘Non-Buyer’

元のDataFrameに新しいカテゴリ列を追加

まず既存の列を削除し、結合した列を追加する

data = data.drop(‘PurchaseAmount_Tier’, errors=’ignore’, axis=1) # 既に列があれば削除
all_customers_tiered = pd.concat([non_buyers, buyers])

print(“\n— 全顧客を対象とした購入金額カテゴリ別のデータ数 —“)
print(all_customers_tiered[‘PurchaseAmount_Tier’].value_counts())

購入金額カテゴリ別の平均年齢と平均年収を分析

print(“\n— 購入金額カテゴリ別の平均年齢と平均年収 —“)
print(all_customers_tiered.groupby(‘PurchaseAmount_Tier’)[[‘Age’, ‘Income’]].mean())
“`

実行結果の例:

“`
— 非購入者数: 299人 — # 約30%
— 購入者数: 701人 — # 約70%

— 購入者層別のデータ数 —
Tier1 233
Tier2 234
Tier3 234
dtype: int64 # 購入者701人がほぼ3等分されている (701 / 3 = 233.66)

— 全顧客を対象とした購入金額カテゴリ別のデータ数 —
Non-Buyer 299
Tier1 233
Tier2 234
Tier3 234
dtype: int64

— 購入金額カテゴリ別の平均年齢と平均年収 —
Age Income
PurchaseAmount_Tier
Non-Buyer 39.015283 114325.362154
Tier1 39.870506 109839.757897
Tier2 41.655191 119831.461816
Tier3 41.421449 129472.943659
“`

この例では、まず購入金額が0の顧客を明確に分けました。残りの購入者に対して qcut を適用し、購入金額に基づいて3つの層(Tier1, Tier2, Tier3)に分割しました。q=3 を指定したため、購入者701人がほぼ3等分されています。非購入者には「Non-Buyer」というラベルを付け、すべての顧客を「Non-Buyer」「Tier1」「Tier2」「Tier3」の4つのカテゴリに分類しました。

このカテゴリ別の平均年齢と平均年収を見ると、購入者は非購入者よりも平均年齢がわずかに高く、平均年収も高い傾向が見られます。また、購入者の中でも高額購入者層(Tier3)は、他の層よりも平均年収が高いことが示唆されます。このように、ビジネス要件に合わせてデータを事前処理し、qcut を組み合わせることで、より意味のある分析が可能になります。

4.3. 例3:境界値とラベルを取得し、報告書やルール設定に活用する

retbins=True を使って qcut が計算した実際の境界値を取得し、これを分析レポートに含めたり、何らかのルール設定(例: 購入金額に基づいて顧客ランクを自動付与するシステムの閾値)に利用したりするシナリオです。

“`python

購入金額(非ゼロ)を四分位数で分割し、境界値とラベルを取得

再度、購入者データを使用

buyers = data[data[‘PurchaseAmount’] > 0].copy()

購入金額の四分位点を計算し、境界値とラベルを取得

q=4 は5つの境界値を返す (0%, 25%, 50%, 75%, 100%)

purchase_quartile_binseries, purchase_quartile_bins = pd.qcut(
buyers[‘PurchaseAmount’],
q=4,
labels=[‘Bottom 25% Buyers’, ’25-50% Buyers’, ’50-75% Buyers’, ‘Top 25% Buyers’],
retbins=True,
duplicates=’drop’ # 重複に対応
)

print(“— 購入者における購入金額の四分位点の境界値 —“)

境界値はソートされて返される

print(purchase_quartile_bins)

print(“\n— 境界値を活用した報告書の記述例 —“)

取得した境界値を使って、各層の定義を分かりやすく記述

labels = [‘Bottom 25% Buyers’, ’25-50% Buyers’, ’50-75% Buyers’, ‘Top 25% Buyers’]
for i in range(len(labels)):
lower_bound = purchase_quartile_bins[i]
upper_bound = purchase_quartile_bins[i+1]
# デフォルトの right=True を考慮して区間を記述
if i == 0:
# include_lowest=False の場合、最小値は含まれないが、ここでは分かりやすさ優先
# 実際には include_lowest=True を使うと最初の区間が [min, bin1] になる
print(f”{labels[i]}: 購入金額が {lower_bound:.2f} より大きく {upper_bound:.2f} 以下”)
else:
print(f”{labels[i]}: 購入金額が {lower_bound:.2f} より大きく {upper_bound:.2f} 以下”)

実際には、最初の区間はデータの最小値から最初の分位点までなので、上記は少し簡易的な表現

正確には、取得した Series の categories 属性を見ると区間がわかる

print(“\n— qcutで生成されたカテゴリの正確な区間 —“)
print(purchase_quartile_binseries.cat.categories)
“`

実行結果の例:

“`
— 購入者における購入金額の四分位点の境界値 —
[ 57.267378 34927.126605 76333.721462 151379.379253 895891.451763]

— 境界値を活用した報告書の記述例 —
Bottom 25% Buyers: 購入金額が 57.27 より大きく 34927.13 以下
25-50% Buyers: 購入金額が 34927.13 より大きく 76333.72 以下
50-75% Buyers: 購入金額が 76333.72 より大きく 151379.38 以下
Top 25% Buyers: 購入金額が 151379.38 より大きく 895891.45 以下

— qcutで生成されたカテゴリの正確な区間 —
IntervalIndex([(57.267, 34927.127], (34927.127, 76333.721], (76333.721, 151379.379], (151379.379, 895891.452]],
closed=’right’,
dtype=’interval[float64]’)
“`

retbins=True を使うことで、qcut が計算した正確な境界値(分位点)を取得できます。これらの値は、後で同じ基準で別のデータをビン化したり(例: 将来の顧客を同じ基準で分類)、BIツールなどで利用したり、ドキュメントに記載したりする際に非常に役立ちます。

4.4. 例4:複数の特徴量をビン化し、クロス集計や可視化に活用する

データ分析では、複数の連続値をビン化し、それらのカテゴリを組み合わせて分析することがよくあります。ここでは、年齢と年収の両方をビン化し、それぞれの組み合わせにおける平均購入金額を見てみましょう。

“`python

年齢を三分位で分割

data[‘Age_Tier’] = pd.qcut(data[‘Age’], q=3, labels=[‘Young’, ‘Middle-Aged’, ‘Senior’])

年収を三分位で分割

data[‘Income_Tier’] = pd.qcut(data[‘Income’], q=3, labels=[‘Low Income’, ‘Medium Income’, ‘High Income’])

print(“— 年齢層別のデータ数 —“)
print(data[‘Age_Tier’].value_counts().sort_index())
print(“\n— 年収層別のデータ数 —“)
print(data[‘Income_Tier’].value_counts().sort_index())

年齢層と年収層を組み合わせてクロス集計し、平均購入金額を計算

pivot_table を使用

pivot_table = data.pivot_table(
values=’PurchaseAmount’,
index=’Age_Tier’,
columns=’Income_Tier’,
aggfunc=’mean’
)

print(“\n— 年齢層 x 年収層別の平均購入金額 —“)
print(pivot_table)
“`

実行結果の例:

“`
— 年齢層別のデータ数 —
Young 333
Middle-Aged 333
Senior 334
dtype: int64

— 年収層別のデータ数 —
Low Income 333
Medium Income 333
High Income 334
dtype: int64

— 年齢層 x 年収層別の平均購入金額 —
Income_Tier Low Income Medium Income High Income
Age_Tier
Young 54849.458602 98013.088869 114014.046142
Middle-Aged 56660.506580 115705.691792 168505.082948
Senior 55429.411504 100477.097774 155083.377882
“`

このように、複数の特徴量をそれぞれ qcut でビン化し、それらを組み合わせて分析することで、より詳細な顧客セグメントごとの傾向を把握できます。例えば上記のピボットテーブルからは、「Middle-Aged」かつ「High Income」の層が最も平均購入金額が高いといったインサイトが得られます。この結果は、ターゲットマーケティング戦略の立案などに役立てることができます。

ビン化されたデータは、Seabornなどのライブラリを使って可視化する際にも非常に便利です。例えば、カテゴリカルな年齢層や年収層を軸にして棒グラフや箱ひげ図を作成することで、層ごとの分布や傾向を視覚的に捉えることができます。

5. qcut を使う上での注意点・考慮事項

qcut は非常に便利な関数ですが、その特性上、いくつか注意しておきたい点があります。

5.1. 重複する境界線(duplicates 引数)の再確認

これは前述しましたが、最も重要な注意点なので再度強調します。データに多数の同じ値が含まれる場合やデータ数が極端に少ない場合、qcut が計算する分位点が重複し、デフォルトの duplicates='raise' ではエラーが発生します。

エラーを回避するために duplicates='drop' を使用することは可能ですが、この場合、指定した q の数よりも少ないビンが生成されることを理解しておく必要があります。生成されたビンの数を確認し、なぜ重複が発生したのか(データに同じ値が多いのか、データが少ないのかなど)を調査することが推奨されます。

例えば、ある商品の購入回数データ(多くの人が0回購入)を qcut で分割しようとした場合、購入回数0の値が大量にあるため、下位の分位点がすべて0になり、重複する境界線が発生しやすいです。この場合は、まず購入回数0の人と1回以上の人を分け、1回以上の購入者に対して qcut を適用するなど、データの前処理や分析設計を工夫する方が適切な結果を得られることがあります(先ほどの購入金額の例のように)。

5.2. ビン間の境界値の扱い (right, include_lowest)

デフォルトでは right=Trueinclude_lowest=False です。これは、区間が (a, b](左端を含まず右端を含む)形式になり、最初の区間はデータの最小値を含まない (min_val, bin1] となることを意味します。

もし、最小値が最初のビンに含まれてほしい場合は、include_lowest=True を指定します。この場合、最初の区間は [min_val, bin1] となります。

例:データの最小値が10で、最初の境界値が20の場合
* デフォルト (right=True, include_lowest=False): 最初の区間は (10, 20]。値10はどの区間にも含まれない。
* right=True, include_lowest=True: 最初の区間は [10, 20]。値10はこの区間に含まれる。
* right=False, include_lowest=False: 最初の区間は [10, 20)。値10はこの区間に含まれる。

デフォルトの挙動が意図通りか確認し、必要に応じてこれらの引数を調整してください。

5.3. 欠損値 (NaN) の扱い

入力 Series に欠損値 (NaN) が含まれている場合、qcut はデフォルトでそれらの欠損値を無視し、結果の Series でも対応する位置に NaN を返します。

“`python
s_with_nan = pd.Series([10, 20, np.nan, 30, 40, np.nan, 50])
qcut_with_nan = pd.qcut(s_with_nan, q=3)

print(“— NaN を含むデータの qcut 結果 —“)
print(qcut_with_nan)
“`

実行結果の例:

--- NaN を含むデータの qcut 結果 ---
0 (9.999, 23.333]
1 (9.999, 23.333]
2 NaN
3 (23.333, 43.333]
4 (23.333, 43.333]
5 NaN
6 (43.333, 50.0]
dtype: category
Categories (3, interval[float64]): [(9.999, 23.333] < (23.333, 43.333] < (43.333, 50.0]]

欠損値をどのように扱うかは分析の目的によります。qcut を適用する前に dropna() で欠損値を除去したり、欠損値を特定の代表値(平均値、中央値など)で補完(impute)したりする必要があるかもしれません。

5.4. ビン数の決定 (q)

q で指定するビン数をいくつにするかは、データ分析において重要な意思決定です。最適なビン数は、データの性質、分析の目的、そして結果の解釈のしやすさによって異なります。

  • 少なすぎるビン数: データの詳細なパターンや傾向を見落とす可能性があります。
  • 多すぎるビン数:
    • 各ビンに含まれるデータ数が少なくなり、統計的な分析が不安定になる可能性があります。
    • 特に重複する値が多い場合など、qcut が指定した数のビンを生成できなくなる可能性が高まります(duplicates='drop' を使っても)。
    • 結果の解釈が難しくなります。
    • 機械学習の文脈では、細かすぎるビン化は過学習につながる可能性もゼロではありません(ただし、qcut は特徴量エンジニアリングとして有用な場合も多いです)。

一般的には、3~10程度のビン数を使用することが多いですが、これはあくまで目安です。データの分布をヒストグラムなどで確認したり、異なるビン数で試したりしながら、分析の目的に最も適したビン数を見つけることが推奨されます。ドメイン知識もビン数を決定する上で重要な手がかりとなります(例: 年齢層なら「若年」「中年」「高年」など、ビジネスで慣習的に使われる区分があればそれに合わせるなど)。

5.5. カテゴリカルデータとしての理解

qcut の出力は Categorical 型であり、これは順序を持つカテゴリ変数として扱われます。これにより、ソートや比較などの操作が直感的に行えます。また、Categorical 型はメモリ効率が良いという利点もあります。

ほとんどの場合、このデフォルトの挙動で問題ありませんが、もしカテゴリの順序が重要でない場合や、他のライブラリが Categorical 型をうまく扱えない場合は、astype('object') などで文字列型に変換することも可能です。

6. 高度なトピック・関連知識

6.1. cutqcut の選択基準(再掲とまとめ)

改めて、cutqcut の選択基準をまとめます。

特徴 pandas.cut pandas.qcut
基準 固定された境界値または区間幅 分位数
目的 事前定義されたカテゴリに分類 各ビンにほぼ等しいデータ数を持たせる
ビンの幅 均一または指定された幅 ビンごとに異なる(データ分布に依存)
データ数 ビンごとに異なる(データ分布に依存) 各ビンでほぼ均一
使いどころ 年齢層、点数評価、物理量など、区間自体に意味がある場合、解釈しやすさが重要な場合 所得、売上、アクセス数など、分布が偏っている場合、データ数を均等にしたい場合、ランク付け(上位N%など)したい場合
注意点 境界値の設定ミス、データが区間外になる可能性 重複する境界線、ビン数が指定より少なくなる可能性

どちらを使うか迷った場合は、まずデータの分布を確認してください。分布に偏りがある場合は qcut が有効なことが多いです。ただし、ビジネス上の要件や分析の目的に応じて、事前に定義されたカテゴリが必要な場合は cut を選びます。

6.2. ビン化と可視化

ビン化されたカテゴリ変数は、データの可視化において非常に強力なツールとなります。Matplotlib や Seaborn と組み合わせることで、カテゴリごとの分布や他の変数との関係性を分かりやすく示すことができます。

“`python
import matplotlib.pyplot as plt
import seaborn as sns

例:年齢層別の年収分布を箱ひげ図で可視化

年齢を三分位で分割 (例4で既に実施済み)

data[‘Age_Tier’] = pd.qcut(data[‘Age’], q=3, labels=[‘Young’, ‘Middle-Aged’, ‘Senior’])

plt.figure(figsize=(8, 5))
sns.boxplot(x=’Age_Tier’, y=’Income’, data=data, order=[‘Young’, ‘Middle-Aged’, ‘Senior’])
plt.title(‘Age Tier vs Income Distribution’)
plt.xlabel(‘Age Tier’)
plt.ylabel(‘Income’)
plt.grid(axis=’y’, linestyle=’–‘)
plt.show()

例:年収層別の購入金額(購入者のみ)分布をバイオリンプロットで可視化

購入者を抽出し、年収層で分割

buyers = data[data[‘PurchaseAmount’] > 0].copy()

buyers[‘Income_Tier’] は例4で既に作成済み

plt.figure(figsize=(8, 5))
sns.violinplot(x=’Income_Tier’, y=’PurchaseAmount’, data=buyers, order=[‘Low Income’, ‘Medium Income’, ‘High Income’])
plt.title(‘Income Tier vs Purchase Amount Distribution (Buyers Only)’)
plt.xlabel(‘Income Tier’)
plt.ylabel(‘Purchase Amount’)
plt.grid(axis=’y’, linestyle=’–‘)
plt.show()
“`

これらの可視化例は、qcut によって作成されたカテゴリが、年齢や年収と購入金額の関係性を視覚的に理解するのに役立つことを示しています。

6.3. 特徴量エンジニアリングとしてのビン化

機械学習モデルを構築する際、連続値をビン化してカテゴリ変数に変換することは、特徴量エンジニアリングの一般的な手法の一つです。

  • 非線形性の捕捉: 線形モデル(例: 線形回帰、ロジスティック回帰)では、特徴量と目的変数との関係が非線形である場合、そのままではうまくモデル化できません。ビン化することで、ビンごとに異なる効果を持たせることができ、非線形性を間接的にモデルに組み込むことが可能になります(ダミー変数化が必要)。
  • 外れ値の影響軽減: ビン化は、極端な外れ値の影響を軽減する効果を持つことがあります。外れ値が特定のビンに集約されることで、個々の外れ値の影響が小さくなります。
  • モデルの頑健性の向上: データにノイズが多い場合、ビン化によってノイズが平滑化され、モデルの頑健性が向上する可能性があります。

ただし、決定木ベースのモデル(決定木、ランダムフォレスト、勾配ブースティングなど)は、データの非線形性や外れ値に対して比較的強い性質を持つため、これらのモデルを使用する場合は、必ずしもビン化が必要ない、あるいはビン化しても性能が大きく向上しないこともあります。モデルの選択とビン化戦略はセットで検討することが重要です。

ビン化されたカテゴリ変数を機械学習モデルに入力する際は、通常、ワンホットエンコーディングなどの手法を用いて数値形式に変換する必要があります。pd.get_dummies() 関数が利用できます。

“`python

例:ビン化された年齢層と年収層をワンホットエンコーディング

data_encoded = pd.get_dummies(data[[‘Age_Tier’, ‘Income_Tier’]], prefix=[‘Age’, ‘Income’])

print(“— ワンホットエンコーディング結果の最初の5行 —“)
print(data_encoded.head())
“`

実行結果の例:

--- ワンホットエンコーディング結果の最初の5行 ---
Age_Young Age_Middle-Aged Age_Senior Income_Low Income Income_Medium Income Income_High Income
0 0 1 0 0 0 1
1 0 1 0 0 1 0
2 0 1 0 0 0 1
3 0 0 1 0 0 1
4 0 1 0 0 1 0

このように、qcut で作成したカテゴリをワンホットエンコーディングすることで、機械学習モデルが利用できる数値特徴量に変換できます。

7. まとめ

本記事では、pandas の強力なビン化関数である qcut について、その機能、使い方、そしてデータ分析における実践的な活用方法を詳細に解説しました。

qcut は、連続値を分位数に基づいて(ほぼ)等しいデータ数のビンに分割する際に非常に有用です。データの分布が偏っている場合や、データポイントのランク付けや層別化を行いたい場合に、qcutcut 関数と比較してより適切な結果をもたらすことが多いです。

qcut を使いこなすための重要なポイントは以下の通りです。

  • q 引数: 整数でビンの数を指定するか、リストで分位点を指定します。
  • labels 引数: 分析結果を分かりやすくするために、カスタムラベルを付けることができます。
  • retbins=True: ビン化に使用された正確な境界値を取得し、後の分析や報告に活用できます。
  • duplicates='drop': データに重複する値が多い場合に発生するエラーを回避できますが、生成されるビン数が指定より少なくなる可能性があることを理解しておく必要があります。
  • right および include_lowest: 区間の境界値の扱い方を制御します。デフォルト設定が意図通りか確認しましょう。
  • 注意点: 特に重複する境界線、ビン数の決定、欠損値の扱いに注意が必要です。
  • 活用法: 特徴量エンジニアリング、カテゴリ別集計、可視化など、様々なデータ分析タスクに役立ちます。

qcut を適切に使用することで、連続値データの傾向をより明確に把握し、グループ間の比較を容易にし、より高度な分析や機械学習モデルの構築につなげることができます。

データ分析は、ツールを理解するだけでなく、データの性質や分析の目的を深く理解し、それに合ったツールを選択・適用するプロセスです。qcut はそのための強力な選択肢の一つであることを、本記事の実践例を通じて感じていただけたなら幸いです。

ぜひ、ご自身のデータを使って qcut を試し、その威力を体感してみてください。そして、本記事が、あなたのデータ分析スキルの向上の一助となれば幸いです。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール