LOWESS (locally weighted scatterplot smoothing) は散布図に対して下記のような近似曲線を描いてくれる便利な手法です。

statsmodelsパッケージだと
import statsmodels.api as sm smoothed = sm.nonparametric.lowess(exog=x, endog=y, frac=0.5, it=3)
といった感じで気軽に実行できますが、frac(使用するサンプルの割合)やit(繰り返し数)といったハイパーパラメータがどう作用するのかについては、アルゴリズムを知らないとよくわからずモヤッとしたので調べてみました。
今回はLOWESSが提案されたとされる論文
Cleveland, W. S. (1979). Robust locally weighted regression and smoothing scatterplots. Journal of the American statistical association, 74(368), 829-836.
を読んだので、アルゴリズムとPythonでの実装例をメモしておきます。
LOWESSのアルゴリズム
番目のサンプルの目的変数
を特徴量
とノンパラメトリックの平滑化関数
で近似することを考えます。
ここでは平均0で分散が一定の確率変数です。
重みの計算と
個の最近傍サンプルの取得
について、
にわたって
で距離を測り、
番目に近いサンプルとの距離を
とします。
重み関数を用いて、
について
を計算します。
ここで重み関数は以下の性質を満たすものとします。
について
は
について 非増加関数
について
論文では の例として tricube function が使われています(ちなみに、Wikipediaではもっと新しいやり方としてGaussian functionを使う方法が書いてありました:Local regression - Wikipedia)
これはグラフを描くと次の図のようになる関数です。

は分子の
の絶対値が分母の
より大きければ
になるので重みが0になります。つまり、サンプルとして回帰に使用されなくなります。なので重み関数は
番目のサンプルの近傍の
個のサンプルを取り出しつつ、
個のサンプルにも距離に応じた重みをかけているわけです。
statsmodelsのlowessのパラメータ「frac(使用するサンプルの割合)」は、このの計算に使われているものと思われます。
多項式回帰のフィッティング
ロバスト性重み
の計算
続いて、外れ値の影響を除外するための重みを計算します。次のように定義される bisquare weight function を用います。
残差の絶対値
の中央値を
とします。ロバスト性重み(robustness weights)を
と定義します。もtricube functionと似た形状であり、残差の絶対値の中央値の6倍(
)以上の絶対値の残差
を持つ外れ値は重み
がゼロになり、推定に含まれなくなるので推定が外れ値の影響を受けなくなります。

を使って重み付け回帰を行う
を重みとして再び
次多項式回帰を行い、新たな推定値
を得ます。
繰り返す
- ロバスト性重み
の計算
を使って重み付け回帰を行う
のステップを回繰り返します。
Pythonでの実装例
例として、次のデータを使います
# サンプルデータ import numpy as np n = 100 np.random.seed(0) x = np.linspace(0, 7, n) y = np.sin(x) + np.random.normal(0, 0.2, n)
図にするとこんな感じです。

重み関数を定義しておきます
def tricube(x: np.array) -> np.array: w = (1 - np.abs(x)**3)**3 w[np.abs(x) >= 1] = 0 return w def bisquare(x: np.array) -> np.array: w = (1 - np.abs(x)**2)**2 w[np.abs(x) >= 1] = 0 return w
ハイパーパラメータを定義してLOWESSを推定します
# ハイパーパラメータの定義 frac = 0.66 # 使用するサンプルの割合 r = int(frac * n) # 使用する近傍のサンプル数 d = 3 # 多項式回帰の次数 t = 3 # iteration # 前処理:d次多項式を作るための特徴量生成 X = np.vstack([x**j for j in range(d)]).T # LOWESSの推定 n = X.shape[0] delta = np.ones_like(x) y_pred = np.zeros_like(x) for _ in range(t): for i in range(n): # 重みの計算 dist = x - x[i] idx = np.argsort(np.abs(dist))[:r] h_i = np.abs(dist[idx]).max() # r番目に近いdiff w = tricube(dist / h_i) W = np.diag(delta * w) # WLS beta = np.linalg.inv(X.T @ W @ X) @ X.T @ W @ y y_pred[i] = X[i,:] @ beta e = y - y_pred s = np.median(np.abs(e)) delta = bisquare(e / (6 * s))
推定した結果をplotすると次のようになります
