結論

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 の表

2026年11月5日更新|Python 3.13.18・numpy 2.5.3・scikit-learn 1.9.1・CPU 2コアで実行確認

この記事で実際に動かしたもの: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]
1

labels_

各点のクラスタ番号。番号そのものに意味は無く、実行の条件で 0・1・2 の付き方が入れ替わる。

番号で意味を決めない

2

cluster_centers_

k 行 × 列数の重心。各クラスタの平均像として読む。

標準化したなら元の単位へ戻して読む

3

inertia_

重心までの距離の2乗の合計。小さいほどまとまっている。k を増やすと必ず下がる。

エルボー法で使う

4

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段です。

1

k 個の重心を置く(初期化)

データの中から k 点を選ぶ。ランダムに選ぶ方法と、離れた点を選びやすくする k-means++ がある。

init

2

各点をいちばん近い重心に割り当てる

点と重心のユークリッド距離(の2乗)を全部の組で計算し、最小の重心の番号をその点のラベルにする。

割り当て(E ステップに相当)

3

重心をクラスタの平均へ動かす

同じラベルの点の平均を、新しい重心にする。平均は「距離の2乗の和」を最小にする点なので、ここでも inertia は下がる。

更新(M ステップに相当)

4

止まるまで 2・3 を繰り返す

ラベルが変わらなくなるか、重心の動きが tol より小さくなるか、max_iter(既定300回)に届いたら終わる。

tol・max_iter

次の節の自作コードで、反復ごとの 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 の最小
random18%50,2530.113
k-means++196%1,2510.834
random1056%3,0120.829
k-means++10100%7340.995
random'auto'(=10回)56%3,0120.829
k-means++'auto'(=1回)96%1,2510.834
8%ランダム初期値1回で最良に届いた100回中8回
96%k-means++ 1回で最良に届いたscikit-learn の既定
100%k-means++ を10回試した最良1回あたり13ms

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 の減り方」です。

kinertia減少率シルエット係数
139,698.3—(計算できない)
218,637.953.1%0.591
34,178.977.6%0.757
41,160.372.2%0.788(最大)
51,045.89.9%0.691
6938.410.3%0.559
7844.210.0%0.427
8748.711.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)と、正解のあるデータで指標の答えが割れる様子が分かります。

指標何を見るか選ぶ kscikit-learn
エルボー法inertia の減り方減り方が急に鈍る kinertia_
シルエット係数クラスタ内の近さと隣との離れ具合最大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)44(0.788)4 / 44 / 4
iris・そのまま(3)2か32(0.681)3 / 26以上 / 2
iris・標準化(3)2か32(0.582)2 / 26以上 / 2
wine・標準化(3)33(0.285)3 / 34 / 2
  • きれいに分かれたデータでは、どの指標も同じ k=4 を選びました。
  • iris では、シルエット・DB・BIC が k=2 を選びました。iris の3品種のうち versicolor と virginica は重なっていて、距離だけで見ると「setosa とそれ以外」の2つに分けるのが最もきれいだからです。正解の数(3)と指標の答えは一致するとは限りません。
  • wine はシルエットの最大でも0.285で、まとまりがはっきりしないデータです。それでも k=3 を選ぶ指標が多く、本当の品種の数と合いました。

指標の答えが割れたときに決める順番を、次のようにしておくと迷いが減ります。

  1. 1使い道から k の範囲を決める営業が扱えるのは5グループまで、など。範囲の外は試さない
  2. 2エルボーとシルエットを並べる両方が同じ k を指せば、それを第一候補にする
  3. 3割れたら候補の k を2〜3個に絞るCH・DB・BIC は補助として見る
  4. 4各 k の重心と人数を見て、名前が付くかを確かめる「よく買う常連」「休眠」のように説明できる k を選ぶ
  5. 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.37170.2%
proline の1列だけ0.37170.2%
StandardScaler(平均0・標準偏差1)0.89796.6%
MinMaxScaler(0〜1)0.86995.5%
RobustScaler(中央値と四分位範囲)0.87896.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 を選んで、そのまま「データには k 個のグループがある」と結論づける
→
代わりに2列に落とした散布図(PCA など)で形を見て、DBSCAN・GMM など別の手法の結果とも比べる

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 で固定し、括弧の中はノイズ(どこにも属さない)と判定した点の数です。

手法三日月同心円斜めに細長い
KMeans0.47-0.000.57
GMM(共分散 full)0.51-0.001.00
階層(Ward)0.420.000.62
階層(単連結)1.001.000.57
DBSCAN1.00(0点)0.99(2点)0.98(8点)
HDBSCAN1.00(0点)1.00(0点)0.82(61点)
スペクトラル1.001.000.98
手法広がりが違う人数が違う丸く離れた3つ
KMeans0.731.001.00
GMM(共分散 full)0.951.001.00
階層(Ward)0.851.001.00
階層(単連結)0.001.001.00
DBSCAN0.81(71点)0.71(48点)1.00(1点)
HDBSCAN0.82(80点)1.00(9点)1.00(0点)
スペクトラル0.800.301.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.9450・50
外れ値ありの KMeans2.46 と 41.0100・3
外れ値ありの k-medoids(自作)0.11 と 4.9650・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 instead1列のデータを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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり