このページで扱うキーワード(クリックで該当節へジャンプ):
scipy.stats.gaussian_kde または seaborn.kdeplot がワンライナー論文の図や報告書で、こんな曲線を見たはずです:
この「なめらかな曲線」が カーネル密度推定(KDE) の出力です。ヒストグラムは「ビン幅・端点」で見た目が変わる一方、KDE は各点に小さな山を重ねることで「ビンの恣意性」を回避し、分布の形を 1 本の連続曲線で表します。観察データの分布形(単峰/双峰、歪み、外れ値)を読み取る基本ツールです。
47 都道府県の 総人口 $x_1, x_2, \dots, x_{47}$ が手元にあるとします。各 $x_i$ の場所に、幅 $h$ の小さな鐘形の山(カーネル)を置きましょう。
カーネルの形は「正規分布(ガウス)」が一番よく使われます。すると:
点が密集する場所では山が重なって高くなり、まばらな場所では低い丘になります。つまり「データ点が密な場所ほど曲線が高い」 = 密度推定そのもの。
| 項目 | ヒストグラム | KDE |
|---|---|---|
| 形状 | 階段状の棒 | 連続したなめらかな曲線 |
| 調整パラメータ | ビン幅・ビン端点 | バンド幅 $h$・カーネル種類 |
| 端点依存性 | あり(同じ幅でも端点を 1 万人ずらすと形が変わる) | なし |
| 外れ値の表現 | その箱が「孤立した棒」になる | その点の周りに小さなコブ |
| 双峰分布の検出 | ビン幅次第で見落としやすい | $h$ が適切なら検出しやすい |
| 境界(0 以上の量など) | 自然に止まる | 負の領域にしみ出す(境界補正が必要) |
$n$ 個の観測値 $x_1, x_2, \dots, x_n$ に対して、点 $x$ における密度推定値は:
カーネル $K$ は通常、次を満たします:
歪んだ分布や外れ値に頑健な改良版:
つまり KDE は「データ点ごとに小さな確率の山を置き、それらを足して大きな密度関数を作る」 という、極めて直感的な操作です。
SSDSE-B-2026 の A1101(総人口)を 47 都道府県ぶん読み込み、人口分布の KDE を求めます。
| 統計量 | 値 |
|---|---|
| サンプル数 $n$ | 47(都道府県) |
| 最小値(鳥取) | 約 55 万人 |
| 中央値 | 約 175 万人 |
| 最大値(東京) | 約 1,400 万人 |
| 平均 | 約 268 万人 |
| 標準偏差 $\hat{\sigma}$ | 約 274 万人 |
| IQR | 約 152 万人 |
本物の 47 県データでこれを行えば、東京・大阪・神奈川のような大人口県はピーク右側に薄い裾を作り、地方県の密集が左側に高いピーク(約 100〜200 万人付近)を作ります。
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 | # SSDSE-B 都道府県人口の KDE import pandas as pd import numpy as np import matplotlib.pyplot as plt from scipy.stats import gaussian_kde # 1) データ読込(2 行目までヘッダ、1 行スキップ) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1) pop = df['A1101'].values # 総人口 # 2) KDE オブジェクトを作る(バンド幅自動) kde = gaussian_kde(pop) # Scott のルール(デフォルト) kde_silver = gaussian_kde(pop, bw_method='silverman') # 3) 評価点を作って密度を計算 xs = np.linspace(pop.min(), pop.max(), 500) ys = kde(xs) ys_silver = kde_silver(xs) # 4) ヒストグラムと重ねて可視化 fig, ax = plt.subplots(figsize=(9, 5)) ax.hist(pop, bins=15, density=True, alpha=0.4, color='steelblue', label='Histogram') ax.plot(xs, ys, color='crimson', lw=2, label='KDE (Scott)') ax.plot(xs, ys_silver, color='darkgreen', lw=2, linestyle='--', label='KDE (Silverman)') ax.set_xlabel('Population'); ax.set_ylabel('Density') ax.legend(); plt.tight_layout(); plt.show() |
1 2 3 4 5 6 7 8 9 10 11 | import seaborn as sns import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1) # ヒストグラム + KDE を一気に sns.histplot(df['A1101'], kde=True, bins=15) # KDE 単独・バンド幅指定 sns.kdeplot(df['A1101'], bw_adjust=0.5) # 半分にして細かく sns.kdeplot(df['A1101'], bw_adjust=2.0) # 2倍にして滑らかに |
1 2 3 4 5 6 7 8 | # h を変えると何が起きるか可視化 import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 4, figsize=(16, 4), sharey=True) for ax, bw in zip(axes, [0.1, 0.3, 1.0, 3.0]): kde = gaussian_kde(pop, bw_method=bw) ax.plot(xs, kde(xs)) ax.set_title(f'bw_method={bw}') plt.show() |
1 2 3 4 | # 2 変数の同時密度(等高線) sns.kdeplot(data=df, x='A1101', y='A1303', fill=True, cmap='mako') plt.xlabel('Population'); plt.ylabel('Elderly ratio (%)') plt.show() |
statsmodels の cut=0 オプション、 (c) 反射法・境界補正カーネル。
KDE の精度評価は 積分二乗誤差 (Integrated Squared Error, ISE) またはその期待値 MISE (Mean ISE) で測ります:
MISE はバイアスと分散に分解できます:
これを微分して 0 にすると、 最適バンド幅 $h^*$:
| $h$ の大きさ | バイアス | 分散 | 結果 |
|---|---|---|---|
| 小さい ($h \to 0$) | 小(真の形に追従) | 大(毎回違う形) | ガタガタ・過適合 |
| 中庸 | 中 | 中 | ちょうど良い |
| 大きい ($h \to \infty$) | 大(平らに) | 小(安定) | つぶれて単峰 |
密度が高い領域では $h$ を狭く、 低い領域では広くすることで、 裾の表現を向上:
人口や所得のような右に長い裾を持つ変数は、 まず $\log$ 変換してから KDE するのが標準。 結果を元のスケールに戻すと境界バイアスが回避できる。
区間 $[0, \infty)$ の量に対して、 ベータカーネル・ガンマカーネルなど「サポートが正しい範囲のカーネル」を使う。 例:ガンマカーネル $K_b(x; t) \propto x^{t/b}\,e^{-x/b}$。
各観測 $x_i$ に重み $w_i$ を付ける:
$d$ 次元では各次元にバンド幅 $h_1, \dots, h_d$ を持つ:
ただし「次元の呪い」が強く、 $d \ge 3$ では実用的にきつい。 ふつう $d = 1$ または 2 までで使う。
本サイトの再現論文集の中で、 KDE が活用される典型シーン:
KDE の素朴な実装は、 $m$ 評価点 × $n$ 観測点で $O(nm)$。 $n=10^6$、 $m=10^3$ なら $10^9$ 演算で数分かかる。
| 手法 | 計算量 | 仕組み |
|---|---|---|
| 素朴な実装 | $O(nm)$ | すべてのペアでカーネル評価 |
| ビニング近似 | $O(n + m)$ | データを格子に集計してから FFT |
| FFT ベース | $O((n+m)\log(n+m))$ | 畳み込みを高速フーリエ変換で |
| Tree ベース (Ball/KD-tree) | $O(n \log n)$ | 遠い点はまとめて近似 |
Python では scipy.stats.gaussian_kde は素朴実装、 KDEpy ライブラリは FFT ベースで圧倒的に速い。
1 2 3 4 5 6 7 8 9 10 | # 大規模データでの高速 KDE from KDEpy import FFTKDE import numpy as np x = pop # 47 県の人口 estimator = FFTKDE(kernel='gaussian', bw='ISJ') # Improved Sheather-Jones xs, ys = estimator.fit(x).evaluate(1024) import matplotlib.pyplot as plt plt.plot(xs, ys); plt.show() |
「47 都道府県の人口分布を表現する」という同じ課題を 3 方式で解いてみます。
| 方式 | 仮定 | 長所 | 短所 | 適する場面 |
|---|---|---|---|---|
| ヒストグラム | ビン幅・端点を選ぶだけ | 計算が単純・誰にも分かる | 階段状で粗い/ビン依存 | 初歩的な探索・大量データ |
| KDE | カーネル・バンド幅を選ぶ | なめらか・端点非依存 | バンド幅選択・境界バイアス | 論文の可視化・密度の数値計算 |
| パラメトリックフィット(例:対数正規) | 分布族を指定(平均・分散) | 少数パラメータで簡潔 | 分布族が間違うと壊滅 | 理論的にその分布が予想される時 |
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 28 29 30 | import pandas as pd import numpy as np import matplotlib.pyplot as plt from scipy.stats import gaussian_kde, lognorm df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1) pop = df['A1101'].values fig, axes = plt.subplots(1, 3, figsize=(15, 4), sharey=True) # (1) ヒストグラム axes[0].hist(pop, bins=15, density=True, color='steelblue', edgecolor='white') axes[0].set_title('Histogram (bins=15)') # (2) KDE kde = gaussian_kde(pop) xs = np.linspace(pop.min(), pop.max(), 500) axes[1].plot(xs, kde(xs), 'crimson', lw=2) axes[1].fill_between(xs, kde(xs), alpha=0.3, color='crimson') axes[1].set_title('KDE (Gaussian)') # (3) 対数正規あてはめ shape, loc, scale = lognorm.fit(pop, floc=0) axes[2].plot(xs, lognorm.pdf(xs, shape, loc, scale), 'darkgreen', lw=2) axes[2].set_title(f'Lognormal fit (μ={np.log(scale):.2f}, σ={shape:.2f})') for ax in axes: ax.set_xlabel('Population') axes[0].set_ylabel('Density') plt.tight_layout(); plt.show() |
「47 都道府県の県内総生産(B4101)と総人口(A1101)の分布を理解する」を例に、 KDE を使うフル工程を示します。
1 2 3 4 5 6 7 8 9 | import pandas as pd import numpy as np import matplotlib.pyplot as plt from scipy.stats import gaussian_kde df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1) print(df['A1101'].describe()) print("歪度(Skewness):", df['A1101'].skew()) print("尖度(Kurtosis):", df['A1101'].kurtosis()) |
1 2 3 4 5 6 7 8 9 10 11 12 13 | pop = df['A1101'].values kde = gaussian_kde(pop) xs = np.linspace(pop.min() - 0.1*pop.std(), pop.max() + 0.1*pop.std(), 500) ys = kde(xs) fig, ax = plt.subplots(figsize=(10, 5)) ax.hist(pop, bins=15, density=True, alpha=0.4, edgecolor='white') ax.plot(xs, ys, 'r-', lw=2) ax.set_xlabel('Total Population (persons)') ax.set_ylabel('Density') ax.set_title('KDE of Prefectural Population (SSDSE-B 2026)') plt.tight_layout(); plt.show() |
人口は強い右裾を持つので、 そのまま KDE するとガウスカーネルが「負の人口」にしみ出します。 対数変換すると分布がほぼ正規に近づき、 KDE がきれいに収まります。
1 2 3 4 5 6 7 8 9 | log_pop = np.log10(pop) kde_log = gaussian_kde(log_pop) xs_log = np.linspace(log_pop.min(), log_pop.max(), 500) ys_log = kde_log(xs_log) fig, axes = plt.subplots(1, 2, figsize=(14, 4)) axes[0].plot(xs, ys); axes[0].set_title('Original scale') axes[1].plot(xs_log, ys_log); axes[1].set_title('Log10 scale (much smoother)') plt.show() |
1 2 3 4 5 6 7 8 9 10 11 | # 東日本=Code 1〜23、 西日本=24〜47 として比較 east = df[df['Code'] 23]['A1101'].values west = df[df['Code'] > 23]['A1101'].values kde_east = gaussian_kde(np.log10(east)) kde_west = gaussian_kde(np.log10(west)) xs = np.linspace(5.5, 7.5, 500) plt.fill_between(xs, kde_east(xs), alpha=0.5, label='East Japan') plt.fill_between(xs, kde_west(xs), alpha=0.5, label='West Japan') plt.xlabel('log10(Population)'); plt.legend(); plt.show() |
1 2 3 4 5 6 7 8 9 10 | import seaborn as sns df_log = pd.DataFrame({ 'log_pop': np.log10(df['A1101']), 'log_gdp': np.log10(df['B4101']) }) sns.jointplot(data=df_log, x='log_pop', y='log_gdp', kind='kde', fill=True, cmap='viridis') plt.show() |
2 次元 KDE は人口と GDP がほぼ直線関係(log-log で)にあることを、 等高線の傾きで示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from sklearn.neighbors import KernelDensity from sklearn.model_selection import GridSearchCV log_pop = np.log10(df['A1101'].values).reshape(-1, 1) params = {'bandwidth': np.linspace(0.05, 0.5, 30)} grid = GridSearchCV(KernelDensity(kernel='gaussian'), params, cv=5) grid.fit(log_pop) print("Best bandwidth:", grid.best_params_['bandwidth']) kde_cv = grid.best_estimator_ xs = np.linspace(5.5, 7.5, 500).reshape(-1, 1) log_dens = kde_cv.score_samples(xs) plt.plot(xs, np.exp(log_dens)) plt.title(f'KDE with CV-selected bandwidth') plt.show() |
真の確率密度関数 $f(x)$ は理論的・未知の関数。 KDE $\hat{f}(x)$ はデータから推定したその近似。 サンプル数 $n \to \infty$、 $h \to 0$(適切な速度で)で $\hat{f}(x) \to f(x)$ に収束します。
「核」の意味で、 各データ点の周りに置く「小さな確率密度の核」のこと。 数学的にはサポート(領域)の中心に置かれる対称な関数なら何でもよい。 機械学習のカーネルトリックの「カーネル」とは別概念。
バンド幅 $h$ を小さくすればバイアス($h^4$)が減るが分散($1/(nh)$)が増える。 両者の合計(MISE)を最小化する $h^*$ は $h^4 \sim 1/(nh)$ から $h \sim n^{-1/5}$。 数学的な最適化の結果です。
実用上はどちらでもほぼ同じ。 「漸近的相対効率」で Epanechnikov が 1.000、 ガウスが 0.951。 5% 程度の差しかなく、 多くのライブラリでデフォルトのガウスを使えば問題なし。
はい。 $\hat{f}(x)$ をたくさんの点で評価し、 最大値を与える $x$ を探せば最頻値の推定値。 ただしバンド幅次第で最頻値の位置が動くので注意。
直接 KDE で検定はしませんが、 KDE は視覚的比較に最適。 統計検定は Kolmogorov-Smirnov 検定、 Anderson-Darling 検定、 Mann-Whitney U 検定など。 KDE は「差をどう描くか」、 検定は「差があるか」を担当。
使えますが、 形は信頼できません。 10 点で密度を表現するには情報が足りない。 そのときは個々のデータ点を「ラグプロット」(横軸上のヒゲ)で示し、 「これだけ少ない」と読み手に伝える方が誠実。
普通の KDE は使えません。 角度データには von Mises カーネルなどの専用カーネル、 球面データには球面 KDE。 「データのトポロジー」に合ったカーネルを選ぶ必要があります。
近年は両方重ねるのが標準。 ヒストグラムでデータの「ありのまま」を、 KDE で「なめらかな全体像」を見せる。 seaborn の histplot(kde=True) がワンライナーで実現してくれます。
評価点と密度値のペアを DataFrame で保存:
1 2 | kde_df = pd.DataFrame({'x': xs, 'density': kde(xs)}) kde_df.to_csv('kde_result.csv', index=False) |
計算が単純で理論解析しやすいから。 適応的 KDE では点ごとに $h_i$ を変えますが、 1) 計算が複雑、 2) 解釈が難しい、 3) 性能向上は中程度、 のため実用では固定 $h$ が標準です。
関係あります。 「事前分布なしのノンパラメトリックベイズ」と捉えられる。 また MCMC の事後分布可視化に KDE は必須ツール。 PyMC・Stan の出力を seaborn でそのまま描けます。
分布の可視化・推定
├── パラメトリック密度推定
│ ├── 正規分布のあてはめ(平均・分散を推定)
│ ├── 混合正規(GMM)
│ └── 一般指数族
├── ノンパラメトリック密度推定 ◀ ここに KDE
│ ├── ヒストグラム(階段状)
│ ├── KDE(なめらか) ◀ このページ
│ │ ├── ガウス KDE(最頻出)
│ │ ├── Epanechnikov KDE(最適)
│ │ ├── 適応的 KDE
│ │ └── 多変量 KDE
│ ├── 最近傍密度推定
│ └── オルソゴナル級数推定
├── 累積分布関数の推定
│ ├── Empirical CDF
│ └── 平滑 ECDF
└── 応用分野
├── 異常検知(密度が低い → 異常)
├── クラスタリング(Mean Shift など)
├── カーネル回帰(Nadaraya-Watson)
└── ベイズ事後分布の可視化
カーネル密度推定 (KDE) は、 離散的観測値 $\{x_1, \ldots, x_n\}$ から滑らかな密度関数を推定します。 ヒストグラムが「ビン境界の人工的な階段」を作るのに対し、 KDE は各観測点に「小さな山」(カーネル関数)を置き、 それを足し合わせるイメージです。
$$\hat f_h(x) = \frac{1}{nh} \sum_{i=1}^{n} K\!\left(\frac{x - x_i}{h}\right)$$
SSDSE-B-2026 の 47 都道府県人口データに KDE を適用すると、 東京・大阪のような大都市が右側に長い裾を作り、 多くの県が中央に固まる「右に歪んだ密度」が観察できます。 ヒストグラムでは見えにくい「双峰性」「希少な裾」が KDE では浮き彫りに。
代表的カーネル関数:
| カーネル | 式 | 特徴 | 効率比 |
|---|---|---|---|
| Gaussian | $\frac{1}{\sqrt{2\pi}}e^{-u^2/2}$ | 滑らか・最もポピュラー | 0.951 |
| Epanechnikov | $\frac{3}{4}(1-u^2)_+$ | MISE 最適 | 1.000 |
| Uniform (矩形) | $\frac{1}{2}\mathbb{1}_{|u|\le1}$ | 階段状 | 0.929 |
| Triangular | $(1-|u|)_+$ | 計算軽い | 0.986 |
| Biweight | $\frac{15}{16}(1-u^2)^2_+$ | Epanechnikov より滑らか | 0.994 |
| Cosine | $\frac{\pi}{4}\cos(\pi u/2)$ | 理論的興味 | 0.999 |
カーネル選択は本質的にあまり重要ではなく、 バンド幅 $h$ の選択が決定的。 Gaussian カーネルは scipy/sklearn のデフォルトで、 微分可能性が必要な場合に推奨。
バンド幅 $h$ の選択法。 大きすぎると過平滑化(バイアス大)、 小さすぎると過適合(分散大)。
import pandas as pd import numpy as np from scipy.stats import gaussian_kde from sklearn.neighbors import KernelDensity from sklearn.model_selection import GridSearchCV df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) pop = df.iloc[:, 3].values # 1) Silverman 自動 kde_silv = gaussian_kde(pop, bw_method='silverman') # 2) CV でバンド幅選択 params = {'bandwidth': np.logspace(3, 6, 30)} grid = GridSearchCV(KernelDensity(kernel='gaussian'), params, cv=5).fit(pop[:, None]) print(f'最適 h = {grid.best_params_["bandwidth"]:.0f}')
2 次元以上の KDE では多変量カーネル関数が必要。 SSDSE-B-2026 で「人口 × GDP」の 2 次元密度推定を行うと、 都道府県の「経済規模ペア」の分布が可視化できます。
from scipy.stats import gaussian_kde import matplotlib.pyplot as plt x = df.iloc[:, 3].values # 人口 y = df.iloc[:, 11].values # GDP xy = np.vstack([x, y]) kde2d = gaussian_kde(xy) # グリッド上で評価 xg = np.linspace(x.min(), x.max(), 100) yg = np.linspace(y.min(), y.max(), 100) X, Y = np.meshgrid(xg, yg) Z = kde2d(np.vstack([X.ravel(), Y.ravel()])).reshape(100, 100) plt.contourf(X, Y, Z, levels=15) plt.scatter(x, y, color='red', s=8) plt.show()
Q. なぜヒストグラムでなく KDE?
A. ヒストグラムはビン境界が人工的で、 同じデータでもビン幅・原点を変えると印象が変わります。 KDE は各点に滑らかなカーネルを置くので、 境界の任意性が消え、 微分可能で連続です。 SSDSE-B-2026 の人口分布のように歪んだデータでは、 ヒストグラムと KDE で見える特徴が異なることがあります。
Q. カーネル選択はどれくらい重要?
A. 本質的に重要ではありません。 Gaussian、 Epanechnikov、 三角など主要カーネルはどれも MISE 効率が 0.93 以上で大差なし。 「バンド幅選択」が桁違いに重要です。
Q. バンド幅自動選択の精度は?
A. Silverman は正規分布前提で過大評価する傾向。 Sheather-Jones plug-in は理論的に最良。 実務では複数手法を試して目視確認するのが安全です。
Q. 多次元 KDE は使える?
A. 2-3 次元までは実用的。 高次元では「次元の呪い」で必要サンプルが指数的に増えるため、 GMM や Mixture Model に切り替えるのが現実的。
Q. 境界バイアスの対処は?
A. 有界データ(例:年齢 0-100)で境界付近に密度が下がる現象。 反射法、 対数変換、 ベータカーネルで補正できます。
Q. 離散データに KDE を当ててもよい?
A. 推奨しません。 整数値の人数データに KDE を当てると人工的滑らかさが出ます。 離散変数なら確率質量関数、 または十分なジッターを加えた後 KDE が代替案。
金融
リスク管理で VaR (Value at Risk) を非パラメトリック分位点として推定。
生態学
動物の活動範囲 (home range) を空間 KDE で可視化。
交通工学
交通事故密度の地理的可視化。
医療画像
腫瘍の輝度分布を KDE で表現。
マーケティング
顧客の購買間隔分布の推定。
スポーツ分析
バスケのシュート位置の密度マップ。
犯罪マッピング
犯罪密度の地理的予測 (hot spot 分析)。
生物統計
遺伝子発現量の分布推定。
| 手法 | パラメトリック | 柔軟性 | 計算量 | 推奨場面 |
|---|---|---|---|---|
| 正規分布フィット | ◎ | 低 | $O(n)$ | 概ね正規時 |
| ヒストグラム | × | 中 | $O(n)$ | 大標本・粗い概観 |
| KDE | × | 高 | $O(n^2)$ | 中小標本・滑らか |
| GMM | △ (半) | 高 | EM 反復 | 多峰性 |
| Normalizing Flow | △ | 最高 | 学習重い | 高次元生成モデル |
| スプライン密度 | × | 高 | 中 | 研究用 |
KDE は「分布形状について仮定を置かない」ことが最大の強み。 SSDSE-B-2026 の人口分布のように「正規ではない歪んだ分布」を素直に表示できます。
KDE の漸近的平均積分二乗誤差 (AMISE) は次のように分解できます:
$$\mathrm{AMISE}(h) = \frac{R(K)}{nh} + \frac{1}{4} h^4 \sigma_K^4 R(f'')$$
第一項は分散(バンド幅が小さいほど大)、 第二項はバイアス(バンド幅が大きいほど大)。 微分して $0$ を解くと、 最適バンド幅は
$$h^\star = \left( \frac{R(K)}{n \sigma_K^4 R(f'')} \right)^{1/5}$$
この $h^\star$ は $n^{-1/5}$ のオーダーで縮みます。 $R(f'')$ は真の密度の二階微分の二乗積分で、 これを推定するのが Sheather-Jones の plug-in 法の核心。 SSDSE-B-2026 ($n=47$) では $h^\star \propto 47^{-0.2} = 0.46$ のオーダー、 SD の半分弱がベースライン値です。
$d$ 次元 KDE では $\mathrm{AMISE} = O(n^{-4/(d+4)})$ に劣化。 $d=10$ では $n=10^7$ が必要というのが「次元の呪い」の正体です。