1. はじめに — 異常検知の基本枠組み

異常検知(Anomaly Detection)は、「正常な状態を多数集めておき、それに似ていないデータを異常とみなす」アプローチです。「不良品 1 万枚を集めてください」という発注はほぼ不可能なため、異常品ラベルを集める教師あり分類とは違い、正常データだけで学習する手法が現場では主役になります。

本記事は、シンプルな統計的手法から、scikit-learn の主要な異常検知アルゴリズムまでを順番にたどり、「現場でどれを使うべきか」の判断材料を提供します。

2. 環境構築とサンプルデータ

本記事で使うアルゴリズムの公式 API ドキュメントは scikit-learn 公式サイト に集約されています。

python -m pip install scikit-learn pandas matplotlib numpy matplotlib-fontja

グラフの日本語化には matplotlib-fontja を使います(かつて定番だった japanize-matplotlib は Python 3.12 以降で動作しません)。社内プロキシで pip が通らない場合は 学習ロードマップ STEP 2 を参照してください。なお、本記事に記載する検出件数はすべて scikit-learn 1.9 / pandas 3.0 / Python 3.12(Windows 11)で実測した値です。バージョンが違うと件数は数件前後することがあります。

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)

# 正常データ: 温度 25 ± 1°C、振動 0.5 ± 0.1 m/s²
n = 1000
df = pd.DataFrame({
    "temperature": 25 + rng.normal(0, 1, n),
    "vibration": 0.5 + rng.normal(0, 0.1, n),
})

# 異常データを少し混ぜる
anomalies = pd.DataFrame({
    "temperature": [30, 31, 18, 25, 25],   # 高温・低温
    "vibration":   [0.5, 0.4, 0.6, 1.2, 0.05],  # 振動異常
})
df = pd.concat([df, anomalies], ignore_index=True)
# 10 章の時系列特徴量で使うタイムスタンプ列(1 分周期。pandas 3.x では小文字の "1min")
df["ts"] = pd.date_range("2026-08-01 09:00", periods=len(df), freq="1min")
df.to_csv("samples.csv", index=False, encoding="utf-8-sig")

このダミーデータで、本記事の全手法を実行できます(以降の各節のコードは、前の節の df を引き継ぐ続きものです。節ごとに別ファイルで試す場合は 3 章の read_csv から貼ってください)。実データへの置き換え時は、温度・振動の収集側を Modbus 記事シリアル通信記事で作れます。なお振動は本記事では加速度(m/s²)扱いですが、設備診断の現場では ISO 20816 系の振動速度(mm/s RMS)で管理することが多い点だけ頭の隅に。

3. 手法 1: しきい値(3σ 法)

1 変数の異常検知でもっとも素朴な手法は、平均 ± 3σ から外れたら異常とみなす方法です。正規分布なら 99.7% が ±3σ 内に収まるので、外れる 0.3% を異常候補とします。

import numpy as np
import pandas as pd

df = pd.read_csv("samples.csv", parse_dates=["ts"])
mu = df["temperature"].mean()
sigma = df["temperature"].std()

upper = mu + 3 * sigma
lower = mu - 3 * sigma
df["is_anomaly_3sigma"] = (df["temperature"] > upper) | (df["temperature"] < lower)
print(f"3σ範囲: [{lower:.2f}, {upper:.2f}]")
print(df[df["is_anomaly_3sigma"]].head())

実行すると 3σ範囲: [21.85, 28.10] と表示され、5 件が検出されます(実測)——仕込んだ温度異常 3 件(30・31・18℃ の行)と、正常データの端が 2 件(誤検知)。一方、仕込んだ振動異常 2 件(1.2 と 0.05 m/s²)は温度の 3σ では捉えられません。これが後半の多変量手法に進む理由です。

長所: シンプル、計算コストゼロ、説明しやすい。短所: 正規分布の前提がある。複数変数の関係性は見られない。

4. 手法 2: IQR(四分位範囲)

3σ は外れ値そのものに強く影響を受けるため、外れ値が多いデータでは過大に評価されがちです。IQR は第 1 四分位点(Q1)と第 3 四分位点(Q3)——データを小さい順に並べた 25% 地点と 75% 地点——を使うため、外れ値の影響を受けにくい手法です。

q1 = df["temperature"].quantile(0.25)
q3 = df["temperature"].quantile(0.75)
iqr = q3 - q1
upper = q3 + 1.5 * iqr
lower = q1 - 1.5 * iqr
df["is_anomaly_iqr"] = (df["temperature"] > upper) | (df["temperature"] < lower)

箱ひげ図のヒゲの外側を異常とみなすイメージです。1.5 という係数は統計学者 Tukey 由来の慣習値で、正規分布なら約 ±2.7σ 相当——つまり 3σ よりやや敏感です(サンプルデータでは 12 件検出。3σ の 5 件より多くなります。筆者環境で実測)。3σ が動かないデータ(非対称分布、外れ値多発)でも機能しやすい。

5. 手法 3: Isolation Forest

scikit-learn の代表的な異常検知アルゴリズムです(IsolationForest 公式 API)。「ランダムに分割して、孤立しやすいデータほど異常」という直感に基づき、決定木のアンサンブルで検出します。多変量・大量データに強く、複数センサーの関係性を含む異常検知では第一選択になりやすい手法です。

from sklearn.ensemble import IsolationForest

X = df[["temperature", "vibration"]].values

clf = IsolationForest(
    contamination=0.01,  # 異常の想定割合(事前知識から設定)
    random_state=42,
)
clf.fit(X)
df["is_anomaly_iforest"] = clf.predict(X) == -1  # -1 が異常、+1 が正常
print(df[df["is_anomaly_iforest"]])

サンプルデータでは 11 件が検出されます(実測)。注目してほしいのは、温度は正常なのに振動が 1.2 m/s² の行——3σ(温度)では見逃した異常——をきちんと捕まえている点です。contamination=0.01 × 約 1000 行 ≒ 10 件前後が「異常枠」になるため、真の異常 5 件+境界付近の正常データが数件、という内訳になります。

ポイント:

  • contamination は「異常の割合」の事前推定で、実質「検出件数の枠」を決める。現場では「0.01〜0.05」程度から始めて調整
  • 分布の仮定がない(ノンパラメトリック)
  • 説明性は低めだが、多変量の関係性を含む異常では古典手法より検出力が高い

6. 手法 4: One-Class SVM

SVM の派生で、「正常データの境界」を学習します(OneClassSVM 公式 API)。Isolation Forest より小規模データに向き、複雑な境界を表現できます。

from sklearn.svm import OneClassSVM
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

clf = OneClassSVM(nu=0.01, kernel="rbf", gamma="scale")
clf.fit(X_scaled)
df["is_anomaly_ocsvm"] = clf.predict(X_scaled) == -1

SVM 系はスケーリングが必須です。StandardScaler で標準化してから渡すのが定石です。nucontamination と同様の意味で「訓練データ中の異常の割合の上限」を表します——が、サンプルデータでは nu=0.01 でも 48 件(約 4.8%)が異常判定になります(実測)。境界が複雑に歪んで正常データを外に出してしまうためで、gamma を小さくするか nu を下げて調整します。

もう 1 つ大事な区別があります。Isolation Forest / LOF が「異常が混ざったデータごと fit する(outlier detection)」のに対し、One-Class SVM は本来「正常と分かっているクリーンなデータだけで fit する(novelty detection)」向きの手法で、訓練データへの異常混入に敏感です。本記事では混入率が低いので全体で fit していますが、実運用では「正常期間のデータだけで fit → 新しいデータを predict」の形が基本です(1 章の「正常データだけで学習する」はこの使い方を指します)。

7. 手法 5: Local Outlier Factor(LOF)

「自分の周辺の密度と、近傍の密度を比べて、自分が周りより疎なら異常」というロジックです。クラスタが複数あるような複雑なデータに強い手法です。

from sklearn.neighbors import LocalOutlierFactor

clf = LocalOutlierFactor(n_neighbors=20, contamination=0.01)
df["is_anomaly_lof"] = clf.fit_predict(X) == -1  # サンプルデータでは 11 件検出(実測)

LOF はpredict が独立して呼べない(学習データに対する判定しかしない)API なので、運用に組み込むときは novelty=True に切り替えて fitpredict を分けます。

8. 手法選定のフローチャート

  1. 1 変数だけ・分布が正規 → 3σ 法
  2. 1 変数・分布が歪んでいる or 外れ値が多い → IQR
  3. 多変量・データ大量・とにかく現場で動かしたい → Isolation Forest
  4. 多変量・データ少量・複雑な境界 → One-Class SVM
  5. 多変量・複数のクラスタが混在 → LOF

製造業の現場では Isolation Forest がデフォルトとして強く、特殊事情がない限りこれから始めるのが推奨です。

9. 可視化で結果を確認する

import matplotlib_fontja  # noqa: F401
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 6))
normal = df[~df["is_anomaly_iforest"]]
anomaly = df[df["is_anomaly_iforest"]]
ax.scatter(normal["temperature"], normal["vibration"], alpha=0.5, label="正常")
ax.scatter(anomaly["temperature"], anomaly["vibration"], color="red", label="異常")
ax.set_xlabel("温度")
ax.set_ylabel("振動")
ax.legend()
ax.set_title("Isolation Forest の判定結果")
fig.tight_layout()
fig.savefig("anomaly.png", dpi=150)  # カレントフォルダに保存される(画面に出したいなら plt.show())

判定結果は必ず可視化して、人間の感覚と合うかを確認してください。「明らかに異常な点が正常になっている」「明らかに正常な点が異常になっている」場合は、特徴量の選び方や contamination の設定を見直します。

10. 時系列データへの応用

製造データは時系列が多いため、「現在値だけでなく直近の動き」を特徴量に加えると検知精度が上がります。

# 直近 5 サンプルの平均・標準偏差・差分を特徴量に追加
# (2 章のデータは 1 分周期なので 5 サンプル = 直近 5 分。周期が違えば読み替える)
df = df.sort_values("ts")
df["temp_mean5"] = df["temperature"].rolling(5).mean()
df["temp_std5"]  = df["temperature"].rolling(5).std()
df["temp_diff"]  = df["temperature"].diff()
df = df.dropna()

features = ["temperature", "vibration", "temp_mean5", "temp_std5", "temp_diff"]
clf = IsolationForest(contamination=0.01, random_state=42)
clf.fit(df[features])
df["is_anomaly"] = clf.predict(df[features]) == -1

「絶対値は正常範囲だが、急変している」のようなパターンも捉えられるようになります(サンプルデータでは 10 件検出。筆者環境で実測)。もう 1 つ、時系列で本気で精度評価するなら訓練と評価を時間で分割してください——「先頭 80% で fit し、残り 20% を predict する」形です。過去と未来を混ぜて fit→predict した精度は、実運用(過去データで学習して将来を判定する)より楽観的に出ます。

11. オンライン推論への組み込み

学習済みモデルをファイルに保存し、別プロセスから読み込んで推論する流れです。

import joblib

# 学習側
clf = IsolationForest(contamination=0.01, random_state=42)
clf.fit(X)
joblib.dump(clf, "iforest.joblib")

# 推論側(別プロセス、別ホストでも可)
clf = joblib.load("iforest.joblib")
result = clf.predict([[26.0, 0.55]])  # -1 なら異常

監視アプリの中に直接埋め込むより、「学習」と「推論」のプロセスを分けるほうが、再学習のオペレーションが楽になります。注意点として、joblib で保存したモデルは学習時と推論時の scikit-learn バージョンが違うと互換性の警告や不整合が起きえます。requirements.txt でバージョンを固定し、学習側と推論側で揃えてください。

12. 運用設計のポイント

12.1 ラベルなし学習でも、運用では「正解」が必要

異常検知のモデルは正常データから学習しますが、運用では「実際に NG が出たときの結果ログ」と突き合わせて、検知精度を継続的に評価します。「アラート → 人間の確認 → 結果記録」のループを設計に組み込みます。

12.2 再学習の頻度

製造ラインは経年で挙動が変わるため、固定モデルだと徐々に検知精度が落ちます。月 1 回〜四半期に 1 回の再学習を運用に組み込むのが現実的です。再学習のたびにモデルを iforest_v2.joblib のようにバージョン管理します。

12.3 アラート設計

異常検知は誤検知(False Positive)が必ず出ます。1 件単発の検知で即停止せず、N 分間に M 件以上連続したらアラートのような時間幅判定を入れると、現場での実用性が上がります。ヒステリシス(発報と復帰でしきい値を分ける)とクールダウン(連続発報の抑止)も併用します——実装例は Modbus 記事のパターン B にあります。

13. ディープラーニングへの橋渡し

古典手法で扱いづらい例として、以下があります。

  • 非定常な時系列(季節性、トレンドが強い)
  • 高次元データ(数十センサー、画像、音声)
  • 複雑な状態遷移を含む工程

これらには Autoencoder ベースの異常検知や、Anomalib のような専用フレームワークが効きます。ただし、まずは Isolation Forest で 1 度実装し、限界を見てから進むほうが投資効率が良いです。

14. おわりに

異常検知は、しきい値判定の延長として無理なく現場に導入できる機械学習の入り口です。Isolation Forest を主軸に、簡単な特徴量設計と再学習フローを組み込むだけで、「単純しきい値では捉えられなかった連動異常」を高い精度で検出できるようになります。

本記事のサンプルコードはすべてダミーデータで再現できます。手元の現場のデータに置き換えて、まずは小さなラインから試してみてください。

⚠️ 実機運用時の注意: 本記事の異常検知モデルは、設備停止・アラート・出荷判定の最終トリガーとして単独で使わないことを前提としています。教師なし異常検知は本質的に見逃し(False Negative)と誤検知(False Positive)を避けられません。実務では: ①N 分間に M 件以上の連続検知でアラート化する等の後段ルール、②複数手法(3σ / Isolation Forest / OCSVM 等)の多数決、③人間のオペレータレビュー、④閾値・パラメータは現場条件で継続的にチューニング、⑤モデルのバージョン管理と再学習フロー、を組み合わせて使ってください。24 時間稼働機器の停止・切り替え判断は、自社の安全基準と保全側の承認を経ることが前提です。本記事のモデル出力による不良流出・過剰停止・機会損失について筆者・GenbaPy は責任を負いません。

関連記事

参考文献・一次情報