1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

導入

センサや観測点がまばらなとき、よくやりたくなるのが「地図にする」ことです。

  • 環境(気温・PM2.5・騒音・水質)
  • 医工学(病院内の混雑・機器温度・電力)
  • 実験(反応条件の空間分布、装置内の温度ムラ)
  • 都市・施設(混雑、遅延、通信品質)

でも、観測点が少ないのに **“滑らかな地図”**だけを出すと危険です。
なぜなら、地図のどこが「ちゃんと観測に支えられた推定」で、どこが「だいぶ当てずっぽう」かが見えないからです。

そこで本記事では、

  • 空間補間(Spatial interpolation)で推定地図を作り、
  • さらに 不確実性(Uncertainty) を地図として出して、
  • 「推定値」と「自信のなさ」をセットで提示する

最小実験を Google Colab で体験します。

このデモは教育用で、地球物理や大気モデルなどの厳密再現ではありません。
ただし「空間補間+不確実性の可視化」という方法論は、多くの現場でそのまま役立ちます。


TL;DR

  • 空間補間は「見た目の地図」を作れるが、どこが信用できるかが重要。
  • IDW(Inverse Distance Weighting)は簡単で速いが、不確実性は出しにくい。
  • Kriging ≒ ガウス過程回帰(Gaussian Process Regression)は、
    予測の 平均(推定値)標準偏差(不確実性) を同時に出せる。
  • 不確実性を「別の地図」にするだけでなく、推定地図の透明度に反映すると“映える”し、誤解も減る。

1. 今日やること(最小構成)

このデモでは、2次元平面(1km四方のイメージ)に「真の場(ground truth)」を作り、ランダムな観測点だけを与えます。

  1. 真の場(見えないはずの分布)を合成
  2. まばらな観測点(センサ)を置き、ノイズ付き観測を得る
  3. IDWで補間(ベースライン)
  4. ガウス過程(Kriging)で補間し、不確実性も地図化
  5. “地図が映える”見せ方(不確実性を透明度へ)

2. 用語を学部生向けに(重要)

2.1 空間補間(Spatial interpolation)

観測点がまばらでも、全域の値を推定してグリッド(格子)上の地図にします。
「どこでも値がある地図」は直感的ですが、観測が無い場所ほど危ういです。

2.2 不確実性(Uncertainty)

ここで言う不確実性は「誤差そのもの」ではなく、

観測点の配置とモデル仮定(滑らかさ等)から見て、
どれくらい自信を持って推定できるか

の指標です。

2.3 Kriging と ガウス過程(GP)の関係

Kriging は地質統計(geostatistics)で有名な空間補間手法です。
実は「ある種の仮定のもとで、ガウス過程回帰とほぼ同じ形」になります。

本記事では実装しやすい scikit-learn の GaussianProcessRegressor を使い、

  • 予測平均(mean)=推定値の地図
  • 予測標準偏差(std)=不確実性の地図

を作ります。


3. Google Colabで動かす(コピペでOK)

以下を Colab に上から貼り付けて実行してください。
fig/ に図を保存します。


セル1:準備

import os
import numpy as np
import matplotlib.pyplot as plt

from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import Matern, WhiteKernel, ConstantKernel
from sklearn.preprocessing import StandardScaler

SEED = 42
rng = np.random.default_rng(SEED)

os.makedirs("fig", exist_ok=True)

セル2:真の場(ground truth)を合成し、観測点を作る

def build_true_field():
    """滑らかな2D場(山が2つ+うねり)を合成。※物理モデルではないデモ用"""
    def f(x, y):
        # 2つのガウス山 + ゆるい波
        g1 = 1.3*np.exp(-(((x-0.75)/0.12)**2 + ((y-0.35)/0.10)**2))
        g2 = 0.9*np.exp(-(((x-0.25)/0.18)**2 + ((y-0.75)/0.12)**2))
        w  = 0.15*np.sin(2*np.pi*x)*np.cos(2*np.pi*y)
        return g1 + g2 + w
    return f

f_true = build_true_field()

# グリッド(地図を描く範囲)
N_GRID = 160
xs = np.linspace(0, 1, N_GRID)
ys = np.linspace(0, 1, N_GRID)
XX, YY = np.meshgrid(xs, ys)

Z_true = f_true(XX, YY)

# 見やすいように 0〜100 にスケーリング
mn, mx = Z_true.min(), Z_true.max()
Z_true_01 = (Z_true - mn) / (mx - mn)
Z_true_100 = 100 * Z_true_01

# 観測点(センサ)をランダム配置
N_SENSORS = 60
X_obs = rng.uniform(0, 1, size=(N_SENSORS, 2))  # (x,y)
z_clean = 100 * (f_true(X_obs[:,0], X_obs[:,1]) - mn) / (mx - mn)

# 観測ノイズ(測定誤差のイメージ)
NOISE_SIGMA = 4.0
z_obs = z_clean + rng.normal(0, NOISE_SIGMA, size=N_SENSORS)

print("z_obs range:", float(z_obs.min()), float(z_obs.max()))

セル3:図1(真の場+観測点)※デモ用の“答え合わせ”

plt.figure(figsize=(7.5,6))
cf = plt.contourf(XX, YY, Z_true_100, levels=30)
plt.colorbar(cf, label="value (0-100, synthetic)")
plt.scatter(X_obs[:,0], X_obs[:,1], s=35, edgecolor="k", linewidth=0.5, label="sensors")
plt.title("Ground truth (toy demo) + sensor locations")
plt.xlabel("x"); plt.ylabel("y")
plt.legend()
plt.tight_layout()
plt.savefig("fig/fig1_ground_truth_and_sensors.png", dpi=160, bbox_inches="tight")
plt.show()

print("Saved: fig/fig1_ground_truth_and_sensors.png")

fig1_ground_truth_and_sensors.png


セル4:ベースライン(IDW補間:Inverse Distance Weighting)

「近い観測点ほど重く、遠いほど軽く」する単純な補間です。
不確実性は出しにくいですが、最初の比較として便利です。

def idw_predict(X_obs, z_obs, X_pred, power=2.0, k=12, eps=1e-12):
    """IDW: 予測点X_predごとに、近いk点の距離の逆数^powerで重み付け平均"""
    # 距離行列: (n_pred, n_obs)
    d = np.sqrt(((X_pred[:,None,:] - X_obs[None,:,:])**2).sum(axis=2)) + eps

    # k近傍だけ使う
    idx = np.argpartition(d, kth=k-1, axis=1)[:, :k]
    d_k = np.take_along_axis(d, idx, axis=1)
    z_k = z_obs[idx]

    w = 1.0 / (d_k**power)
    w = w / (w.sum(axis=1, keepdims=True) + eps)
    return (w * z_k).sum(axis=1)

# 予測点(グリッド)
X_grid = np.column_stack([XX.ravel(), YY.ravel()])
Z_idw = idw_predict(X_obs, z_obs, X_grid, power=2.0, k=12).reshape(XX.shape)

plt.figure(figsize=(7.5,6))
cf = plt.contourf(XX, YY, Z_idw, levels=30)
plt.colorbar(cf, label="IDW predicted value")
plt.scatter(X_obs[:,0], X_obs[:,1], s=25, edgecolor="k", linewidth=0.4)
plt.title("IDW interpolation (baseline)")
plt.xlabel("x"); plt.ylabel("y")
plt.tight_layout()
plt.savefig("fig/fig2_idw.png", dpi=160, bbox_inches="tight")
plt.show()

print("Saved: fig/fig2_idw.png")

fig2_idw.png


セル5:ガウス過程(Kriging)で平均と不確実性を出す

ポイント:

  • Maternカーネル:滑らかすぎない現実寄りの相関を表現しやすい
  • WhiteKernel:測定ノイズ(“ナゲット”に相当)の表現
  • return_std=True で予測標準偏差を得る
# 入力スケールの安定化(座標を標準化)
scaler = StandardScaler()
X_obs_s = scaler.fit_transform(X_obs)
X_grid_s = scaler.transform(X_grid)

kernel = ConstantKernel(1.0, (1e-2, 1e2)) * Matern(
    length_scale=[1.0, 1.0],
    length_scale_bounds=(1e-2, 1e2),
    nu=1.5
) + WhiteKernel(noise_level=1.0, noise_level_bounds=(1e-5, 1e2))

gp = GaussianProcessRegressor(
    kernel=kernel,
    normalize_y=True,
    n_restarts_optimizer=3,
    random_state=SEED
)

gp.fit(X_obs_s, z_obs)

mu, std = gp.predict(X_grid_s, return_std=True)
Z_gp_mean = mu.reshape(XX.shape)
Z_gp_std  = std.reshape(XX.shape)

print("Learned kernel:", gp.kernel_)
print("mean range:", float(Z_gp_mean.min()), float(Z_gp_mean.max()))
print("std  range:", float(Z_gp_std.min()), float(Z_gp_std.max()))

セル6:図3(GPの推定値の地図)

plt.figure(figsize=(7.5,6))
cf = plt.contourf(XX, YY, Z_gp_mean, levels=30)
plt.colorbar(cf, label="GP mean (predicted value)")
plt.scatter(X_obs[:,0], X_obs[:,1], s=25, edgecolor="k", linewidth=0.4)
plt.title("Gaussian Process / Kriging: mean (estimate)")
plt.xlabel("x"); plt.ylabel("y")
plt.tight_layout()
plt.savefig("fig/fig3_gp_mean.png", dpi=160, bbox_inches="tight")
plt.show()

print("Saved: fig/fig3_gp_mean.png")

fig3_gp_mean.png


セル7:図4(GPの不確実性の地図:標準偏差)

plt.figure(figsize=(7.5,6))
cf = plt.contourf(XX, YY, Z_gp_std, levels=30)
plt.colorbar(cf, label="GP std (uncertainty)")
plt.scatter(X_obs[:,0], X_obs[:,1], s=25, edgecolor="k", linewidth=0.4)
plt.title("Gaussian Process / Kriging: std (uncertainty map)")
plt.xlabel("x"); plt.ylabel("y")
plt.tight_layout()
plt.savefig("fig/fig4_gp_std.png", dpi=160, bbox_inches="tight")
plt.show()

print("Saved: fig/fig4_gp_std.png")

fig4_gp_std.png


セル8:図5(“映える”見せ方:不確実性を透明度にする)

「推定値の色」は同じでも、不確実性が大きい領域を薄くすると、
“分かっている場所/分かっていない場所”が直感的になります。

# 不確実性を 0〜1 に正規化して、alpha(透明度)に変換
std_norm = (Z_gp_std - Z_gp_std.min()) / (Z_gp_std.max() - Z_gp_std.min() + 1e-12)
alpha = 1.0 - 0.75*std_norm  # 不確実性が大きいほど薄く

plt.figure(figsize=(7.5,6))
im = plt.imshow(
    Z_gp_mean,
    origin="lower",
    extent=[0,1,0,1],
    alpha=alpha
)
plt.colorbar(im, label="GP mean (predicted value)")
plt.scatter(X_obs[:,0], X_obs[:,1], s=25, edgecolor="k", linewidth=0.4)
plt.title("Mean map with uncertainty as transparency (visually safer)")
plt.xlabel("x"); plt.ylabel("y")
plt.tight_layout()
plt.savefig("fig/fig5_mean_with_uncertainty_alpha.png", dpi=160, bbox_inches="tight")
plt.show()

print("Saved: fig/fig5_mean_with_uncertainty_alpha.png")

fig5_mean_with_uncertainty_alpha.png


セル9:図6「次にセンサを置くならここ」(不確実性上位点)

これは “空間補間+不確実性” の強い使いどころです。
不確実性が高い場所を優先して観測すると、地図の品質が上がりやすいです。

# 不確実性が高い上位K点を提案
K = 12
flat_idx = np.argsort(Z_gp_std.ravel())[::-1][:K]
X_next = X_grid[flat_idx]

plt.figure(figsize=(7.5,6))
cf = plt.contourf(XX, YY, Z_gp_std, levels=30)
plt.colorbar(cf, label="GP std (uncertainty)")
plt.scatter(X_obs[:,0], X_obs[:,1], s=25, edgecolor="k", linewidth=0.4, label="current sensors")
plt.scatter(X_next[:,0], X_next[:,1], s=80, marker="*", edgecolor="k", linewidth=0.8, label="suggested next")
plt.title("Where to sample next? (top uncertainty points)")
plt.xlabel("x"); plt.ylabel("y")
plt.legend()
plt.tight_layout()
plt.savefig("fig/fig6_next_sampling.png", dpi=160, bbox_inches="tight")
plt.show()

print("Saved: fig/fig6_next_sampling.png")

fig6_next_sampling.png


セル10:図をまとめてダウンロード(Colab用)

import shutil
shutil.make_archive("figures", "zip", "fig")
print("Created figures.zip")

4. 実行結果の図

図1:真の場+観測点(デモ用の答え合わせ)

fig1_ground_truth_and_sensors.png

図2:IDW(ベースライン補間)

fig2_idw.png

図3:GP/Kriging 推定値(平均)

fig3_gp_mean.png

図4:GP/Kriging 不確実性(標準偏差)

fig4_gp_std.png

図5:“映える”安全な地図(不確実性を透明度へ)

fig5_mean_with_uncertainty_alpha.png

図6:次に観測すべき候補(不確実性上位)

fig6_next_sampling.png


5. 図の読み方(推定値と不確実性をセットで読む)

5.1 推定値(図3)は「地図」だが、単体では危険

図3だけ見ると、どこでもそれっぽい値が塗られていて安心してしまいます。
しかし実際は、観測点が少ない場所では「そう見えるだけ」の可能性があります。

5.2 不確実性(図4)は「どこが分かっていないか」の地図

一般に、不確実性が高くなりやすいのは:

  • 観測点から遠い場所
  • 境界付近(外挿に近い)
  • 観測点配置が偏っている領域

です。
図4が明るい場所は「推定値が間違っている」ではなく、
「推定値に自信がない」という意味です。

5.3 図5(透明度)は、誤解を減らす“伝え方”

地図を使うと「色=事実」に見えやすいです。
そこで、不確実性が大きい場所を薄くすることで、

  • “ここは確度が高い”
  • “ここは推測が強い”

が一目で伝わり、意思決定が安全になります。


6. 実務で使うときの注意点(ここがインフォマティクス)

6.1 不確実性は“モデル仮定の上”にある

GP/Krigingの不確実性は、主に次に依存します。

  • 相関の仮定(カーネル、長さスケール)
  • 定常性・等方性(場所によらず同じ性質とみなす等)
  • ノイズ(測定誤差)

仮定が外れると、不確実性が過小評価・過大評価されることがあります。
「不確実性が小さいから絶対に正しい」ではなく、“仮定の中で自信がある”です。

6.2 座標が緯度経度なら距離の扱いに注意

本記事は 0〜1 の平面座標として扱いました。
緯度経度(lat/lon)をそのままユークリッド距離で扱うと歪む場合があります。
狭い範囲なら近似できても、広域なら投影や測地距離を検討します。

6.3 外部特徴(標高、土地利用、風向など)があると強い

純粋な空間相関だけでなく、説明変数を加えたモデル(回帰+GP残差など)にすると、
「なぜその分布になるか」も含めて推定が安定します。


まとめ

本記事では、ダミーデータを使って

  • 空間補間(IDW / GP=Kriging)
  • 不確実性(GP std)
  • “映える”かつ誤解を減らす可視化(透明度)

を Google Colab で再現しました。

「地図を作る」だけで終わらず、
地図の信用度(不確実性)を一緒に出すと、研究でも運用でも意思決定が強くなります。

次は、あなたのデータに合わせて

  • 観測点の偏り
  • ノイズの大きさ
  • 長さスケール(どれくらい滑らかか)
  • 次にどこを測るべきか

を調整し、現場にフィットした“見せ方”を作ってみてください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?