この記事で学べること
- センサーデータを通常の表形式データと同じように扱えない理由
- タイムスタンプ・単位・重複を確認し、5秒間隔へそろえる方法
- 短い欠測の補間、Hampel法によるスパイク候補の検出、ローパスフィルタの使い方
- 過去5分の平均・標準偏差・傾きなどを、Pythonで特徴量へ変換する方法
- 将来情報の混入を避け、実工程へ持ち込む前に確認すべき限界と停止条件
製造設備や実験装置から温度、圧力、流量、電流、振動などのログを取得できても、そのまま機械学習へ渡せるとは限りません。時刻が重なる、測定間隔が揺れる、短い欠測がある、瞬間的なスパイクが混じる、単位が途中で変わる、といった問題があるからです。
結論は、前処理の目的を「きれいな波形を作ること」ではなく、時点ごとに利用可能だった情報を、工程上の意味を保ったまま比較可能な数値へ変えることと考えることです。異常に見える値を機械的に削除すると、本当に知りたい設備異常まで消す危険があります。
この記事では、編集部が作成した温度・圧力・モーター電流の合成ログを使います。入力検査、時間格子の統一、短い欠測の補間、スパイク候補の置換、Butterworthローパスフィルタ、過去5分の特徴量作成までを一つのPythonコードで実行します。合成値は教材であり、実在する製品・装置・運転条件を表しません。
センサーデータは普通の表と何が違うのか
時系列データでは、値だけでなく順序と時間間隔が情報になります。NISTは、時系列解析では自己相関、トレンド、季節変動など、時間方向の内部構造を考慮すると説明しています。[1] 同じ温度50℃でも、安定状態の50℃、急上昇中の50℃、冷却中の50℃では意味が異なります。
通常の回帰表では、各行が独立した試料であることを暗黙に期待しがちです。一方、1秒ごとのセンサー値は隣接時点と強く関係します。1時間のログが3,600行あっても、3,600個の独立試料があるわけではありません。装置3台から得たログなら、独立性を決める単位は装置、バッチ、運転日、原料ロットなどかもしれません。
前処理前に、まず次の4点を一文で定義します。
1. 対象単位:一つの装置、一つのバッチ、一つの実験のどれか 2. 予測時点:工程途中、工程終了時、品質測定後のどこか 3. 目的:状態監視、異常候補抽出、品質予測、故障予兆のどれか 4. 利用できる情報:その時点までに到着・確定したセンサーと記録
プロセスインフォマティクスとは?では製造条件・品質・時系列解析の全体像を扱いました。本記事はその中の「連続ログを解析可能な入力へ変換する部分」に絞ります。既存の製造プロセスの時系列データを解析する方法は、バッチ波形から工程特徴量を作り品質予測する記事です。本記事は、その前段にあるセンサー単位の品質確認と、連続した過去窓の特徴量化を詳しく扱います。
最初に決める前処理の順番
今回の順序は次の通りです。
1. 列名、時刻、センサー名、単位、重複、数値を検査する 2. 全センサーが存在する共通時間へ切りそろえる 3. 5秒間隔の時間格子へ並べ直す 4. 15秒以内の短い欠測だけを補間する 5. Hampel法で局所的なスパイク候補を付け、中央値へ置換する 6. 低周波側の工程変化を残すローパスフィルタを適用する 7. 過去5分窓から平均、ばらつき、傾きなどを作る 8. 品質レポート、処理後ログ、特徴量、図を別ファイルへ保存する
この順序は唯一の正解ではありません。振動解析で高周波成分そのものが故障兆候なら、ローパスフィルタで消してはいけません。制御用途では未来側の値を使うゼロ位相フィルタも利用できません。工程、測定原理、予測時点に合わせて設計します。
入力検査:補正より先にデータの意味を確認する
推奨するlong形式
記事のコードは、次の4列を持つlong形式CSVを読みます。
timestamp,sensor,value,unit
2026-01-01 09:00:00+00:00,temperature,45.06,degC
2026-01-01 09:00:00+00:00,pressure,210.43,kPa
2026-01-01 09:00:00+00:00,motor_current,7.98,A
long形式には、センサーを追加しやすく、各行が何の測定かを明示できる利点があります。ただし、センサー名の表記揺れや単位混在を放置すると危険です。tempとtemperatureが別センサーになったり、kPaとMPaが同じ列へ混ざったりするからです。
コードは、必須列の欠落、時刻の解析不能、非数値・無限値、同一センサー内の重複時刻、未知のセンサー、単位不一致、観測数不足で停止します。自動変換せず停止するのは、単位変換やセンサー対応を解析者が確認する機会を残すためです。材料データの品質を高める方法で説明したように、値の修正だけでなく測定条件とメタデータを残すことが再利用性につながります。
重複時刻は平均してよいとは限らない
同じセンサー・同じ時刻に2値ある場合、通信再送、二重登録、複数チャンネルの誤統合、丸めた時刻の衝突などが考えられます。平均すればエラーが消えたように見えますが、原因は解決しません。元のイベントID、サブ秒時刻、装置側の採番を確認し、統合規則を決めます。
等間隔化と欠測補間
なぜ時間格子をそろえるのか
移動平均、デジタルフィルタ、自己相関、周波数解析などの多くは、等間隔のデータを前提とします。NISTの自己相関の定義でも、観測が等間隔であることが前提として示されています。[2] pandasのresampleは時系列データを指定した時間規則へ再サンプリングする機能です。[3]
今回のコードは全センサーが存在する共通期間を求め、5秒刻みの格子を作ります。元データを上書きせず、regularized列へ並べ直した値を保存します。開始前や終了後への外挿はしません。
補間するのは短い欠測だけ
5秒間隔なら、3点までの補間は15秒以内の欠測をつなぐ設定です。前後の値を使う線形補間は、オフライン解析には使いやすい一方、リアルタイム予測では未来値を待つ必要があります。オンライン利用では前方保持、状態空間モデル、センサー冗長化など別の方針が必要です。
長い欠測を滑らかな線で埋めると、停止、通信断、装置切替などの重要なイベントを隠します。そのためコードは上限を超えた欠測をNaNのまま残し、完全な値がそろわない特徴量窓を作りません。単に削除するのではなく、data_quality.csvに未補完数を残します。
| 状況 | 初期判断 | 確認すること |
|---|---|---|
| 1~2点の孤立欠測 | 短い補間の候補 | 信号変化が十分遅いか |
| 数十秒~数分の連続欠測 | 原則保留 | 停止・通信断・保全作業か |
| 工程境界をまたぐ欠測 | 補間しない方向 | 条件変更を直線で結んでよいか |
| 開始・終了端の欠測 | 外挿しない | 共通期間を切るか、センサーを除外するか |
スパイク候補をHampel法で検出する
局所中央値とMADを使う
Hampel法では、注目点の周辺窓から中央値を求め、中央値からの絶対偏差の中央値、MADをばらつきの基準にします。平均と標準偏差より極端値の影響を受けにくいのが利点です。コードでは25点、すなわち約125秒の中央窓を使い、4 × 1.4826 × MADを超える点を候補にします。
ただし、これは「誤測定の証明」ではありません。NISTも、外れ値は誤データの場合もあれば、ランダム変動や科学的に興味深い現象の場合もあり、通常は単純に削除すべきではないと説明しています。[4] 急なバルブ操作、原料投入、装置保護動作が本物なら、置換すると情報を失います。
したがって、本番ではspike_flagを確認し、イベントログと照合します。誤測定と確認できる場合だけ置換する、異常検知が目的ならフラグ自体を特徴量として残す、といった使い分けをします。この記事のコードは実装を分かりやすくするため中央値へ置換しますが、元値は別の入力CSVに残ります。
窓幅は工程の変化時間より短くする
窓が短すぎると通常ノイズへ反応し、長すぎると緩やかな工程変化まで外れと判定します。まず、センサーの応答速度、制御周期、工程イベントの継続時間を確認します。フラグ箇所を原波形へ重ね、真のステップ変化が連続して検出されていないか確認します。
Butterworthローパスフィルタでノイズを抑える
カットオフ周波数の意味
ローパスフィルタは、ゆっくりした変化を通し、速い変動を抑える処理です。Butterworthフィルタは通過域の周波数応答が平坦になる特徴を持ちます。SciPyのbutterは次数、カットオフ周波数、フィルタ種別、サンプリング周波数を指定して設計できます。公式文書は、一般用途では数値安定性の点からsecond-order sections(SOS)形式を推奨しています。[5]
5秒間隔のデータではサンプリング周波数は0.2 Hz、Nyquist周波数は0.1 Hzです。カットオフは0と0.1 Hzの間で設定します。記事の合成例では、温度0.01 Hz、圧力0.03 Hz、電流0.05 Hzとしますが、実装例の値であり実設備への推奨値ではありません。
カットオフは「残したい変化の時間スケール」から考えます。周期20秒の変化は0.05 Hz、周期100秒の変化は0.01 Hzです。工程イベントの立ち上がりが重要なら、その周波数帯を消さない設定を検討し、処理前後の振幅・傾き・イベント時刻を比較します。
sosfiltfiltはオフライン用
コードは前向きと後ろ向きにフィルタするsosfiltfiltを使います。この方法では位相遅れを抑えられ、SciPyの例でも前向きだけのフィルタより入力との位相が合うことが示されています。[6] 一方、後ろ向き処理は未来の値を使います。工程途中のリアルタイム予測、警報、制御には、そのまま利用できません。
オンライン用途では、因果的なsosfilt等へ変更し、初期状態、位相遅れ、通信遅れを含めて評価します。安全・制御ループへ組み込む場合は、データ解析だけでなく制御・設備・品質担当者のレビューが必要です。
特徴量設計:波形を何の数値へ変えるか
過去5分の窓を1分ごとに更新する
コードは各window_endまでの過去5分だけを切り出し、1分間隔で特徴量を作ります。未来側を含む中央窓ではありません。作成する特徴量は次の7種類です。
| 特徴量 | 表すもの | 工程での読み方の例 |
|---|---|---|
| mean | 窓内の平均レベル | 最近5分の代表温度 |
| std | ばらつき | 制御の揺れ、不安定性 |
| min/max | 範囲と極値 | 上限・下限への接近 |
| slope | 時間方向の傾き | 上昇・下降の速さ |
| delta | 窓の末端-始端 | 最近5分の総変化 |
| RMS | 二乗平均平方根 | 振幅や負荷の大きさ |
時系列特徴量には、自己相関、ピーク数、エントロピー、周波数帯別パワーなど多くの候補があります。tsfreshを報告したChristらは、時系列の特徴量作成と選択を自動化する枠組みを示しています。[7] ただし、候補を大量に作れば必ず良くなるわけではありません。少数データでは偶然相関する特徴が増え、説明も難しくなります。
最初は、工程知識から説明できる少数特徴を使います。例えば温度なら保持区間の平均と標準偏差、圧力なら最大値と上限超過時間、振動なら周波数帯別エネルギーなどです。一般的な特徴量設計の位置づけは、材料開発における特徴量設計も参照してください。
窓が重なることと独立試料数は別問題
5分窓を1分ごとに作ると隣の行は4分間を共有します。116窓ができても、116件の独立実験が増えたわけではありません。学習と評価を窓単位でランダム分割すると、ほぼ同じ波形が双方へ入ります。
装置、バッチ、日付などで分け、評価期間を学習期間より後に置きます。scikit-learnのTimeSeriesSplitは、時間順データで後の評価を行うための分割器で、比較可能な評価指標を得るため標本が等間隔であることを前提としています。[8] 実工程では、同一バッチの窓を分割しないグループ制約と時間順の両方が必要になることがあります。
Pythonで一連の処理を実行する
必要な環境と実行方法
検証環境はPython 3.12.14、NumPy 2.3.5、pandas 2.2.3、SciPy 1.17.0、Matplotlib 3.10.8です。
python -m pip install numpy==2.3.5 pandas==2.2.3 scipy==1.17.0 matplotlib==3.10.8
python sensor_preprocessing.py --output sensor_results
引数なしではなく、--inputを省略した場合に合成CSVを自動生成します。手元のCSVを使うときは次のようにします。
python sensor_preprocessing.py --input your_sensor_log.csv --output sensor_results
完全なPythonコード
完全なPythonコード(保存名:sensor_preprocessing.py)
"""Beginner tutorial: turn long-format sensor CSV into leakage-aware features."""
from __future__ import annotations
from pathlib import Path
import argparse
import json
import platform
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import scipy
from scipy.signal import butter, sosfiltfilt
REQUIRED = {"timestamp", "sensor", "value", "unit"}
CONFIG = {
"temperature": {"unit": "degC", "cutoff_hz": 0.010},
"pressure": {"unit": "kPa", "cutoff_hz": 0.030},
"motor_current": {"unit": "A", "cutoff_hz": 0.050},
}
def make_example(path: Path) -> pd.DataFrame:
"""Create editorial synthetic data; values are not a process recipe."""
rng = np.random.default_rng(42)
start = pd.Timestamp("2026-01-01 09:00:00", tz="UTC")
seconds = np.arange(0, 2 * 60 * 60 + 1, 5)
t = seconds / 60
rows: list[tuple] = []
signals = {
"temperature": (45 + 0.025 * t + 3.0 * (t >= 55) + 0.35 * np.sin(t / 7), "degC", 0.20),
"pressure": (210 + 4.5 * np.sin(t / 9) + 7.0 * (t >= 75), "kPa", 0.80),
"motor_current": (8.0 + 0.35 * np.sin(t / 3) + 1.1 * ((t >= 85) & (t < 95)), "A", 0.12),
}
for sensor, (truth, unit, noise) in signals.items():
values = truth + rng.normal(0, noise, len(t))
for i in [330, 780, 1120]:
values[i] += {"temperature": 8, "pressure": -30, "motor_current": 4}[sensor]
for i, (sec, value) in enumerate(zip(seconds, values)):
# Short gaps are intentionally inserted for interpolation practice.
if (sensor == "temperature" and 500 <= i <= 502) or (sensor == "pressure" and 900 <= i <= 901):
continue
rows.append((start + pd.Timedelta(seconds=int(sec)), sensor, float(value), unit))
frame = pd.DataFrame(rows, columns=["timestamp", "sensor", "value", "unit"])
frame.to_csv(path, index=False)
return frame
def validate(frame: pd.DataFrame) -> pd.DataFrame:
if not REQUIRED <= set(frame.columns):
raise ValueError(f"Required columns: {sorted(REQUIRED)}")
out = frame[list(REQUIRED)].copy()
if out[["timestamp", "sensor", "unit"]].isna().any().any():
raise ValueError("Identity, timestamp, or unit is missing")
out["timestamp"] = pd.to_datetime(out["timestamp"], utc=True, errors="raise")
out["value"] = pd.to_numeric(out["value"], errors="raise")
if not np.isfinite(out["value"]).all():
raise ValueError("Sensor values must be finite")
if out.duplicated(["sensor", "timestamp"]).any():
raise ValueError("Duplicate timestamp within a sensor")
unknown = set(out["sensor"]) - set(CONFIG)
if unknown:
raise ValueError(f"Unknown sensors; add reviewed settings: {sorted(unknown)}")
for sensor, group in out.groupby("sensor"):
units = set(group["unit"])
if units != {CONFIG[sensor]["unit"]}:
raise ValueError(f"Unit mismatch for {sensor}: {sorted(units)}")
if len(group) < 100:
raise ValueError(f"Too few observations for {sensor}")
return out.sort_values(["sensor", "timestamp"]).reset_index(drop=True)
def hampel(series: pd.Series, window: int = 25, n_sigma: float = 4.0):
if window < 5 or window % 2 == 0 or n_sigma <= 0:
raise ValueError("Hampel window must be odd >=5 and n_sigma >0")
median = series.rolling(window, center=True, min_periods=window // 2).median()
deviation = (series - median).abs()
mad = deviation.rolling(window, center=True, min_periods=window // 2).median()
threshold = n_sigma * 1.4826 * mad
mask = (deviation > threshold) & threshold.gt(0) & series.notna()
cleaned = series.mask(mask, median)
return cleaned, mask
def filter_segments(series: pd.Series, fs_hz: float, cutoff_hz: float, order: int = 3) -> pd.Series:
if not 0 < cutoff_hz < fs_hz / 2:
raise ValueError("cutoff_hz must be between 0 and the Nyquist frequency")
sos = butter(order, cutoff_hz, btype="low", fs=fs_hz, output="sos")
result = pd.Series(np.nan, index=series.index, dtype=float)
block = series.notna().ne(series.notna().shift()).cumsum()
for _, part in series.dropna().groupby(block[series.notna()]):
if len(part) >= 30:
result.loc[part.index] = sosfiltfilt(sos, part.to_numpy())
return result
def preprocess(frame: pd.DataFrame, freq="5s", max_interp="15s"):
frame = validate(frame)
starts = frame.groupby("sensor")["timestamp"].min()
ends = frame.groupby("sensor")["timestamp"].max()
start, end = starts.max(), ends.min()
grid = pd.date_range(start.ceil(freq), end.floor(freq), freq=freq)
if len(grid) < 100:
raise ValueError("Common time range is too short")
step_s = pd.Timedelta(freq).total_seconds()
max_points = int(pd.Timedelta(max_interp).total_seconds() // step_s)
if max_points < 1:
raise ValueError("max_interp must be at least one grid interval")
rows, quality = [], []
for sensor, group in frame.groupby("sensor", sort=True):
observed = group.set_index("timestamp")["value"].sort_index()
union = observed.index.union(grid)
regular = observed.reindex(union).sort_index().interpolate(
method="time", limit=max_points, limit_area="inside"
).reindex(grid)
cleaned, spike = hampel(regular)
filtered = filter_segments(cleaned, 1.0 / step_s, CONFIG[sensor]["cutoff_hz"])
sensor_rows = pd.DataFrame({
"timestamp": grid, "sensor": sensor,
"regularized": regular.to_numpy(), "cleaned": cleaned.to_numpy(),
"filtered": filtered.to_numpy(), "spike_flag": spike.to_numpy(),
"unit": CONFIG[sensor]["unit"],
})
rows.append(sensor_rows)
quality.append({
"sensor": sensor, "observations": len(group), "grid_points": len(grid),
"interpolated_or_missing_points": int((~grid.isin(observed.index)).sum()),
"remaining_missing_points": int(regular.isna().sum()),
"spikes_replaced": int(spike.sum()), "cutoff_hz": CONFIG[sensor]["cutoff_hz"],
})
return pd.concat(rows, ignore_index=True), pd.DataFrame(quality)
def feature_table(long: pd.DataFrame, window="5min", step="1min") -> pd.DataFrame:
wide = long.pivot(index="timestamp", columns="sensor", values="filtered").sort_index()
window_n = int(pd.Timedelta(window) / (wide.index[1] - wide.index[0]))
step_n = int(pd.Timedelta(step) / (wide.index[1] - wide.index[0]))
if window_n < 3 or step_n < 1:
raise ValueError("Window/step is too short")
records = []
# End-aligned, past-only windows: [end-window, end].
for stop in range(window_n - 1, len(wide), step_n):
part = wide.iloc[stop - window_n + 1:stop + 1]
if part.isna().any().any():
continue
record = {"window_end": part.index[-1], "window_start": part.index[0]}
x = np.arange(len(part), dtype=float)
for sensor in wide.columns:
y = part[sensor].to_numpy()
record.update({
f"{sensor}_mean": float(y.mean()), f"{sensor}_std": float(y.std(ddof=1)),
f"{sensor}_min": float(y.min()), f"{sensor}_max": float(y.max()),
f"{sensor}_slope_per_sample": float(np.polyfit(x, y, 1)[0]),
f"{sensor}_delta": float(y[-1] - y[0]),
f"{sensor}_rms": float(np.sqrt(np.mean(y ** 2))),
})
records.append(record)
if not records:
raise ValueError("No complete windows; inspect missing intervals")
return pd.DataFrame(records)
def make_plots(long: pd.DataFrame, features: pd.DataFrame, out: Path):
temp = long[long.sensor == "temperature"]
fig, axes = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
axes[0].plot(temp.timestamp, temp.regularized, color="0.6", lw=1, label="regularized")
axes[0].plot(temp.timestamp, temp.filtered, color="#087e8b", lw=1.6, label="filtered")
marked = temp[temp.spike_flag]
axes[0].scatter(marked.timestamp, marked.regularized, color="#d1495b", s=28, label="spike flag")
axes[0].set(ylabel="Temperature / degC", title="Editorial synthetic sensor data")
axes[0].legend(ncol=3, fontsize=8)
axes[1].plot(features.window_end, features.temperature_mean, label="5 min trailing mean")
axes[1].plot(features.window_end, features.temperature_std, label="5 min trailing SD")
axes[1].set(xlabel="Time / UTC", ylabel="Feature value", title="Past-only window features")
axes[1].legend(fontsize=8)
fig.autofmt_xdate(); fig.tight_layout(); fig.savefig(out / "sensor_preprocessing.png", dpi=160)
plt.close(fig)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--input", help="Long-format CSV; omit to create a synthetic example")
parser.add_argument("--output", default="sensor_results")
args = parser.parse_args()
out = Path(args.output); out.mkdir(parents=True, exist_ok=True)
if args.input:
raw = pd.read_csv(args.input); mode = "user CSV"
else:
raw = make_example(out / "sensor_example.csv"); mode = "editorial synthetic data"
long, quality = preprocess(raw)
features = feature_table(long)
long.to_csv(out / "regularized_signals.csv", index=False)
quality.to_csv(out / "data_quality.csv", index=False)
features.to_csv(out / "sensor_features.csv", index=False)
make_plots(long, features, out)
summary = {
"mode": mode, "input_rows": len(raw), "regularized_rows": len(long),
"feature_windows": len(features), "spikes_replaced": int(quality.spikes_replaced.sum()),
"remaining_missing_points": int(quality.remaining_missing_points.sum()),
"versions": {"python": platform.python_version(), "numpy": np.__version__,
"pandas": pd.__version__, "scipy": scipy.__version__},
}
(out / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2))
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
出力ファイル
sensor_example.csv:編集部作成の合成入力データregularized_signals.csv:等間隔化、補間、フラグ、平滑化後の値data_quality.csv:観測数、補間候補数、未補完数、スパイク候補数、設定sensor_features.csv:過去5分窓から作った特徴量sensor_preprocessing.png:処理前後と特徴量の確認図summary.json:実行モード、行数、特徴量窓数、ライブラリ版
図の上段では、灰色が5秒格子へそろえた温度、青緑がHampel処理とローパスフィルタ後、赤点がスパイク候補です。下段は過去5分の平均と標準偏差です。55分付近の温度ステップは、合成データに意図的に入れた工程変化です。スパイク候補と異なり、工程変化として残っているかを確認します。
今回の検証実行では入力4,318行から等間隔ログ4,323行を作り、116個の完全な過去窓を出力しました。Hampel法は3センサー合計24点を候補として置換しました。これは人工データ上の動作確認であり、実設備の誤検出率や最適設定を示す数値ではありません。
公開データでは何を学べるか
NASAのC-MAPSSターボファンエンジン劣化シミュレーションデータは、異なる運転条件・故障モードで複数のセンサーチャンネルを記録したrun-to-failureデータです。NASA公式リポジトリは、4種類のデータセットと、エンジンごとの多変量時系列を提供しています。[9]
この公開例からは、センサー点数だけでなく「エンジン個体」が独立単位であること、運転条件の違いと劣化変化を分ける必要があることを学べます。ただし、本記事のCSVと同じ構造ではなく、この記事のコードでC-MAPSSを直接解析することはできません。公開データの意味・列定義・利用条件を確認し、別途変換が必要です。
失敗例と改善方法
| 失敗 | 起こる問題 | 改善方法 |
|---|---|---|
| 単位確認前に結合する | 桁違いを異常として学ぶ | 単位列と変換履歴を検証する |
| 長い欠測を全部補間する | 停止や異常を消す | 補間上限を決め、未補完を記録する |
| スパイク候補を全削除する | 真のイベントも消える | イベントログと原波形で確認する |
| 強いローパスを使う | 故障兆候や立ち上がりを消す | 周波数応答とイベント保存性を比べる |
| 中央窓でリアルタイム特徴を作る | 未来情報が入る | 過去窓だけで再計算する |
| 窓をランダム分割する | 似た窓が学習・評価に混ざる | バッチ・装置・期間単位で分ける |
| 特徴量を大量作成する | 偶然相関と過学習が増える | 工程仮説と検証計画から絞る |
限界・不確かさ・責任範囲
このコードは教材であり、センサー校正、タイムゾーン、時計ずれ、サンプリングジッタ、通信再送、装置切替、保全記録をすべて扱いません。複数装置では、同名センサーでも校正差や取付位置が異なります。装置ID、校正日、レンジ、分解能、ファームウェア等を追加し、比較可能性を確認してください。
Hampel法の閾値とButterworthのカットオフは、合成信号で動作を説明するための設定です。実工程では、既知イベント、正常運転、異常運転、校正信号で候補を比較します。処理後の見た目だけでなく、ピーク時刻、立ち上がり時間、振幅、面積、下流モデルの期間外性能が過度に変わらないか確認します。
品質、安全、設備保護、出荷可否に関する判断を、このコードだけで自動化しません。元ログを保存し、処理版と区別し、設定変更の承認者を決めます。入力が想定外、長い欠測が多い、単位や装置状態が不明、学習範囲外である場合は、数値を出し続けず予測や判定を保留する設計が必要です。
実務チェックリスト
- 解析単位、予測時点、目的、利用可能情報を定義した
- 時刻のタイムゾーン、重複、順序、時計ずれを確認した
- センサー名、装置ID、単位、校正情報を確認した
- 補間できる最長時間を工程知識から決めた
- 外れ値候補を原波形とイベント記録へ戻して確認した
- フィルタが残す周波数帯と消す周波数帯を説明できる
- オンライン用途で未来値を使っていない
- 特徴量窓の始端・終端と工程境界を記録した
- バッチ・装置・期間をまたぐデータリークを点検した
- 元ログ、処理条件、処理後ログ、除外理由を保存した
- 欠測や想定外条件で停止・保留する規則を決めた
FAQ
Q.欠測は何秒まで補間してよいですか?
一律の秒数はありません。信号の変化速度、制御周期、測定周期、補間後の利用目的で決めます。残したい最短イベントより欠測が長いなら、補間で再現することは困難です。複数の上限を比べ、既知イベントの形が保たれるか確認します。
Q.移動平均とButterworthフィルタはどちらがよいですか?
移動平均は説明しやすく、窓内の平均という意味が明確です。Butterworthは周波数特性を指定できますが、次数、端点、位相、オンライン利用を理解する必要があります。目的と検証方法を説明できる方から始めます。
Q.スパイクは必ずHampel法で消しますか?
いいえ。異常検知ではスパイクが目的情報かもしれません。まず候補フラグとして残し、装置イベントや別センサーとの同時変化を確認します。自動置換するなら、誤検出と見逃しを既知データで評価します。
Q.特徴量窓は5分でよいですか?
5分は教材用です。反応、混合、昇温、設備回転、品質測定などの時定数から候補を選びます。1分、5分、15分などを比較するときも、評価データへ合わせて窓を選び続けると過学習するため、最終確認期間を別に残します。
Q.この特徴量で品質予測までできますか?
入力表としては利用できます。ただし、品質ラベルの利用可能時刻、バッチへの対応、学習・評価分割、ベースラインモデル、適用領域を別途設計します。まず特徴量と元波形を結び付けて説明できることを確認してください。
まとめ
センサーデータの前処理では、補間や平滑化のアルゴリズムより前に、測定の意味、時刻、単位、解析単位を確定します。短い欠測だけを限定的に補間し、スパイクは候補として可視化し、フィルタが工程イベントを壊していないか確認します。
特徴量は、平均、標準偏差、傾きなど説明可能なものから始め、予測時点までの過去窓だけで作ります。作られた窓数を独立試料数と誤解せず、装置・バッチ・期間で評価を分けます。合成データでコードを動かした後、実工程のセンサー仕様とイベントへ合わせて設定を変更してください。
参考文献
[1]NIST/SEMATECH. “6.4. Introduction to Time Series Analysis.” *e-Handbook of Statistical Methods*. https://www.itl.nist.gov/div898/handbook/pmc/section4/pmc4.htm (2026年10月10日確認)
[2]NIST/SEMATECH. “1.3.5.12. Autocorrelation.” *e-Handbook of Statistical Methods*. https://www.itl.nist.gov/div898/handbook/eda/section3/eda35c.htm (2026年10月10日確認)
[3]pandas development team. “pandas.DataFrame.resample.” pandas 3.0.6 documentation. https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.resample.html (2026年10月10日確認)
[4]NIST/SEMATECH. “1.3.5.17. Detection of Outliers.” *e-Handbook of Statistical Methods*. https://www.itl.nist.gov/div898/handbook/eda/section3/eda35h.htm (2026年10月10日確認)
[5]SciPy community. “scipy.signal.butter.” SciPy v1.18.0 Manual. https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.butter.html (2026年10月10日確認)
[6]SciPy community. “scipy.signal.sosfiltfilt.” SciPy v1.18.0 Manual. https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.sosfiltfilt.html (2026年10月10日確認)
[7]Christ, M., Braun, N., Neuffer, J., & Kempa-Liehr, A. W. (2018). Time Series FeatuRe Extraction on basis of Scalable Hypothesis tests (tsfresh—A Python package). *Neurocomputing*, 307, 72–77. https://doi.org/10.1016/j.neucom.2018.03.067
[8]scikit-learn developers. “sklearn.model_selection.TimeSeriesSplit.” scikit-learn 1.9.1 documentation. https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html (2026年10月10日確認)
[9]NASA Ames Research Center. “Turbofan Engine Degradation Simulation Data Set.” Prognostics Center of Excellence Data Set Repository. Data set citation: A. Saxena & K. Goebel (2008). https://www.nasa.gov/intelligent-systems-division/discovery-and-systems-health/pcoe/pcoe-data-set-repository/ (2026年10月10日確認)


