クラスタリングの結果を、変数の値に従ってソートする

今回の記事の主題は、
クラスタリングの結果(ラベル、番号)を、ある変数の値の順序に従って並び替えるにはどうすればよいか?
という話である。
……しかし、こう書いただけで何のことか分かる人は多分少ないだろう。だから順を追って説明していく。 まずは、今回の問題が起きるクラスタリングの例を作ろう。

クラスタリングの例:2変数・5クラスターのデータをクラスタリングする

クラスタリングの対象となるデータの例を適当に作ろう。 scikit-learnのmake_blobを使って中心を指定し、2変数・5クラスターのデータを作成する。

import pandas as pd
import seaborn as sns
pd.options.display.notebook_repr_html = False  # jupyter notebook上での出力形式を制御するために書いています。無くても動きます。
import sklearn
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
# 動作環境の確認
print(pd.__version__)
print(sns.__version__)
print(sklearn.__version__)

# --------------------

1.1.2
0.11.0
0.24.1
sns.set_style('whitegrid') # seaborn見た目の変更。グラフ内にグリッド線を表示する
random_state = 123
# 例示用にクラスタリングするデータを作成する
center_coordinates = [[0, 0], [1, 2], [3, 1], [2, 3], [4, 4]] 
n_clusters = len(center_coordinates)
X, y = make_blobs(n_samples=30*n_clusters, centers=center_coordinates, n_features=2, cluster_std=0.3, random_state=random_state)

データの様子を散布図にしよう。今回はseabornを使う。

df = pd.DataFrame(data=X, columns=['x1', 'x2'])
ax = sns.scatterplot(x='x1', y='x2', data=df)
ax.set_aspect('equal') # グラフの縦横比を同じにする 参考:https://xnn.sakura.ne.jp/blog/2019/07/match-the-scatterplot-grid-width-in-matplotlib/

f:id:soratokimitonoaidani:20210214150556p:plain

期待通りに5つのクラスターができていることが見えた。

このデータをk-meansでクラスタリングし、結果を出力しよう。 *1

y_pred = KMeans(n_clusters=n_clusters, random_state=random_state).fit_predict(df)
df['y_pred'] =  y_pred
df.head()

# --------------------

         x1        x2  y_pred
0  4.190783  4.085381       0
1  1.917537  2.575175       2
2  1.771612  3.001094       2
3  0.992612  2.010243       1
4  1.925955  3.020636       2

ちょっと話が脇道に入るが、クラスタリングの結果、出力、番号、所属……これをなんと呼ぶか、呼び方に困るんだよね。scikit-learn公式のk-meansの説明によると、

labels_ ndarray of shape (n_samples,)
Labels of each point

と書いてあるので、「クラスタリングの結果、出力、番号」「各点がどのクラスタに分類されたか」を以降ではラベルと呼ぶことにする。

では本題に戻ろう。ラベルによって色を分けて、クラスタリングの結果を散布図にしよう *2

ax = sns.scatterplot(x='x1', y='x2', hue='y_pred', data=df, palette='colorblind')
ax.set_aspect('equal')

f:id:soratokimitonoaidani:20210214150612p:plain

scatterplotで散布図ができた。しかし、この散布図には問題がある。
散布図の上で、ラベル0と1が近い、ラベル3と4が近いというわけではない。 クラスタリングのラベルはクラスタ間の近さを考慮して付くわけではないからだ。どういう規則でラベルの番号がついているかは正直謎だが。
(予想:K-meansを使う場合、最初にランダムな点を取るので、それによって最終的なクラスターの番号が決まるんじゃないか? つまり初期の点の位置を決める乱数次第?)

しかし見づらい場合がある。 クラスター番号に規則性が無いので、0番がどこで1番がどこで、と探すのが大変だ。今回は5クラスターだからいいけど、もっとクラスター数が多い場合は探すのが大変になる。

クラスタリングのラベルを、ある変数の値の順序に従ってソートする方法

前提条件を説明するのが遅くなったが、ここまで今回の記事のための問題設定は完了である。

クラスタリングのラベルを、ある変数の値の順序に従って並び替えるにはどうすればよいか?

より正確にいうと、今回は、

  • x1の平均が最も小さいクラスターがラベル0
  • x1の平均が2番目に小さいクラスターがラベル1
  • ……

になるようにクラスターのラベルを振り直したい、としよう。(他の変数、昇順/降順の場合も同様である)

結論から言うと、このようにすれば良い。

df['y_pred_sorted'] =  df['y_pred'].replace(
    df.groupby('y_pred')['x1'].mean().sort_values().index,
    range(n_clusters)
)

正解を一気に書くと結構長いけど、pandasにある程度慣れていればそこまで難しい話ではない。

ラベルが入ったy_pred列の数値を、ある規則によって置換すれば良さそうだ。これにはreplace関数を使えば良い。

置換したい対称は、df['y_pred']の一列だけなので今回はSeriesに対するreplace関数となる。

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.replace.html

置換の指定方法は色々あって、ドキュメントを見ると細かく書いてある。今回は、list(状のもの)で指定する方法を使う。

「各クラスターのx1の平均値はいくつか」は df.groupby('y_pred')['x1'].mean() で求まる。
したがって、これをsort_valuesで昇順に並び替え、最後にindexを取れば 「クラスターのラベルを、クラスター内のx1の平均値が小さい順に並び替えたもの」が得られる。

あとは置換後のリストとしてrangeを指定すれば、「置換前のリストと置換後のリスト」が求まる。これをreplaceの引数に入れれば完成である。

最後に、もう一度seabornで散布図を描いてみよう。

ax = sns.scatterplot(x='x1', y='x2', hue='y_pred_sorted', data=df, palette='colorblind')
ax.set_aspect('equal')

f:id:soratokimitonoaidani:20210214150632p:plain

クラスターが、散布図で左から順に0, 1, 2, ……と並んでいる。 x1の平均値が小さい順にクラスター番号を振り直せたことが確認できた。

*1:クラスタリング自体は今回の記事においてそれほど重要ではない。したがって「手法としてk-meansを使う理由」は例を簡潔に説明するために一番シンプルな手法を選んでいるからです。「正解のクラスター数を知っている理由」もクラスタリングを簡単に済ませたいからです。

*2:余談:xとyを「機械学習における説明変数がX、目的変数がy」として使っている箇所と、「散布図(scatterplot)を描くときの横軸方向がx、縦軸方向がy」として使っている箇所があるけど、大丈夫だよね?
最初は'cluster_index_pred' という列名にしたけど、scatterplotの凡例が場所を取りすぎて汚くなったのでy_predに変えた。