機械学習の教科書を開くと、 こんなフレーズが必ず登場します:
これらは全て 距離・類似度(Distance and Similarity Metrics) の話。 距離は「遠ければ違う、 近ければ似ている」という当たり前の感覚を、 多次元空間で厳密に数値化する道具です。 k-NN、 k-means、 階層的クラスタリング、 推薦システム、 検索エンジン、 異常検知――現代のデータ分析で 距離を使わない手法を探す方が難しい ほどの基礎技術です。
2 次元平面に 2 つの点 A=(1, 1)、 B=(4, 5) があるとき、 「A と B の距離」は何でしょう?答えは 「どの距離関数で測るか」によって違います。
| 距離の種類 | イメージ | A=(1,1), B=(4,5) の距離 |
|---|---|---|
| ユークリッド距離 | 鳥のように直線で飛ぶ | $\sqrt{3^2 + 4^2} = 5.0$ |
| マンハッタン距離 | マンハッタンの碁盤目を歩く(縦横のみ) | $|3| + |4| = 7.0$ |
| チェビシェフ距離 | チェスのキングが進む(縦横斜め同コスト) | $\max(3, 4) = 4.0$ |
| コサイン距離 | 原点から見た「角度」だけ気にする | $1 - \cos\theta \approx 1 - 0.99 \approx 0.01$ |
マンハッタン島の街路は碁盤目で、 タクシーは縦か横にしか進めません。 (1,1) から (4,5) へ行くには、 横に 3 ブロック + 縦に 4 ブロック = 7 ブロック。 これが マンハッタン距離(L1 距離、 タクシー距離)。
鳥なら最短直線 $\sqrt{3^2+4^2}=5$ で飛べる。 これが ユークリッド距離(L2 距離)。
チェスのキングは斜めも 1 手で動けるので、 「8 マス先まで」を測るなら max(縦, 横)。 これが チェビシェフ距離(L∞ 距離)。
原点からの距離が 1 となる点の集合(単位ボール)を描くと、 距離関数の性格が一目で分かります:
コサイン距離は 「ベクトルの向き」だけ を見ます。 (1, 1) と (10, 10) は方向が完全に同じなのでコサイン距離 = 0(最大限類似)、 ユークリッド距離は大きく離れる。 テキスト分類や推薦のように「文書の長さ」より「単語の比率」が重要な場面 でコサインが活躍します。
2 つの $n$ 次元ベクトル $\mathbf{x} = (x_1, \dots, x_n)$, $\mathbf{y} = (y_1, \dots, y_n)$ について:
SSDSE-B-2026 の最新年から 5 指標を抜粋し、 都道府県間の距離を 4 種類(ユークリッド・マンハッタン・コサイン・マハラノビス)で計算してみます。
| 県 | 人口(万人) | 大学進学率(%) | 持家率(%) | 平均所得(万円) | 高齢化率(%) |
|---|---|---|---|---|---|
| 東京 | 1404 | 72.8 | 45.0 | 568 | 22.8 |
| 神奈川 | 923 | 67.5 | 59.3 | 421 | 25.7 |
| 大阪 | 880 | 64.2 | 54.7 | 373 | 27.9 |
| 秋田 | 93 | 49.8 | 78.4 | 248 | 39.1 |
| 沖縄 | 147 | 40.5 | 44.4 | 234 | 22.8 |
| 愛知 | 748 | 61.6 | 57.6 | 389 | 25.6 |
「人口(万人)」と「進学率(%)」は桁が違いすぎるので、 そのまま距離を取ると人口だけで決まってしまいます。 各指標を Z 標準化:
| 東京 vs | ユークリッド | マンハッタン | コサイン距離 | マハラノビス |
|---|---|---|---|---|
| 神奈川 | 1.42 | 2.95 | 0.16 | 2.10 |
| 大阪 | 1.83 | 3.66 | 0.39 | 2.61 |
| 愛知 | 2.18 | 4.32 | 0.45 | 2.85 |
| 沖縄 | 3.27 | 6.81 | 0.82 | 4.18 |
| 秋田 | 4.81 | 9.97 | 1.79 | 5.92 |
解釈の差異:ユークリッド・マンハッタン・マハラノビスはほぼ同じ順位を返す。 一方、 コサイン距離は「ベクトルの向き」だけを見るので、 沖縄(小規模・低所得+若年比率)と東京の差を強調する。 用途に応じて選ぶこと。
import pandas as pd
import numpy as np
from scipy.spatial.distance import pdist, squareform
from sklearn.preprocessing import StandardScaler
# データ読込
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1)
df = df[df['年度'] == df['年度'].max()].copy()
df = df.set_index('都道府県')
features = df.select_dtypes(include=[np.number]).dropna(axis=1)
# 標準化
X = StandardScaler().fit_transform(features)
# 距離計算(pdist は上三角の condensed form を返す)
d_euc = squareform(pdist(X, metric='euclidean'))
d_man = squareform(pdist(X, metric='cityblock'))
d_cos = squareform(pdist(X, metric='cosine'))
d_che = squareform(pdist(X, metric='chebyshev'))
d_mah = squareform(pdist(X, metric='mahalanobis')) # 内部で逆共分散を推定
# DataFrame 化
prefs = features.index
euc_df = pd.DataFrame(d_euc, index=prefs, columns=prefs)
print('東京と各県のユークリッド距離(昇順 Top10):')
print(euc_df['東京都'].sort_values().head(10))
from sklearn.metrics.pairwise import euclidean_distances, manhattan_distances, cosine_distances
D_euc = euclidean_distances(X)
D_man = manhattan_distances(X)
D_cos = cosine_distances(X)
# 東京(インデックス 13 番目を想定)の最近傍 5 県
i_tokyo = list(prefs).index('東京都')
nearest = np.argsort(D_euc[i_tokyo])[1:6] # 自分自身を除く
print('東京の最近傍:', [prefs[j] for j in nearest])
farthest = np.argsort(D_euc[i_tokyo])[-5:]
print('東京の最遠県:', [prefs[j] for j in farthest])
from scipy.spatial.distance import mahalanobis
# 共分散と逆行列
cov = np.cov(X.T)
inv_cov = np.linalg.pinv(cov) # 特異性対策で擬似逆行列
# 47×47 のマハラノビス距離行列
n = X.shape[0]
D_mah = np.zeros((n, n))
for i in range(n):
for j in range(n):
D_mah[i, j] = mahalanobis(X[i], X[j], inv_cov)
mah_df = pd.DataFrame(D_mah, index=prefs, columns=prefs)
print('東京と各県のマハラノビス距離(昇順):')
print(mah_df['東京都'].sort_values().head(10))
# 数値をビン分割してカテゴリ化、 ハミング距離で類似度を測る
from sklearn.preprocessing import KBinsDiscretizer
discretizer = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='quantile')
X_cat = discretizer.fit_transform(features).astype(int)
D_ham = squareform(pdist(X_cat, metric='hamming'))
ham_df = pd.DataFrame(D_ham, index=prefs, columns=prefs)
print('東京とのハミング距離(カテゴリ化後):')
print(ham_df['東京都'].sort_values().head(10))
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
fig, axes = plt.subplots(1, 2, figsize=(20, 9))
sns.heatmap(euc_df, ax=axes[0], cmap='viridis', xticklabels=False)
axes[0].set_title('ユークリッド距離行列(47×47)')
sns.heatmap(d_cos_df := pd.DataFrame(D_cos, index=prefs, columns=prefs),
ax=axes[1], cmap='magma', xticklabels=False)
axes[1].set_title('コサイン距離行列')
plt.tight_layout()
plt.show()
| カテゴリ | 代表的距離 | 主な用途 |
|---|---|---|
| 連続値・幾何 | ユークリッド、 マンハッタン、 ミンコフスキー、 チェビシェフ | k-NN, k-means, 階層的クラスタリング |
| 角度ベース | コサイン、 角距離 | テキスト、 推薦、 NLP 埋め込み |
| 統計的 | マハラノビス、 KL ダイバージェンス、 JS ダイバージェンス、 Wasserstein | 異常検知、 分布比較、 生成モデル |
| カテゴリ・離散 | ハミング、 ジャッカード、 Dice 係数、 Sørensen | 文字列、 集合、 DNA、 タグセット |
| 編集ベース | レーベンシュタイン、 Damerau-Levenshtein、 Jaro-Winkler | スペルチェック、 重複検出 |
| グラフ | 最短経路距離、 共通隣接数、 SimRank、 Personalized PageRank | SNS、 ナレッジグラフ |
| 時系列 | DTW(Dynamic Time Warping)、 Fréchet 距離 | 音声、 株価、 センサーデータ |
| 学習型 | Siamese ネット、 Triplet loss、 Contrastive Learning | 顔認証、 画像検索、 自己教師あり学習 |
「結局どれを使えばいいの?」に答える実用ガイド:
| 状況 | 推奨距離 | 理由 |
|---|---|---|
| 連続値、 標準化済、 低次元(≤20) | ユークリッド | 標準的、 直感的、 多くのアルゴリズムが前提 |
| 連続値、 高次元(>50) | マンハッタン or コサイン | 高次元の呪いに比較的強い |
| 外れ値が多い | マンハッタン | 絶対値なので外れ値の影響が穏やか |
| テキスト・ベクトル埋め込み | コサイン | 文書長を無視して内容の方向を比較 |
| 多変量で相関がある | マハラノビス | 共分散構造を考慮した楕円距離 |
| カテゴリ・二値データ | ハミング or ジャッカード | 離散値専用、 ユークリッドは意味なし |
| 文字列・配列 | レーベンシュタイン | 編集操作数で違いを測る |
| 時系列・波形 | DTW | 時間軸の伸縮を吸収 |
| 確率分布の比較 | KL or Wasserstein | 分布の「重なり度合い」「移動コスト」 |
「距離」と呼ぶには 4 つの公理 を満たす必要があります(距離関数 $d: X \times X \to \mathbb{R}_{\ge 0}$):
| 公理 | 式 | 意味 |
|---|---|---|
| 非負性 | $d(x, y) \ge 0$ | 距離はマイナスにならない |
| 同一性 | $d(x, y) = 0 \iff x = y$ | 同じ点同士の距離は 0、 0 なら同じ点 |
| 対称性 | $d(x, y) = d(y, x)$ | どちらから測っても同じ |
| 三角不等式 | $d(x, z) \le d(x, y) + d(y, z)$ | 「寄り道は遠い」 |
数学的には、 距離空間(metric space)の特殊例として:
古典的距離(ユークリッド等)は 「全次元を平等に扱う」 ため、 タスクに関係ない次元のノイズに弱い。 たとえば顔認証で 背景の色 は顔の同一性と無関係なのに、 そのまま距離計算すると誤判定の原因になる。 距離学習はこれを克服します。
| 手法 | 仕組み | 応用 |
|---|---|---|
| LMNN(Large Margin Nearest Neighbor) | 同クラス点を近く、 異クラス点を遠くするマハラノビス距離を最適化 | k-NN 改良 |
| NCA(Neighborhood Components Analysis) | 確率的近傍分類の精度を最大化 | 次元削減 + 距離学習 |
| Siamese Network | 2 入力を共有 NN に通し、 埋め込み空間でコサイン or ユークリッド距離 | 顔認証、 画像検索 |
| Triplet Loss | (anchor, positive, negative) の 3 つ組で正例を近く、 負例を遠くする | FaceNet(Google の顔認証) |
| Contrastive Loss | 同ペアは近く、 異ペアはマージン以上離す | SimCLR、 MoCo(自己教師あり学習) |
距離学習は 深層学習時代の標準的アプローチです。 「データから良い類似度関数を学ぶ」発想が、 古典的距離関数の選択問題を一段抽象化しました。
これまでの議論を踏まえ、 SSDSE-B-2026 から得られる 47×47 距離行列 を 4 種類すべてで計算し、 結果を比較する End-to-End パイプラインを示します。
import pandas as pd
import numpy as np
from scipy.spatial.distance import pdist, squareform
from sklearn.preprocessing import StandardScaler, RobustScaler
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
# === 1. データ取得・前処理 ===
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1)
df = df[df['年度'] == df['年度'].max()].copy()
df = df.set_index('都道府県')
features = df.select_dtypes(include=[np.number]).dropna(axis=1)
print(f'データ形状: {features.shape}') # (47, ~100)
# === 2. 通常標準化 + ロバスト標準化 ===
X_std = StandardScaler().fit_transform(features)
X_rob = RobustScaler().fit_transform(features) # 中央値・IQR ベース
# === 3. 4 種類の距離行列 ===
prefs = list(features.index)
metrics = ['euclidean', 'cityblock', 'cosine', 'chebyshev']
distance_dfs = {}
for m in metrics:
D = squareform(pdist(X_std, metric=m))
distance_dfs[m] = pd.DataFrame(D, index=prefs, columns=prefs)
# === 4. 東京の Top 10 近傍を比較 ===
print('\\n=== 東京都の最近傍 Top 10(4 つの距離関数で比較)===')
for m, df_d in distance_dfs.items():
top10 = df_d['東京都'].sort_values().head(11).iloc[1:] # 自分自身を除く
print(f'\\n[{m}]')
print(top10.round(3))
# === 5. マハラノビス距離(共分散から)===
cov = np.cov(X_std.T)
inv_cov = np.linalg.pinv(cov)
D_mah = np.zeros((len(prefs), len(prefs)))
for i in range(len(prefs)):
diff = X_std - X_std[i]
D_mah[i] = np.sqrt(np.sum((diff @ inv_cov) * diff, axis=1))
mah_df = pd.DataFrame(D_mah, index=prefs, columns=prefs)
print('\\n[mahalanobis]')
print(mah_df['東京都'].sort_values().head(11).iloc[1:].round(3))
# === 6. 距離行列の相関を見る(手法選択の参考)===
print('\\n=== 4 距離関数の上三角ベクトル間の相関 ===')
def upper_tri(D):
return D.values[np.triu_indices_from(D.values, k=1)]
methods = list(distance_dfs.keys()) + ['mahalanobis']
vecs = [upper_tri(distance_dfs[m]) for m in methods[:-1]] + [upper_tri(mah_df)]
corr = pd.DataFrame(np.corrcoef(vecs), index=methods, columns=methods)
print(corr.round(3))
# === 7. ヒートマップ可視化 ===
fig, axes = plt.subplots(2, 3, figsize=(22, 14))
for ax, m in zip(axes.ravel(), metrics + ['mahalanobis']):
df_d = mah_df if m == 'mahalanobis' else distance_dfs[m]
sns.heatmap(df_d, ax=ax, cmap='viridis', xticklabels=False, yticklabels=False)
ax.set_title(f'{m} 距離行列')
axes[1, 2].axis('off')
plt.tight_layout()
plt.savefig('distance_matrices.png', dpi=120)
plt.show()
| 距離関数 | 東京の Top 5 最近傍 | 解釈 |
|---|---|---|
| ユークリッド | 神奈川, 大阪, 愛知, 千葉, 埼玉 | 大都市プロファイル。 量と質を総合 |
| マンハッタン | 神奈川, 大阪, 愛知, 千葉, 兵庫 | 外れ値の影響が少ない順位 |
| コサイン | 大阪, 京都, 福岡, 神奈川, 愛知 | 「都市型ライフスタイル」の方向性で選定 |
| マハラノビス | 神奈川, 千葉, 埼玉, 大阪, 愛知 | 共分散考慮で「首都圏」をより強く識別 |
| Euc | Man | Cos | Cheb | Mah | |
|---|---|---|---|---|---|
| Euc | 1.00 | 0.97 | 0.78 | 0.92 | 0.81 |
| Man | 0.97 | 1.00 | 0.79 | 0.86 | 0.83 |
| Cos | 0.78 | 0.79 | 1.00 | 0.72 | 0.65 |
| Cheb | 0.92 | 0.86 | 0.72 | 1.00 | 0.76 |
| Mah | 0.81 | 0.83 | 0.65 | 0.76 | 1.00 |
解釈:ユークリッドとマンハッタンは r ≈ 0.97 とほぼ同等。 コサインは他の距離と r ≈ 0.7〜0.8 でやや異なる順位を返す。 マハラノビスは独自性が最も強く、 共分散構造の取り込みが効いている。
2 つの確率分布 $P, Q$ の「違い」を測る指標は機械学習で頻出します(生成モデル、 ベイズ、 強化学習)。
| 場面 | 推奨 |
|---|---|
| 変分推論、 VAE の目的関数 | KL ダイバージェンス |
| GAN(古典) | JS ダイバージェンス |
| WGAN、 分布輸送最適化 | Wasserstein 距離 |
| 確率の最大ズレ評価 | Total Variation |
| 真の距離公理が必要 | Hellinger 距離 |
| マルコフ連鎖の収束証明 | Total Variation または KL |
文字列 $s_1, s_2$ について、 一方を他方に変換するのに必要な 挿入・削除・置換の最小回数:
応用:スペルチェック、 文字列検索、 DNA 配列アライメント、 OCR 誤り訂正、 重複検出(fuzzy matching)。
レーベンシュタインの改良版。 「先頭が一致するほどボーナス」 という人名・住所マッチングに特化した重み付け:
jellyfish.jaro_winkler_similarity("Smith", "Smithe")2 ノード間の エッジ数(重みなし)または重み和(重み付き)。 Dijkstra 法、 BFS、 Floyd-Warshall で計算:
応用:SNS の「友達の友達」、 地図ナビ、 知識グラフ。
2 つの時系列 $x = (x_1, \dots, x_n)$, $y = (y_1, \dots, y_m)$ の「形の類似度」。 時間軸の伸縮を吸収しつつ最小コスト整合を見つける:
DTW[i,j] = |x_i - y_j| + min(DTW[i-1,j], DTW[i,j-1], DTW[i-1,j-1])
応用:音声認識(話速差を吸収)、 ジェスチャー認識、 株価パターン分析、 心電図比較。 標準ライブラリ tslearn で実装可能。
「高次元では距離が意味を失う」と教科書は言う。 これは本当か?SSDSE データを使って実証してみます。
import pandas as pd
import numpy as np
from scipy.spatial.distance import pdist, squareform
from sklearn.preprocessing import StandardScaler
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1)
df = df[df['年度'] == df['年度'].max()].copy()
df = df.set_index('都道府県')
features_all = df.select_dtypes(include=[np.number]).dropna(axis=1)
dims = [2, 5, 10, 20, 50, 100, features_all.shape[1]]
results = []
for d in dims:
if d > features_all.shape[1]:
continue
cols = features_all.columns[:d]
X = StandardScaler().fit_transform(features_all[cols])
D = squareform(pdist(X, metric='euclidean'))
# 「東京」を基準点に
i0 = list(features_all.index).index('東京都')
distances = np.delete(D[i0], i0)
contrast = (distances.max() - distances.min()) / distances.min()
print(f'次元 d={d}: max/min = {distances.max():.2f}/{distances.min():.2f}, '
f'contrast = {contrast:.2f}')
results.append((d, contrast))
| 次元 d | min 距離 | max 距離 | contrast |
|---|---|---|---|
| 2 | 0.3 | 4.5 | 14.0 |
| 5 | 0.9 | 5.8 | 5.4 |
| 10 | 1.5 | 7.2 | 3.8 |
| 20 | 2.5 | 9.1 | 2.6 |
| 50 | 4.2 | 11.5 | 1.7 |
| 100 | 6.1 | 13.8 | 1.3 |
観察:低次元では contrast = 14 と「最近傍は明らかに最遠点より近い」。 高次元になると contrast が 1.3 まで小さくなり、 「全ての点がほぼ等距離」 という呪いが顕在化します。 47 都道府県 × 数百指標の SSDSE データでこの現象が観察できる点がポイント。
実務でよく出会う場面ごとに「結局どれを使うか」を整理:
| ドメイン | 具体的タスク | 定番距離 |
|---|---|---|
| テキスト | 文書類似度、 検索、 重複検出 | コサイン類似度(TF-IDF, BERT embedding)/Jaccard(n-gram) |
| 画像 | 画像検索、 顔認証 | CNN 埋め込みのコサイン/L2/Siamese 学習距離 |
| 音声 | 音声認識、 話者識別 | DTW(時系列伸縮)/MFCC 埋め込みのコサイン |
| 推薦 | ユーザー類似、 アイテム類似 | コサイン類似度/Pearson 相関/Jaccard(暗黙的) |
| 異常検知 | 不正取引、 故障予知 | マハラノビス距離/LOF(Local Outlier Factor)/Isolation Forest 距離 |
| クラスタリング | 顧客セグメント、 地理データ | ユークリッド(標準化済)/マンハッタン(外れ値多)/DTW(時系列) |
| SNS / グラフ | 友達推薦、 コミュニティ検出 | 最短経路/共通隣接数/Personalized PageRank |
| 遺伝学 | DNA 配列比較、 系統樹 | レーベンシュタイン/Needleman-Wunsch/Smith-Waterman |
| 地理 | ルート検索、 配送最適化 | 球面(Haversine)距離/道路網最短経路 |
| 金融 | 株価類似、 ポートフォリオ | 相関係数距離 $\sqrt{2(1-\rho)}$/DTW(時系列) |
緯度・経度の 2 点間距離を地球の曲率を考慮して計算:
2 つの時系列の相関 $\rho$ から距離を作る伝統的方法:
「距離」を測ることは人類の最も古い数学的営みの 1 つ。 その歴史をたどると、 抽象化と一般化の連続です。
| 時期 | 研究・人物 | 意義 |
|---|---|---|
| 紀元前 300 年 | ユークリッド『原論』 | 2 点間最短経路としての「直線距離」を公理化 |
| 17 世紀 | デカルト座標系 | 「距離」を代数的に表現可能に |
| 19 世紀 | 非ユークリッド幾何学(リーマン、 ロバチェフスキー) | 「直線」も「距離」も場によって異なる多様体 |
| 1906 | フレシェ — 抽象距離空間 | 距離公理を満たす任意の集合へ一般化 |
| 1930s | マハラノビス — 統計的距離 | 分布の形を考慮した距離(人類学者集団の比較) |
| 1950 年代 | ハミング — 情報理論の距離 | 誤り訂正符号、 ビット単位距離 |
| 1965 | レーベンシュタイン — 編集距離 | 文字列の違いを編集操作回数で測定 |
| 1970s | 多次元尺度法(MDS)の発展 | 距離行列から座標を復元する手法群 |
| 1990s | カーネル法、 SVM | 「高次元空間での内積 = カーネル」で距離を一般化 |
| 2000s | 距離学習(LMNN、 NCA) | データから「タスクに合った距離」を学習 |
| 2010s | Word2Vec、 BERT 埋め込み | 「意味の距離」を埋め込みで表現 |
| 2020s | 対照学習、 自己教師あり距離学習 | SimCLR、 CLIP、 MoCo — 大規模埋め込み |
現代の機械学習における「距離」は、 もはや幾何学的な意味を超えて、 「ある問いに対するデータ点の類似性を表す抽象的関数」 として捉えられています。 ニューラルネットワークの埋め込み空間でのコサイン類似度は、 「物理的距離」ではなく「意味的近さ」を表現する道具となっています。
距離行列 $D \in \mathbb{R}^{n \times n}$ だけが与えられているとき、 $n$ 個の点を $k$ 次元空間に「距離関係を保つように」配置する方法が 多次元尺度法(Multidimensional Scaling, MDS)。 距離の逆問題と言えます。
古典的 MDS は 距離が正確にユークリッド距離由来なら、 元の座標を正確に復元します。 これは「PCA を距離行列の世界に持ち込んだ」と言っても良い手法で、 PCA と数学的に等価です。
距離の 大小関係(順位)だけ 保存する版。 距離の絶対値が信頼できない場合(アンケート、 心理実験)に有効。 ストレス関数を最小化する反復解法。
import pandas as pd
import numpy as np
from sklearn.manifold import MDS
from sklearn.preprocessing import StandardScaler
from scipy.spatial.distance import squareform, pdist
import matplotlib.pyplot as plt
import japanize_matplotlib
# データ読込
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1)
df = df[df['年度'] == df['年度'].max()].copy()
df = df.set_index('都道府県')
features = df.select_dtypes(include=[np.number]).dropna(axis=1)
X = StandardScaler().fit_transform(features)
# 古典的 MDS(距離行列 → 2 次元座標)
D = squareform(pdist(X, metric='euclidean'))
mds = MDS(n_components=2, dissimilarity='precomputed', random_state=0)
coords_2d = mds.fit_transform(D)
# 可視化:47 県を 2 次元平面に配置
plt.figure(figsize=(14, 10))
plt.scatter(coords_2d[:, 0], coords_2d[:, 1], s=200, alpha=0.7)
for i, name in enumerate(features.index):
plt.annotate(name, (coords_2d[i, 0], coords_2d[i, 1]), fontsize=10)
plt.title('多次元尺度法による 47 都道府県の 2D 配置')
plt.xlabel('MDS 第 1 軸')
plt.ylabel('MDS 第 2 軸')
plt.grid(True)
plt.tight_layout()
plt.savefig('mds_prefectures.png', dpi=120)
plt.show()
マハラノビス距離の最も典型的な応用が 多変量データの外れ値検出。 「単一指標では普通」だが、 「変数の組み合わせで異常」を発見できます。
データ $X \in \mathbb{R}^{n \times d}$ の平均 $\mu$ と共分散 $\Sigma$ を推定し、 各点のマハラノビス距離:
from scipy.stats import chi2
import numpy as np
# 主要 5 指標を抜粋
cols = ['人口', '大学進学率', '持家率', '平均所得', '高齢化率']
Y = features[cols].values
mu = Y.mean(axis=0)
S = np.cov(Y.T)
S_inv = np.linalg.pinv(S)
# 各県のマハラノビス距離(の二乗)
d2 = np.array([(y - mu) @ S_inv @ (y - mu) for y in Y])
# カイ二乗分布で p 値(自由度 = 5)
p_values = 1 - chi2.cdf(d2, df=5)
result = pd.DataFrame({
'都道府県': features.index,
'mahalanobis_d2': d2,
'p_value': p_values
}).sort_values('p_value')
# p < 0.05 なら統計的に「外れ値」と判定
print('多変量外れ値候補 (p < 0.05):')
print(result[result['p_value'] < 0.05])
| 都道府県 | マハラノビス距離² | p 値 | 解釈 |
|---|---|---|---|
| 東京都 | 15.2 | 0.009 | 人口・所得・進学率が極端 → 外れ値 |
| 沖縄県 | 13.8 | 0.017 | 所得低 × 高齢化率低 という珍しい組み合わせ |
| 秋田県 | 11.4 | 0.043 | 高齢化率最高、 過疎が極端 |
| 愛知県 | 5.1 | 0.402 | 大都市圏内では「典型的」 |
注意:マハラノビス距離は 自分自身を含むデータの共分散から計算するため、 外れ値が共分散推定を歪める「マスキング効果」が問題になることがあります。 対策:Minimum Covariance Determinant (MCD) などロバスト共分散推定を使う。
これまでの議論を踏まえ、 実務で「結局どの距離を使うか」を決めるための徹底ガイドを提供します。 3 つの質問に答えれば、 ほぼ自動的に適切な距離が見つかります。
| データ種類 | 第一選択 | 第二選択 |
|---|---|---|
| 連続値(標準化済) | ユークリッド距離 | マハラノビス距離 |
| 連続値(外れ値あり) | マンハッタン距離 | ロバスト Z + ユークリッド |
| テキスト(TF-IDF や埋め込み) | コサイン類似度 | Pearson 相関 |
| カテゴリ(One-hot エンコード) | ハミング距離 | Jaccard 係数 |
| 集合(タグ、 単語) | Jaccard 係数 | Dice 係数 |
| 文字列・配列 | レーベンシュタイン距離 | Jaro-Winkler |
| 時系列・波形 | DTW | Pearson 相関 + シフト |
| 確率分布 | Wasserstein 距離 | KL ダイバージェンス |
| グラフ・ネットワーク | 最短経路距離 | PageRank 類似度 |
| 地理座標(緯度経度) | Haversine 距離 | 道路網最短経路 |
| 画像(CNN 埋め込み) | コサイン類似度 | L2 距離(ユークリッド) |
| 音声(MFCC) | DTW | コサイン類似度 |
| 症状 | 原因 | 対処 |
|---|---|---|
| 距離が「ある変数」だけで決まる | 標準化漏れ | StandardScaler() 必須 |
| 「全てが等距離」になる | 高次元の呪い | PCA で次元削減、 マンハッタンに変更 |
| 外れ値で結果が激変 | L2 距離が外れ値に弱い | L1 距離 or ロバスト標準化 |
| マハラノビスで NaN や ∞ | 共分散行列が特異 | 擬似逆行列、 Ledoit-Wolf shrinkage |
| 「大きいベクトル」が「常に似ている」 | コサインを使っていない | 方向ベースに切り替え |
| 意味的に近い文書が遠く判定 | 単語の表記揺れ | 埋め込み(BERT)+ コサイン |
$n$ 個のデータ点から作る $n \times n$ 距離行列は、 単にクラスタリングの入力以上の 「データ構造の宝庫」 です。 様々な可視化手法で、 データの構造的特徴が浮き彫りになります。
距離行列を色付きの行列として描画すると、 自然なグループ構造が浮かび上がります。 行・列を クラスタリングで並べ替え(seriation)すると、 対角線付近に「ブロック」が見え、 これがクラスタの存在を視覚的に示します:
from scipy.cluster.hierarchy import linkage, leaves_list from scipy.spatial.distance import squareform # 距離行列を上三角に変換 → 階層的クラスタリング condensed = squareform(D, checks=False) Z = linkage(condensed, method='average') order = leaves_list(Z) # 並べ替えてヒートマップ D_sorted = D[order][:, order] sns.heatmap(D_sorted, cmap='viridis', xticklabels=[prefs[i] for i in order])
距離行列から階層的クラスタリングをすると、 樹形図でデータの階層構造が一目瞭然。 「どの県とどの県が最も近いか」「いつ大グループに統合されるか」が見えます。
距離が閾値以下のペアだけをエッジとして描き、 ノードを力学的レイアウト(Fruchterman-Reingold 等)で配置すると、 「データの島(密な部分グラフ)」と「橋(島を繋ぐエッジ)」が見えます。 SNS 分析、 共起ネットワークの定番手法。
「東京から見た各県の距離順位」のような 個別データ点を中心とした近傍構造を可視化。 推薦結果の説明、 異常検知の根拠提示に有効。
「距離」と「内積」は表裏一体。 ベクトルの内積 $\langle x, y\rangle$ が大きければ、 距離 $\|x - y\|$ は小さい:
$\phi: X \to \mathcal{H}$ を非線形写像、 $K(x, y) = \langle \phi(x), \phi(y)\rangle_{\mathcal{H}}$ をカーネル関数と呼ぶ。 重要なのは $\phi$ を明示せずに $K$ だけ計算できること(カーネルトリック)。
| カーネル | 定義 | 用途 |
|---|---|---|
| 線形 | $K(x, y) = x^{\top}y$ | 通常の内積。 高次元線形分離可能データ |
| 多項式 | $K(x, y) = (x^{\top}y + c)^d$ | d 次の相互作用を捉える |
| RBF(Gaussian) | $K(x, y) = \exp(-\gamma\|x - y\|^2)$ | 最も汎用、 局所的類似度 |
| Laplacian | $K(x, y) = \exp(-\gamma\|x - y\|_1)$ | L1 距離ベースの RBF |
| シグモイド | $K(x, y) = \tanh(\alpha x^{\top}y + c)$ | ニューラルネット風 |
| 文字列カーネル | 部分文字列の出現頻度内積 | テキスト、 DNA 配列 |
| グラフカーネル | 共有部分構造の頻度 | 分子構造、 SNS |
距離・類似度は 「線形代数」「クラスタリング」「機械学習基礎」 のグループに属します。