- Python
k-means法とは|Pythonで自作・クラスタ数の決め方・苦手な形を実測
結論
k-means法(k平均法)は、データを決めた数 k 個のグループ(クラスタ)に分ける教師なし学習の手法です。「各点をいちばん近い重心に割り当てる」「重心をそのグループの平均に動かす」の2つを、重心が動かなくなるまで繰り返します。速くて分かりやすい反面、k を自分で決める必要があり、丸くないまとまり(三日月形・同心円)は苦手です。
この記事でわかること
- numpy で20行ほどの k-means を自作し、scikit-learn の KMeans とラベル・重心(差 8.9e-16)・反復回数まで一致させた結果
- クラスタ数の決め方:エルボー法・シルエット係数・CH・DB・ギャップ統計・BIC を4つのデータで並べた実測と、指標の答えが割れたときの考え方
- 初期値と k-means++、標準化の有無、苦手な形(make_moons など)で DBSCAN・階層クラスタリング・GMM と比べた ARI の表
この記事で実際に動かしたもの:numpy だけで書いた k-means(Lloyd 法)・k-means++(素の版と greedy 版)・シルエット係数・ギャップ統計・k-medoids と、scikit-learn 1.9.1 の KMeans・MiniBatchKMeans・DBSCAN・HDBSCAN・AgglomerativeClustering・SpectralClustering・GaussianMixture。データは scikit-learn 同梱の iris・wine と、make_blobs・make_moons・make_circles で作ったもの、自作の架空の顧客データです。
k-means は「重心を置く → 近い点を集める → 重心を真ん中へ動かす」を繰り返すだけの手法です。流れを図にすると次のとおりです。
flowchart TD
A[k を決める] --> B[k 個の重心を置く<br>k-means++ で離して選ぶ]
B --> C[各点を<br>いちばん近い重心へ割り当て]
C --> D[重心を<br>そのクラスタの平均へ動かす]
D --> E{重心は<br>まだ動いた?}
E -->|はい| C
E -->|いいえ| F[終了<br>ラベルと重心が決まる]
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| k-means法とは | 距離が近い点どうしを k 個にまとめる教師なしのクラスタリング | k-means法とは |
| k-means クラスタリングの手順 | 割り当てと重心の更新を、重心が動かなくなるまで繰り返す | k-meansのアルゴリズム |
| Python での書き方 | KMeans(n_clusters=3).fit(X)。結果は labels_ と cluster_centers_ | 最初の5分 |
| クラスタ数の決め方 | エルボー法とシルエット係数を並べ、最後は使い道で決める | クラスタ数の決め方 |
| エルボー法とシルエットの違い | エルボーは誤差の減り方、シルエットは分かれ具合(-1〜1)を見る | クラスタ数の決め方 |
| 実行ごとに結果が変わる | 初期値のせい。random_state を固定し、k-means++ と n_init で安定させる | 初期値とk-means++ |
| 標準化は要る? | 単位が違う列が混ざるなら要る。wine で ARI が 0.37 → 0.90 | 標準化しないと崩れる |
| うまく分かれない形 | 三日月形・同心円・細長い形。DBSCAN やスペクトラルを使う | 苦手な形 |
確認環境:Ubuntu 24.04.5 LTS(Intel Xeon 2.10GHz・2コア)の上で、Python 3.13.18・numpy 2.5.3(OpenBLAS 0.3.34、OPENBLAS_NUM_THREADS=1)・scikit-learn 1.9.1・SciPy 1.18.1・pandas 3.0.5 を使いました。自作の k-means は scikit-learn に同じ初期値を渡して、ラベル・重心・inertia・反復回数が一致することを確かめています。時間は同じマシンで他の処理も動いている中での1回の値で、目安です。KMeans の引数(n_init='auto' の挙動、greedy k-means++、tol)と各手法の向き不向きは scikit-learn 1.9.1 の公式ドキュメント、k-means++ は Arthur と Vassilvitskii の論文、ギャップ統計は Tibshirani らの論文、x-means は Pelleg と Moore の論文で確かめました。図は mermaid 11.16.0 で描画を確かめています(確認日 2026年11月5日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「k-means法とは|Pythonで自作・クラスタ数の決め方・苦手な形を実測」
k-means法とは(k平均法の意味と「クラスタ」の意味)
この節では、k-means法が何をする手法なのか、「クラスタ」という言葉が場面ごとにどう違うのか、k近傍法とどう違うのかが分かります。
k-means法は、正解ラベルの無いデータを、距離の近いもの同士で k 個のグループに分ける手法です。「k」はグループの数、「means」は平均のことで、各グループの代表点(重心)をそのグループの平均で表すことから名前が付いています。日本語では k平均法とも呼びます。正解を教えずにデータの中のまとまりを見つけるので、機械学習の中では「教師なし学習」に入ります。
k-means が小さくしようとしている量は1つだけです。各点から、その点が属するクラスタの重心までの距離の2乗を、全部の点で足したもので、scikit-learn では inertia_(クラスタ内誤差平方和、SSE・WCSS とも呼ぶ)という名前で読めます。割り当ての段でも重心の更新の段でもこの値は増えないので、繰り返せば必ず止まります。ただし止まった先が全体で最も小さい答え(大域最適)とは限りません。
「クラスタ」「クラスター」は、IT の外でも内でも別の意味で使われます。検索結果で混ざりやすいので、先に整理します。
| 場面 | 「クラスタ(クラスター)」の意味 | 例 |
|---|---|---|
| ふだんの言葉 | ひとかたまりの集団・房 | 感染者の集団(クラスター)、ブドウの房 |
| サーバー・インフラ | 複数の機械を束ねて1つのように動かす仕組み | Kubernetes クラスタ、DB のクラスタ構成 |
| ストレージ | ディスクの読み書きの単位 | ファイルシステムのクラスタサイズ |
| 統計・機械学習(この記事) | 似たデータ同士のまとまり。分ける作業がクラスタリング | 購買履歴の似た顧客のグループ |
名前に k が付く手法として、k近傍法(k-NN)がよく並べられます。名前は似ていますが、目的も k の意味も別物です。
k-means法(教師なし)
目的:正解の無いデータをグループに分ける
k はクラスタの数
学習の結果は k 個の重心
k近傍法(教師あり)
目的:正解つきのデータから新しい点のラベルを当てる
k は多数決に使う近くの点の数
学習の結果は訓練データそのもの
クラスタリングには、大きく「階層的」(近いもの同士を順にくっつけて木を作る)と「非階層的」(最初に数を決めて分ける)があり、k-means は非階層的の代表です。階層的な方法(階層クラスタリング)との違いは、後半の比較の節で同じデータを使って確かめます。
| 良いところ | 弱いところ |
|---|---|
| 仕組みが単純で説明しやすい | クラスタ数 k を先に決める必要がある |
| 速い。100万点×10列が2.6秒(この記事の実測) | 初期値で結果が変わる(局所最適) |
新しい点を predict で振り分けられる | 丸くない形・大きさや密度が違うまとまりが苦手 |
| 重心が「各グループの平均像」として読める | 外れ値に重心が引っ張られる。単位の違う列は標準化が要る |
最初の5分:scikit-learnのKMeansで動かす
この節では、Python の scikit-learn で k-means クラスタリングを動かす最小のコードと、その結果の読み方が分かります。
scikit-learn が入っていれば、追加のダウンロード無しで次のコードが動きます。make_blobs は、まとまりが3つある2次元の点を300個作る関数です。
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
X, _ = make_blobs(n_samples=300, centers=3, cluster_std=1.2, random_state=0)
km = KMeans(n_clusters=3, random_state=0).fit(X)
print(km.labels_[:10]) # 各点が入ったクラスタの番号
print(km.cluster_centers_) # 3つの重心
print(round(km.inertia_, 1)) # 重心までの距離の2乗の合計
print(km.n_iter_) # 繰り返した回数
print(km.predict([[0, 4], [2, 1]])) # 新しい点の所属
[1 2 1 0 0 0 1 1 0 1]
[[-1.97014045 2.69321113]
[ 0.79595376 4.52674953]
[ 1.85640015 0.74304534]]
709.3
13
[1 2]
labels_
各点のクラスタ番号。番号そのものに意味は無く、実行の条件で 0・1・2 の付き方が入れ替わる。
番号で意味を決めない
cluster_centers_
k 行 × 列数の重心。各クラスタの平均像として読む。
標準化したなら元の単位へ戻して読む
inertia_
重心までの距離の2乗の合計。小さいほどまとまっている。k を増やすと必ず下がる。
エルボー法で使う
predict / transform
新しい点の所属と、各重心までの距離。学習に使っていないデータにも使える。
DBSCAN には無い
scikit-learn 1.4 以降、n_init の既定は 'auto' で、init='k-means++'(既定)なら初期値を1回だけ試します(公式ドキュメント)。上の実行でも初期値は1回で、13回の繰り返しで止まっています。
k-meansのアルゴリズム(割り当てと重心の更新)
この節では、k-means クラスタリングの手順を1段ずつと、繰り返すたびに inertia がどう減るかの実物が分かります。
scikit-learn の既定の計算方法は Lloyd 法(algorithm="lloyd")で、次の4段です。
k 個の重心を置く(初期化)
データの中から k 点を選ぶ。ランダムに選ぶ方法と、離れた点を選びやすくする k-means++ がある。
各点をいちばん近い重心に割り当てる
点と重心のユークリッド距離(の2乗)を全部の組で計算し、最小の重心の番号をその点のラベルにする。
重心をクラスタの平均へ動かす
同じラベルの点の平均を、新しい重心にする。平均は「距離の2乗の和」を最小にする点なので、ここでも inertia は下がる。
止まるまで 2・3 を繰り返す
ラベルが変わらなくなるか、重心の動きが tol より小さくなるか、max_iter(既定300回)に届いたら終わる。
次の節の自作コードで、反復ごとの inertia と重心の動き(前の重心との差の2乗和)を出すと、こう変わりました(データは上と同じ300点、初期値はデータから無作為に3点)。
iter 1 inertia(割り当て時) 1453.266 重心の移動 5.035102
iter 2 inertia(割り当て時) 882.578 重心の移動 1.025514
iter 3 inertia(割り当て時) 744.457 重心の移動 0.185814
iter 4 inertia(割り当て時) 718.187 重心の移動 0.055491
iter 5 inertia(割り当て時) 711.041 重心の移動 0.010861
iter 6 inertia(割り当て時) 709.824 重心の移動 0.001110
iter 7 inertia(割り当て時) 709.667 重心の移動 0.001040
iter 8 inertia(割り当て時) 709.525 重心の移動 0.002179
iter 9 inertia(割り当て時) 709.318 重心の移動 0.000000
- inertia は毎回下がり、最初の3回で大半が決まっています(1453 → 744)。後半は境目の数点が入れ替わるだけです。
- 止まる条件の
tol=1e-4は「相対値」です。scikit-learn は、各列の分散の平均に tol を掛けた値を、重心の動きのしきい値にしています。列の値が大きいデータでも同じ tol で済むのはこのためです。 - 1回の計算量は「点の数 × k × 列の数」に比例します。
algorithm="elkan"は三角不等式で距離の計算を省きますが、点の数 × k の配列を余分に持ちます(公式ドキュメント)。手元の20万点×10列では lloyd 0.44秒・elkan 0.48秒で、結果の inertia は同じでした。
numpyでk-meansを自作してscikit-learnと一致させる
この節では、k-means を numpy で20行ほどで書く方法と、scikit-learn の KMeans と結果が一致することの確かめ方が分かります。
距離の計算は、点の配列 X(n×列数)と重心 C(k×列数)に軸を足して引き算する「ブロードキャスト」で、n×k の表を一度に作ります。
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
def kmeans_np(X, init, max_iter=300, tol=1e-4):
C = init.copy()
tol_abs = tol * np.var(X, axis=0).mean() # scikit-learn と同じ換算
for it in range(1, max_iter + 1):
d2 = ((X[:, None, :] - C[None, :, :]) ** 2).sum(axis=2) # 点×重心の距離の2乗
labels = d2.argmin(axis=1) # 1. いちばん近い重心へ
new_C = np.array([X[labels == j].mean(axis=0) if np.any(labels == j) else C[j]
for j in range(len(C))]) # 2. 平均を新しい重心に
shift = ((new_C - C) ** 2).sum()
C = new_C
if shift <= tol_abs: # 3. 動かなくなったら終わり
break
d2 = ((X[:, None, :] - C[None, :, :]) ** 2).sum(axis=2)
labels = d2.argmin(axis=1)
return labels, C, d2.min(axis=1).sum(), it
X, _ = make_blobs(n_samples=300, centers=3, cluster_std=1.2, random_state=0)
init = X[np.random.default_rng(1).choice(len(X), 3, replace=False)]
labels, C, inertia, n_iter = kmeans_np(X, init)
km = KMeans(n_clusters=3, init=init, n_init=1, algorithm="lloyd").fit(X)
print("自作 inertia", round(inertia, 6), "反復", n_iter)
print("sklearn inertia", round(km.inertia_, 6), "反復", km.n_iter_)
print("ラベル一致", np.array_equal(labels, km.labels_))
print("重心の最大差", np.abs(C - km.cluster_centers_).max())
自作 inertia 709.31756 反復 9
sklearn inertia 709.31756 反復 9
ラベル一致 True
重心の最大差 8.881784197001252e-16
1同じ初期値を渡す
init に配列を渡し n_init=1 にすると、scikit-learn も同じ出発点から計算する。乱数の違いを消すと、ラベルまで1点残らず一致した。
2差は浮動小数点の端数
重心の差 8.9e-16 は、足し算の順番の違いで出る丸めの誤差。手法としては同じ計算をしている。
3空のクラスタに注意
点が1つも割り当たらない重心が出ると、平均が計算できない。自作では前の位置に残し、scikit-learn は遠い点へ移し替える。
この素朴な書き方は n×k×列数 の一時配列を作るので、点が数十万を超えるとメモリを使います。実務では scikit-learn を使い、自作は「何をしているか」を確かめるためのものと考えるのが安全です。numpy のブロードキャストや軸の指定に慣れていない場合は、関連記事「NumPyの使い方 基本一覧|配列の作成・演算・形状変更」が役に立ちます。
初期値で結果が変わる:k-means++とn_init
この節では、k-means が実行のたびに違う結果になる理由と、k-means++・n_init でどれだけ安定するかの実測が分かります。
k-means は「今より inertia が下がる方向」にしか動かないので、出発点が悪いと、2つのまとまりを1つの重心が抱え、別の1つのまとまりを2つの重心で割ったまま止まります(局所最適)。まとまりが8つある1,500点のデータで、乱数の種を100通り変えて試しました。「最良に届いた」は、50回試した中で最も小さい inertia(734.17)との差が0.1%以内だった割合です。
| 初期化 | n_init | 最良に届いた | inertia の最大 | ARI の最小 |
|---|---|---|---|---|
| random | 1 | 8% | 50,253 | 0.113 |
| k-means++ | 1 | 96% | 1,251 | 0.834 |
| random | 10 | 56% | 3,012 | 0.829 |
| k-means++ | 10 | 100% | 734 | 0.995 |
| random | 'auto'(=10回) | 56% | 3,012 | 0.829 |
| k-means++ | 'auto'(=1回) | 96% | 1,251 | 0.834 |
ARI(調整ランド指数)は、正解のグループ分けとの一致度で、1が完全一致、0が偶然と同じです。ランダム初期値の悪い例では、本来187点ずつの8つのまとまりが「375点・72点…」に分かれていました。2つのまとまりを1つの重心が抱えた形です。
k-means++ を自作して分かったこと
k-means++ は、1つめの重心を無作為に選んだあと、「今ある重心から遠い点ほど選ばれやすく」(距離の2乗に比例した確率で)次の重心を選ぶ方法です(Arthur・Vassilvitskii 2007)。自作して同じ100通りで比べると、結果が分かれました。
def pp_init(X, k, rng): # 論文どおりの素の k-means++
C = [X[rng.integers(len(X))]]
for _ in range(k - 1):
d2 = ((X[:, None, :] - np.array(C)[None]) ** 2).sum(2).min(1)
C.append(X[rng.choice(len(X), p=d2 / d2.sum())]) # 遠い点ほど選ばれやすい
return np.array(C)
| 初期化(自作の Lloyd 法で100通り) | 最良に届いた | inertia の最大 |
|---|---|---|
| データから無作為に8点 | 6% | 13,172 |
| 素の k-means++ | 52% | 3,666 |
| greedy k-means++(候補4点から最良を選ぶ) | 96% | 1,248 |
素の k-means++ は52%にとどまり、scikit-learn の96%に届きませんでした。公式ドキュメントによると、scikit-learn の実装は「greedy k-means++」で、各段で候補を何点か引き、inertia を最も下げる点を選びます。候補の数は 2 + log k(k=8 なら4点)です。これを足すと自作でも96%になり、scikit-learn と同じ水準になりました。実務では、既定の k-means++ のまま n_init=10 前後を指定し、random_state を固定して再現できるようにするのが確実です。
クラスタ数の決め方:エルボー法とシルエット係数
この節では、k-means のクラスタ数の決め方として代表的なエルボー法とシルエット係数の考え方・コード・実測値が分かります。
k-means は k を増やすほど inertia が下がり、k = 点の数で0になります。そのため「inertia が最小の k」は選べません。代わりに次の2つを見ます。
- エルボー法:k を1つずつ増やしたときの inertia の減り方を見て、急に減らなくなる「ひじ(elbow)」の k を選ぶ。
- シルエット係数:各点について、自分のクラスタの点までの平均距離 a と、いちばん近い別のクラスタの点までの平均距離 b から
(b - a) / max(a, b)を計算し、全点で平均する。-1〜1 で、1に近いほどよく分かれている。最大になる k を選ぶ。
from sklearn.metrics import silhouette_score
for k in range(2, 10):
km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X)
print(k, round(km.inertia_, 1), round(silhouette_score(X, km.labels_), 3))
まとまりが4つある600点(make_blobs)での結果です。減少率は「1つ前の k からの inertia の減り方」です。
| k | inertia | 減少率 | シルエット係数 |
|---|---|---|---|
| 1 | 39,698.3 | — | (計算できない) |
| 2 | 18,637.9 | 53.1% | 0.591 |
| 3 | 4,178.9 | 77.6% | 0.757 |
| 4 | 1,160.3 | 72.2% | 0.788(最大) |
| 5 | 1,045.8 | 9.9% | 0.691 |
| 6 | 938.4 | 10.3% | 0.559 |
| 7 | 844.2 | 10.0% | 0.427 |
| 8 | 748.7 | 11.3% | 0.333 |
k=4 までは毎回50%以上減り、k=5 からは10%前後に落ちます。ここがひじで、シルエット係数も k=4 が最大です。文字で描くと次の形です。
inertia(k=1 を 40 本としたときの棒の長さ)
k=1 ████████████████████████████████████████
k=2 ███████████████████
k=3 ████
k=4 █ ← ここから先はほとんど減らない
k=5 █
k=6 █
シルエット係数を自作して一致させる
from sklearn.metrics import silhouette_samples
def sil_np(X, lab):
D = np.sqrt(((X[:, None, :] - X[None, :, :]) ** 2).sum(2)) # 全点の距離表
s = np.zeros(len(X))
for i in range(len(X)):
same = lab == lab[i]
a = D[i, same].sum() / (same.sum() - 1) # 自分を除いた平均
b = min(D[i, lab == c].mean() for c in np.unique(lab) if c != lab[i])
s[i] = (b - a) / max(a, b)
return s
s = sil_np(X, km.labels_)
print(s.mean(), silhouette_score(X, km.labels_), np.abs(s - silhouette_samples(X, km.labels_)).max())
0.7880284559632772 0.7880284559632772 1.887379141862766e-15
平均は小数点以下16桁まで一致し、点ごとの差も 1.9e-15 でした。シルエット係数は全点の距離表(n×n)を使うので、点が数万を超えると重くなります。その場合は silhouette_score(X, labels, sample_size=5000, random_state=0) のように一部の点で計算します。
クラスタ数の決め方:ほかの指標とiris・wineでの答え
この節では、エルボー法・シルエット係数以外の指標(Calinski-Harabasz・Davies-Bouldin・ギャップ統計・BIC)と、正解のあるデータで指標の答えが割れる様子が分かります。
| 指標 | 何を見るか | 選ぶ k | scikit-learn |
|---|---|---|---|
| エルボー法 | inertia の減り方 | 減り方が急に鈍る k | inertia_ |
| シルエット係数 | クラスタ内の近さと隣との離れ具合 | 最大 | silhouette_score |
| Calinski-Harabasz(CH) | クラスタ間の分散 ÷ クラスタ内の分散 | 最大 | calinski_harabasz_score |
| Davies-Bouldin(DB) | 各クラスタと最も似たクラスタの近さ | 最小 | davies_bouldin_score |
| ギャップ統計 | 一様な乱数のデータと比べた log(inertia) の差 | 規則で決める(下記) | 無し(自作) |
| BIC(x-means の考え方) | 当てはまりの良さ − 複雑さの罰 | 最小 | GaussianMixture.bic |
ギャップ統計(Tibshirani ら 2001)は、データの範囲で一様な乱数を作って同じ k で k-means をかけ、「本物のデータの方がどれだけまとまっているか」を測ります。gap(k) ≥ gap(k+1) − s(k+1) を満たす最小の k を選びます(s は乱数による揺れ)。x-means(Pelleg・Moore 2000)は、小さい k から始めて、クラスタを2つに割ると BIC が良くなるときだけ割る方法で、scikit-learn には入っていません。ここでは同じ BIC の考え方として、混合ガウスモデル(GMM)の BIC を並べました。
4つのデータで、各指標が選んだ k は次のとおりです(k=2〜9、iris・wine は k=2〜6)。
| データ(本当のグループ数) | エルボー | シルエット | CH / DB | ギャップ / BIC |
|---|---|---|---|---|
| make_blobs(4) | 4 | 4(0.788) | 4 / 4 | 4 / 4 |
| iris・そのまま(3) | 2か3 | 2(0.681) | 3 / 2 | 6以上 / 2 |
| iris・標準化(3) | 2か3 | 2(0.582) | 2 / 2 | 6以上 / 2 |
| wine・標準化(3) | 3 | 3(0.285) | 3 / 3 | 4 / 2 |
- きれいに分かれたデータでは、どの指標も同じ k=4 を選びました。
- iris では、シルエット・DB・BIC が k=2 を選びました。iris の3品種のうち versicolor と virginica は重なっていて、距離だけで見ると「setosa とそれ以外」の2つに分けるのが最もきれいだからです。正解の数(3)と指標の答えは一致するとは限りません。
- wine はシルエットの最大でも0.285で、まとまりがはっきりしないデータです。それでも k=3 を選ぶ指標が多く、本当の品種の数と合いました。
指標の答えが割れたときに決める順番を、次のようにしておくと迷いが減ります。
- 1使い道から k の範囲を決める営業が扱えるのは5グループまで、など。範囲の外は試さない
- 2エルボーとシルエットを並べる両方が同じ k を指せば、それを第一候補にする
- 3割れたら候補の k を2〜3個に絞るCH・DB・BIC は補助として見る
- 4各 k の重心と人数を見て、名前が付くかを確かめる「よく買う常連」「休眠」のように説明できる k を選ぶ
- 5乱数の種を変えて同じ分かれ方になるか確かめる種で ARI が大きく揺れる k は避ける
標準化しないとk-meansは崩れる(StandardScaler)
この節では、列の単位や大きさが違うときに k-means がどう崩れるかと、標準化で直る様子が分かります。
k-means は距離で分けるので、値の幅が大きい列ほど距離を支配します。scikit-learn 同梱の wine(13列)では、proline(プロリンの量)の標準偏差が314、2番目の magnesium が14、3番目の alcalinity_of_ash が3.3で、残りはそれ以下です。
| wine の前処理(k=3) | ARI | 正解率(ラベルの対応を最適化) |
|---|---|---|
| そのまま | 0.371 | 70.2% |
| proline の1列だけ | 0.371 | 70.2% |
| StandardScaler(平均0・標準偏差1) | 0.897 | 96.6% |
| MinMaxScaler(0〜1) | 0.869 | 95.5% |
| RobustScaler(中央値と四分位範囲) | 0.878 | 96.1% |
「そのまま」と「proline の1列だけ」の結果は、ラベルが100%同じでした。13列を渡したつもりでも、実際には proline だけで分けていたことになります。
架空の顧客400人(年齢で2グループ、年収は両グループで同じ分布)でも試しました。年齢の標準偏差は12歳、年収は円で150万円です。
| 年収の単位 | ARI(年齢の2グループとの一致) | 何が起きたか |
|---|---|---|
| 円 | -0.002 | 年収の高い・低いで分かれた |
| 万円 | -0.002 | 年収の幅(150)がまだ年齢(12)より大きい |
| 百万円 | 0.980 | 今度は年齢が支配して、たまたま合った |
| 標準化 | 0.970 | 両方の列を同じ重みで見た |
単位を変えるだけで答えが入れ替わるのは、k-means が「列の幅=重要さ」として扱うからです。標準化は「どの列も同じ重みで見る」という選択で、使い道として特定の列を重く見たいなら、標準化のあとにその列へ係数を掛けます。
ただし、標準化すれば必ず良くなるわけではありません。iris は4列とも同じ cm の長さで、そのままの正解率は89.3%(ARI 0.730)、標準化すると83.3%(ARI 0.620)に下がりました。花びらの長さのように幅が大きい列が、そもそも品種を分ける情報を多く持っていたためです。単位が同じで幅に意味がある列なら、そのままも試して比べます。
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
pipe = make_pipeline(StandardScaler(), KMeans(n_clusters=3, n_init=10, random_state=0))
pipe.fit(X_train)
labels_new = pipe.predict(X_new) # 新しいデータにも同じ平均・標準偏差で変換してから振り分ける
標準化と k-means を Pipeline でつなぐと、学習時の平均・標準偏差で新しいデータも変換されます。表計算から読んだデータの前処理は、関連記事「pandasの使い方 逆引き81選|pandas 3.0の実行結果つき」が参考になります。
k-meansが苦手な形(make_moons・同心円・細長い・大きさ違い)
この節では、k-means がうまく分けられないデータの形と、その理由が実測で分かります。
scikit-learn の公式ドキュメントは、inertia は「クラスタが凸で等方的(どの方向にも同じように広がる)」と仮定しており、細長いまとまりや不規則な形にはうまく応えない、と説明しています。k-means の境目は、隣り合う2つの重心のちょうど真ん中に引いた直線(ボロノイ分割)にしかならないからです。
5種類の形で、本当のグループとの一致度(ARI)を測りました。どれも標準化してから、本当のグループ数を k に渡しています。
| データの形 | 作り方 | KMeans の ARI | 崩れ方 |
|---|---|---|---|
| 三日月が2つ | make_moons(500, noise=0.06) | 0.47 | 三日月の先を相手側に取られ、直線で半分に切られる |
| 同心円 | make_circles(500, factor=0.5) | -0.00 | 内と外ではなく、左右に割られる |
| 斜めに細長い3つ | make_blobs を線形変換 | 0.57 | 細長いまとまりを横切って切る |
| 広がりが違う3つ | cluster_std=[1.0, 2.5, 0.5] | 0.73 | 広いまとまりの端を、隣の狭いまとまりが取る |
| 人数が違う3つ | 300点・30点・10点 | 1.00 | 離れていれば人数差は問題にならなかった |
三日月形で k を増やすとどうなるかも見ました。「純度」は、各クラスタの中で多数派の三日月に属する点の割合です。
moons KMeans k=2 純度 84.4%
moons KMeans k=4 純度 83.8%
moons KMeans k=6 純度 98.8%
moons KMeans k=8 純度 99.6%
moons KMeans k=10 純度 100.0%
k を多めにして細かく割り、あとで隣り合うものをまとめると、三日月の形もたどれます。これは「k-means で小さく切ってから、階層クラスタリングでまとめる」という使い方の考え方です。
形の分からないデータに k-means だけをかける
k-means は、どんなデータでも必ず k 個に分けて返す
DBSCAN・階層クラスタリング・GMMとの比較と選び方
この節では、k-means と DBSCAN・HDBSCAN・階層クラスタリング(Ward・単連結)・スペクトラルクラスタリング・混合ガウスモデル(GMM)を同じデータで比べた結果と、手法の選び方が分かります。
同じ5種類の形で、各手法の ARI を並べました。DBSCAN は eps=0.2, min_samples=5、HDBSCAN は min_cluster_size=15 で固定し、括弧の中はノイズ(どこにも属さない)と判定した点の数です。
| 手法 | 三日月 | 同心円 | 斜めに細長い |
|---|---|---|---|
| KMeans | 0.47 | -0.00 | 0.57 |
| GMM(共分散 full) | 0.51 | -0.00 | 1.00 |
| 階層(Ward) | 0.42 | 0.00 | 0.62 |
| 階層(単連結) | 1.00 | 1.00 | 0.57 |
| DBSCAN | 1.00(0点) | 0.99(2点) | 0.98(8点) |
| HDBSCAN | 1.00(0点) | 1.00(0点) | 0.82(61点) |
| スペクトラル | 1.00 | 1.00 | 0.98 |
| 手法 | 広がりが違う | 人数が違う | 丸く離れた3つ |
|---|---|---|---|
| KMeans | 0.73 | 1.00 | 1.00 |
| GMM(共分散 full) | 0.95 | 1.00 | 1.00 |
| 階層(Ward) | 0.85 | 1.00 | 1.00 |
| 階層(単連結) | 0.00 | 1.00 | 1.00 |
| DBSCAN | 0.81(71点) | 0.71(48点) | 1.00(1点) |
| HDBSCAN | 0.82(80点) | 1.00(9点) | 1.00(0点) |
| スペクトラル | 0.80 | 0.30 | 1.00 |
- すべての形で1位になる手法はありませんでした。三日月・同心円は DBSCAN・スペクトラル、斜めに細長い・広がり違いは GMM、丸く離れたまとまりはどれでも分けられます。
- DBSCAN は
eps(近いとみなす距離)に敏感です。三日月で eps を変えると、0.05 で10クラスタ・ノイズ445点(ARI 0.011)、0.15 で ARI 0.961、0.2〜0.5 で 1.000、1.0 で全部が1つ(ARI 0.000)でした。各点から5番目に近い点までの距離を並べると、中央値0.09・99%点0.225で、その少し上(0.2〜0.3)が良い値でした。 - DBSCAN・HDBSCAN・階層クラスタリング・スペクトラルには
predictがありません。新しい点を後から振り分けたいなら、k-means か GMM です(公式ドキュメントの表では inductive と transductive の違い)。 - 階層(Ward)は点の数の2乗のメモリと時間を使います。2万点で、KMeans 0.11秒・GMM 0.26秒・DBSCAN 0.32秒に対し、Ward は28.8秒かかりました(5,000点では1.2秒)。
| 状況 | 選ぶ手法 | 理由 |
|---|---|---|
| まとまりが丸く、数の見当がつく・新しいデータも振り分けたい | k-means | 速く、重心で説明でき、predict がある |
| 点が数十万〜数百万 | k-means(MiniBatchKMeans も候補) | 点の数に比例する計算で済む |
| 楕円・広がりの違うまとまり、所属の確率がほしい | GMM | まとまりごとの共分散を持つ |
| 形が不規則、外れ値を「どこにも属さない」にしたい | DBSCAN・HDBSCAN | 密度でつながった点をまとめ、ノイズを返す |
| k が分からず、木(デンドログラム)を見て決めたい | 階層クラスタリング | どの k で切ったかを後から選べる。数千点まで |
| 距離が測れない値(カテゴリだけ) | k-means は使わない | 平均が意味を持たない。k-modes などを検討 |
外れ値・大きなデータ・顧客セグメントでの使い方
この節では、k-means を実務で使うときに出てくる外れ値・データの大きさ・結果の読み方の3点が、実測と架空データで分かります。
外れ値は重心を引っ張る(k-medoids との比較)
0付近と5付近に50点ずつ、40付近に外れ値を3点置いて k=2 で分けました。
| 手法 | 代表点(横軸の値) | 人数 |
|---|---|---|
| 外れ値なしの KMeans | -0.01 と 4.94 | 50・50 |
| 外れ値ありの KMeans | 2.46 と 41.0 | 100・3 |
| 外れ値ありの k-medoids(自作) | 0.11 と 4.96 | 50・53 |
k-means は2乗の距離を小さくしようとするので、遠い3点に重心を1つ使い、本来の2つのまとまりを1つにしました。k-medoids は代表点を「実在する点」から選び、距離の合計(2乗しない)を小さくするので、外れ値の影響を受けにくくなります。scikit-learn 本体には k-medoids は入っていないため、ここでは自作の交互法で確かめました。実務では、外れ値を先に取り除くか、k を1つ多めにして外れ値用のクラスタを作らせる(この例で k=3 にすると 50・3・50 に分かれました)方法もあります。
大きなデータと MiniBatchKMeans
100万点×10列(約80MB)で比べると、KMeans は2.64秒、MiniBatchKMeans(batch_size=4096)は18.65秒で、inertia も MiniBatch の方が約15%大きくなりました。このデータは KMeans が数回で収束する分かれやすいデータで、メモリにも載るため、MiniBatch の良さが出なかった例です。公式ドキュメントは、MiniBatchKMeans は KMeans より速く収束するが結果の質は下がる、と説明しています。メモリに載らないデータを partial_fit で少しずつ渡すときや、反復回数の多いデータで、時間と inertia を両方測って選ぶのが確実です。
顧客セグメント(架空データ)での使い方と読み方
「最終購入からの日数」「購入回数」「平均購入額(円)」の3列を持つ架空の顧客1,200人(常連・ふつう・高額・休眠の4グループを作って混ぜたもの)で、標準化 → k-means → 重心を元の単位に戻す、の順に進めました。シルエット係数は k=2〜7 で 0.433・0.527・0.576・0.515・0.480・0.436 となり、k=4 を選びました。
pipe = make_pipeline(StandardScaler(), KMeans(n_clusters=4, n_init=10, random_state=0)).fit(df)
centers = pd.DataFrame(pipe[0].inverse_transform(pipe[1].cluster_centers_), columns=df.columns)
centers["人数"] = np.bincount(pipe[1].labels_)
| 最終購入からの日数 | 購入回数 | 平均購入額 | 人数 | 付けた名前 |
|---|---|---|---|---|
| 11.1日 | 13.3回 | 3,957円 | 293 | 常連 |
| 68.8日 | 3.5回 | 2,881円 | 651 | ふつう |
| 21.1日 | 4.8回 | 15,702円 | 142 | 高額 |
| 336.0日 | 2.1回 | 2,562円 | 114 | 休眠 |
作ったときの人数は常連300・ふつう500・高額150・休眠250でした。休眠のうち最終購入が比較的最近の人は「ふつう」に入り、休眠は114人と少なく出ています。k-means は距離で境目を引くだけで、業務上の定義(「180日以上買っていない人を休眠とする」など)を知りません。決まった定義があるなら、その定義でルールとして分けるのが先で、k-means は「定義の無いところに、どんなグループがあるかを探す」ときに向いています。
新しい顧客は pipe.predict(new) で振り分けられ、pipe.transform(new) で各重心までの距離(標準化後)が分かります。5日前に買った15回目の顧客は常連の重心まで0.37、300日前に1回だけの顧客は休眠の重心まで0.44でした。距離が大きい顧客は、どのグループにも近くない例外として扱うと、施策の当て外れが減ります。
重心の比較を図にしたいときは、関連記事「matplotlibの使い方と日本語文字化け対策|OS別・Colab・3.12対応」で日本語のラベルを付けた散布図・棒グラフを描けます。
k-means はベクトル検索の索引にも使われています。pgvector の IVFFlat 索引は、ベクトルを lists 個のリストに分けてから、問い合わせに近いリストだけを探す仕組みで、公式の README は索引作成の段階に「performing k-means」を挙げています。索引をデータを入れた後に作るのは、重心をデータから学ぶためです。使い方は関連記事「pgvectorの使い方|PostgreSQLでベクトル検索をコピペで構築」、検索した文書を生成 AI に渡す流れは関連記事「RAGの作り方|Pythonで動かす最小構成と精度の上げ方」で扱っています。
k-means を業務のデータにかける前に確かめておくことをまとめます。
- 1数値の列だけになっている文字列・カテゴリは数値にするか外す。欠損(NaN)は埋めるか行を落とす
- 2単位の違う列を標準化した円と歳、回数と日数が混ざるなら StandardScaler。Pipeline でつなぐ
- 3外れ値を見た極端な値が数点あると重心を1つ奪う。人数が極端に少ないクラスタが出たら疑う
- 4random_state と n_init を決めた再現できるように固定し、n_init=10 前後で局所最適を避ける
- 5重心を元の単位に戻して読んだinverse_transform で戻し、人数と一緒に表にして名前を付ける
よくあるエラー・つまずき(KMeansのエラー原文)
この節では、scikit-learn 1.9.1 の KMeans とシルエット係数で実際に出たエラー・警告の原文と、その直し方が分かります。
| 出た文言(原文) | 原因 | 直し方 |
|---|---|---|
ValueError: n_samples=10 should be >= n_clusters=20. | 点の数より多い k を指定した | k を点の数より小さくする |
ValueError: Input X contains NaN. | 欠損値が入っている。KMeans は NaN を受け付けない | dropna() で落とすか、SimpleImputer で埋める |
ValueError: could not convert string to float: 'a' | 文字列の列が混ざっている | 数値の列だけを選ぶ。カテゴリは数値にしてから |
ValueError: Expected 2D array, got 1D array instead | 1列のデータを1次元の配列で渡した | X.reshape(-1, 1) で2次元にする |
ConvergenceWarning: Number of distinct clusters (2) found smaller than n_clusters (3). Possibly due to duplicate points in X. | 違う値の点が k 個より少ない(同じ点の重複) | 重複を確かめ、k を減らす |
ValueError: Number of labels is 1. Valid values are 2 to n_samples - 1 (inclusive) | k=1 や全点が同じラベルでシルエット係数を計算した | シルエットは k=2 から。k=1 は inertia だけ見る |
ValueError: X has 3 features, but KMeans is expecting 2 features as input. | 学習時と列の数が違うデータで predict した | 学習と同じ列・同じ順番で渡す |
NotFittedError: This KMeans instance is not fitted yet. | fit の前に predict した | 先に fit する |
RuntimeWarning: Explicit initial center position passed: performing only one init in KMeans instead of n_init=5. | init に配列を渡すと初期値は1つしかない | 配列を渡すなら n_init=1 にする |
InvalidParameterError: The 'algorithm' parameter of KMeans must be a str among {'lloyd', 'elkan'}. Got 'full' instead. | 以前の版の名前 "full" を使った(1.1 で "lloyd" に改名) | algorithm="lloyd" にする |
- エラーにならないつまずきの方が多いです。実行ごとにラベルの番号が入れ替わる(番号に意味は無い)、標準化を忘れて1列だけで分けている、
max_iter=2のように途中で止めても警告が出ない(手元で確かめた結果)、の3つは結果を見ないと気づけません。 - 途中で止まったときは
labels_とcluster_centers_の平均が合わないことがあります(公式ドキュメントの Notes)。n_iter_がmax_iterと同じなら、max_iter を増やします。
よくある質問
Q. k-means法と k-means クラスタリングは同じものですか。
A. 同じものを指します。k平均法・k-means 法・k-means クラスタリングはどれも、この記事で扱った「割り当てと重心の更新を繰り返す」手法です。
Q. クラスタ数の決め方で、エルボー法とシルエット係数のどちらを信じればいいですか。
A. 片方だけで決めず、両方を並べます。きれいに分かれたデータでは同じ k を指しました。iris のように割れたときは、候補の k の重心と人数を見て、使い道で説明できる方を選びます。
Q. エルボーがはっきりしないときはどうしますか。
A. 減少率(前の k からの減り方)を数値で並べると、目で見るより判断しやすくなります。それでも曲がり目が無いなら、データにはっきりしたまとまりが無い可能性があり、シルエット係数が0.3を下回るかどうかも合わせて見ます。
Q. 結果が実行のたびに変わります。
A. 初期値が乱数で決まるためです。random_state=0 のように固定し、n_init=10 で10回試した中の最良を使うと安定します。手元の8クラスタのデータでは、k-means++ を10回試すと100回とも同じ最良の答えになりました。
Q. ラベルの番号(0・1・2)に意味はありますか。
A. ありません。同じ分かれ方でも、初期値によって番号の付き方が変わります。正解ラベルと比べるときは ARI のような番号に左右されない指標を使い、表にするときは重心の値で並べ替えます。
Q. k-means に正規化(MinMaxScaler)と標準化(StandardScaler)のどちらを使うべきですか。
A. 多くの場合は標準化から試します。wine では標準化0.897・MinMax 0.869・Robust 0.878 で大差はありませんでした。外れ値が多い列があるなら RobustScaler、単位が同じで幅に意味がある列(iris)ならそのままも比べます。
Q. カテゴリの列(性別・地域など)も入れられますか。
A. k-means は平均と距離を使うので、カテゴリをそのまま入れると意味のない距離になります。0/1 の列にして入れる方法もありますが、数値の列との重み付けが難しくなります。カテゴリが中心なら k-modes や、別の距離を使える階層クラスタリングを検討します。
Q. 列が多い(高次元の)データにも使えますか。
A. 使えますが、列が多いと距離の差が出にくくなります(次元の呪い)。scikit-learn の公式ドキュメントは、先に PCA で次元を減らすと、この問題が和らぎ計算も速くなると説明しています。
Q. x-means は scikit-learn で使えますか。
A. scikit-learn 1.9.1 には入っていません。BIC で k を選ぶ考え方を使いたいなら、GaussianMixture(k).fit(X).bic(X) を k ごとに比べると近いことができます。手元の make_blobs では BIC も k=4 を選びました。
Q. どのくらいのデータ量まで k-means で扱えますか。
A. 計算は点の数に比例するので、メモリに載る範囲ならかなり大きなデータまで扱えます。手元の2コアのマシンで、100万点×10列が2.64秒でした。メモリに載らないなら MiniBatchKMeans の partial_fit で少しずつ渡します。
まとめ
- k-means法は、各点を最も近い重心に割り当て、重心を平均へ動かすことを繰り返し、距離の2乗の合計(inertia)を小さくする手法です。numpy の20行ほどで書け、scikit-learn と同じ初期値ならラベル・重心・反復回数まで一致しました。
- 初期値で答えが変わります。ランダム1回では最良に届いたのが8%、scikit-learn の k-means++(greedy 版)1回で96%、10回試すと100%でした。
- クラスタ数は、エルボー法(減少率)とシルエット係数を並べて候補を絞り、重心と人数を見て使い道で決めます。iris のように正解の数と指標の答えが一致しないデータもあります。
- 単位の違う列は標準化します。wine は ARI 0.371 → 0.897。ただし iris のように標準化で下がるデータもあるので、両方を比べます。
- 三日月・同心円・細長い形は苦手です。DBSCAN・スペクトラル・GMM と比べ、新しい点を振り分けたいか、外れ値を外したいかで手法を選びます。
参考・出典
いずれも確認日は2026年11月5日です。
- scikit-learn 1.9.1「KMeans」:https://scikit-learn.org/stable/modules/generated/sklearn.cluster.KMeans.html
- scikit-learn 1.9.1「2.3. Clustering」:https://scikit-learn.org/stable/modules/clustering.html
- scikit-learn「MiniBatchKMeans」:https://scikit-learn.org/stable/modules/generated/sklearn.cluster.MiniBatchKMeans.html
- scikit-learn「silhouette_score」:https://scikit-learn.org/stable/modules/generated/sklearn.metrics.silhouette_score.html
- scikit-learn「DBSCAN」:https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html
- Arthur, D. and Vassilvitskii, S.「k-means++: The Advantages of Careful Seeding」(SODA 2007)
- Tibshirani, R., Walther, G. and Hastie, T.「Estimating the number of clusters in a data set via the gap statistic」(Journal of the Royal Statistical Society B, 2001)
- Pelleg, D. and Moore, A.「X-means: Extending K-means with Efficient Estimation of the Number of Clusters」(ICML 2000)
- Google for Developers「K 平均法クラスタリングとは」:https://developers.google.com/machine-learning/clustering/kmeans/overview?hl=ja
- pgvector「README」(IVFFlat):https://github.com/pgvector/pgvector
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「k-means法とは|Pythonで自作・クラスタ数の決め方・苦手な形を実測」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。