時系列の異常検知手法には、「まず時系列をトレンド・季節・残差に分解し、季節やトレンドで説明できない残差を見て異常を判断する」という有力な系譜があります。Time-CADやAE-STLといった手法はこの発想の上に立っています。では、その「分解」はどうやって行うのでしょうか。最も広く使われる答えが STL(Seasonal-Trend decomposition using Loess) です。
STLは「季節パターンが年々少しずつ変わってもよい」「外れ値があってもトレンドが歪まない」という、実データに嬉しい性質を持っています。なぜそんなことができるのか——その秘密は、STLの心臓部にある LOESS(局所重み付き回帰) と、トレンドと季節を交互に磨き上げる2重ループにあります。本記事では、この内部構造を一段深く掘り下げます。
STLの仕組みを理解すると、次のような場面で効いてきます。
- 分解系の異常検知を使いこなす — 残差の素性(なぜ外れ値が残差に落ちるのか)が分かり、閾値設計の根拠を持てる
- 予測の前処理を設計する — 季節やトレンドの平滑度を、データの性質に合わせて調整できる
本記事の内容
- STLが分けるもの(トレンド・季節・残差)と加法の前提
- 心臓部のLOESS — 局所重み付き回帰とトリキューブ重み
- 季節を「同じ周期位置どうし」で平滑化するcycle-subseries
- 内側ループ(トレンドと季節を交互に精緻化)と外側ループ(ロバスト重み)
- パラメータ $n_s$・$n_t$・$n_l$ の役割と、残差による異常検知
なお、加法/乗法分解や古典的分解との比較を含む時系列分解の全体像は、別記事で扱っています。本記事はその先の「STLの中身」に集中します。
前提知識
この記事を読む前に、以下の記事を読んでおくと理解が深まります。
STLが分けるもの
STLは、時系列 $x_t$ を3つの成分の和に分解します。
$$ \begin{equation} x_t = T_t + S_t + R_t \end{equation} $$
ここで $T_t$ はトレンド(長期的な傾向)、$S_t$ は季節成分(周期 $s$ で繰り返すパターン)、$R_t$ は残差(どちらでも説明できない変動)です。STLは加法分解を前提としますが、季節振幅がトレンドに比例して伸びる乗法的なデータでも、対数をとれば $\log x_t = \log T_t + \log S_t + \log R_t$ と加法的に扱えるので、実用上の制約にはなりません。

上の図は、合成した月次データ(周期12)をSTLで分解した結果です。観測(最上段)に埋もれていた滑らかな上昇トレンド、12か月周期の季節、そしてランダムな残差がきれいに取り出せています。異常検知で見たいのは一番下の残差です。トレンドと季節という「予測できる構造」を取り除いた後に残る大きな逸脱こそが、異常の候補だからです。
では、STLはどうやってこの滑らかなトレンドや季節を推定するのでしょうか。すべての出発点は、LOESSという局所的な回帰です。
STLの心臓部 ― LOESS(局所重み付き回帰)
LOESS(LOcally Estimated Scatterplot Smoothing)は、「曲線全体を1本の式で表す」のではなく、各点のまわりだけを見て局所的に直線を当てることを繰り返して滑らかな曲線を作る手法です。グローバルなモデルを仮定しないので、複雑な形でも柔軟に追従できます。
ある点 $x_0$ における推定値は、近傍のデータ点に重みを付けた重み付き最小二乗の解として得られます。
$$ \begin{equation} \min_{\beta_0, \beta_1} \sum_{i=1}^{N} w_i(x_0)\,(y_i – \beta_0 – \beta_1 x_i)^2 \end{equation} $$
重み $w_i(x_0)$ は $x_0$ に近い点ほど大きく、遠い点ほど小さく(ある距離より遠いと0に)なります。この「近いものを重視」を実現するのがトリキューブ(tricube)重みです。
$$ \begin{equation} w_i(x_0) = W\!\left(\frac{|x_i – x_0|}{d_q(x_0)}\right), \quad W(u) = \begin{cases} (1 – u^3)^3 & 0 \le u < 1 \\ 0 & u \ge 1 \end{cases} \end{equation} $$
ここで $d_q(x_0)$ は $x_0$ から $q$ 番目に近い点までの距離で、これが「近傍の広さ(帯域幅)」を決めます。$q$ を大きくするほど多くの点に重みが乗って推定が滑らかになり、小さくするほど局所的で敏感になります。距離そのものではなく「$q$ 番目に近い点までの距離」で正規化するため、データ点の粗密が場所によって違っても近傍に含める点数が一定に保たれる、という利点があります。
当てはめる多項式の次数は通常1次か2次です。1次(局所直線)は計算が軽く端の挙動が安定しますが、曲率の大きい山や谷では系統的なズレ(バイアス)が出やすい。2次(局所放物線)は曲がった部分への追従がよい代わりに、近傍を広く取らないと分散が大きくなります。STLのトレンド平滑化では1次がよく使われます。

この図がLOESSの1ステップです。点 $x_0$(縦の点線)のまわりだけを見て、近い点(濃いオレンジ=重みが大きい)を重視した直線(紫)を当て、その直線上の $x_0$ での値(赤い星)を推定値とします。これを全ての点について行えば、滑らかな曲線が得られます。データ全体の形(右側で下がる)に引きずられず、$x_0$ 付近の上昇傾向だけを正しく捉えている点に注目してください。
重みの形と「近傍の広さ」が、LOESSの滑らかさを決めます。

左はトリキューブ重み $W(u)$ の形です。$u=0$(最も近い)で重み1、$u=1$(近傍の端)で滑らかに0になります。右は帯域幅(span)を変えたときの当てはめです。狭いと敏感に振動し、広いと滑らかになる——この帯域幅こそ、後で出てくるSTLのパラメータ $n_s$・$n_t$ の正体です。
LOESSは「1本の系列を滑らかにする」道具です。ところが季節成分の推定では、STLはこのLOESSを少し変わった向きに使います。
季節を「同じ周期位置どうし」で平滑化する ― cycle-subseries
STLの最も巧妙なアイデアが、季節成分の推定の仕方です。素朴には「季節は毎年同じパターン」と決め打ちしたくなりますが、STLは季節が年々少しずつ変化することを許します。どうやるかというと、時系列を時間順に平滑化するのではなく、同じ周期位置の点だけを集めて(cycle-subseries)、年方向に平滑化するのです。

左の図は、トレンドを除いた系列 $x_t – T_t$ を「同じ月」ごとに色分けしたものです。1月は1月だけ、7月は7月だけ、というように12本のサブシリーズに分けます。右の図は、そのうち1つの月(例: 7月)を年方向に並べたものです。横軸は「その月の何年目か」になっています。これをLOESSで平滑化すると、「7月の季節効果が年々どう変化したか」という滑らかな曲線が得られます。
この操作を12か月ぶん行い、時間順に戻すと季節成分 $S_t$ ができます。同じ周期位置を年方向に平滑化するから、季節パターンが時間とともに緩やかに変わることを自然に表現できるわけです。固定パターンを仮定する古典的分解との決定的な違いがここにあります。
このサブシリーズ平滑化は、STLの「内側ループ」の一部です。次に、ループ全体の流れを見ましょう。
内側ループ ― トレンドと季節を交互に精緻化する
トレンドを推定するには季節を取り除きたいし、季節を推定するにはトレンドを取り除きたい——両者は鶏と卵の関係です。STLはこれを、トレンドと季節を交互に更新する反復(内側ループ)で解きます。

内側ループの1周は、次の5ステップからなります。
- トレンド除去: 現在のトレンド推定を引き、$x_t – T_t$(季節+残差)を取り出す
- サブシリーズ平滑化: 前節のcycle-subseries平滑化で季節成分の候補を作る(LOESS、窓幅 $n_s$)
- 低周波フィルタ: 季節候補に紛れ込んだ低周波(トレンド的)成分を移動平均で取り除く。季節成分にトレンドが漏れ込むのを防ぐ
- 季節成分の確定: 候補からステップ3の低周波分を引き、$S_t$ を確定する
- トレンド更新: $x_t – S_t$(観測から季節を除いたもの)をLOESS(窓幅 $n_t$)で平滑化し、$T_t$ を更新する
最初は $T_t = 0$ から始め、この5ステップを $n_i$ 回(既定では2回程度)くり返すと、トレンドと季節が互いに矛盾しない安定した推定に収束します。ステップ2と4(季節側)、ステップ5(トレンド側)がいずれもLOESSで動いている点が、STLが「Loessを使った分解」と呼ばれる所以です。
なぜ少ない反復で収束するのでしょうか。各ステップは「相手の推定を所与として自分を最小二乗的に更新する」操作になっており、これは座標降下法に似た構造です。トレンドを固定して季節を更新し、季節を固定してトレンドを更新する——この交互更新を繰り返すたびに分解の食い違いが減っていきます。季節とトレンドは周波数帯が大きく異なる(季節は周期 $s$、トレンドはそれより長周期)ため互いの干渉が小さく、2〜3回で実用上十分な精度に達します。これがSTLの軽快さの一因です。
ちなみに、トレンドや季節の平滑化にスプラインやフーリエ級数ではなくLOESSを使うのは、局所的でノンパラメトリックな当てはめが、形を決め打ちせず端まで素直に推定でき、しかもロバスト重みを自然に組み込めるからです。重み付き最小二乗という同じ枠組みのまま外れ値対策まで一貫して扱える点が、STLの設計上の妙です。
ここまでで分解はできますが、もう1つ重要な仕掛けが残っています。外れ値への対処です。
外側ループ ― ロバスト重みで外れ値を抑える
実データには、センサーの誤動作や突発イベントによる外れ値が混じります。普通の最小二乗は外れ値に弱く、1点の異常がトレンドや季節の推定を引っ張ってしまいます。STLはこれを、内側ループの外側にもう1つの反復(外側ループ)を回して防ぎます。
外側ループでは、内側ループが出した残差 $R_t$ を見て、残差が大きい点ほど次の反復で重みを下げる「ロバスト重み」$\rho_t$ を計算します。典型的にはバイスクエア(bisquare)関数が使われます。
$$ \begin{equation} \rho_t = B\!\left(\frac{|R_t|}{h}\right), \quad B(u) = \begin{cases}(1 – u^2)^2 & 0 \le u < 1 \\ 0 & u \ge 1\end{cases} \end{equation} $$
ここで $h$ は残差の大きさの基準(残差の中央絶対偏差の定数倍)です。この $\rho_t$ を内側ループのLOESSの重みに掛け合わせると、外れ値は「ほとんど重み0」として扱われ、トレンドや季節の推定から実質的に締め出されます。

左がバイスクエア重みの形です。残差が小さいうちは重みがほぼ1ですが、残差が基準 $h$ を超えると一気に0へ落ちます。右は、外れ値(赤点)を3つ仕込んだデータでトレンドを推定した比較です。非ロバスト(破線)は外れ値に引っ張られてトレンドが波打つのに対し、ロバスト(緑)は外れ値を無視して滑らかなトレンドを保っています。これが robust=True の効果です。
外れ値がトレンド・季節に吸われず残差に残るからこそ、後で残差を見れば異常を検出できる——この性質は異常検知にとって本質的です。仕組みが分かったところで、挙動を決めるパラメータを整理しましょう。
パラメータ設計 ― $n_s$・$n_t$・$n_l$
STLの挙動は、主に3つの窓幅パラメータで決まります。いずれもLOESSや移動平均の「近傍の広さ」に対応する奇数です。
| パラメータ | 対応する平滑化 | 役割 |
|---|---|---|
| $n_s$ | サブシリーズ平滑化(季節) | 季節パターンの変化のしやすさ。小=年々変化を許容 / 大=ほぼ固定 |
| $n_t$ | トレンド平滑化 | トレンドの滑らかさ。小=細かい変動を追う / 大=大局のみ |
| $n_l$ | 低周波フィルタ | 季節候補から除く低周波の窓。通常 $s$ 以上の最小奇数 |
最も重要なのは $n_s$ です。季節成分の「変われる速さ」を直接制御します。

この図は、同じデータで $n_s$ を変えたときの季節成分です。$n_s$ が小さい($n_s=7$)と季節パターンが年ごとに振幅を変えられ、大きい($n_s=35$)とほぼ固定された波形になります。季節が安定しているデータなら大きめに、季節が経年変化するデータなら小さめに設定します。
$n_t$ はトレンドの滑らかさを決めます。

$n_t$ を小さくするとトレンドが細かい山谷まで追い、大きくすると滑らかな大局トレンドだけを残します。トレンドが季節の一部を吸ってしまったり、逆に季節がトレンドに漏れたりしないよう、$n_t$ は季節周期に対して十分大きく取るのが定石で、目安は $n_t \ge \lceil 1.5\,s/(1 – 1.5/n_s) \rceil$ です。$n_l$ は季節候補からトレンド的成分を除くフィルタ窓で、通常は周期 $s$ 以上の最小の奇数に自動設定されます。
これらのパラメータの自由度こそ、STLが古典的分解より柔軟である理由です。両者の違いを端的に見ておきましょう。
古典的分解との違い
移動平均でトレンドを取り、季節を期間平均で求める古典的分解は、概念は明快ですが「季節は固定」「外れ値に弱い」「データ端が欠損する」という弱点があります。STLはLOESSベースなので、これらを克服します。

この図はトレンド推定を比べたものです。古典的分解(オレンジ)は中心化移動平均を使うため、両端の $s/2$ 期分(赤い帯)が欠損します。最新データの端でトレンドが出ないのは、リアルタイム監視では致命的です。一方STL(緑)はLOESSで端まで外挿的に推定できるため、系列の最後までトレンドが得られます。
両手法の違いを整理すると次のようになります。
| 観点 | 古典的分解(移動平均) | STL |
|---|---|---|
| 季節パターンの変化 | 不可(全期間で固定) | 可能($n_s$ で制御) |
| 外れ値への頑健性 | 低い | 高い(ロバスト重み) |
| データ端の処理 | 両端 $s/2$ 期が欠損 | 端まで推定可 |
| パラメータ | ほぼなし | $n_s, n_t, n_l$(要調整) |
| 計算コスト | 低い | やや高い(反復+LOESS) |
| 理解のしやすさ | 容易 | やや複雑 |
季節パターンの経年変化を許容でき、ロバスト重みで外れ値に強く、端まで推定できる——STLが実データ向きの分解と言われる理由がここに集約されています。
最後に、本記事の出発点だった異常検知への接続を見ましょう。
STL残差にもとづく異常検知
トレンドと季節という「予測できる構造」をSTLで取り除くと、残った残差 $R_t$ には、構造で説明できない変動だけが残ります。残差が概ねホワイトノイズなら、$|R_t|$ が異常に大きい時点を異常とみなせます。閾値には、外れ値に強い中央絶対偏差(MAD)を使うのが定石です。
$$ \text{MAD} = \text{median}\big(|R_t – \text{median}(R_t)|\big), \qquad \hat{\sigma} = \frac{\text{MAD}}{0.6745} $$
ここで $0.6745$ は、正規分布で $\text{MAD} \approx 0.6745\,\sigma$ が成り立つことから来る換算係数です。$|R_t| > k\hat{\sigma}$ の時点を異常とします。
import numpy as np
from statsmodels.tsa.seasonal import STL
# 外れ値(t=30,78,119に+8)を仕込んだ月次データ y_out があるとする
res = STL(y_out, period=12, seasonal=13, trend=37, robust=True).fit()
resid = res.resid
mad = np.median(np.abs(resid - np.median(resid)))
sigma = mad / 0.6745
thr = 4 * sigma # STL残差は完全な正規ではないので、やや保守的に
anomalies = np.where(np.abs(resid) > thr)[0]
print(anomalies) # -> [30 78 119]

この図は、外れ値を3つ仕込んだデータをSTLで分解し、残差にMAD閾値をかけた結果です。上段の観測では、外れ値が季節変動に紛れて目立ちにくいことに注意してください。しかし下段の残差では、3つの外れ値だけが閾値(±0.89)を突き抜けており、仕込んだ3点を過不足なく検出できています(偽陽性ゼロ)。robust=True のおかげで外れ値がトレンド・季節に吸われず残差にきれいに残ったことが、この成功の鍵です。
ここで閾値を標準的な $3\hat{\sigma}$ ではなく $4\hat{\sigma}$ にしているのは、STL残差が端点付近などで完全な正規ノイズにはならず、わずかに裾が重くなるためです。閾値を少し保守的にすることで偽陽性を抑えています。「どの閾値で測るか」で検出結果が変わるという事実は、評価指標の議論とも地続きです。
なぜ生の観測値に直接閾値をかけず、わざわざSTLで分解してから残差を見るのでしょうか。観測値そのものは季節変動で大きく上下するので、「夏に値が高い」だけで異常と誤判定したり、逆に「冬の谷に隠れた異常」を見逃したりします。つまり何が正常かは季節やトレンドという文脈に依存するのです。STLで $T_t$ と $S_t$ を取り除けば、残差 $R_t$ は「その時刻に期待される水準からのズレ」になり、季節やトレンドに左右されない公平なものさしで異常を測れます。図の上段で外れ値が観測波形に紛れて見えにくいのに、下段の残差では明瞭に突き出ているのが、まさにこの効果です。これは「文脈の中での逸脱を捉える」という条件付き異常検知の発想そのものであり、STLはその最も古典的な実装と言えます。
実務では、残差にさらに自己相関が残っていないか(季節やトレンドの取り残しがないか)を確認し、必要なら $n_s$・$n_t$ を調整します。残差がきれいなホワイトノイズに近いほど、MAD閾値による異常判定の信頼性は高まります。
この「分解して残差を見る」発想を、深層学習や文脈情報と組み合わせて発展させたのがTime-CADやAE-STLです。STLという古典を理解しておくことは、こうした現代的な分解系異常検知を読み解く確かな足場になります。
STLの強みと限界、そして拡張
STLの強みは、ここまで見たとおり「季節の経年変化を許容」「外れ値に頑健」「端まで推定可能」「平滑度を $n_s$・$n_t$ で直感的に制御できる」点にあります。これらは、季節性のある実データを扱う多くの場面でそのまま利点になります。
一方で限界もあります。第一に、季節周期 $s$ は事前に与える必要があり、しかも一定だと仮定します。週次と年次のような複数の季節周期が重なるデータは、素のSTLではうまく扱えません。これに対しては、複数周期を順に剥がしていく MSTL(Multiple STL) という拡張があり、statsmodels にも実装されています。第二に、STLは加法分解が基本なので、振幅がトレンドに比例する乗法的な季節には対数変換などの前処理が要ります。第三に、内側・外側ループの反復とLOESSの当てはめを繰り返すため、単純な移動平均より計算コストが高めです(とはいえ実用上は十分高速です)。
異常検知の観点で重要なのは、STLが「正常な構造(トレンド+季節)のモデル」を作る前処理だという点です。STL単体は残差の大きさで異常を測るだけですが、ここに「文脈(イベントや休日)で分解を条件づける」発想を足すとTime-CADになり、「残差をオートエンコーダで再構成する」発想を足すとAE-STLになります。つまりSTLは、分解系異常検知という大きな系譜の共通の土台なのです。
まとめ
本記事では、STL分解のアルゴリズム内部を掘り下げました。
- STLはLOESSを土台に $x_t = T_t + S_t + R_t$ を加法分解する(乗法は対数で対応)
- LOESSは各点の近傍だけに(トリキューブ)重みを付けて局所直線を当てる。帯域幅が滑らかさを決める
- cycle-subseries平滑化(同じ周期位置を年方向にLOESS)により、季節パターンの経年変化を許容する
- 内側ループでトレンドと季節を交互に精緻化し、外側ループのロバスト重みで外れ値を抑える
- パラメータ $n_s$(季節の変化しやすさ)・$n_t$(トレンドの滑らかさ)・$n_l$ で挙動を制御する
- STL残差のMAD閾値で季節を考慮した異常検知ができ、Time-CADやAE-STLなど分解系異常検知の前提になる
時系列をトレンド・季節・残差という意味のある成分に分けることは、予測でも異常検知でも分析の出発点です。STLの中身を理解すれば、「なぜこの分解になるのか」「パラメータをどう動かせばよいか」を根拠を持って判断できるようになります。
次のステップとして、以下の記事も参考にしてください。