クラスタ数を指定不要・外れ値を自動で識別 できる。論文の手法欄で、こんな記述を見たことがあるはずです:
この DBSCAN は、 k-means や階層的クラスタリングとは 発想がまったく違うクラスタリング手法 です。 「データ点の密度」だけで自然なまとまりを見つけ、 例外的に孤立した点を「noise」として除外 します。 GPS 軌跡・画像解析・地理空間データ・ソーシャルネットワーク分析など、 不規則な形状や外れ値を含むデータで特に強力です。
夜の繁華街を上空から見るところを想像してください。 ある場所には人が密集しています(駅前広場、 ライブ会場、 屋台)。 別の場所はぽつぽつとしか歩いていません(住宅街、 公園の隅)。 DBSCAN は「人がたくさんいる場所」を 1 つのかたまりと認識し、 「ぽつんと孤立した人」は「群衆ではない」と分離する アルゴリズムです。
DBSCAN は、 すべてのデータ点を次の 3 種類に分類します:
| 種類 | 定義 | 例えるなら |
|---|---|---|
| コア点 (Core point) |
半径 ε 以内に minPts 個以上 の点が存在する点 | 「人だかりの真ん中で、 周りが密集している人」 |
| 境界点 (Border point) |
コア点ではないが、 あるコア点の ε 以内 にいる点 | 「人だかりの周縁にいて、 中の人と肩が触れている人」 |
| 雑音点 (Noise point) |
コア点でも境界点でもない点(誰のクラスタにも属さない) | 「ぽつんと一人で離れて立っている人」 → 外れ値扱い |
コア点同士で「半径 ε 以内」に到達できる関係を全部つないでいくと、 1 つのクラスタが浮かび上がります。 その周りに境界点をくっつければ完成。 残った点はすべて noise です。
データ集合 $D = \{x_1, x_2, \dots, x_n\} \subset \mathbb{R}^d$ と 2 つのパラメータ ($\varepsilon, \text{minPts}$) を考えます。
minPts = 4 程度、 一般的には minPts ≥ d + 1($d$ は特徴量の次元数)を推奨。metric 引数でマンハッタン距離・コサイン距離なども選択可。labels_ 配列で -1 として返される。SSDSE-B(2026 年版)の 「人口総数」 ($x$) と 「県内総生産」 ($y$) の 2 変数で、 47 都道府県を DBSCAN でクラスタリングします。 まずは 7 県だけのサンプルで手計算してみましょう(標準化済み)。
| 都道府県 | 人口 $z_x$(標準化) | 県内総生産 $z_y$(標準化) | 位置イメージ |
|---|---|---|---|
| 東京 | +4.91 | +5.43 | 右上の極端な外れ値 |
| 神奈川 | +1.86 | +1.55 | 右上中央 |
| 大阪 | +1.71 | +1.74 | 右上中央 |
| 愛知 | +1.22 | +1.66 | 右上中央 |
| 福岡 | +0.39 | +0.21 | 中央付近 |
| 広島 | −0.10 | −0.05 | 中央付近 |
| 島根 | −0.97 | −1.08 | 左下小規模県 |
ポイント:DBSCAN の結果は ε と minPts の設定に強く依存します。 上の例では ε=0.5 でしたが、 ε=0.8 にすれば noise が 2 県(東京・島根)に減ります。 「最適な ε」を探すには次節の k-distance graph を使います。
scikit-learn の DBSCAN クラスを使えば数行で実行できます。 標準化 を忘れないこと。
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 | import pandas as pd import numpy as np from sklearn.cluster import DBSCAN from sklearn.preprocessing import StandardScaler # SSDSE-B 2026 を読み込む(自治体コードと人口/県内総生産) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8-sig', header=1) # 2023 年だけに絞り、必要 2 変数を抜き出す df_23 = df[df['年度'] == 2023].copy() X = df_23[['A1101', 'A1102']].values # 人口総数, 県内総生産(億円) # 標準化(必須!) X_std = StandardScaler().fit_transform(X) # DBSCAN 実行 db = DBSCAN(eps=0.5, min_samples=4).fit(X_std) labels = db.labels_ # クラスタ数(noise=−1 は除く)と noise 数 n_clusters = len(set(labels)) - (1 if -1 in labels else 0) n_noise = list(labels).count(-1) print(f'クラスタ数: {n_clusters}, noise: {n_noise}件') # どの県が noise か df_23['cluster'] = labels print(df_23[df_23['cluster'] == -1][['地域', 'A1101', 'A1102']]) |
「ε をいくつにすればよいか」迷ったときは、 k-distance graph(k 番目最近傍点までの距離の昇順プロット)の 肘 (elbow) を ε に取るのが定石です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from sklearn.neighbors import NearestNeighbors import matplotlib.pyplot as plt k = 4 # minPts と同じ値が定番 nn = NearestNeighbors(n_neighbors=k).fit(X_std) dist, _ = nn.kneighbors(X_std) k_dist = np.sort(dist[:, k-1]) # k番目最近傍までの距離を昇順ソート plt.figure(figsize=(8, 4)) plt.plot(k_dist) plt.xlabel('Points sorted by distance') plt.ylabel(f'{k}-NN distance') plt.title('k-distance graph (elbow = eps)') plt.grid(True, alpha=0.3) plt.show() # 急に立ち上がる点(肘)の y 座標を eps に採用する |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(9, 6)) colors = ['#1976D2', '#E64A19', '#388E3C', '#7B1FA2', '#FBC02D'] for lbl in set(labels): mask = labels == lbl color = '#333' if lbl == -1 else colors[lbl % len(colors)] name = 'noise' if lbl == -1 else f'cluster {lbl}' ax.scatter(X_std[mask, 0], X_std[mask, 1], c=color, s=90, alpha=0.85, label=name, edgecolor='white', linewidth=1.2) ax.set_xlabel('人口(標準化)') ax.set_ylabel('県内総生産(標準化)') ax.legend(loc='lower right') ax.set_title('DBSCAN: 47 都道府県の密度ベースクラスタリング') plt.tight_layout() plt.show() |
同じデータでも (ε, minPts) を変えると結果は大きく変わります。 感度を確認しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 | from itertools import product results = [] for eps, mp in product([0.3, 0.5, 0.8, 1.0], [3, 4, 5]): db = DBSCAN(eps=eps, min_samples=mp).fit(X_std) lab = db.labels_ n_cl = len(set(lab)) - (1 if -1 in lab else 0) n_no = int((lab == -1).sum()) results.append({'eps': eps, 'minPts': mp, 'クラスタ数': n_cl, 'noise数': n_no}) print(pd.DataFrame(results).pivot(index='eps', columns='minPts', values='noise数')) |
1 2 3 4 5 6 7 8 9 | from sklearn.metrics import silhouette_score # noise を除く点だけで評価する mask = labels != -1 if len(set(labels[mask])) >= 2: s = silhouette_score(X_std[mask], labels[mask]) print(f'silhouette = {s:.3f}') else: print('クラスタ数 ) |
StandardScaler や MinMaxScaler で同じスケールに揃える こと。 ε もスケール後の単位で考える。
OPTICS がある。
min_samples ≈ 2d(d は次元数)。
algorithm='ball_tree' や kd_tree を指定すると $O(n \log n)$ に近くなる。 さらに大規模なら GPU 版 cuML、 分散版 ELKI を検討。
| 手法 | 特徴 | DBSCAN との関係 |
|---|---|---|
| OPTICS | 各点の到達可能距離をプロットし、 「ε を 1 つに決めずに」階層的に密度クラスタを抽出 | 密度が不均一なデータに強い。 sklearn にあり |
| HDBSCAN | 階層的 DBSCAN。 局所的に最適な ε を自動で見つける | 近年の実務での 第一選択。 hdbscan パッケージ |
| Mean Shift | 密度の山(モード)に向かって点を移動させる | 密度ベースだが、 noise 概念は持たない |
| Gaussian Mixture Model (GMM) | 混合ガウス分布で確率的にクラスタ割り当て | パラメトリック。 球形〜楕円形クラスタに向く |
| k-means | 重心ベース、 K を事前指定 | 球形クラスタに最適。 DBSCAN とよく比較される |
| 階層的クラスタリング | 樹形図(dendrogram)で階層を可視化 | 小規模データで構造を可視化したいとき有用 |
DBSCAN を含むクラスタリング手法を体系的に学ぶには、 以下のグループ教材が役立ちます:
| 階層 | 概念 | 関係 |
|---|---|---|
| 上位 | 教師なし学習 → クラスタリング | DBSCAN はこのカテゴリの 1 手法 |
| 同列 | k-means, 階層的, GMM, Mean Shift | 同じ「クラスタリング」だが原理が異なる |
| 下位(発展) | OPTICS, HDBSCAN | DBSCAN の弱点(密度不均一性)を改良 |
| 前提 | 距離計算、 標準化、 ハイパーパラメータ | どれが欠けても結果が崩れる |
| 応用 | 外れ値検出、 異常検知、 GPS 軌跡分析 | noise ラベルをそのまま活用 |