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?

ばらつきのある測定値から、現代的な補間とスムージングで “信頼できる曲線とその信頼区間” を得る方法

Last updated at Posted at 2025-12-04

はじめに

この記事では、Gaussian Process まで入った “ちゃんとした” 補間である、NIST の Joe Fowler 氏が書いた interpolate.py について解説します。

普通の補間から一歩進んで、

  • 厳密な三次スプライン補間(CubicSpline)
  • スムージングスプライン(SmoothingSpline)
  • ガウス過程回帰(Gaussian Process Regression; GPR)に基づくスムージングスプライン(GPRSpline)
  • 自然境界条件付き B スプライン基底(NaturalBsplineBasis)

をひとまとめに実装した、かなり“濃い”補間モジュールです。

原理のコア部分については下記で説明を試みています。

コードは下記から公開されています。

この記事では、このファイルの構造と中身を

  • 「何をしているのか」
  • 「どんなときに使えるのか」
  • 「SciPy の標準 API とどこが違うのか」

という観点で、コードを読みながら噛み砕いて解説します。

全体の構成

モジュールの先頭コメントにもあるように、主なクラス・関数は以下のとおりです。

  • CubicSpline:厳密な三次スプライン補間(端点の傾き or 自然境界条件)
  • CubicSplineFunctionCubicSpline を「微分可能な Function オブジェクト」として包んだもの
  • LinterpCubicSpline:2 つの CubicSpline の線形補間
  • k_spline:GPR 用のスプラインカーネル
  • GPRSpline / GPRSplineFunction:ガウス過程回帰に基づくスムージングスプライン
  • NaturalBsplineBasis:自然境界条件を満たす B スプライン基底
  • SmoothingSpline / SmoothingSplineFunction:Reinsch 型のスムージングスプライン
  • SmoothingSplineLog:x, y を log スケールにしたスムージングスプライン

Function / ConstantFunction は別モジュール mass.mathstat.derivative から来ており、
「関数オブジェクト+その導関数」を扱うための抽象クラスだと考えてよさそうです。

1. CubicSpline:教科書通りの「三次スプライン補間」

1.1 何をしているクラスか

CubicSpline は、与えられた点列 (xᵢ, yᵢ) を厳密に通る三次スプラインを構成します。
いわゆる「Numerical Recipes 3 版 §3.3」に出てくる実装をそのまま Python 化したような形です。

特徴は:

  • x を昇順にソートしたうえで、
  • 各区間ごとの x 間隔 xstep、y の差 ystep から、
  • 各ノット点での二階微分 y2 を解く

という典型的なアルゴリズムになっています。

端点条件は

  • 傾き y′(x₁), y′(x_N) を指定する
  • 何も指定しない場合は「自然境界条件」:y''=0 at 両端

から選べます。

cs = CubicSpline(x, y)  # 両端は自然境界条件
y_interp = cs(x_new)    # 0 次導関数(値)
dy_dx    = cs(x_new, der=1)  # 1 次導関数

1.2 SciPy の InterpolatedUnivariateSpline との違い

モジュールの docstring にも書かれていますが、SciPy の
InterpolatedUnivariateSpline とは振る舞いが微妙に異なります

  • SciPy 版は「自由度を調整するために 2 番目と N−1 番目のデータ点を内部ノットから外す」
  • この CubicSpline は、「端点の 1階 or 2階微分で自由度を使う」

という違いがあります。
したがって、数値的にはほぼ同じでも、完全には一致しないことがあります。

境界条件をちゃんと意識したい場合には、この実装の方が「透けて見える」ので教育的です。

1.3 呼び出しの中身

__call__ メソッドは

  1. np.searchsorted で x が属する区間を特定
  2. 区間外は線形外挿(端点の値+端点の傾き×距離)
  3. 区間内は、教科書通りの三次スプラインの式
    s(x) = a y_k + b y_{k+1}
    + ((a^3 - a) y''*k + (b^3 - b) y''*{k+1}) \frac{h^2}{6}
    
    などを使って評価

導関数(der=1,2,3)も、同じく解析的な式を実装しており、3階導関数までは正確に計算できます。


2. CubicSplineFunction:微分可能な関数オブジェクトとしてのスプライン

CubicSplineFunction

class CubicSplineFunction(CubicSpline, Function):
    ...

と多重継承していて、

  • CubicSpline としての補間機能
  • Function としての「derivative メソッド」

を両方持つラッパーです。

2.1 derivative の実装方針

CubicSpline 自体は「der 引数を増やす」ことで高階微分を返せますが、
CubicSplineFunction はさらに

  • 一度 der=1 の微分を取ったら、そのオブジェクト自体を「1階導関数」とみなす
  • さらに derivative(1) を呼ぶと「2階導関数オブジェクト」を返す

といった「関数解析っぽい」使い方ができます。

f  = CubicSplineFunction(x, y)
df = f.derivative(1)  # f' を表す新しい CubicSplineFunction
d2f = f.derivative(2) # f'' を表す

内部的には単に der のオフセットを管理しているだけですが、
「関数をオブジェクトとして扱う」設計が気持ちいいポイントです。

3. LinterpCubicSpline:スプライン同士の線形補間

LinterpCubicSpline(s1, s2, fraction)

は、2 つの CubicSpline オブジェクト s1, s2 の「線形補間」用のクラスです。
イメージとしては

s(x; f) = f ~ s_1(x) + (1-f)~ s_2(x)

で、f = fraction

3.1 実装のポイント

入力としては CubicSpline ですが、中身では

  • ノット数 _n
  • ノット位置 _x
  • _y
  • 二階微分 _y2
  • 端点の傾き yprime1, yprimeN
  • 区間長 xstep, ystep

など、スプラインの内部パラメータを重み付き平均して新しいスプラインを作っています。

各 x で「2 本のスプラインを評価して足す」のではなく、
あらかじめスプラインそのものを補間してしまうため、呼び出しが高速になります。

  • 時間変化するスペクトルをスプラインで表現し、その間を smooth に補間したい
  • パラメータの異なる 2 つの「モデル曲線」を連続的に行き来したい

といった用途で便利です。

4. NaturalBsplineBasis:自然境界条件を満たす B スプライン基底

SmoothingSpline で使われる基底クラスです。
普通の B スプラインは、そのままだと自然境界条件 f''(端点)=0 を満たしてくれません。

そこで NaturalBsplineBasis は、

  1. ノット列 knots を両端で 3 つずつパディング

  2. BSpline を使って、

    • 左端での f'' をゼロにするために
      先頭 3 個の基底(#0, #1, #2)の線形結合係数 coef_b を求める
    • 右端でも同様に coef_e を求める
  3. 実際の基底呼び出しでは、
    左端・右端付近の基底にこの線形結合を足し込むことで f''=0 を実現

という仕掛けをしています。

4.1 使い方イメージ

knots = [0,5,8,9,10,12]
basis = NaturalBsplineBasis(knots)
x = np.linspace(0, 12, 200)
for i in range(len(knots)):
    plt.plot(x, basis(x, i))

これで、各基底関数が全て f''(x_min)=f''(x_max)=0 を満たすようになります。
この上で線形結合しても、最終的なスプラインは自然境界条件を満たす、というわけです。

5. SmoothingSpline:Reinsch 型スムージングスプライン

5.1 問題設定

SmoothingSpline は、以下のような最適化問題を解くクラスです:

  • データ:$(x_i, y_i)$, 誤差 $\sigma_i$

  • 曲線 $f(x)$ は「自然境界条件付き三次スプライン」

  • 目的:

    • 曲率エネルギー(二階導関数の二乗の積分)を最小化しつつ
    • $\chi^2 = \sum \frac{(y_i - f(x_i))^2}{\sigma_i^2} \le \chi^2_{\max}$
      を満たす

この「曲率 vs. データ忠実度」のトレードオフを、Reinsch (1967) の方法で数値的に解いています。

5.2 実装の流れ

  1. 誤差 dy, dx から「全体の誤差ベクトル err」を作る

    • dx が与えられている場合は、2 次多項式で rough fit → slope を推定
      → $\sigma^2 = (\mathrm{dx} \cdot slope)^2 + dy^2$
  2. x をスケーリングして xscale で正規化(数値安定性のため)

  3. NaturalBsplineBasis で基底を構成

    • N0:各ノットでの基底値
    • N2:各ノットでの二階導関数
  4. _compute_Omega で「二階導関数の内積行列」(\Omega) を計算

    • $\Omega_{ij} = \int B_i''(x) B_j''(x) dx$
  5. smooth() で「曲率 vs. χ²」を調整するパラメータ p を探索

    • ある p に対して最適係数 β(p) を解く
    • そこから χ²(p) を計算
    • 目標 chisq になるように brentq で p を 1D root-finding
  6. 得られた β を basis.expand_coeff で FITPACK 形式の係数に変換

    • splev で値・導関数を計算可能にする
  7. スプラインの外側は線形外挿(lowline, highline)で繋ぐ

5.3 実際の使い方

spline = SmoothingSpline(x, y, dy)  # maxchisq がデフォルトで Ndat
y_smooth = spline(x_new)
  • 「荒いノイズが乗ったスペクトルを滑らかにしたい」
  • 「曲率を抑えつつ、χ² が妥当なスムーズ曲線を作りたい」

といった用途で便利です。

6. GPRSpline:ガウス過程回帰としてのスムージングスプライン

このモジュールのハイライトの一つが GPRSpline です。

6.1 スプラインカーネル k_spline

まず、カーネル関数 k_spline(x, y) が定義されています:

def k_spline(x, y):
    v = np.minimum(x, y)
    return v**3/3 + v**2/2*np.abs(x-y)

これは、Rasmussen & Williams の GP 本の Eq (6.28) で紹介される
“cubic spline” に対応するカーネルです。

このカーネルを使って GPR を行うと、

  • 平均関数としては「自然境界条件付き三次スプライン」と同じ形
  • かつ GPR 理論に基づいた「不確かさ(分散)」も計算できる

という、スプライン+ベイズのハイブリッドなモデルが得られます。

6.2 GPRSpline の流れ

GPRSpline.__init__ では、おおまかに次のことをしています:

  1. データ x, y, 誤差 dy(+必要なら dx)から「誤差ベクトル err」を計算

  2. sigmaf(関数のスケール)の値を決める

    • 指定されなければ best_sigmaf() で「周辺尤度を最大化する値」を自動探索
  3. スプラインカーネル行列 K を構成

    • $K_{ij} = σ_f^2 ~ k_{spline}(x_i, x_j)$
  4. データ共分散 Ky = K + diag(err²) を作り、Cholesky 分解 L

  5. 線形トレンド H = [1, x] を入れた「線形平均+GP」の枠組みで
    係数 β を解く

  6. その結果から「自然スプラインのノット値 gbar」を計算し、
    CubicSpline.__init__ に渡してスプラインとして初期化

6.3 分散・共分散の計算

  • variance(xtest):テスト点 xtest における事後分散(対角成分だけ)を返す
  • covariance(xtest):テスト点同士の共分散行列を返す

となっており、GPR の公式に沿って

  • カーネル行列 Ktest
  • 事後共分散
    \mathrm{cov}(f_*, f_*) = K_{**} - K_*^T K_y^{-1} K_*
    + R^T A^{-1} R
    
    のような形を実装しています(線形トレンド込み)。

6.4 いつ SmoothingSpline より良いか

GPRSpline は docstring にもある通り、

  1. 曲率 vs. データ忠実度のトレードオフが、ベイズ的に principled
  2. 曲線の不確かさも推定できる

という点で SmoothingSpline よりリッチです。

一方で、実装が重く、計算量も O(N³) になるので、

  • データ点がそんなに多くない(数百点くらいまで)
  • 不確かさも含めてちゃんと扱いたい

という場合に向いています。

7. ログスケール版:SmoothingSplineLog

対数スケール向けには SmoothingSplineLog が用意されています。

class SmoothingSplineLog:
    def __init__(self, x, y, dy, dx=None, maxchisq=None):
        if np.any(x <= 0) or np.any(y <= 0):
            raise ValueError("The x and y data must all be positive ...")
        ...
        self.linear_model = SmoothingSpline(np.log(x), np.log(y), dy/y, dx, maxchisq=maxchisq)

    def __call__(self, x, der=0):
        return np.exp(self.linear_model(np.log(x), der=der))
  • x, y > 0 のときにのみ利用可能
  • (log x, log y) 空間でスムージングスプラインをかけてから、exp で戻す

という構造なので、べき則っぽい挙動をするデータに対して自然です。

スペクトル解析や、パワーロー的な挙動をする物理量のフィッティングなどに使えます。

8. *Function 系:派生クラスの共通パターン

CubicSplineFunction, SmoothingSplineFunction, GPRSplineFunction はいずれも

  • 元のスプラインクラスを継承
  • Function インターフェースを実装し、
  • derivative() メソッドで「高階導関数オブジェクト」を返せるようにする

という共通パターンになっています。

いずれも

  • 3階導関数を超えると ConstantFunction(0) を返す
  • __call__ では self.der + der を合算して親クラスに渡す

というロジックで、導関数の階数管理を内部で一元化しています。

9. まとめ:このモジュールの「おいしい使いどころ」

この interpolate.py の中身をざっくりまとめると、

  • 教科書レベルの三次スプライン(境界条件が明示的に扱える)
  • B スプライン+自然境界条件の基底構成
  • Reinsch 型スムージングスプライン(曲率エネルギー最小)
  • Gaussian Process Regression としてのスムージングスプライン(不確かさ付き)
  • ログスケール対応版
  • “関数オブジェクト+導関数” として扱うためのラッパー

ひととおり実装した「補間・スムージングの小さな実験場」 になっています。

実務での使いどころとしては:

  • SciPy の UnivariateSpline では“裏側が見えにくい”と感じたときに、
    アルゴリズムを学びつつ、挙動を細かく制御したい
  • スペクトルや時系列を「曲率制御付きのスムーズな曲線」で近似したい
  • GPR の数理は知っているけど、専用ライブラリを入れるほどでは…というときに
    「スプラインカーネル+Cholesky の最小構成」を動かしてみたい

といった場面で、とても勉強になるコードです。

おまけ:最小実行例

最後に、CubicSplineSmoothingSpline の超シンプルな例を載せておきます。

import numpy as np
import matplotlib.pyplot as plt
from interpolate import CubicSpline, SmoothingSpline

# ダミーデータ
x = np.linspace(0, 10, 20)
y = np.sin(x) + 0.2 * np.random.randn(len(x))
dy = 0.2 * np.ones_like(y)

# 厳密なスプライン補間
cs = CubicSpline(x, y)

# スムージングスプライン
ss = SmoothingSpline(x, y, dy)

# プロット
xx = np.linspace(0, 10, 400)
plt.errorbar(x, y, dy, fmt='o', label='data')
plt.plot(xx, cs(xx),  label='CubicSpline (exact)')
plt.plot(xx, ss(xx),  label='SmoothingSpline')
plt.legend()
plt.show()
  • CubicSpline はデータ点を「必ず通る」ので、ノイズも拾います
  • SmoothingSpline は曲率を抑えているので、ノイズをうまく平均化します

ここから GPRSpline に切り替えて、事後分散もプロットしてみると、
「ガウス過程としてのスプライン」がどんなものか感覚的に掴めるはずです。


以上、interpolate.py を一通り読み解きながらの解説でした。

Appendix

以下では、Fowler+ (2022)「Energy Calibration of Nonlinear Microcalorimeters」 と interpolate.py の関係解説をしてみます。
です。

Fowler et al. (2022) *Energy Calibration of Nonlinear Microcalorimeters with Uncertainty Estimates from Gaussian Process Regression
(Journal of Low Temperature Physics, 2022)

本 Appendix では、interpolate.py に実装されている CubicSpline / SmoothingSpline / GPRSpline が、Fowler+ (2022) のエネルギーキャリブレーション理論とどのように対応しているかを整理します。

1. 論文の主張の核心とコード構造の対応

論文の中心的なメッセージは以下の 3 点です(本文 1〜4章):

  1. TES のエネルギー応答は非線形なので、経験的な補正曲線が必須
  2. その補正関数は「補間 spline」ではなく「近似 smoothing spline」であるべき
  3. スムージングスプラインは Gaussian Process Regression(GPR)と完全に等価であり、
      GPR を使うと補正曲線の「不確かさ」も同時に算出できる

これら 3 点は interpolate.py の以下の実装と直接リンクしています:

論文の概念 コード中の対応クラス 役割
厳密な補間 CubicSpline 与えられた点を厳密に通る spline
近似 spline (従来法) SmoothingSpline 曲率ペナルティと χ² 制約で近似
GPR による smoothing spline GPRSpline 論文の方法をそのまま実装(分散も算出)

2.論文で導かれる smoothing spline と GPR 等価性

GPRSpline の実装そのもの

論文 4章では、「1 回積分された Wiener 過程(integrated Wiener process)」を GP の prior に選ぶと、その posterior の期待値は 必ず cubic smoothing spline になる
という重要な定理(Wahba)を使っています。

実際、論文では次の式が示されます(p.1051–1052):

  • GP の平均:
    $ m(x) = \beta_0 + \beta_1 x $
  • GP の共分散(スプラインカーネル):
    k(x,x') = \sigma_f^2 \left( \frac{v^3}{3} + \frac{v^2}{2}|x-x'| \right),
    \quad v = \min(x,x')
    

これがコードの

def k_spline(x, y):
    v = np.minimum(x, y)
    return v**3/3 + v**2/2*np.abs(x-y)

に完全に一致しています。

3. 論文の Bayesian 最適化(σ_f の最尤推定)

GPRSpline.best_sigmaf() に対応

論文では、曲率ペナルティ λ と GP のスケール σ_f が以下で関係づけられ、
σ_f を「周辺尤度最大化」で決めるべきであると議論します(Eq.8)。

コードではこれに対応するのが:

result = sp.optimize.minimize_scalar(
    lambda x: -self._marginal_like(x),
    [guess/1e4, guess*1e4]
)

さらに _marginal_like() 内の計算は論文式と完全対応しており、

  • K 行列(カーネル)
  • Ky = K + diag(err²)
  • L = Cholesky(Ky)
  • A = (L⁻¹ H)^T (L⁻¹ H)
  • yCy や log det(Ky)

など、論文の Eq.(8) に出てくる構造がほぼ一対一で実装されています。

4. Posterior mean の spline 化(論文 Eq.5 → コードの spline 生成)

論文の GPR posterior の期待値(Eq.5) は

f_*(x) = m(x) + K_*^T K_y^{-1} (y - m(x))

ですが、Fowler+ (2022) が特筆しているのは以下の点です:

posterior mean を “すべての x” で評価する必要はない。
knot x_i 上だけ posterior を計算し、それらを natural cubic spline で結べば良い。

(論文 p.1052 下部)

これが GPRSpline.__init__() の最重要部分:

fbar = np.linalg.solve(L.T, np.linalg.solve(L, K)).T.dot(y)
gbar = fbar + R.T.dot(beta)
CubicSpline.__init__(self, self.x, gbar)

この gbarposterior mean を knot 上で評価した値であり、
それを CubicSpline に渡すことで、期待値 spline を生成しています。


5. 論文の「予測分散(calibration uncertainty)」

GPRSpline.variance() / .covariance() に対応

論文 Fig.1(p.1053)では、アンカー点の間隔が粗いほど不確かさが大きい様子が示されます。

コードの

def variance(self, xtest):
    ...
    cov_ftest = self.sigmaf**2*k_spline(x, x) - (LinvKtest**2).sum()
    ...

は GPR の分散公式をそのまま実装したものです。

\mathrm{Var}[f_*(x)] =
k(x,x) - K_x^T K_y^{-1} K_x + R^T A^{-1} R

という論文の構造に完全に一致します(p.1052 Eq.(6))。

この計算によって、Fig.1 の「不確かさバンド」が再現できます。

6. 従来法 smoothing spline との関係

SmoothingSpline クラスは論文 3章の Reinsch 法を実装

論文 3章(p.1049–1050)にある smoothing spline の functional

C[h] = \sum_i \left(\frac{h(x_i)-y_i}{\sigma_i}\right)^2 +
\lambda \int |h''(x)|^2 dx

これは SmoothingSpline

  • Omega(二階導関数の内積)
  • chisq_difference() で λ(p)を調整し χ² を合わせる
  • FITPACK B-spline 基底で表現

という形で実装されています。

論文は

smoothing spline と GPR は 完全に等価(Wahba)

と述べており、SmoothingSplineGPRSpline が同じ母関数を別の数値的アプローチで実装していることがわかります。

7. コードと論文の対応まとめ(早見表)

概念 論文の章・式 コード
スプラインカーネル Eq.(3) k_spline()
smoothing spline の汎関数 Eq.(1) SmoothingSpline(Omega, chi² root-finding)
smoothing spline = GPR posterior mean p.1052 GPRSpline(posterior gbar → CubicSpline)
σ_f の最尤推定 Eq.(8) best_sigmaf()
posterior variance(不確かさ) Eq.(6) variance() / covariance()
外挿が線形になる性質 論文の GP モデルの線形 mean lowline / highline

8. 結論:interpolate.py は Fowler+ (2022) の "reference implementation"

Fowler+ (2022) の主張は、エネルギーキャリブレーションを

  • spline で近似し
  • 曲率を最小化しつつ
  • GPR により不確かさを付与する

という統一的な方法にまとめた点にあります。

interpolate.pyGPRSpline は、その理論を完全にコード化したものであり、
論文の「数式 → 実装」のほぼ全工程を忠実に反映した reference implementation
といってよい内容です。

(コードについて理解を深めたい方向けに下記にtipsをまとめています。)

コードの手本と改善点

1. 見習うべきポイント(かなり良いところ)

(1) アルゴリズムが教科書レベルで「素直に」実装されている

  • CubicSpline は Numerical Recipes の章をほぼそのままコードに落としていて、

    • 2階微分 self._y2 を解く
    • 区間での a, b を使った三次スプラインの公式で値や導関数を計算
  • SmoothingSpline は Reinsch のスムージングスプラインの式を忠実に実装

  • GPRSpline は Rasmussen & Williams の GPR の式を、そのままコード化

👉 「ライブラリの黒魔術に頼らず、自分で数式をコードに落とす」良い見本です。
数値計算や物理の学生がやるべき練習として、とても参考になります。

(2) 「補間」と「スムージング」が明確に分かれている

  • CubicSpline:データ点を必ず通る厳密補間
  • SmoothingSpline / GPRSplineノイズを考慮してなめらかに近似するスプライン

👉 「データを全部通れば良い」わけではなく、

  • 補間:測定点はほぼノイズなし・物理的に厳密な制約を再現したいとき
  • スムージング:測定値に誤差がある前提で“傾向”を取りたいとき

という目的の違いをクラス設計で表現しているのは、とても良い設計です。

(3) 境界条件(boundary condition)をちゃんと扱っている

  • CubicSpline は端点の傾き yprime1, yprimeN を指定するか、指定しなければ自然境界(y''=0)
  • NaturalBsplineBasisBスプライン基底を組み合わせて f''(端点)=0 を満たすように補正

👉 「境界条件が数値解の性質を決める」という教科書の話を、そのままコードにしているところは、物理系・工学系の学生が特に見習うべき設計です。

(4) 外挿の扱いが明示的でわかりやすい

CubicSpline.__call__ では、x が範囲外のとき:

  • 端点の値+端点傾きの線形外挿で対応
  • 導関数のときも一定(端点の傾き)を返す
  • 高次導関数は 0
if extrap_low.any():
    if der == 0:
        h = x[extrap_low]-self._x[0]
        result[extrap_low] = self._y[0] + h*self.yprime1

👉 「範囲外はよくわからないけれど、とりあえず線形外挿にする」というポリシーがコードで明示されていて、“暗黙の挙動”がないのはかなり良い点です。

(5) 線形代数の基本がきちんと守られている

  • 連立一次方程式は np.linalg.solve を使っていて、愚直な逆行列は作らない
  • GPR では np.linalg.cholesky を使って、対称正定値行列を安定に扱う
  • 「対角行列」はちゃんと np.diag(self.err**2) で作る

👉 線形代数の“よくある悪手”を避けているところは、学生コードのお手本にしてよいです。

(6) 「関数オブジェクト+導関数」という設計

CubicSplineFunction, SmoothingSplineFunction, GPRSplineFunction

  • 元のスプラインクラスを継承
  • Function 抽象クラスのインターフェース(derivative() など)を実装
  • 「このオブジェクトは f(x) だけでなく f'(x), f''(x) も返せる」という設計
f  = CubicSplineFunction(x, y)
df = f.derivative(1)   # 1階導関数

👉 “関数をオブジェクトとして扱う” 発想は、数値解析コードを書ける学生にとって良い刺激になります。

2. 改善した方が良さそうな点・注意したい点

ここからは、「学生が真似するときはここは直した方がいいかも」ポイントです。

(1) コメント・docstring がアルゴリズムレベルまで書かれていない

クラスやメソッドの docstring はありますが、

  • 「この for ループは何を解いているのか」
  • 「この行列はどういう物理量/数学的量なのか」

といったアルゴリズムの意図がコードの中ではあまり説明されていません

学生が真似するなら:

  • _compute_y2 のところに
    「三重対角行列を前進消去+後退代入して 2階微分を解いている」
  • _compute_Omega
    「二階導関数の内積 ∫B_i'' B_j'' dx を、区分線形性を使って数値積分している」

など、数学とコードの橋渡しコメントを足すと、読みやすさがかなり増します。

(2) 型ヒント(type hints)がない

最近の Python では、学習用コードでも

def __init__(self, x: np.ndarray, y: np.ndarray, dy: np.ndarray, ...):

のように 型ヒント をつけておくと、

  • エディタの補完が良くなる
  • 間違った型を渡した時にすぐ気づける

ので、学生さんが自分で書くときは、このコードを参考にしつつ type hints を足すと良いです。

(3) エラー処理や入力チェックが最小限

例えば:

  • CubicSpline__init__ では、x が重複していたり、長さが 2 以下でも特にチェックしていない
  • SmoothingSplinesmooth()brentq している区間 [1e-20, 1] に根がない場合は LinAlgError 以外も起こり得るが、そのときの例外処理は 1 パターンのみ

学生コードとして安全にするなら:

  • 「x は単調増加であること」「len(x) >= 3」などを assert でチェック

  • brentq が失敗したときに

    • エラーメッセージを出す
    • あるいは p=1(単純な線形フィット)にフォールバック

といった 「最悪どうするか」 も書いておくと実用的です。

(4) SmoothingSplineLog で引数を書き換えてしまう

if dx is not None:
    dx /= x

ここは、呼び出し側が渡した dx 配列を破壊的に変更してしまう可能性があり、
副作用としてはあまり望ましくありません。

学生が書くなら:

if dx is not None:
    dx = dx / x   # 新しい配列を作る

のようにしておく方が、バグを生みやすくない書き方です。

dx /= xインプレース(in-place)演算です。

dx /= x が何を意味するか?

以下のような書き方は、Python/NumPy では

dx = dx / x

(新しい配列を作る)ではなく、

dx = dx.__itruediv__(x)   # in-place true division

として扱われます。つまり dx の中身を書き換えてしまいます

(5) SmoothingSplineFunction.derivative() で元パラメータが引き継がれていない

def derivative(self, der=1):
    if self.der + der > 3:
        return ConstantFunction(0)
    return SmoothingSplineFunction(self.x, self.y, self.dy, self.dx, der=self.der + der)

ここでは maxchisq が新しいオブジェクトに渡されていません。
結果として、導関数を取るたびに デフォルトの maxchisq=N で再フィットし直すことになります。

厳密なバグとは言い切れませんが、設計としては

  • maxchisq(=どれだけフィットを許すか)も含めて「同じモデル」を共有したかったはず

なので、学生が真似するなら

return SmoothingSplineFunction(self.x, self.y, self.dy, self.dx,
                               maxchisq=self.maxchisq,
                               der=self.der + der)

のように 元パラメータを維持する方が自然です。

(6) GPR の部分は N³ スケーリングなので、データ数が多い場合の注意が必要

GPRSpline では

  • K: Nk×Nk 行列
  • Cholesky 分解や solve を多用

しているので、計算量は O(Nk³) です。
100点程度なら余裕ですが、1万点のデータにそのまま適用するのは現実的ではありません。

学生が実験データに適用する場合は、

  • アンカー点(校正線など)の数を絞る
  • 非等間隔の高密度データの一部を代表値に圧縮する
  • どうしても大量データに使うなら、近似 GP や分割統治を検討

といった方針が必要になる、ということを併せて教えると良いです。

3. このコードから学ぶと良いことまとめ

このコードを「お手本」にして学ぶべきこと

  • 数式(教科書)→ コードへの落とし方
  • 補間とスムージングの違いとその実装
  • 境界条件・外挿・誤差の扱い
  • np.linalg.solve や Cholesky を使った安定な線形代数
  • 「関数オブジェクト+導関数」という設計パターン

自分で書くときに「改善した状態」で真似したいところ

  • アルゴリズムの意図をコメントで書く
  • 型ヒント・入力チェック・エラー処理を足す
  • 引数(配列)を破壊的に変更しない
  • パラメータ(maxchisq など)を derivative でもきちんと引き継ぐ
  • 計算量(O(N³))の限界を意識する
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?