100本ノック / 異常検知 / 異常検知100本ノック
製造業のセンサーデータ異常検知入門|急変・張り付き・関係崩れをPythonで実践
センサーデータの異常を「止める判断」に変える:設備監視の実践10本ノック
製造設備の温度・振動・圧力・電流を題材に、センサー固有の異常と設備状態の異常を切り分け、保全担当者が確認すべき設備・時刻・原因候補を絞り込む方法を扱います。対象は異常検知100本ノックの No.041〜No.050(第5章:センサーデータ異常検知) です。
単一のしきい値だけで判断せず、急変、張り付き、物理限界、センサー間の関係、設備別集計を段階的に組み合わせます。架空データを使い、外部データには依存しません。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
はじめに:この記事で扱う製造業の実務課題
設備監視の目的は、異常らしい点を大量に表示することではありません。目的は、品質不良や突発停止につながる兆候を早めに捉え、継続運転・現場確認・計画保全・緊急停止の判断を支えることです。
今回は、同型の加工設備3台に複数センサーが付いている現場を想定します。監視担当者は「値が高い」だけでなく、計測器の故障なのか、負荷変動なのか、設備劣化なのかを短時間で見分ける必要があります。
現場でよくある状況
- サンプリング周期や単位、欠測表現がセンサーごとに違う
- 起動・停止・品種切替による正常な変化がアラートになる
- 通信断やセンサー故障が「0」として記録される
- 単独では正常範囲でも、温度と電流などの関係が崩れている
- 設備台数が多く、個々のグラフを人が見続けられない
なぜこの問題は判断が難しいのか
センサーデータは自己相関を持つ時系列であり、設備・運転条件・経年状態によって正常範囲が変わります。さらに、統計的な外れと業務上の危険は一致しません。したがって、検知ルールには「物理的意味」「継続時間」「複数信号の整合性」「誤検知時と見逃し時のコスト」を組み込む必要があります。
今回扱うノックの全体像
| No. | テーマ | 現場で答えたい問い |
|---|---|---|
| 041 | センサーデータの特徴 | 監視前に何を確認するか |
| 042 | 複数センサー時系列 | 解析しやすい形にどう整えるか |
| 043 | センサー別分布 | 通常域や裾の広がりはどう違うか |
| 044 | 急上昇・急下降 | 瞬間的な変化をどう拾うか |
| 045 | ゼロ張り付き | 通信断・故障らしい連続ゼロをどう拾うか |
| 046 | 上限・下限超過 | 管理基準違反をどう即時判定するか |
| 047 | センサー間相関 | 正常時の連動をどう把握するか |
| 048 | 関係崩れ | 単変量では見えない異常をどう拾うか |
| 049 | 設備別比較 | どの設備を優先確認するか |
| 050 | 現場目線の解釈 | 検知を保全アクションへどう変えるか |
検知は一本道ではなく、データ品質 → 単変量ルール → 多変量の整合性 → 設備別優先度 → 現場確認 の順で判断材料を積み上げます。
Python環境の準備
numpy と pandas でデータを作成・集計し、matplotlib で可視化します。乱数シードを固定して再現性を確保します。グラフ内は環境依存の文字化けを避けるため英語表記とし、読み取りは本文で詳しく説明します。
import sys
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
from IPython.display import display
SEED = 42
rng = np.random.default_rng(SEED)
pd.set_option("display.max_columns", 20)
print(f"Python {sys.version.split()[0]} / pandas {pd.__version__} / matplotlib {matplotlib.__version__}")
Python 3.13.1 / pandas 3.0.3 / matplotlib 3.11.0
架空データの作成
10分周期で40時間、設備A〜Cを観測します。電流(負荷)が上がると温度・振動が上がり、圧力も緩やかに連動するように生成します。そのうえで、設備Bの振動スパイク、設備Cの圧力ゼロ張り付き、設備Aの温度上昇、設備Bの「電流に対して振動だけ高い」関係崩れを埋め込みます。injected_event は教材上の答え合わせ用で、実運用では通常存在しません。
timestamps = pd.date_range("2026-06-01 08:00", periods=240, freq="10min")
rows = []
offsets = {"設備A": (0.0, 0.00), "設備B": (1.0, 0.08), "設備C": (-0.7, -0.05)}
for machine, (temp_offset, vib_offset) in offsets.items():
phase = {"設備A": 0.0, "設備B": 0.8, "設備C": 1.6}[machine]
t = np.arange(len(timestamps))
load = 64 + 8*np.sin(2*np.pi*t/72 + phase) + rng.normal(0, 2.0, len(t))
temperature = 38 + temp_offset + 0.36*load + rng.normal(0, 0.8, len(t))
vibration = 0.75 + vib_offset + 0.018*load + rng.normal(0, 0.07, len(t))
pressure = 4.0 + 0.020*load + rng.normal(0, 0.08, len(t))
for i, ts in enumerate(timestamps):
rows.append([ts, machine, temperature[i], vibration[i], pressure[i], load[i], "normal"])
df = pd.DataFrame(rows, columns=["timestamp", "machine", "temperature_c", "vibration_mm_s", "pressure_mpa", "current_a", "injected_event"])
def inject(machine, start, end, column, operation, label):
mask = (df["machine"] == machine) & df.groupby("machine").cumcount().between(start, end)
df.loc[mask, column] = operation(df.loc[mask, column].to_numpy())
df.loc[mask, "injected_event"] = label
inject("設備B", 75, 75, "vibration_mm_s", lambda x: x + 2.1, "vibration_spike")
inject("設備C", 120, 128, "pressure_mpa", lambda x: np.zeros_like(x), "pressure_zero_stuck")
inject("設備A", 170, 180, "temperature_c", lambda x: x + np.linspace(8, 13, len(x)), "temperature_high")
inject("設備B", 195, 207, "vibration_mm_s", lambda x: x + 0.75, "relation_break")
df = df.sort_values(["machine", "timestamp"]).reset_index(drop=True)
print(f"行数: {len(df):,} / 設備数: {df.machine.nunique()} / 期間: {df.timestamp.min()} 〜 {df.timestamp.max()}")
display(df.head())
行数: 720 / 設備数: 3 / 期間: 2026-06-01 08:00:00 〜 2026-06-02 23:50:00
| timestamp | machine | temperature_c | vibration_mm_s | pressure_mpa | current_a | injected_event | |
|---|---|---|---|---|---|---|---|
| 0 | 2026-06-01 08:00:00 | 設備A | 60.557908 | 1.928585 | 5.284494 | 64.609434 | normal |
| 1 | 2026-06-01 08:10:00 | 設備A | 60.466810 | 1.977436 | 5.342587 | 62.617278 | normal |
| 2 | 2026-06-01 08:20:00 | 設備A | 60.674249 | 1.960428 | 5.155343 | 66.890088 | normal |
| 3 | 2026-06-01 08:30:00 | 設備A | 61.288969 | 2.013785 | 5.239303 | 67.951682 | normal |
| 4 | 2026-06-01 08:40:00 | 設備A | 62.323670 | 1.877039 | 5.182851 | 62.834091 | normal |
No.041:センサーデータの特徴を整理する
実務での意味
列名だけでは、単位、周期、欠測表現、物理範囲、校正履歴は分かりません。監視ルールを作る前に「1行が何を表すか」と各信号の品質を確認しなければ、設備異常と計測異常を取り違えます。
分析・モデル化の考え方
最低限、データ型、欠損率、ユニーク数、最小・最大、時間間隔をデータプロファイルとして残します。特にユニーク数が極端に少ない信号や一定値の継続は、センサー固着の候補です。今回の10分周期では、差分1点は10分間の変化を意味します。
Pythonで確認する
sensor_cols = ["temperature_c", "vibration_mm_s", "pressure_mpa", "current_a"]
profile = pd.DataFrame({
"dtype": df[sensor_cols].dtypes.astype(str),
"missing_rate_pct": df[sensor_cols].isna().mean().mul(100),
"unique_values": df[sensor_cols].nunique(),
"min": df[sensor_cols].min(),
"max": df[sensor_cols].max(),
}).round(3)
intervals = df.groupby("machine")["timestamp"].diff().dropna().value_counts()
display(profile)
print("主な観測間隔:", intervals.index[0], "/ 該当行数:", intervals.iloc[0])
| dtype | missing_rate_pct | unique_values | min | max | |
|---|---|---|---|---|---|
| temperature_c | float64 | 0.0 | 720 | 55.242 | 75.080 |
| vibration_mm_s | float64 | 0.0 | 720 | 1.568 | 4.222 |
| pressure_mpa | float64 | 0.0 | 712 | 0.000 | 5.669 |
| current_a | float64 | 0.0 | 720 | 50.810 | 76.257 |
主な観測間隔: 0 days 00:10:00 / 該当行数: 717
結果の読み取り
欠損はなく観測間隔も10分で揃っています。一方、圧力の最小値は0で、通常運転の圧力として不自然です。最大値だけでなく「あり得ない値」と「継続性」を確認する必要があります。実務ではこの表に単位、タグID、レンジ、校正日、設備状態を追加します。
No.042:複数センサーの時系列データを読み込む
実務での意味
現場データはセンサーごとの縦持ちで収集されることも多く、分析時には同一時刻・同一設備で横に並べる必要があります。結合のずれは、存在しない相関崩れを生みます。
分析・モデル化の考え方
キーを timestamp × machine とし、重複の有無を確認してからwide形式にします。時刻が完全一致しない場合は、許容時間を定めた近傍結合やリサンプリングが必要です。ここでは整形済みデータを一度long形式へ変換し、読み込み後の標準形を確認します。
Pythonで確認する
long_df = df.melt(
id_vars=["timestamp", "machine"], value_vars=sensor_cols,
var_name="sensor", value_name="value"
)
wide_df = long_df.pivot(index=["timestamp", "machine"], columns="sensor", values="value").reset_index()
duplicate_keys = df.duplicated(["timestamp", "machine"]).sum()
print("long形式:", long_df.shape, "/ wide形式:", wide_df.shape, "/ キー重複:", duplicate_keys)
display(long_df.head(8))
display(wide_df.head(3))
long形式: (2880, 4) / wide形式: (720, 6) / キー重複: 0
| timestamp | machine | sensor | value | |
|---|---|---|---|---|
| 0 | 2026-06-01 08:00:00 | 設備A | temperature_c | 60.557908 |
| 1 | 2026-06-01 08:10:00 | 設備A | temperature_c | 60.466810 |
| 2 | 2026-06-01 08:20:00 | 設備A | temperature_c | 60.674249 |
| 3 | 2026-06-01 08:30:00 | 設備A | temperature_c | 61.288969 |
| 4 | 2026-06-01 08:40:00 | 設備A | temperature_c | 62.323670 |
| 5 | 2026-06-01 08:50:00 | 設備A | temperature_c | 60.289633 |
| 6 | 2026-06-01 09:00:00 | 設備A | temperature_c | 61.694617 |
| 7 | 2026-06-01 09:10:00 | 設備A | temperature_c | 63.933736 |
| sensor | timestamp | machine | current_a | pressure_mpa | temperature_c | vibration_mm_s |
|---|---|---|---|---|---|---|
| 0 | 2026-06-01 08:00:00 | 設備A | 64.609434 | 5.284494 | 60.557908 | 1.928585 |
| 1 | 2026-06-01 08:00:00 | 設備B | 72.972598 | 5.339398 | 64.212102 | 2.096983 |
| 2 | 2026-06-01 08:00:00 | 設備C | 69.809115 | 5.443274 | 62.014050 | 1.925339 |
結果の読み取り
720行×4信号が、long形式では2,880行、wide形式では720行に復元され、キー重複はありません。実務ではここで設備マスタとの対応、タイムゾーン、サンプリング遅延も検査し、整形処理を毎回同じ手順で再実行できるようにします。
No.043:センサーごとの分布を確認する
実務での意味
分布を見ると、通常域、左右の歪み、複数の運転モード、物理的に不自然な裾を把握できます。全設備を一括集計すると設備差を異常と誤認するため、設備別の要約も必要です。
分析・モデル化の考え方
平均・標準偏差だけでなく、中央値と分位点を併用します。異常の影響を受けにくい中央値やIQRは基準作りに有効です。ただし、分布は時間順序を失うため、ヒストグラムだけで検知を完結させません。
Pythonで確認する
summary = df.groupby("machine")[sensor_cols].agg(["mean", "std", "median", "min", "max"]).round(2)
display(summary)
fig, axes = plt.subplots(2, 2, figsize=(11, 7))
for ax, col in zip(axes.ravel(), sensor_cols):
ax.hist(df[col], bins=30, color="#2878B5", alpha=0.8, edgecolor="white")
ax.set_title(col)
ax.set_xlabel("value")
ax.set_ylabel("count")
ax.grid(alpha=0.25)
fig.suptitle("Sensor distributions", fontsize=14)
plt.tight_layout()
plt.show()
| temperature_c | vibration_mm_s | pressure_mpa | current_a | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| mean | std | median | min | max | mean | std | median | min | max | mean | std | median | min | max | mean | std | median | min | max | |
| machine | ||||||||||||||||||||
| 設備A | 61.71 | 3.30 | 61.53 | 55.89 | 75.08 | 1.90 | 0.12 | 1.91 | 1.57 | 2.27 | 5.29 | 0.13 | 5.28 | 4.96 | 5.61 | 64.46 | 5.88 | 65.21 | 52.97 | 76.26 |
| 設備B | 62.28 | 2.35 | 62.25 | 55.84 | 67.40 | 2.04 | 0.24 | 2.01 | 1.70 | 4.22 | 5.28 | 0.15 | 5.28 | 4.94 | 5.64 | 64.69 | 6.02 | 65.30 | 53.16 | 74.73 |
| 設備C | 60.44 | 2.35 | 60.54 | 55.24 | 65.40 | 1.86 | 0.12 | 1.86 | 1.58 | 2.18 | 5.09 | 1.02 | 5.28 | 0.00 | 5.67 | 64.24 | 6.01 | 64.35 | 50.81 | 76.22 |

結果の読み取り
圧力には0付近の独立した山、温度と振動には上側の裾が見えます。これらは詳細確認の入口です。一方、設備Bの平均振動が高めでも、それだけで故障とは断定できません。設備固有の基準と時系列上の発生位置を重ねて判断します。
No.044:センサー値の急上昇・急下降を検出する
実務での意味
瞬間的な衝撃、断線、制御不安定は、値そのものが管理範囲内でも前回値からの急変として現れます。軸受振動のスパイクなど、短時間イベントの拾い上げに有効です。
分析・モデル化の考え方
設備ごとの差分を と定義し、絶対差が業務しきい値を超えた点を検知します。差分はノイズも増幅するため、しきい値はセンサー分解能、周期、正常時差分の分布から決めます。今回は振動の10分差が0.8 mm/s超を急変とします。
Pythonで確認する
df["vibration_diff"] = df.groupby("machine")["vibration_mm_s"].diff()
df["rapid_vibration"] = df["vibration_diff"].abs() > 0.8
rapid = df.loc[df["rapid_vibration"], ["timestamp", "machine", "vibration_mm_s", "vibration_diff", "injected_event"]]
display(rapid.round(3))
b = df[df.machine == "設備B"]
fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(b.timestamp, b.vibration_mm_s, label="vibration", lw=1.3)
hit = b[b.rapid_vibration]
ax.scatter(hit.timestamp, hit.vibration_mm_s, color="crimson", label="rapid change", zorder=3)
ax.set_title("Rapid vibration changes - Machine B")
ax.set_xlabel("timestamp"); ax.set_ylabel("vibration (mm/s)")
ax.grid(alpha=0.3); ax.legend(); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_19414/1863854110.py:4: UserWarning: obj.round has no effect with datetime, timedelta, or period dtypes. Use obj.dt.round(...) instead.
display(rapid.round(3))
| timestamp | machine | vibration_mm_s | vibration_diff | injected_event | |
|---|---|---|---|---|---|
| 315 | 2026-06-01 20:30:00 | 設備B | 4.222 | 2.258 | vibration_spike |
| 316 | 2026-06-01 20:40:00 | 設備B | 2.099 | -2.124 | normal |
| 448 | 2026-06-02 18:40:00 | 設備B | 1.969 | -0.912 | normal |

結果の読み取り
スパイクの立ち上がりと戻りの両方が検知されます。1つの事象から2アラート出るため、実運用では近接アラートを1イベントに束ねます。また、品種切替や起動時刻と照合し、正常な急変を抑制することが重要です。
No.045:センサー値のゼロ張り付きを検出する
実務での意味
連続ゼロは設備停止の結果かもしれませんが、通信断、断線、センサー故障でも起こります。他の信号が稼働を示すのに圧力だけがゼロなら、計測系異常の可能性が高まります。
分析・モデル化の考え方
ゼロ判定 を作り、設備内の連続区間(run)を数えます。浮動小数では完全一致を避け、小さな許容幅 を設けます。今回は3点、すなわち30分以上の連続を張り付きとします。
Pythonで確認する
is_zero = df["pressure_mpa"].abs() < 0.01
run_id = (~is_zero).groupby(df["machine"]).cumsum()
df["zero_run_length"] = is_zero.groupby([df["machine"], run_id]).transform("sum")
df["pressure_zero_stuck"] = is_zero & (df["zero_run_length"] >= 3)
stuck_summary = (df[df.pressure_zero_stuck]
.groupby("machine").agg(start=("timestamp", "min"), end=("timestamp", "max"), points=("timestamp", "size"), mean_current_a=("current_a", "mean")))
display(stuck_summary.round(2))
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_19414/85408853.py:7: UserWarning: obj.round has no effect with datetime, timedelta, or period dtypes. Use obj.dt.round(...) instead.
display(stuck_summary.round(2))
| start | end | points | mean_current_a | |
|---|---|---|---|---|
| machine | ||||
| 設備C | 2026-06-02 04:00:00 | 2026-06-02 05:20:00 | 9 | 63.23 |
結果の読み取り
設備Cで9点、90分の圧力ゼロ張り付きが検出され、その間も電流が流れています。したがって単なる設備停止より、圧力センサーまたは通信経路の確認を優先する判断が合理的です。PLCの運転フラグがあれば、この切り分けはさらに確実になります。
No.046:センサー値の上限・下限超過を検出する
実務での意味
安全・品質・設備仕様に由来する上下限は、統計モデルより優先すべき明確な業務ルールです。危険域への侵入は即時通知、警戒域は継続時間を見て通知するなど、段階的な設計ができます。
分析・モデル化の考え方
今回は例として温度70°C、振動3.0 mm/s、圧力4.7〜5.7 MPa、電流45〜85 Aを管理範囲とします。これは教材上の仮定であり、実務ではメーカー仕様、工程能力、品質影響、計測誤差を踏まえて設備・品種別に承認します。
Pythonで確認する
limits = {
"temperature_c": (None, 70.0),
"vibration_mm_s": (None, 3.0),
"pressure_mpa": (4.7, 5.7),
"current_a": (45.0, 85.0),
}
limit_flags = []
for col, (lower, upper) in limits.items():
flag = pd.Series(False, index=df.index)
if lower is not None: flag |= df[col] < lower
if upper is not None: flag |= df[col] > upper
name = f"limit_{col}"
df[name] = flag
limit_flags.append(name)
df["any_limit_violation"] = df[limit_flags].any(axis=1)
limit_counts = df.groupby("machine")[limit_flags].sum().astype(int)
display(limit_counts)
display(df[df.any_limit_violation][["timestamp", "machine", *sensor_cols, "injected_event"]].head(12).round(2))
| limit_temperature_c | limit_vibration_mm_s | limit_pressure_mpa | limit_current_a | |
|---|---|---|---|---|
| machine | ||||
| 設備A | 10 | 0 | 0 | 0 |
| 設備B | 0 | 1 | 0 | 0 |
| 設備C | 0 | 0 | 9 | 0 |
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_19414/3734468854.py:18: UserWarning: obj.round has no effect with datetime, timedelta, or period dtypes. Use obj.dt.round(...) instead.
display(df[df.any_limit_violation][["timestamp", "machine", *sensor_cols, "injected_event"]].head(12).round(2))
| timestamp | machine | temperature_c | vibration_mm_s | pressure_mpa | current_a | injected_event | |
|---|---|---|---|---|---|---|---|
| 171 | 2026-06-02 12:30:00 | 設備A | 70.03 | 2.01 | 5.37 | 70.55 | temperature_high |
| 172 | 2026-06-02 12:40:00 | 設備A | 71.02 | 2.09 | 5.33 | 68.23 | temperature_high |
| 173 | 2026-06-02 12:50:00 | 設備A | 71.91 | 1.86 | 5.33 | 66.14 | temperature_high |
| 174 | 2026-06-02 13:00:00 | 設備A | 71.33 | 1.97 | 5.28 | 65.44 | temperature_high |
| 175 | 2026-06-02 13:10:00 | 設備A | 73.04 | 1.99 | 5.44 | 67.73 | temperature_high |
| 176 | 2026-06-02 13:20:00 | 設備A | 75.03 | 1.95 | 5.34 | 69.89 | temperature_high |
| 177 | 2026-06-02 13:30:00 | 設備A | 74.46 | 1.96 | 5.26 | 66.39 | temperature_high |
| 178 | 2026-06-02 13:40:00 | 設備A | 73.40 | 2.01 | 5.34 | 65.15 | temperature_high |
| 179 | 2026-06-02 13:50:00 | 設備A | 75.08 | 1.90 | 5.18 | 65.27 | temperature_high |
| 180 | 2026-06-02 14:00:00 | 設備A | 75.05 | 1.83 | 5.36 | 66.61 | temperature_high |
| 315 | 2026-06-01 20:30:00 | 設備B | 64.57 | 4.22 | 5.47 | 70.98 | vibration_spike |
| 600 | 2026-06-02 04:00:00 | 設備C | 60.63 | 1.80 | 0.00 | 62.05 | pressure_zero_stuck |
結果の読み取り
設備Aの高温、設備Bの振動上限超過、設備Cの圧力下限超過が拾われます。ただし、1点超過と長時間超過では対応が異なります。実装時は警戒・異常・危険の多段階、復帰側のヒステリシス、継続点数を設け、境界付近の通知連発を防ぎます。
No.047:センサー間の相関を確認する
実務での意味
設備の信号は独立ではありません。負荷電流が上がれば発熱や振動も増える、といった物理的な連動があります。この通常関係を把握すると、「値は正常範囲だが組み合わせが不自然」という異常を検討できます。
分析・モデル化の考え方
Pearson相関係数は線形関係を から で要約します。異常点に引っ張られるため、まず既知の明白な管理限界違反を除いた基準データで計算します。相関は因果関係ではなく、運転モードの混在でも変わる点に注意します。
Pythonで確認する
baseline = df[df.injected_event == "normal"]
corr = baseline[sensor_cols].corr()
display(corr.round(2))
fig, ax = plt.subplots(figsize=(6.5, 5.2))
im = ax.imshow(corr, cmap="coolwarm", vmin=-1, vmax=1)
ax.set_xticks(range(len(sensor_cols)), sensor_cols, rotation=35, ha="right")
ax.set_yticks(range(len(sensor_cols)), sensor_cols)
for i in range(len(sensor_cols)):
for j in range(len(sensor_cols)):
ax.text(j, i, f"{corr.iloc[i,j]:.2f}", ha="center", va="center")
ax.set_title("Baseline sensor correlation")
ax.set_xlabel("sensor"); ax.set_ylabel("sensor"); ax.grid(False)
fig.colorbar(im, ax=ax, label="correlation")
plt.tight_layout(); plt.show()
| temperature_c | vibration_mm_s | pressure_mpa | current_a | |
|---|---|---|---|---|
| temperature_c | 1.00 | 0.79 | 0.72 | 0.90 |
| vibration_mm_s | 0.79 | 1.00 | 0.63 | 0.77 |
| pressure_mpa | 0.72 | 0.63 | 1.00 | 0.82 |
| current_a | 0.90 | 0.77 | 0.82 | 1.00 |

結果の読み取り
電流と温度・振動・圧力には正の相関が見られ、共通する負荷変動を反映しています。相関が高いから一方を削除できるとは限りません。温度は熱、振動は機械状態という異なる故障モードを表すため、物理的役割を残したまま関係監視に利用します。
No.048:センサー間の関係崩れを検出する
実務での意味
例えば電流が通常通りなのに振動だけが高まる場合、負荷増では説明できず、アンバランスや緩みなどの候補が生まれます。単純な振動上限に達する前に兆候を捉えられる可能性があります。
分析・モデル化の考え方
正常データで振動を電流から説明する直線 を学習し、残差 を求めます。正常残差の中央値とMAD(中央絶対偏差)からロバストな尺度 を作り、残差が中央値±4尺度を超えた点を関係崩れとします。これは説明用の単純モデルで、運転モードが複数ならモード別モデルが必要です。
Pythonで確認する
train = df[df.injected_event == "normal"]
slope, intercept = np.polyfit(train.current_a, train.vibration_mm_s, 1)
df["vibration_expected"] = intercept + slope * df.current_a
df["relation_residual"] = df.vibration_mm_s - df.vibration_expected
train_resid = train.vibration_mm_s - (intercept + slope * train.current_a)
center = train_resid.median()
robust_sigma = 1.4826 * (train_resid - center).abs().median()
df["relation_break_flag"] = (df.relation_residual - center).abs() > 4 * robust_sigma
print(f"期待式: vibration = {intercept:.3f} + {slope:.4f} × current")
print(f"関係崩れ検知数: {df.relation_break_flag.sum()} 点")
fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(df.current_a, df.vibration_mm_s, s=12, alpha=0.35, label="observed")
flag = df[df.relation_break_flag]
ax.scatter(flag.current_a, flag.vibration_mm_s, s=30, color="crimson", label="relation break")
x = np.linspace(df.current_a.min(), df.current_a.max(), 100)
ax.plot(x, intercept+slope*x, color="black", lw=2, label="baseline relation")
ax.set_title("Current-vibration relationship")
ax.set_xlabel("current (A)"); ax.set_ylabel("vibration (mm/s)")
ax.grid(alpha=0.3); ax.legend(); plt.tight_layout(); plt.show()
期待式: vibration = 0.780 + 0.0176 × current
関係崩れ検知数: 14 点

結果の読み取り
電流で説明できない高振動の集まりが可視化されます。急変検知が瞬間スパイクに強いのに対し、関係崩れは緩やかに継続する異常にも反応します。原因確定ではないため、回転数、加工品種、工具交換履歴と合わせてアンバランス・摩耗・固定緩みを切り分けます。
No.049:設備ごとの異常傾向を比較する
実務での意味
保全資源は有限です。アラート総数だけでなく、異常の種類、継続時間、設備固有のベースラインを比較し、点検の優先順位を決めます。
分析・モデル化の考え方
検知ルールを設備別に集計し、同じ時刻に複数ルールが反応しても「異常点」としては1件にまとめます。ここでは説明用に、管理限界2点、関係崩れ1点、急変1点、張り付き2点の重み付きスコアを作ります。重みは停止損失や安全影響に基づき現場合意で決めるべきものです。
Pythonで確認する
df["priority_score"] = (
2*df.any_limit_violation.astype(int)
+ df.relation_break_flag.astype(int)
+ df.rapid_vibration.astype(int)
+ 2*df.pressure_zero_stuck.astype(int)
)
machine_summary = df.groupby("machine").agg(
limit_points=("any_limit_violation", "sum"),
rapid_points=("rapid_vibration", "sum"),
stuck_points=("pressure_zero_stuck", "sum"),
relation_break_points=("relation_break_flag", "sum"),
total_priority_score=("priority_score", "sum"),
max_temperature_c=("temperature_c", "max"),
max_vibration_mm_s=("vibration_mm_s", "max"),
).sort_values("total_priority_score", ascending=False)
display(machine_summary.round(2))
fig, ax = plt.subplots(figsize=(8, 4.5))
machine_summary[["limit_points", "rapid_points", "stuck_points", "relation_break_points"]].plot.bar(ax=ax)
ax.set_title("Anomaly signals by machine")
ax.set_xlabel("machine"); ax.set_ylabel("detected points")
ax.grid(axis="y", alpha=0.3); ax.legend(title="rule")
plt.xticks(rotation=0); plt.tight_layout(); plt.show()
| limit_points | rapid_points | stuck_points | relation_break_points | total_priority_score | max_temperature_c | max_vibration_mm_s | |
|---|---|---|---|---|---|---|---|
| machine | |||||||
| 設備C | 9 | 0 | 9 | 0 | 36 | 65.40 | 2.18 |
| 設備A | 10 | 0 | 0 | 0 | 20 | 75.08 | 2.27 |
| 設備B | 1 | 3 | 0 | 14 | 19 | 67.40 | 4.22 |
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_19414/1402267224.py:23: UserWarning: Glyph 35373 (\N{CJK UNIFIED IDEOGRAPH-8A2D}) missing from font(s) DejaVu Sans.
plt.xticks(rotation=0); plt.tight_layout(); plt.show()
/var/folders/3y/fmw40k0x78xblvb3gkcyvy1h0000gn/T/ipykernel_19414/1402267224.py:23: UserWarning: Glyph 20633 (\N{CJK UNIFIED IDEOGRAPH-5099}) missing from font(s) DejaVu Sans.
plt.xticks(rotation=0); plt.tight_layout(); plt.show()
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 35373 (\N{CJK UNIFIED IDEOGRAPH-8A2D}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)
/Users/hiroshi/private/kobo/notebook/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 20633 (\N{CJK UNIFIED IDEOGRAPH-5099}) missing from font(s) DejaVu Sans.
fig.canvas.print_figure(bytes_io, **kw)

結果の読み取り
設備ごとに異常の「型」が違います。設備Aは高温継続、設備Bは振動の急変・関係崩れ、設備Cは圧力張り付きが中心です。単純な総数ランキングでは長い張り付きが過大評価されるため、実務では連続点をイベント化し、安全・品質・停止リスクを加味して優先度を決めます。
No.050:センサーデータ異常を現場目線で解釈する
実務での意味
分析結果は「異常です」で終わらせず、誰が、いつまでに、何を確認し、どの条件なら運転を継続・停止するかまで翻訳して初めて価値になります。センサー異常と設備異常では担当者も対応も異なります。
分析・モデル化の考え方
ルールの組み合わせから原因仮説と推奨初動を割り当てます。これは故障診断の確定ではなく、トリアージです。設備台帳、保全履歴、運転状態、品質実績を追加し、対応後の結果を記録することでルールと優先度を改善します。
Pythonで確認する
events = pd.DataFrame([
["設備A", "温度上限超過が継続", "冷却不足・過負荷・温度計ずれ", "冷却流量と加工条件を確認。上昇継続なら計画停止を判断", "高"],
["設備B", "振動スパイク/電流との関係崩れ", "衝撃・アンバランス・工具摩耗・固定緩み", "加工品と工具を確認し、携帯振動計で再測定", "高"],
["設備C", "稼働中に圧力ゼロ張り付き", "圧力センサー断線・通信断・入力カード異常", "現場計器とPLC値を照合し、計装系を点検", "中"],
], columns=["設備", "検知の要約", "原因候補", "推奨初動", "優先度"])
display(events)
detected = df[["rapid_vibration", "pressure_zero_stuck", "any_limit_violation", "relation_break_flag"]].any(axis=1)
known = df.injected_event.ne("normal")
tp = int((detected & known).sum()); fp = int((detected & ~known).sum())
fn = int((~detected & known).sum())
print(f"教材上の点単位確認: 検知できた異常点={tp}, 正常点への反応={fp}, 見逃した異常点={fn}")
| 設備 | 検知の要約 | 原因候補 | 推奨初動 | 優先度 | |
|---|---|---|---|---|---|
| 0 | 設備A | 温度上限超過が継続 | 冷却不足・過負荷・温度計ずれ | 冷却流量と加工条件を確認。上昇継続なら計画停止を判断 | 高 |
| 1 | 設備B | 振動スパイク/電流との関係崩れ | 衝撃・アンバランス・工具摩耗・固定緩み | 加工品と工具を確認し、携帯振動計で再測定 | 高 |
| 2 | 設備C | 稼働中に圧力ゼロ張り付き | 圧力センサー断線・通信断・入力カード異常 | 現場計器とPLC値を照合し、計装系を点検 | 中 |
教材上の点単位確認: 検知できた異常点=33, 正常点への反応=2, 見逃した異常点=1
結果の読み取り
設備Aはプロセス・冷却系、設備Bは機械系、設備Cは計装系という異なる点検ルートになります。複数ルールの反応を原因候補と初動へ変換することで、監視画面から作業指示への距離が縮まります。点単位の精度だけでなく、故障イベントを事前に捉えたか、平均確認時間が短くなったか、不要停止が減ったかで業務効果を評価します。
対象ノックを通して見える実務上の示唆
- データ品質異常を先に分ける:ゼロ張り付きや欠測を設備劣化モデルへ入れると誤学習につながります。
- 物理ルールと統計ルールを併用する:安全上限は即時性、関係崩れは早期兆候という異なる役割があります。
- 点ではなくイベントで管理する:連続する異常点を開始・終了・最大値・継続時間でまとめると、現場の案件単位になります。
- 設備・運転モード別に基準を持つ:同型設備でも設置条件や劣化状態は異なり、全体一律の基準は誤検知を増やします。
- 原因候補と推奨初動まで設計する:検知数ではなく、停止回避・品質損失低減・点検時間短縮をKPIにします。
実務導入する場合に必要なこと
- データ契約:タグ、単位、周期、欠測コード、時刻同期、設備・品種・運転状態を定義する
- 基準の承認:メーカー仕様、安全基準、工程能力、現場知見から警戒・異常・危険のしきい値を決める
- アラート運用:抑制時間、ヒステリシス、イベント統合、通知先、エスカレーションを定める
- 検証設計:過去故障だけでなく、誤検知時の確認工数と見逃し損失を含めて評価する
- 改善ループ:現場の確認結果、原因、処置、交換部品を記録し、ルールとモデルを定期見直しする
- システム連携:PLC/SCADA、ヒストリアン、MES、CMMSとつなぎ、権限・監査ログ・可用性を確保する
小さく始める場合は、重要設備1〜3台、既知の故障モード1つ、現場が説明できるルールからPoCを行い、アラート件数と対応フローを確認してから範囲を広げます。
まとめ
No.041〜No.050では、センサーデータの基礎確認から、急変、ゼロ張り付き、上下限、相関、関係崩れ、設備比較、現場解釈までを一続きで確認しました。重要なのは、高度なモデルを先に導入することではなく、計測異常と設備異常を分け、物理的に説明可能な検知を、具体的な保全判断へ接続することです。
法人向けのご相談
数理工房では、製造データの棚卸し、異常検知PoC、しきい値・アラート設計、設備保全システムへの実装、現場向け研修まで、課題とデータ成熟度に合わせて支援します。「データはあるが、どの設備・故障モードから始めるべきか分からない」という段階でもご相談いただけます。
📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。