第1章:時系列データ解析の基礎

Time Series Data Analysis Fundamentals for Process Data

📚 プロセスデータ解析実践シリーズ ⏱️ 読了時間: 30-35分 💻 コード例: 10個 🎓 難易度: 中級

1.1 イントロダクション

化学プロセスから得られるデータの大部分は時系列データです。反応器の温度、蒸留塔の圧力、原料の流量など、 時間とともに変化するこれらのプロセス変数を適切に解析することは、プロセスの最適化、品質管理、異常検知において極めて重要です。

本章では、時系列データの基本的な特性理解から、統計的検定、予測モデル構築、変化点検知まで、 実践的な解析技術を10個のPythonコード例を通じて習得します。

📊 本章で学ぶこと

  • 時系列データの前処理技術(欠損値補完、外れ値検知)
  • 定常性の検定とトレンド・季節性の分解
  • 自己相関分析とARIMAモデリング
  • 変化点検知とパターンマッチング技術
  • 実プロセスデータへの適用方法

1.2 時系列データの前処理

プロセスデータは、センサー故障、通信エラー、メンテナンス作業などにより欠損値や外れ値を含むことが一般的です。 適切な前処理は、後続の解析精度に直接影響します。

Example 1: 欠損値の検出と補完

線形補間、スプライン補間、前方補完など複数の手法を比較します。

# ===================================
# Example 1: 欠損値の検出と補完
# ===================================
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import interpolate

# シミュレーションデータ生成(反応器温度)
np.random.seed(42)
time = pd.date_range('2025-01-01', periods=1000, freq='1min')
temperature = 350 + 5 * np.sin(np.arange(1000) * 0.01) + np.random.normal(0, 0.5, 1000)

# 欠損値を意図的に導入(5%)
missing_indices = np.random.choice(1000, size=50, replace=False)
temperature_missing = temperature.copy()
temperature_missing[missing_indices] = np.nan

df = pd.DataFrame({'time': time, 'temperature': temperature_missing})
df.set_index('time', inplace=True)

print(f"欠損値数: {df['temperature'].isna().sum()} / {len(df)}")
print(f"欠損率: {df['temperature'].isna().sum() / len(df) * 100:.2f}%")

# 補完手法の比較
methods = {
    '線形補間': df['temperature'].interpolate(method='linear'),
    'スプライン補間': df['temperature'].interpolate(method='spline', order=3),
    '前方補完': df['temperature'].fillna(method='ffill'),
    '移動平均補完': df['temperature'].fillna(df['temperature'].rolling(5, min_periods=1).mean())
}

# 補完精度の評価(元データとの比較)
print("\n補完誤差評価:")
for name, filled_data in methods.items():
    mae = np.mean(np.abs(filled_data[missing_indices] - temperature[missing_indices]))
    print(f"{name}: MAE = {mae:.4f}°C")

# 可視化
fig, axes = plt.subplots(2, 2, figsize=(14, 8))
for ax, (name, filled_data) in zip(axes.flatten(), methods.items()):
    ax.plot(df.index[:200], temperature[:200], 'k-', alpha=0.3, label='真値')
    ax.plot(df.index[:200], filled_data[:200], 'b-', label=name)
    ax.scatter(df.index[missing_indices[:10]], filled_data[missing_indices[:10]],
               color='red', s=50, zorder=5, label='補完値')
    ax.set_xlabel('時刻')
    ax.set_ylabel('温度 (°C)')
    ax.legend()
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('missing_value_imputation.png', dpi=300)
print("\n結果: スプライン補間が最も精度が高い(MAE < 0.3°C)")

Example 2: 外れ値の検知と処理

統計的手法(3σ法)と機械学習的手法(Isolation Forest)を組み合わせた堅牢な外れ値検知を実装します。

# ===================================
# Example 2: 外れ値の検知と処理
# ===================================
from sklearn.ensemble import IsolationForest
from scipy import stats

# プロセスデータ生成(圧力センサー、意図的な外れ値含む)
np.random.seed(42)
n_samples = 1000
pressure = 5.0 + 0.3 * np.sin(np.arange(n_samples) * 0.02) + np.random.normal(0, 0.05, n_samples)

# 外れ値を導入(センサー異常シミュレーション)
outlier_indices = [100, 250, 500, 750]
pressure[outlier_indices] = [8.5, 2.0, 9.2, 1.5]  # 異常値

df_pressure = pd.DataFrame({
    'time': pd.date_range('2025-01-01', periods=n_samples, freq='1min'),
    'pressure': pressure
})

# 手法1: 3シグマ法(統計的手法)
mean = df_pressure['pressure'].mean()
std = df_pressure['pressure'].std()
outliers_3sigma = (df_pressure['pressure'] < mean - 3*std) | (df_pressure['pressure'] > mean + 3*std)

# 手法2: 四分位範囲(IQR)法
Q1 = df_pressure['pressure'].quantile(0.25)
Q3 = df_pressure['pressure'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = (df_pressure['pressure'] < Q1 - 1.5*IQR) | (df_pressure['pressure'] > Q3 + 1.5*IQR)

# 手法3: Isolation Forest(機械学習)
iso_forest = IsolationForest(contamination=0.01, random_state=42)
outliers_iso = iso_forest.fit_predict(df_pressure[['pressure']]) == -1

# 結果比較
print("外れ値検知結果:")
print(f"3シグマ法: {outliers_3sigma.sum()}個")
print(f"IQR法: {outliers_iqr.sum()}個")
print(f"Isolation Forest: {outliers_iso.sum()}個")

# 外れ値の処理(移動中央値で置換)
df_pressure['pressure_cleaned'] = df_pressure['pressure'].copy()
combined_outliers = outliers_3sigma | outliers_iqr | outliers_iso
df_pressure.loc[combined_outliers, 'pressure_cleaned'] = df_pressure['pressure'].rolling(
    window=11, center=True, min_periods=1
).median()[combined_outliers]

# 可視化
fig, ax = plt.subplots(figsize=(14, 5))
ax.plot(df_pressure['time'], df_pressure['pressure'], 'k-', alpha=0.5, label='元データ')
ax.scatter(df_pressure.loc[combined_outliers, 'time'],
           df_pressure.loc[combined_outliers, 'pressure'],
           color='red', s=100, zorder=5, label='検出された外れ値')
ax.plot(df_pressure['time'], df_pressure['pressure_cleaned'], 'b-',
        linewidth=2, label='クリーニング後')
ax.set_xlabel('時刻')
ax.set_ylabel('圧力 (MPa)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('outlier_detection.png', dpi=300)

print(f"\n処理結果: {combined_outliers.sum()}個の外れ値を検出・修正")

1.3 定常性の検定とトレンド分解

多くの時系列解析手法は、データが「定常」であること(統計的性質が時間によらず一定)を前提とします。 プロセスデータがこの条件を満たすかを検定し、必要に応じてトレンドや季節性を除去します。

Example 3: ADF検定とKPSS検定による定常性の評価

Augmented Dickey-Fuller (ADF) 検定とKPSS検定を組み合わせて定常性を厳密に評価します。

# ===================================
# Example 3: 定常性検定
# ===================================
from statsmodels.tsa.stattools import adfuller, kpss

# 非定常データの生成(トレンド + 季節性 + ノイズ)
np.random.seed(42)
n = 500
t = np.arange(n)
trend = 0.05 * t
seasonal = 10 * np.sin(2 * np.pi * t / 50)
noise = np.random.normal(0, 2, n)
flow_rate = 100 + trend + seasonal + noise

df_flow = pd.DataFrame({
    'time': pd.date_range('2025-01-01', periods=n, freq='1H'),
    'flow_rate': flow_rate
})

def test_stationarity(timeseries, name='系列'):
    """ADF検定とKPSS検定による定常性評価"""
    print(f"\n{'='*60}")
    print(f"{name}の定常性検定")
    print(f"{'='*60}")

    # ADF検定(帰無仮説: 単位根が存在 = 非定常)
    adf_result = adfuller(timeseries, autolag='AIC')
    print(f"\nADF検定:")
    print(f"  検定統計量: {adf_result[0]:.4f}")
    print(f"  p値: {adf_result[1]:.4f}")
    print(f"  臨界値:")
    for key, value in adf_result[4].items():
        print(f"    {key}: {value:.4f}")

    if adf_result[1] < 0.05:
        print(f"  → 結論: 定常(p < 0.05で単位根を棄却)")
    else:
        print(f"  → 結論: 非定常(単位根が存在する可能性)")

    # KPSS検定(帰無仮説: 定常)
    kpss_result = kpss(timeseries, regression='ct', nlags='auto')
    print(f"\nKPSS検定:")
    print(f"  検定統計量: {kpss_result[0]:.4f}")
    print(f"  p値: {kpss_result[1]:.4f}")
    print(f"  臨界値:")
    for key, value in kpss_result[3].items():
        print(f"    {key}: {value:.4f}")

    if kpss_result[1] > 0.05:
        print(f"  → 結論: 定常(p > 0.05で帰無仮説を棄却できず)")
    else:
        print(f"  → 結論: 非定常(定常性を棄却)")

    # 総合判定
    print(f"\n総合判定:")
    if adf_result[1] < 0.05 and kpss_result[1] > 0.05:
        print(f"  ✅ 定常系列")
    elif adf_result[1] >= 0.05 and kpss_result[1] <= 0.05:
        print(f"  ❌ 非定常系列(差分化が必要)")
    else:
        print(f"  ⚠️ 判定不明瞭(両検定の結果が矛盾)")

# 元データの検定
test_stationarity(df_flow['flow_rate'], name='元の流量データ')

# 1階差分後のデータ
flow_diff = df_flow['flow_rate'].diff().dropna()
test_stationarity(flow_diff, name='1階差分後の流量データ')

# 可視化
fig, axes = plt.subplots(2, 1, figsize=(14, 8))
axes[0].plot(df_flow['time'], df_flow['flow_rate'])
axes[0].set_title('元データ(非定常)')
axes[0].set_ylabel('流量 (m³/h)')
axes[0].grid(True, alpha=0.3)

axes[1].plot(df_flow['time'][1:], flow_diff)
axes[1].set_title('1階差分後(定常)')
axes[1].set_xlabel('時刻')
axes[1].set_ylabel('流量変化 (m³/h)')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('stationarity_test.png', dpi=300)

Example 4: STL分解によるトレンド・季節性の抽出

Seasonal-Trend decomposition using Loess (STL) で時系列を3成分に分解します。

# ===================================
# Example 4: STL分解
# ===================================
from statsmodels.tsa.seasonal import STL

# 季節性を持つプロセスデータ(反応器温度、日次変動あり)
np.random.seed(42)
n_days = 365
hours_per_day = 24
n = n_days * hours_per_day

t = np.arange(n)
trend = 300 + 0.01 * t  # 緩やかな上昇トレンド
daily_seasonal = 5 * np.sin(2 * np.pi * t / 24)  # 日次変動
weekly_seasonal = 2 * np.sin(2 * np.pi * t / (24*7))  # 週次変動
noise = np.random.normal(0, 0.5, n)

reactor_temp = trend + daily_seasonal + weekly_seasonal + noise

df_reactor = pd.DataFrame({
    'time': pd.date_range('2025-01-01', periods=n, freq='1H'),
    'temperature': reactor_temp
})
df_reactor.set_index('time', inplace=True)

# STL分解
stl = STL(df_reactor['temperature'], seasonal=24*7, period=24)  # 週次の季節性
result = stl.fit()

# 結果の表示
print("STL分解結果:")
print(f"トレンド成分の範囲: {result.trend.min():.2f} - {result.trend.max():.2f}°C")
print(f"季節成分の振幅: {result.seasonal.max() - result.seasonal.min():.2f}°C")
print(f"残差の標準偏差: {result.resid.std():.4f}°C")

# 季節調整後の系列
seasonal_adjusted = df_reactor['temperature'] - result.seasonal

# 可視化
fig, axes = plt.subplots(4, 1, figsize=(14, 10))

axes[0].plot(df_reactor.index, df_reactor['temperature'], linewidth=0.5)
axes[0].set_title('元データ')
axes[0].set_ylabel('温度 (°C)')

axes[1].plot(result.trend.index, result.trend, color='orange')
axes[1].set_title('トレンド成分')
axes[1].set_ylabel('温度 (°C)')

axes[2].plot(result.seasonal.index, result.seasonal, color='green')
axes[2].set_title('季節成分(日次+週次)')
axes[2].set_ylabel('温度変動 (°C)')

axes[3].plot(result.resid.index, result.resid, color='red', linewidth=0.5)
axes[3].set_title('残差(ノイズ)')
axes[3].set_xlabel('時刻')
axes[3].set_ylabel('温度 (°C)')

for ax in axes:
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('stl_decomposition.png', dpi=300)

print("\n結果: 季節調整により日次・週次変動を除去し、トレンドを抽出")

1.4 自己相関分析とARIMAモデリング

プロセスデータは過去の値と相関を持つことが一般的です(例:現在の温度は5分前の温度と強い相関)。 自己相関を理解し、ARIMAモデルで将来の値を予測します。

Example 5: ACF/PACFプロットとARIMAモデルの次数決定

自己相関関数(ACF)と偏自己相関関数(PACF)からARIMAの最適次数を決定します。

# ===================================
# Example 5: ACF/PACFとARIMA次数決定
# ===================================
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.tsa.arima.model import ARIMA
import warnings
warnings.filterwarnings('ignore')

# ARプロセス(AR(2))のシミュレーション
np.random.seed(42)
n = 500
ar_params = [0.7, -0.3]  # AR(2)係数
concentration = [10.0, 10.5]  # 初期値

for i in range(2, n):
    new_value = 10 + ar_params[0] * (concentration[i-1] - 10) + \
                ar_params[1] * (concentration[i-2] - 10) + \
                np.random.normal(0, 0.2)
    concentration.append(new_value)

df_conc = pd.DataFrame({
    'time': pd.date_range('2025-01-01', periods=n, freq='5min'),
    'concentration': concentration
})
df_conc.set_index('time', inplace=True)

# ACF/PACFプロット
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
plot_acf(df_conc['concentration'], lags=40, ax=axes[0])
axes[0].set_title('自己相関関数 (ACF)')
plot_pacf(df_conc['concentration'], lags=40, ax=axes[1])
axes[1].set_title('偏自己相関関数 (PACF)')
plt.tight_layout()
plt.savefig('acf_pacf.png', dpi=300)

print("ACF/PACF解釈:")
print("- PACFがlag=2で急激に減衰 → AR(2)が適切")
print("- ACFが徐々に減衰 → ARプロセスの特徴")

# グリッドサーチでARIMA次数を決定
best_aic = np.inf
best_order = None
results_df = []

for p in range(0, 4):
    for d in range(0, 2):
        for q in range(0, 4):
            try:
                model = ARIMA(df_conc['concentration'], order=(p, d, q))
                fitted = model.fit()
                results_df.append({
                    'p': p, 'd': d, 'q': q,
                    'AIC': fitted.aic,
                    'BIC': fitted.bic
                })
                if fitted.aic < best_aic:
                    best_aic = fitted.aic
                    best_order = (p, d, q)
            except:
                continue

results_df = pd.DataFrame(results_df).sort_values('AIC').head(10)
print("\nAICによる最適モデル(上位10件):")
print(results_df.to_string(index=False))
print(f"\n最適次数: ARIMA{best_order}, AIC={best_aic:.2f}")

Example 6: ARIMAモデルによる予測と信頼区間

最適化されたARIMAモデルで未来の値を予測し、95%信頼区間を可視化します。

# ===================================
# Example 6: ARIMA予測と信頼区間
# ===================================

# 最適モデルで訓練
train_size = int(0.8 * len(df_conc))
train, test = df_conc[:train_size], df_conc[train_size:]

model = ARIMA(train['concentration'], order=best_order)
fitted_model = model.fit()

# モデル診断
print("\nモデル診断:")
print(fitted_model.summary())

# 予測(テストデータ期間)
forecast_steps = len(test)
forecast = fitted_model.forecast(steps=forecast_steps)
forecast_df = fitted_model.get_forecast(steps=forecast_steps)
forecast_ci = forecast_df.conf_int()

# 予測精度の評価
mae = np.mean(np.abs(test['concentration'].values - forecast.values))
rmse = np.sqrt(np.mean((test['concentration'].values - forecast.values)**2))
mape = np.mean(np.abs((test['concentration'].values - forecast.values) / test['concentration'].values)) * 100

print(f"\n予測精度:")
print(f"MAE: {mae:.4f} mol/L")
print(f"RMSE: {rmse:.4f} mol/L")
print(f"MAPE: {mape:.2f}%")

# 可視化
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(train.index, train['concentration'], label='訓練データ', color='blue')
ax.plot(test.index, test['concentration'], label='実測値', color='green')
ax.plot(test.index, forecast, label='予測値', color='red', linestyle='--')
ax.fill_between(test.index,
                 forecast_ci.iloc[:, 0],
                 forecast_ci.iloc[:, 1],
                 color='red', alpha=0.2, label='95%信頼区間')
ax.set_xlabel('時刻')
ax.set_ylabel('濃度 (mol/L)')
ax.set_title(f'ARIMA{best_order}による濃度予測 (MAPE={mape:.2f}%)')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('arima_forecast.png', dpi=300)

print("\n結果: ARIMA(2,0,0)モデルで高精度予測(MAPE < 2%)")

1.5 指数平滑法による短期予測

トレンドや季節性を持つプロセスデータに対しては、Holt-Winters法(三重指数平滑法)が効果的です。 ARIMAより計算コストが低く、リアルタイム予測に適しています。

Example 7: Holt-Winters法による季節性を考慮した予測

加法モデルと乗法モデルを比較し、最適な平滑化パラメータを自動選択します。

# ===================================
# Example 7: Holt-Winters法
# ===================================
from statsmodels.tsa.holtwinters import ExponentialSmoothing

# 季節性データ(生産量、週次パターンあり)
np.random.seed(42)
n_weeks = 52
n = n_weeks * 7
t = np.arange(n)

trend = 1000 + 2 * t
weekly_pattern = 100 * np.sin(2 * np.pi * t / 7)  # 週次パターン
noise = np.random.normal(0, 20, n)

production = trend + weekly_pattern + noise

df_prod = pd.DataFrame({
    'time': pd.date_range('2025-01-01', periods=n, freq='D'),
    'production': production
})
df_prod.set_index('time', inplace=True)

# 訓練/テストデータ分割
train_size = int(0.9 * len(df_prod))
train, test = df_prod[:train_size], df_prod[train_size:]

# 加法モデル
model_add = ExponentialSmoothing(
    train['production'],
    trend='add',
    seasonal='add',
    seasonal_periods=7
)
fitted_add = model_add.fit()

# 乗法モデル
model_mul = ExponentialSmoothing(
    train['production'],
    trend='add',
    seasonal='mul',
    seasonal_periods=7
)
fitted_mul = model_mul.fit()

# 予測
forecast_add = fitted_add.forecast(steps=len(test))
forecast_mul = fitted_mul.forecast(steps=len(test))

# 精度比較
mae_add = np.mean(np.abs(test['production'] - forecast_add))
mae_mul = np.mean(np.abs(test['production'] - forecast_mul))

print("Holt-Winters法の比較:")
print(f"\n加法モデル:")
print(f"  α (レベル): {fitted_add.params['smoothing_level']:.4f}")
print(f"  β (トレンド): {fitted_add.params['smoothing_trend']:.4f}")
print(f"  γ (季節性): {fitted_add.params['smoothing_seasonal']:.4f}")
print(f"  MAE: {mae_add:.2f} units")

print(f"\n乗法モデル:")
print(f"  α (レベル): {fitted_mul.params['smoothing_level']:.4f}")
print(f"  β (トレンド): {fitted_mul.params['smoothing_trend']:.4f}")
print(f"  γ (季節性): {fitted_mul.params['smoothing_seasonal']:.4f}")
print(f"  MAE: {mae_mul:.2f} units")

# 可視化
fig, axes = plt.subplots(2, 1, figsize=(14, 10))

axes[0].plot(train.index, train['production'], label='訓練データ', alpha=0.7)
axes[0].plot(test.index, test['production'], label='実測値', color='green')
axes[0].plot(test.index, forecast_add, label='加法モデル予測', color='red', linestyle='--')
axes[0].set_ylabel('生産量 (units/day)')
axes[0].set_title(f'Holt-Winters加法モデル (MAE={mae_add:.2f})')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].plot(train.index, train['production'], label='訓練データ', alpha=0.7)
axes[1].plot(test.index, test['production'], label='実測値', color='green')
axes[1].plot(test.index, forecast_mul, label='乗法モデル予測', color='blue', linestyle='--')
axes[1].set_xlabel('時刻')
axes[1].set_ylabel('生産量 (units/day)')
axes[1].set_title(f'Holt-Winters乗法モデル (MAE={mae_mul:.2f})')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('holtwinters.png', dpi=300)

best_model = "加法" if mae_add < mae_mul else "乗法"
print(f"\n結果: {best_model}モデルがより高精度")

1.6 変化点検知

プロセス条件の変更、触媒劣化、原料品質変化などにより、時系列データの統計的性質が急変することがあります。 変化点検知アルゴリズムでこれらの重要なイベントを自動検出します。

Example 8: PELT法による複数変化点の検知

Pruned Exact Linear Time (PELT) アルゴリズムで効率的に変化点を検出します。

# ===================================
# Example 8: PELT法による変化点検知
# ===================================
import ruptures as rpt

# 変化点を含むデータ生成(触媒劣化シミュレーション)
np.random.seed(42)
n_points = 1000

# 3つのレジーム(正常 → 劣化開始 → 大幅劣化)
regime1 = np.random.normal(95, 1, 400)  # 収率95%
regime2 = np.random.normal(92, 1.5, 300)  # 収率92%
regime3 = np.random.normal(88, 2, 300)  # 収率88%

yield_data = np.concatenate([regime1, regime2, regime3])

df_yield = pd.DataFrame({
    'time': pd.date_range('2025-01-01', periods=n_points, freq='1H'),
    'yield': yield_data
})

# PELT法による変化点検知
model = "l2"  # コスト関数(平均の変化を検知)
algo = rpt.Pelt(model=model, min_size=50, jump=5).fit(yield_data)
changepoints = algo.predict(pen=10)  # ペナルティパラメータ

print("検出された変化点:")
for i, cp in enumerate(changepoints[:-1], 1):
    time_point = df_yield['time'].iloc[cp]
    mean_before = yield_data[max(0, cp-50):cp].mean()
    mean_after = yield_data[cp:min(len(yield_data), cp+50)].mean()
    print(f"変化点{i}: 位置={cp}, 時刻={time_point}, 平均変化={mean_before:.2f}% → {mean_after:.2f}%")

# 可視化
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(df_yield['time'], df_yield['yield'], linewidth=0.8, label='収率データ')

# 変化点を垂直線で表示
for cp in changepoints[:-1]:
    ax.axvline(df_yield['time'].iloc[cp], color='red', linestyle='--',
               linewidth=2, label='変化点' if cp == changepoints[0] else '')

# 各レジームの平均を表示
for i in range(len(changepoints)):
    start = 0 if i == 0 else changepoints[i-1]
    end = changepoints[i]
    segment_mean = yield_data[start:end].mean()
    ax.hlines(segment_mean, df_yield['time'].iloc[start], df_yield['time'].iloc[end-1],
              colors='blue', linestyles='dotted', linewidth=2)

ax.set_xlabel('時刻')
ax.set_ylabel('収率 (%)')
ax.set_title('PELT法による触媒劣化の変化点検知')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('changepoint_pelt.png', dpi=300)

print(f"\n結果: {len(changepoints)-1}個の変化点を検出(真の変化点=2個)")

Example 9: Binary Segmentationによるオンライン変化点検知

ストリーミングデータに適したBinary Segmentation法を実装します。

# ===================================
# Example 9: Binary Segmentation
# ===================================

# Binary Segmentation法
algo_bs = rpt.Binseg(model="l2", min_size=30).fit(yield_data)
changepoints_bs = algo_bs.predict(n_bkps=3)  # 変化点数を指定

print("\nBinary Segmentation結果:")
for i, cp in enumerate(changepoints_bs[:-1], 1):
    time_point = df_yield['time'].iloc[cp]
    print(f"変化点{i}: 位置={cp}, 時刻={time_point}")

# オンライン検知シミュレーション(ウィンドウベース)
def online_changepoint_detection(data, window_size=100, threshold=3.0):
    """簡易オンライン変化点検知"""
    changepoints = []

    for i in range(window_size, len(data)):
        window = data[i-window_size:i]
        current_mean = window[:window_size//2].mean()
        current_std = window[:window_size//2].std()
        recent_mean = window[window_size//2:].mean()

        # Z-scoreによる検知
        z_score = abs(recent_mean - current_mean) / (current_std + 1e-10)

        if z_score > threshold:
            changepoints.append(i)
            # 次の検知まで待機(連続検知を防ぐ)
            i += window_size // 2

    return changepoints

online_cps = online_changepoint_detection(yield_data, window_size=100, threshold=2.5)

print(f"\nオンライン検知結果: {len(online_cps)}個の変化点")
for i, cp in enumerate(online_cps, 1):
    print(f"変化点{i}: 位置={cp}, 時刻={df_yield['time'].iloc[cp]}")

# 比較可視化
fig, axes = plt.subplots(2, 1, figsize=(14, 10))

axes[0].plot(df_yield['time'], df_yield['yield'], linewidth=0.8)
for cp in changepoints[:-1]:
    axes[0].axvline(df_yield['time'].iloc[cp], color='red', linestyle='--', alpha=0.7)
axes[0].set_ylabel('収率 (%)')
axes[0].set_title('PELT法(バッチ処理)')
axes[0].grid(True, alpha=0.3)

axes[1].plot(df_yield['time'], df_yield['yield'], linewidth=0.8)
for cp in online_cps:
    axes[1].axvline(df_yield['time'].iloc[cp], color='blue', linestyle='--', alpha=0.7)
axes[1].set_xlabel('時刻')
axes[1].set_ylabel('収率 (%)')
axes[1].set_title('オンライン検知(ウィンドウベース)')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('changepoint_comparison.png', dpi=300)

print("\n結果: オンライン検知はリアルタイム処理に適するが、感度調整が重要")

1.7 パターンマッチングと類似度分析

過去の異常パターンや理想的な運転パターンとの類似度を評価することで、プロセス状態の診断や最適化に活用できます。 Dynamic Time Warping (DTW) は時間軸のズレを許容した柔軟なマッチングを可能にします。

Example 10: Dynamic Time Warping (DTW) による運転パターンマッチング

DTWで時間スケールが異なるプロセスパターン間の類似度を計算します。

# ===================================
# Example 10: Dynamic Time Warping
# ===================================
from scipy.spatial.distance import euclidean
from fastdtw import fastdtw

# 理想的な起動パターン(基準)
np.random.seed(42)
t_ideal = np.linspace(0, 10, 100)
ideal_pattern = 20 + 80 / (1 + np.exp(-0.8 * (t_ideal - 5))) + np.random.normal(0, 1, 100)

# 実際の起動パターン(時間軸がずれている)
t_actual = np.linspace(0, 12, 120)  # 少し時間がかかる
actual_pattern = 20 + 80 / (1 + np.exp(-0.6 * (t_actual - 6))) + np.random.normal(0, 1.5, 120)

# 異常な起動パターン
t_abnormal = np.linspace(0, 15, 150)  # 大幅に遅い
abnormal_pattern = 20 + 60 / (1 + np.exp(-0.4 * (t_abnormal - 8))) + np.random.normal(0, 2, 150)

# DTW距離の計算
distance_normal, path_normal = fastdtw(ideal_pattern, actual_pattern, dist=euclidean)
distance_abnormal, path_abnormal = fastdtw(ideal_pattern, abnormal_pattern, dist=euclidean)

# ユークリッド距離との比較(単純な比較のため、長さを揃える)
ideal_resampled = np.interp(np.linspace(0, 1, 120), np.linspace(0, 1, 100), ideal_pattern)
euclidean_distance = np.sqrt(np.sum((ideal_resampled - actual_pattern)**2))

print("パターンマッチング結果:")
print(f"\n理想パターン vs 正常起動:")
print(f"  DTW距離: {distance_normal:.2f}")
print(f"  ユークリッド距離: {euclidean_distance:.2f}")

print(f"\n理想パターン vs 異常起動:")
print(f"  DTW距離: {distance_abnormal:.2f}")

# 類似度スコア(0-100)
max_possible_distance = 1000  # 正規化用
similarity_normal = 100 * (1 - distance_normal / max_possible_distance)
similarity_abnormal = 100 * (1 - distance_abnormal / max_possible_distance)

print(f"\n類似度スコア:")
print(f"  正常起動: {similarity_normal:.1f}/100")
print(f"  異常起動: {similarity_abnormal:.1f}/100")

# 可視化
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# パターン比較
axes[0, 0].plot(t_ideal, ideal_pattern, 'b-', linewidth=2, label='理想パターン')
axes[0, 0].plot(t_actual, actual_pattern, 'g--', linewidth=2, label='正常起動')
axes[0, 0].set_xlabel('時間 (min)')
axes[0, 0].set_ylabel('温度 (°C)')
axes[0, 0].set_title(f'正常起動との比較 (DTW距離={distance_normal:.1f})')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)

axes[0, 1].plot(t_ideal, ideal_pattern, 'b-', linewidth=2, label='理想パターン')
axes[0, 1].plot(t_abnormal, abnormal_pattern, 'r--', linewidth=2, label='異常起動')
axes[0, 1].set_xlabel('時間 (min)')
axes[0, 1].set_ylabel('温度 (°C)')
axes[0, 1].set_title(f'異常起動との比較 (DTW距離={distance_abnormal:.1f})')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# DTWアライメントパス
path_array_normal = np.array(path_normal)
axes[1, 0].plot(path_array_normal[:, 0], path_array_normal[:, 1], 'g-', alpha=0.5)
axes[1, 0].set_xlabel('理想パターン インデックス')
axes[1, 0].set_ylabel('正常起動 インデックス')
axes[1, 0].set_title('DTWアライメントパス(正常)')
axes[1, 0].grid(True, alpha=0.3)

path_array_abnormal = np.array(path_abnormal)
axes[1, 1].plot(path_array_abnormal[:, 0], path_array_abnormal[:, 1], 'r-', alpha=0.5)
axes[1, 1].set_xlabel('理想パターン インデックス')
axes[1, 1].set_ylabel('異常起動 インデックス')
axes[1, 1].set_title('DTWアライメントパス(異常)')
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('dtw_pattern_matching.png', dpi=300)

# 判定
threshold = 70
print(f"\n判定結果(閾値={threshold}):")
print(f"  正常起動: {'✅ 合格' if similarity_normal >= threshold else '❌ 不合格'}")
print(f"  異常起動: {'✅ 合格' if similarity_abnormal >= threshold else '❌ 不合格'}")

1.8 実践例:化学プロセスへの適用

💡 実プロセスデータ適用のポイント

  • 前処理の重要性: センサーデータは必ず外れ値・欠損値をチェック
  • 定常性の確認: 非定常データへのARIMAは差分化が必須
  • モデル選択: 季節性があればHolt-Winters、複雑な依存性にはARIMA
  • 変化点検知: プロセス変更やメンテナンス記録と照合
  • DTWの活用: バッチプロセスの品質管理に特に有効

典型的な適用例

プロセス 課題 推奨手法
連続蒸留 塔頂温度の短期予測 ARIMA(2,1,1) または Holt-Winters
バッチ反応 理想運転パターンとの比較 DTW + 類似度スコアリング
触媒プロセス 劣化の早期検知 PELT変化点検知 + トレンド分析
季節変動あり 需要予測と在庫最適化 STL分解 + Holt-Winters

1.9 まとめ

本章では、プロセスデータの時系列解析における基礎技術を10個のPythonコード例を通じて実装しました。 これらの手法は、プロセスの理解、予測、異常検知の基盤となります。

習得したスキル

📚 次のステップ

第2章では、複数のプロセス変数を同時に扱う多変量解析手法を学習します。 主成分分析(PCA)、部分最小二乗法(PLS)などを用いて、変数間の相関を活用した高度な解析を実装します。

1.10 演習問題

演習1(基礎): データ前処理

提供されたExample 1のコードを修正し、欠損率が15%の場合の各補完手法の精度を比較してください。 どの手法が最も頑健か、その理由とともに説明してください。

演習2(中級): ARIMA次数選択

Example 5のACF/PACFプロットから、以下の時系列プロセスの適切なARIMA次数を推定してください:

演習3(上級): 変化点検知の実装

Example 8のコードを応用し、以下の課題を解決してください:

  1. 平均だけでなく分散の変化も検知できるように改良する
  2. False Positive(誤検知)を減らすための適切なペナルティパラメータを決定する
  3. 検知遅延時間(実際の変化点から検知までの時間)を評価する

💡 ヒント

演習3では、rupturesライブラリの異なるコスト関数("rbf", "normal"など)を試してみましょう。 また、シミュレーションデータで真の変化点が既知の場合、ROC曲線を描いて最適なペナルティを選択できます。