第5章:リアルタイムデータ解析と可視化

ストリーミングデータ処理と動的監視システムの構築

📖 読了時間: 20-25 minutes 📊 難易度: Beginner 💻 コード例: 0個 📝 演習問題: 0問

学習目標

この章を読むことで、以下を習得できます:


5.1 リアルタイム処理の基礎

リアルタイムデータ処理の課題

化学プロセスのリアルタイム監視では、以下の技術的課題に対応する必要があります:

課題説明解決策
低レイテンシミリ秒〜秒単位での応答効率的なアルゴリズム、バッファリング
高スループット多数のセンサーからのデータ並列処理、分散システム
データ品質欠損値、ノイズ、外れ値リアルタイムフィルタリング
メモリ制約無限のデータストリーム移動窓、メモリ効率的なデータ構造
モデル適応プロセス条件の変化オンライン学習、適応アルゴリズム

5.2 ストリーミングデータ処理

コード例1: pandasによるストリーミングデータ処理の基礎

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from collections import deque
import time

class StreamingDataProcessor:
    """
    リアルタイムデータストリーム処理クラス

    Parameters:
    -----------
    window_size : int
        移動窓のサイズ(データポイント数)
    sampling_rate : float
        サンプリングレート [Hz]
    """

    def __init__(self, window_size=100, sampling_rate=1.0):
        self.window_size = window_size
        self.sampling_rate = sampling_rate

        # データバッファ(高速なdequeを使用)
        self.buffer = deque(maxlen=window_size)

        # 統計量の履歴
        self.timestamps = []
        self.means = []
        self.stds = []
        self.mins = []
        self.maxs = []

    def add_data_point(self, value, timestamp=None):
        """
        新しいデータポイントを追加

        Parameters:
        -----------
        value : float
            データ値
        timestamp : float
            タイムスタンプ(Noneの場合は現在時刻)
        """
        if timestamp is None:
            timestamp = time.time()

        self.buffer.append(value)

        # 統計量の計算(窓内のデータ)
        if len(self.buffer) > 0:
            data_array = np.array(self.buffer)
            self.timestamps.append(timestamp)
            self.means.append(np.mean(data_array))
            self.stds.append(np.std(data_array))
            self.mins.append(np.min(data_array))
            self.maxs.append(np.max(data_array))

    def get_statistics(self):
        """
        現在の窓での統計量を取得

        Returns:
        --------
        stats : dict
            統計量の辞書
        """
        if len(self.buffer) == 0:
            return {}

        data_array = np.array(self.buffer)
        return {
            'mean': np.mean(data_array),
            'std': np.std(data_array),
            'min': np.min(data_array),
            'max': np.max(data_array),
            'median': np.median(data_array),
            'count': len(data_array)
        }

    def detect_anomaly(self, threshold_std=3.0):
        """
        異常検知(移動平均 ± threshold_std × 標準偏差)

        Parameters:
        -----------
        threshold_std : float
            標準偏差の倍数(閾値)

        Returns:
        --------
        is_anomaly : bool
            異常かどうか
        """
        if len(self.buffer) < 2:
            return False

        data_array = np.array(self.buffer)
        mean = np.mean(data_array)
        std = np.std(data_array)
        latest_value = data_array[-1]

        # 3シグマルール
        is_anomaly = abs(latest_value - mean) > threshold_std * std

        return is_anomaly


# シミュレーション:リアルタイムデータストリームの処理
np.random.seed(42)

# プロセッサの初期化
processor = StreamingDataProcessor(window_size=50, sampling_rate=10.0)

# データ生成とストリーミング処理
n_points = 300
anomaly_indices = [100, 150, 250]  # 異常を注入する位置

for i in range(n_points):
    # 通常データ + 異常
    if i in anomaly_indices:
        value = 180 + np.random.randn() * 15  # 異常に大きな変動
    else:
        value = 180 + 2 * np.sin(0.1 * i) + np.random.randn() * 1

    # データポイントの追加
    processor.add_data_point(value, timestamp=i)

    # リアルタイム異常検知
    if processor.detect_anomaly(threshold_std=3.0) and i > 50:
        print(f"Anomaly detected at time {i}: value = {value:.2f}")


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

# 生データと移動平均
time_axis = processor.timestamps
axes[0].plot(time_axis, processor.means, color='#11998e', linewidth=2, label='Moving Mean')
axes[0].fill_between(time_axis,
                      np.array(processor.means) - np.array(processor.stds),
                      np.array(processor.means) + np.array(processor.stds),
                      color='#11998e', alpha=0.2, label='±1σ')
axes[0].plot(time_axis, processor.mins, color='red', linewidth=1, alpha=0.5, label='Min/Max')
axes[0].plot(time_axis, processor.maxs, color='red', linewidth=1, alpha=0.5)

# 異常位置をマーク
for idx in anomaly_indices:
    if idx < len(time_axis):
        axes[0].axvline(x=time_axis[idx], color='red', linestyle='--',
                        linewidth=2, alpha=0.7)

axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Temperature [°C]')
axes[0].set_title('Real-time Streaming Data Processing (Moving Window)', fontweight='bold')
axes[0].legend()
axes[0].grid(alpha=0.3)

# 移動標準偏差(変動性の監視)
axes[1].plot(time_axis, processor.stds, color='orange', linewidth=2)
axes[1].axhline(y=np.mean(processor.stds) + 2*np.std(processor.stds),
                color='red', linestyle='--', linewidth=2, label='Threshold (Mean + 2σ)')
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Moving Std Dev [°C]')
axes[1].set_title('Process Variability Monitoring', fontweight='bold')
axes[1].legend()
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

# 最終統計
final_stats = processor.get_statistics()
print("\nFinal Window Statistics:")
print("=" * 50)
for key, value in final_stats.items():
    print(f"{key:10s}: {value:10.4f}")

解説: dequeを使った移動窓は、固定サイズのバッファを効率的に管理し、O(1)の時間複雑度で新しいデータを追加できます。リアルタイム異常検知は3シグマルールに基づき、統計的な外れ値を検出します。


コード例2: リアルタイム統計モニタリング(EWMA)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

class EWMAMonitor:
    """
    指数移動平均(EWMA)によるリアルタイムモニタリング

    Parameters:
    -----------
    alpha : float
        平滑化パラメータ(0 < alpha <= 1)
        小さいほど過去のデータに重みを置く
    """

    def __init__(self, alpha=0.1):
        self.alpha = alpha
        self.ewma = None
        self.ewmvar = None  # EWMA variance

        # 履歴
        self.values = []
        self.ewma_values = []
        self.ewmvar_values = []
        self.control_limits_upper = []
        self.control_limits_lower = []

    def update(self, new_value):
        """
        新しいデータポイントでEWMAを更新

        Parameters:
        -----------
        new_value : float
            新しいデータ値
        """
        # 初期化
        if self.ewma is None:
            self.ewma = new_value
            self.ewmvar = 0
        else:
            # EWMA更新
            self.ewma = self.alpha * new_value + (1 - self.alpha) * self.ewma

            # EWMA分散更新
            deviation = new_value - self.ewma
            self.ewmvar = self.alpha * (deviation ** 2) + (1 - self.alpha) * self.ewmvar

        # 管理限界(UCL/LCL: Upper/Lower Control Limit)
        ewm_std = np.sqrt(self.ewmvar)
        ucl = self.ewma + 3 * ewm_std
        lcl = self.ewma - 3 * ewm_std

        # 履歴に保存
        self.values.append(new_value)
        self.ewma_values.append(self.ewma)
        self.ewmvar_values.append(self.ewmvar)
        self.control_limits_upper.append(ucl)
        self.control_limits_lower.append(lcl)

    def is_out_of_control(self, threshold=3.0):
        """
        管理限界を超えているか判定

        Returns:
        --------
        bool : 管理限界外かどうか
        """
        if len(self.values) < 2:
            return False

        latest_value = self.values[-1]
        ucl = self.control_limits_upper[-1]
        lcl = self.control_limits_lower[-1]

        return latest_value > ucl or latest_value < lcl


# シミュレーション
np.random.seed(42)
n_points = 300

# プロセスシフト(100時点、200時点で平均がシフト)
data = []
for i in range(n_points):
    if i < 100:
        value = 180 + np.random.randn() * 2
    elif i < 200:
        value = 185 + np.random.randn() * 2  # 平均シフト(+5°C)
    else:
        value = 180 + np.random.randn() * 3  # 変動増加

    data.append(value)

# EWMAモニタリング
monitor = EWMAMonitor(alpha=0.2)

for i, value in enumerate(data):
    monitor.update(value)
    if monitor.is_out_of_control():
        print(f"Out of control at time {i}: value = {value:.2f}")

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

time_axis = np.arange(len(data))

# EWMA管理図
axes[0].plot(time_axis, monitor.values, color='gray', alpha=0.5,
             linewidth=1, label='Raw Data')
axes[0].plot(time_axis, monitor.ewma_values, color='#11998e',
             linewidth=2.5, label='EWMA')
axes[0].plot(time_axis, monitor.control_limits_upper, color='red',
             linestyle='--', linewidth=2, label='UCL/LCL')
axes[0].plot(time_axis, monitor.control_limits_lower, color='red',
             linestyle='--', linewidth=2)
axes[0].fill_between(time_axis, monitor.control_limits_lower,
                      monitor.control_limits_upper,
                      color='green', alpha=0.1, label='Control Region')

# プロセスシフト位置をマーク
axes[0].axvline(x=100, color='blue', linestyle=':', linewidth=2, alpha=0.7)
axes[0].axvline(x=200, color='blue', linestyle=':', linewidth=2, alpha=0.7)

axes[0].set_xlabel('Time [samples]')
axes[0].set_ylabel('Temperature [°C]')
axes[0].set_title('EWMA Control Chart (α=0.2)', fontweight='bold')
axes[0].legend()
axes[0].grid(alpha=0.3)

# EWMA分散(変動性監視)
axes[1].plot(time_axis, np.sqrt(monitor.ewmvar_values), color='orange', linewidth=2)
axes[1].set_xlabel('Time [samples]')
axes[1].set_ylabel('EWMA Std Dev [°C]')
axes[1].set_title('Process Variability (EWMA Variance)', fontweight='bold')
axes[1].axvline(x=200, color='blue', linestyle=':', linewidth=2, alpha=0.7,
                label='Variability Increase')
axes[1].legend()
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

print(f"\nEWMA Monitor Summary:")
print(f"  Total samples: {len(data)}")
print(f"  Final EWMA: {monitor.ewma:.2f}")
print(f"  Final EWMA Std: {np.sqrt(monitor.ewmvar):.2f}")

解説: EWMA(Exponentially Weighted Moving Average)は、小さなプロセスシフトを迅速に検出できるリアルタイム監視手法です。単純移動平均よりも最近のデータに重みを置き、レイテンシが低いため、化学プロセスの制御に適しています。


5.3 オンライン機械学習

コード例3: オンライン線形回帰(増分学習)

import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import SGDRegressor
from sklearn.preprocessing import StandardScaler

class OnlineLinearRegressor:
    """
    オンライン学習による線形回帰モデル

    Parameters:
    -----------
    learning_rate : str
        学習率スケジュール ('constant', 'optimal', 'adaptive')
    """

    def __init__(self, learning_rate='constant', eta0=0.01):
        self.model = SGDRegressor(
            learning_rate=learning_rate,
            eta0=eta0,
            random_state=42,
            warm_start=True  # 増分学習を有効化
        )
        self.scaler_X = StandardScaler()
        self.scaler_y = StandardScaler()
        self.is_fitted = False

        # 履歴
        self.predictions = []
        self.actuals = []
        self.errors = []

    def partial_fit(self, X, y):
        """
        新しいデータで部分的にフィット(増分学習)

        Parameters:
        -----------
        X : array-like, shape (n_samples, n_features)
            特徴量
        y : array-like, shape (n_samples,)
            目的変数
        """
        X = np.atleast_2d(X)
        y = np.atleast_1d(y)

        # スケーリング
        if not self.is_fitted:
            self.scaler_X.fit(X)
            self.scaler_y.fit(y.reshape(-1, 1))
            self.is_fitted = True

        X_scaled = self.scaler_X.transform(X)
        y_scaled = self.scaler_y.transform(y.reshape(-1, 1)).ravel()

        # 増分学習
        self.model.partial_fit(X_scaled, y_scaled)

    def predict(self, X):
        """
        予測

        Parameters:
        -----------
        X : array-like, shape (n_samples, n_features)
            特徴量

        Returns:
        --------
        y_pred : array-like
            予測値
        """
        if not self.is_fitted:
            return np.zeros(X.shape[0])

        X = np.atleast_2d(X)
        X_scaled = self.scaler_X.transform(X)
        y_scaled_pred = self.model.predict(X_scaled)
        y_pred = self.scaler_y.inverse_transform(y_scaled_pred.reshape(-1, 1)).ravel()

        return y_pred

    def update_and_predict(self, X, y):
        """
        予測してから学習(オンライン学習の典型パターン)

        Parameters:
        -----------
        X : array-like
            特徴量
        y : float
            実際の値

        Returns:
        --------
        y_pred : float
            予測値
        """
        # 予測
        y_pred = self.predict(X)

        # 学習
        self.partial_fit(X, y)

        # 履歴更新
        self.predictions.append(y_pred[0] if len(y_pred) > 0 else 0)
        self.actuals.append(y)
        self.errors.append(y - (y_pred[0] if len(y_pred) > 0 else 0))

        return y_pred[0] if len(y_pred) > 0 else 0


# シミュレーション:オンライン回帰
np.random.seed(42)
n_samples = 500

# 時間変化する線形関係(概念ドリフト)
X_stream = []
y_stream = []

for i in range(n_samples):
    # 特徴量
    x1 = np.random.randn()
    x2 = np.random.randn()
    X_stream.append([x1, x2])

    # 真の関係(時間とともに変化)
    if i < 200:
        # Phase 1: y = 3*x1 + 2*x2
        y_true = 3*x1 + 2*x2
    elif i < 400:
        # Phase 2: y = 2*x1 + 4*x2 (関係性が変化)
        y_true = 2*x1 + 4*x2
    else:
        # Phase 3: y = 1*x1 + 5*x2 (さらに変化)
        y_true = 1*x1 + 5*x2

    y_stream.append(y_true + np.random.randn() * 0.5)  # ノイズ追加

X_stream = np.array(X_stream)
y_stream = np.array(y_stream)

# オンライン学習
online_model = OnlineLinearRegressor(learning_rate='constant', eta0=0.01)

# 初期学習(最初の10サンプル)
online_model.partial_fit(X_stream[:10], y_stream[:10])

# ストリーミング学習
for i in range(10, n_samples):
    X_current = X_stream[i:i+1]
    y_current = y_stream[i]
    online_model.update_and_predict(X_current, y_current)


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

time_axis = np.arange(10, n_samples)

# 予測 vs 実際
axes[0].plot(time_axis, online_model.actuals, color='#11998e',
             linewidth=1.5, label='Actual', alpha=0.7)
axes[0].plot(time_axis, online_model.predictions, color='orange',
             linewidth=2, label='Predicted (Online)', linestyle='--')
axes[0].axvline(x=200, color='red', linestyle=':', linewidth=2, alpha=0.7,
                label='Concept Drift')
axes[0].axvline(x=400, color='red', linestyle=':', linewidth=2, alpha=0.7)
axes[0].set_xlabel('Time [samples]')
axes[0].set_ylabel('Target Value')
axes[0].set_title('Online Learning with Concept Drift', fontweight='bold')
axes[0].legend()
axes[0].grid(alpha=0.3)

# 予測誤差(オンライン学習の適応性を評価)
axes[1].plot(time_axis, online_model.errors, color='purple', linewidth=1.5)
axes[1].axhline(y=0, color='red', linestyle='--', linewidth=1)
axes[1].fill_between(time_axis, -1, 1, color='green', alpha=0.1, label='Acceptable Error')
axes[1].axvline(x=200, color='red', linestyle=':', linewidth=2, alpha=0.7)
axes[1].axvline(x=400, color='red', linestyle=':', linewidth=2, alpha=0.7)
axes[1].set_xlabel('Time [samples]')
axes[1].set_ylabel('Prediction Error')
axes[1].set_title('Prediction Error Over Time', fontweight='bold')
axes[1].legend()
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

# 各フェーズでのRMSE
phase1_rmse = np.sqrt(np.mean(np.array(online_model.errors[:190])**2))
phase2_rmse = np.sqrt(np.mean(np.array(online_model.errors[190:390])**2))
phase3_rmse = np.sqrt(np.mean(np.array(online_model.errors[390:])**2))

print(f"\nOnline Learning Performance:")
print(f"  Phase 1 RMSE: {phase1_rmse:.4f}")
print(f"  Phase 2 RMSE: {phase2_rmse:.4f}")
print(f"  Phase 3 RMSE: {phase3_rmse:.4f}")

解説: オンライン学習(増分学習)は、データが到着するたびにモデルを更新し、概念ドリフト(データ分布の変化)に適応します。バッチ学習と異なり、全データを保持する必要がなく、メモリ効率的です。


コード例4: オンラインPCAによる適応的異常検知

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import IncrementalPCA

class OnlineAnomalyDetector:
    """
    オンラインPCAによる適応的異常検知

    Parameters:
    -----------
    n_components : int
        主成分数
    threshold : float
        異常判定の閾値(SPEの標準偏差の倍数)
    """

    def __init__(self, n_components=2, threshold=3.0):
        self.n_components = n_components
        self.threshold = threshold
        self.ipca = IncrementalPCA(n_components=n_components)
        self.is_fitted = False
        self.spe_history = []
        self.anomalies = []

    def fit(self, X_initial):
        """
        初期データでPCAをフィット

        Parameters:
        -----------
        X_initial : array-like, shape (n_samples, n_features)
            初期学習データ
        """
        self.ipca.partial_fit(X_initial)
        self.is_fitted = True

    def detect(self, X):
        """
        異常検知

        Parameters:
        -----------
        X : array-like, shape (n_features,) or (n_samples, n_features)
            監視対象データ

        Returns:
        --------
        is_anomaly : bool or array
            異常かどうか
        spe : float or array
            SPE (Squared Prediction Error)
        """
        if not self.is_fitted:
            return False, 0.0

        X = np.atleast_2d(X)

        # 主成分空間への投影と再構成
        X_transformed = self.ipca.transform(X)
        X_reconstructed = self.ipca.inverse_transform(X_transformed)

        # SPE(二乗予測誤差)計算
        spe = np.sum((X - X_reconstructed) ** 2, axis=1)

        # 閾値判定
        if len(self.spe_history) > 30:
            spe_mean = np.mean(self.spe_history)
            spe_std = np.std(self.spe_history)
            threshold_value = spe_mean + self.threshold * spe_std
        else:
            threshold_value = np.inf  # 初期段階は閾値なし

        is_anomaly = spe > threshold_value

        # 履歴更新
        self.spe_history.extend(spe.tolist())

        return is_anomaly[0] if len(is_anomaly) == 1 else is_anomaly, spe[0] if len(spe) == 1 else spe


# シミュレーション:多変量プロセスデータ
np.random.seed(42)
n_samples = 500
n_features = 10

# 正常データ生成(相関のある多変量データ)
mean_normal = np.zeros(n_features)
cov_normal = np.eye(n_features) + 0.3 * np.random.randn(n_features, n_features)
cov_normal = cov_normal @ cov_normal.T  # 正定値対称行列

X_normal = np.random.multivariate_normal(mean_normal, cov_normal, size=400)

# 異常データの注入(大きな偏差)
anomaly_indices = [100, 150, 250, 350, 450]
X_stream = []
anomaly_labels = []

for i in range(n_samples):
    if i < 400:
        X_stream.append(X_normal[i])
        anomaly_labels.append(0)
    else:
        # 末尾100サンプルは異常を注入
        if i in anomaly_indices:
            X_anomaly = X_normal[i-400] + np.random.randn(n_features) * 5
        else:
            X_anomaly = X_normal[i-400] + np.random.randn(n_features) * 0.5
        X_stream.append(X_anomaly)
        anomaly_labels.append(1 if i in anomaly_indices else 0)

X_stream = np.array(X_stream)
anomaly_labels = np.array(anomaly_labels)

# オンラインPCA異常検知
detector = OnlineAnomalyDetector(n_components=3, threshold=3.0)
detector.fit(X_stream[:50])  # 初期学習

detected_anomalies = []
spe_values = []

for i in range(50, n_samples):
    X_current = X_stream[i:i+1]
    is_anomaly, spe = detector.detect(X_current)
    detected_anomalies.append(is_anomaly)
    spe_values.append(spe)

    # 定期的にモデル更新
    if i % 10 == 0:
        detector.ipca.partial_fit(X_stream[i-10:i])

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

time_axis = np.arange(50, n_samples)

# SPE値と閾値
axes[0].plot(time_axis, spe_values, color='#11998e', linewidth=1.5, label='SPE')

# 動的閾値
if len(detector.spe_history) > 30:
    spe_mean = np.mean(detector.spe_history)
    spe_std = np.std(detector.spe_history)
    threshold_line = spe_mean + 3 * spe_std
    axes[0].axhline(y=threshold_line, color='red', linestyle='--',
                    linewidth=2, label=f'Threshold (μ+3σ)')

# 真の異常位置をマーク
for idx in anomaly_indices:
    if idx >= 50:
        axes[0].axvline(x=idx, color='red', linestyle=':', linewidth=2, alpha=0.7)

axes[0].set_xlabel('Time [samples]')
axes[0].set_ylabel('SPE (Squared Prediction Error)')
axes[0].set_title('Online PCA Anomaly Detection', fontweight='bold')
axes[0].legend()
axes[0].set_yscale('log')
axes[0].grid(alpha=0.3)

# 検知結果の可視化
axes[1].scatter(time_axis[np.array(detected_anomalies)],
                np.ones(np.sum(detected_anomalies)),
                color='red', s=100, marker='x', label='Detected Anomalies', zorder=5)
axes[1].scatter(time_axis[~np.array(detected_anomalies)],
                np.zeros(np.sum(~np.array(detected_anomalies))),
                color='green', s=50, marker='o', alpha=0.3, label='Normal')

# 真の異常
true_anomaly_times = [idx for idx in anomaly_indices if idx >= 50]
axes[1].scatter(true_anomaly_times, [1.5]*len(true_anomaly_times),
                color='blue', s=150, marker='*', label='True Anomalies', zorder=5)

axes[1].set_xlabel('Time [samples]')
axes[1].set_yticks([0, 1, 1.5])
axes[1].set_yticklabels(['Normal', 'Detected', 'True'])
axes[1].set_title('Anomaly Detection Results', fontweight='bold')
axes[1].legend()
axes[1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

# 検知精度の評価
true_labels = anomaly_labels[50:]
detected_labels = np.array(detected_anomalies).astype(int)

from sklearn.metrics import precision_score, recall_score, f1_score

precision = precision_score(true_labels, detected_labels, zero_division=0)
recall = recall_score(true_labels, detected_labels, zero_division=0)
f1 = f1_score(true_labels, detected_labels, zero_division=0)

print(f"\nAnomaly Detection Performance:")
print(f"  Precision: {precision:.4f}")
print(f"  Recall:    {recall:.4f}")
print(f"  F1-Score:  {f1:.4f}")

解説: オンラインPCAは、多変量プロセスデータの主成分を逐次的に更新し、再構成誤差(SPE)に基づいて異常を検知します。プロセスの正常変動に適応しながら、異常パターンを検出できます。


5.4 リアルタイムダッシュボード

コード例5: Plotly Dashによるリアルタイムダッシュボード

import dash
from dash import dcc, html
from dash.dependencies import Input, Output
import plotly.graph_objs as go
import numpy as np
import pandas as pd
from collections import deque
from datetime import datetime
import random

# データバッファ(最新1000ポイントを保持)
max_length = 1000
time_buffer = deque(maxlen=max_length)
temp_buffer = deque(maxlen=max_length)
pressure_buffer = deque(maxlen=max_length)
flow_buffer = deque(maxlen=max_length)

# Dashアプリケーション
app = dash.Dash(__name__)

app.layout = html.Div([
    html.H1("Chemical Process Real-Time Dashboard",
            style={'textAlign': 'center', 'color': '#11998e'}),

    html.Div([
        html.Div([
            html.H3("Temperature Monitor", style={'textAlign': 'center'}),
            dcc.Graph(id='temperature-graph')
        ], className='six columns'),

        html.Div([
            html.H3("Pressure Monitor", style={'textAlign': 'center'}),
            dcc.Graph(id='pressure-graph')
        ], className='six columns'),
    ], className='row'),

    html.Div([
        html.Div([
            html.H3("Flow Rate Monitor", style={'textAlign': 'center'}),
            dcc.Graph(id='flow-graph')
        ], className='six columns'),

        html.Div([
            html.H3("Process Statistics", style={'textAlign': 'center'}),
            html.Div(id='stats-display', style={
                'fontSize': '18px',
                'padding': '20px',
                'backgroundColor': '#f0f0f0',
                'borderRadius': '10px',
                'marginTop': '20px'
            })
        ], className='six columns'),
    ], className='row'),

    # 更新インターバル(1秒ごと)
    dcc.Interval(
        id='interval-component',
        interval=1000,  # ミリ秒
        n_intervals=0
    )
])


@app.callback(
    [Output('temperature-graph', 'figure'),
     Output('pressure-graph', 'figure'),
     Output('flow-graph', 'figure'),
     Output('stats-display', 'children')],
    [Input('interval-component', 'n_intervals')]
)
def update_dashboard(n):
    """
    ダッシュボードのリアルタイム更新
    """
    # 新しいデータを生成(実際にはセンサーデータを取得)
    current_time = datetime.now()
    temperature = 180 + 5 * np.sin(n * 0.1) + np.random.randn() * 1
    pressure = 3.0 + 0.2 * np.cos(n * 0.08) + np.random.randn() * 0.1
    flow_rate = 50 + 5 * np.random.randn()

    # バッファに追加
    time_buffer.append(current_time)
    temp_buffer.append(temperature)
    pressure_buffer.append(pressure)
    flow_buffer.append(flow_rate)

    # 温度グラフ
    temp_figure = {
        'data': [
            go.Scatter(
                x=list(time_buffer),
                y=list(temp_buffer),
                mode='lines',
                name='Temperature',
                line=dict(color='#11998e', width=2)
            )
        ],
        'layout': go.Layout(
            xaxis=dict(title='Time'),
            yaxis=dict(title='Temperature [°C]', range=[170, 190]),
            margin=dict(l=50, r=50, t=20, b=50),
            hovermode='closest'
        )
    }

    # 圧力グラフ
    pressure_figure = {
        'data': [
            go.Scatter(
                x=list(time_buffer),
                y=list(pressure_buffer),
                mode='lines',
                name='Pressure',
                line=dict(color='#38ef7d', width=2)
            )
        ],
        'layout': go.Layout(
            xaxis=dict(title='Time'),
            yaxis=dict(title='Pressure [bar]', range=[2.5, 3.5]),
            margin=dict(l=50, r=50, t=20, b=50),
            hovermode='closest'
        )
    }

    # 流量グラフ
    flow_figure = {
        'data': [
            go.Scatter(
                x=list(time_buffer),
                y=list(flow_buffer),
                mode='lines',
                name='Flow Rate',
                line=dict(color='orange', width=2)
            )
        ],
        'layout': go.Layout(
            xaxis=dict(title='Time'),
            yaxis=dict(title='Flow Rate [L/h]', range=[30, 70]),
            margin=dict(l=50, r=50, t=20, b=50),
            hovermode='closest'
        )
    }

    # 統計情報
    if len(temp_buffer) > 0:
        stats = html.Div([
            html.P(f"Temperature: {temperature:.2f} °C"),
            html.P(f"  Mean: {np.mean(temp_buffer):.2f} °C"),
            html.P(f"  Std Dev: {np.std(temp_buffer):.2f} °C"),
            html.Br(),
            html.P(f"Pressure: {pressure:.2f} bar"),
            html.P(f"  Mean: {np.mean(pressure_buffer):.2f} bar"),
            html.P(f"  Std Dev: {np.std(pressure_buffer):.2f} bar"),
            html.Br(),
            html.P(f"Flow Rate: {flow_rate:.2f} L/h"),
            html.P(f"  Mean: {np.mean(flow_buffer):.2f} L/h"),
            html.P(f"  Std Dev: {np.std(flow_buffer):.2f} L/h"),
        ])
    else:
        stats = html.P("Waiting for data...")

    return temp_figure, pressure_figure, flow_figure, stats


# 注意:このコードはJupyter Notebookでは動作しません
# ターミナルで実行してください:python dashboard.py
if __name__ == '__main__':
    print("Starting Real-Time Dashboard...")
    print("Open browser and navigate to: http://127.0.0.1:8050/")
    app.run_server(debug=True, port=8050)

実行方法:

# dashboard.pyとして保存後、実行
pip install dash plotly pandas
python dashboard.py

# ブラウザで http://127.0.0.1:8050/ を開く

解説: Plotly Dashは、Pythonでインタラクティブなリアルタイムダッシュボードを構築できるフレームワークです。dcc.Intervalにより定期的に更新され、ブラウザで動的なプロセス監視が可能です。本番環境では、DCSやMESからのデータストリームと統合します。


5.5 データベースストリーミング

コード例6: InfluxDBによる時系列データストリーミング

# InfluxDB 2.x用のクライアント
from influxdb_client import InfluxDBClient, Point, WriteOptions
from influxdb_client.client.write_api import SYNCHRONOUS
import numpy as np
from datetime import datetime, timedelta
import time

# 注意:このコードを実行するには、InfluxDBサーバーが必要です
# Dockerで簡単に起動可能:
# docker run -d -p 8086:8086 influxdb:2.0

class InfluxDBStreamer:
    """
    InfluxDBへのリアルタイムデータストリーミング

    Parameters:
    -----------
    url : str
        InfluxDBのURL
    token : str
        認証トークン
    org : str
        組織名
    bucket : str
        バケット名
    """

    def __init__(self, url="http://localhost:8086", token="your-token",
                 org="your-org", bucket="process-data"):
        self.url = url
        self.token = token
        self.org = org
        self.bucket = bucket

        # クライアント接続
        try:
            self.client = InfluxDBClient(url=url, token=token, org=org)
            self.write_api = self.client.write_api(write_options=SYNCHRONOUS)
            self.query_api = self.client.query_api()
            print("Connected to InfluxDB successfully")
        except Exception as e:
            print(f"Failed to connect to InfluxDB: {e}")
            self.client = None

    def write_data_point(self, measurement, tags, fields, timestamp=None):
        """
        データポイントを書き込み

        Parameters:
        -----------
        measurement : str
            測定名(例: "temperature", "pressure")
        tags : dict
            タグ(メタデータ、例: {"sensor_id": "T101", "location": "reactor"})
        fields : dict
            フィールド(実データ、例: {"value": 180.5})
        timestamp : datetime
            タイムスタンプ(Noneの場合は現在時刻)
        """
        if self.client is None:
            print("InfluxDB client not initialized")
            return

        if timestamp is None:
            timestamp = datetime.utcnow()

        # データポイント作成
        point = Point(measurement)
        for key, value in tags.items():
            point = point.tag(key, value)
        for key, value in fields.items():
            point = point.field(key, value)
        point = point.time(timestamp)

        # 書き込み
        try:
            self.write_api.write(bucket=self.bucket, org=self.org, record=point)
        except Exception as e:
            print(f"Failed to write data: {e}")

    def query_recent_data(self, measurement, time_range="-1h"):
        """
        最近のデータをクエリ

        Parameters:
        -----------
        measurement : str
            測定名
        time_range : str
            時間範囲(例: "-1h", "-30m", "-10s")

        Returns:
        --------
        data : list
            クエリ結果
        """
        if self.client is None:
            return []

        # Fluxクエリ
        query = f'''
        from(bucket: "{self.bucket}")
          |> range(start: {time_range})
          |> filter(fn: (r) => r["_measurement"] == "{measurement}")
        '''

        try:
            result = self.query_api.query(org=self.org, query=query)
            data = []
            for table in result:
                for record in table.records:
                    data.append({
                        'time': record.get_time(),
                        'value': record.get_value(),
                        'field': record.get_field(),
                        'tags': record.values
                    })
            return data
        except Exception as e:
            print(f"Failed to query data: {e}")
            return []

    def close(self):
        """クライアントを閉じる"""
        if self.client:
            self.client.close()


# 使用例(シミュレーション)
def simulate_streaming():
    """
    プロセスデータのストリーミングシミュレーション
    """
    # InfluxDBストリーマー初期化
    # 注意:実際の環境に合わせてパラメータを変更してください
    streamer = InfluxDBStreamer(
        url="http://localhost:8086",
        token="your-influxdb-token",  # 実際のトークンを設定
        org="my-org",
        bucket="process-data"
    )

    print("Starting data streaming simulation...")
    print("Press Ctrl+C to stop")

    try:
        for i in range(100):
            # センサーデータ生成
            temperature = 180 + 5 * np.sin(i * 0.1) + np.random.randn()
            pressure = 3.0 + 0.2 * np.cos(i * 0.08) + np.random.randn() * 0.1
            flow_rate = 50 + 5 * np.random.randn()

            # データ書き込み
            streamer.write_data_point(
                measurement="temperature",
                tags={"sensor_id": "T101", "unit": "reactor"},
                fields={"value": temperature},
                timestamp=datetime.utcnow()
            )

            streamer.write_data_point(
                measurement="pressure",
                tags={"sensor_id": "P201", "unit": "reactor"},
                fields={"value": pressure},
                timestamp=datetime.utcnow()
            )

            streamer.write_data_point(
                measurement="flow_rate",
                tags={"sensor_id": "F301", "unit": "feed"},
                fields={"value": flow_rate},
                timestamp=datetime.utcnow()
            )

            print(f"Sample {i+1}: T={temperature:.2f}°C, P={pressure:.2f}bar, F={flow_rate:.2f}L/h")

            time.sleep(1)  # 1秒間隔

    except KeyboardInterrupt:
        print("\nStreaming stopped by user")

    finally:
        # 最近のデータをクエリ
        print("\nQuerying recent temperature data...")
        recent_data = streamer.query_recent_data("temperature", time_range="-5m")
        print(f"Retrieved {len(recent_data)} data points")

        if len(recent_data) > 0:
            print("\nSample data:")
            for i, point in enumerate(recent_data[:5]):
                print(f"  {i+1}. Time: {point['time']}, Value: {point['value']:.2f}")

        streamer.close()


# 注意:InfluxDBサーバーが起動している場合のみ実行可能
# simulate_streaming()

print("""
InfluxDB Streaming Example Code

To run this example:
1. Install InfluxDB: docker run -d -p 8086:8086 influxdb:2.0
2. Create an API token in InfluxDB UI (http://localhost:8086)
3. Update the connection parameters in the code
4. Install client: pip install influxdb-client
5. Run: python influxdb_streaming.py

Key Features:
- Real-time data ingestion
- Tag-based indexing (sensor_id, location, etc.)
- Powerful time-series queries (Flux language)
- Automatic data downsampling and retention policies
- Integration with Grafana for visualization
""")

解説: InfluxDBは時系列データに特化したデータベースで、高速な書き込みとクエリが可能です。タグによるメタデータ管理、自動ダウンサンプリング、Grafanaとの統合により、産業用リアルタイム監視システムの基盤として広く使用されています。


コード例7: WebSocketによるリアルタイムデータ配信

import asyncio
import websockets
import json
import numpy as np
from datetime import datetime

# WebSocketサーバー(データ配信側)
class ProcessDataServer:
    """
    WebSocketサーバーでプロセスデータを配信
    """

    def __init__(self, host='localhost', port=8765):
        self.host = host
        self.port = port
        self.clients = set()

    async def register_client(self, websocket):
        """クライアント登録"""
        self.clients.add(websocket)
        print(f"Client connected: {websocket.remote_address}")

    async def unregister_client(self, websocket):
        """クライアント登録解除"""
        self.clients.remove(websocket)
        print(f"Client disconnected: {websocket.remote_address}")

    async def send_data_to_clients(self, data):
        """全クライアントにデータを送信"""
        if self.clients:
            message = json.dumps(data)
            await asyncio.gather(
                *[client.send(message) for client in self.clients],
                return_exceptions=True
            )

    async def handler(self, websocket, path):
        """
        クライアント接続ハンドラ
        """
        await self.register_client(websocket)
        try:
            # クライアントからのメッセージを待機(接続維持)
            async for message in websocket:
                print(f"Received from client: {message}")
        except websockets.exceptions.ConnectionClosed:
            pass
        finally:
            await self.unregister_client(websocket)

    async def generate_and_broadcast_data(self):
        """
        定期的にデータを生成してブロードキャスト
        """
        iteration = 0
        while True:
            # プロセスデータ生成
            temperature = 180 + 5 * np.sin(iteration * 0.1) + np.random.randn()
            pressure = 3.0 + 0.2 * np.cos(iteration * 0.08) + np.random.randn() * 0.1
            flow_rate = 50 + 5 * np.random.randn()

            data = {
                'timestamp': datetime.now().isoformat(),
                'temperature': round(temperature, 2),
                'pressure': round(pressure, 2),
                'flow_rate': round(flow_rate, 2),
                'iteration': iteration
            }

            # 全クライアントに送信
            await self.send_data_to_clients(data)

            iteration += 1
            await asyncio.sleep(1)  # 1秒間隔

    async def start(self):
        """サーバー起動"""
        # WebSocketサーバー開始
        server = await websockets.serve(self.handler, self.host, self.port)
        print(f"WebSocket server started on ws://{self.host}:{self.port}")

        # データ生成・配信タスク開始
        await self.generate_and_broadcast_data()


# WebSocketクライアント(データ受信側)
class ProcessDataClient:
    """
    WebSocketクライアントでデータを受信
    """

    def __init__(self, uri='ws://localhost:8765'):
        self.uri = uri
        self.data_buffer = []

    async def receive_data(self):
        """
        データを受信して処理
        """
        async with websockets.connect(self.uri) as websocket:
            print(f"Connected to server: {self.uri}")

            try:
                async for message in websocket:
                    data = json.loads(message)
                    self.data_buffer.append(data)

                    # リアルタイム処理
                    print(f"Received: T={data['temperature']}°C, "
                          f"P={data['pressure']}bar, "
                          f"F={data['flow_rate']}L/h")

                    # 異常検知の例
                    if data['temperature'] > 185:
                        print(f"  WARNING: High temperature detected!")

                    # バッファサイズ制限
                    if len(self.data_buffer) > 1000:
                        self.data_buffer.pop(0)

            except websockets.exceptions.ConnectionClosed:
                print("Connection closed by server")


# サーバー起動スクリプト
async def run_server():
    server = ProcessDataServer(host='localhost', port=8765)
    await server.start()


# クライアント起動スクリプト
async def run_client():
    client = ProcessDataClient(uri='ws://localhost:8765')
    await client.receive_data()


# 使用例
print("""
WebSocket Real-Time Data Streaming Example

Server Side:
-----------
python
import asyncio
from your_module import run_server
asyncio.run(run_server())

Client Side:
-----------
python
import asyncio
from your_module import run_client
asyncio.run(run_client())

Key Features:
- Low-latency bidirectional communication
- Multiple clients can connect simultaneously
- Automatic reconnection handling
- JSON data serialization
- Real-time anomaly detection on client side

Installation:
pip install websockets
""")

# 実行例(コメント解除して使用)
# asyncio.run(run_server())  # サーバー起動
# asyncio.run(run_client())  # クライアント起動

解説: WebSocketは双方向通信プロトコルで、HTTPよりも低レイテンシでリアルタイムデータを配信できます。DCSからのデータをWebSocketサーバーで配信し、複数のクライアント(ダッシュボード、分析ツール)が同時に受信できます。


5.6 アラートシステムと自動化

コード例8: ルールベースアラートシステム

import numpy as np
import pandas as pd
from datetime import datetime
from enum import Enum
import smtplib
from email.mime.text import MIMEText

class AlertSeverity(Enum):
    """アラート重要度"""
    INFO = 1
    WARNING = 2
    CRITICAL = 3
    EMERGENCY = 4


class Alert:
    """アラート情報"""
    def __init__(self, severity, message, value, threshold, timestamp=None):
        self.severity = severity
        self.message = message
        self.value = value
        self.threshold = threshold
        self.timestamp = timestamp if timestamp else datetime.now()

    def __str__(self):
        return (f"[{self.severity.name}] {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')} - "
                f"{self.message} (Value: {self.value:.2f}, Threshold: {self.threshold:.2f})")


class ProcessAlertSystem:
    """
    プロセス監視アラートシステム

    Parameters:
    -----------
    rules : list of dict
        アラートルールのリスト
        例: {'variable': 'temperature', 'condition': '>', 'threshold': 185, 'severity': AlertSeverity.WARNING}
    """

    def __init__(self, rules):
        self.rules = rules
        self.alerts = []
        self.alert_counts = {severity: 0 for severity in AlertSeverity}

    def evaluate_rules(self, data):
        """
        ルールを評価してアラートを生成

        Parameters:
        -----------
        data : dict
            監視データ(変数名: 値)

        Returns:
        --------
        alerts : list of Alert
            生成されたアラートのリスト
        """
        current_alerts = []

        for rule in self.rules:
            variable = rule['variable']
            condition = rule['condition']
            threshold = rule['threshold']
            severity = rule['severity']

            if variable not in data:
                continue

            value = data[variable]

            # 条件評価
            triggered = False
            if condition == '>':
                triggered = value > threshold
            elif condition == '<':
                triggered = value < threshold
            elif condition == '>=':
                triggered = value >= threshold
            elif condition == '<=':
                triggered = value <= threshold
            elif condition == '==':
                triggered = abs(value - threshold) < 1e-6
            elif condition == '!=':
                triggered = abs(value - threshold) >= 1e-6

            if triggered:
                message = f"{variable} {condition} {threshold}"
                alert = Alert(severity, message, value, threshold)
                current_alerts.append(alert)
                self.alerts.append(alert)
                self.alert_counts[severity] += 1

        return current_alerts

    def send_alert_notification(self, alert):
        """
        アラート通知を送信(メール、SMS、等)

        Parameters:
        -----------
        alert : Alert
            送信するアラート
        """
        print(f"\n*** ALERT NOTIFICATION ***")
        print(alert)
        print("*" * 50)

        # 実際の実装では、メール送信やSMS送信を行う
        # if alert.severity == AlertSeverity.CRITICAL:
        #     self._send_email(alert)
        #     self._send_sms(alert)

    def _send_email(self, alert):
        """メール送信(実装例)"""
        # 注意:実際の環境に合わせて設定してください
        smtp_server = "smtp.example.com"
        smtp_port = 587
        sender = "alerts@example.com"
        recipient = "operator@example.com"
        password = "your-password"

        msg = MIMEText(str(alert))
        msg['Subject'] = f"[{alert.severity.name}] Process Alert"
        msg['From'] = sender
        msg['To'] = recipient

        try:
            with smtplib.SMTP(smtp_server, smtp_port) as server:
                server.starttls()
                server.login(sender, password)
                server.send_message(msg)
                print(f"Email sent to {recipient}")
        except Exception as e:
            print(f"Failed to send email: {e}")

    def get_alert_summary(self):
        """アラートサマリーを取得"""
        return {
            'total_alerts': len(self.alerts),
            'by_severity': self.alert_counts,
            'recent_alerts': self.alerts[-10:] if len(self.alerts) > 0 else []
        }


# シミュレーション
np.random.seed(42)

# アラートルール定義
alert_rules = [
    {'variable': 'temperature', 'condition': '>', 'threshold': 185, 'severity': AlertSeverity.WARNING},
    {'variable': 'temperature', 'condition': '>', 'threshold': 190, 'severity': AlertSeverity.CRITICAL},
    {'variable': 'temperature', 'condition': '<', 'threshold': 170, 'severity': AlertSeverity.WARNING},
    {'variable': 'pressure', 'condition': '>', 'threshold': 3.5, 'severity': AlertSeverity.WARNING},
    {'variable': 'pressure', 'condition': '<', 'threshold': 2.5, 'severity': AlertSeverity.WARNING},
    {'variable': 'flow_rate', 'condition': '<', 'threshold': 40, 'severity': AlertSeverity.INFO},
]

# アラートシステム初期化
alert_system = ProcessAlertSystem(rules=alert_rules)

# データストリーム処理
print("Starting Process Monitoring...")
print("=" * 70)

for i in range(100):
    # プロセスデータ生成(時々異常値を含む)
    if i in [25, 50, 75]:
        # 異常データ
        temperature = 192 + np.random.randn()
        pressure = 3.8 + np.random.randn() * 0.1
    else:
        # 正常データ
        temperature = 180 + 5 * np.sin(i * 0.1) + np.random.randn() * 2
        pressure = 3.0 + 0.2 * np.cos(i * 0.08) + np.random.randn() * 0.1

    flow_rate = 50 + 5 * np.random.randn()

    data = {
        'temperature': temperature,
        'pressure': pressure,
        'flow_rate': flow_rate
    }

    # ルール評価
    current_alerts = alert_system.evaluate_rules(data)

    # アラート通知
    for alert in current_alerts:
        alert_system.send_alert_notification(alert)

# アラートサマリー
summary = alert_system.get_alert_summary()
print("\n\n" + "=" * 70)
print("ALERT SUMMARY")
print("=" * 70)
print(f"Total Alerts: {summary['total_alerts']}")
print("\nBy Severity:")
for severity, count in summary['by_severity'].items():
    print(f"  {severity.name:12s}: {count}")

print("\nRecent Alerts (last 10):")
for alert in summary['recent_alerts']:
    print(f"  {alert}")

解説: ルールベースのアラートシステムは、閾値や条件に基づいてリアルタイムに異常を検知し、重要度に応じて通知します。複数の通知チャネル(メール、SMS、SCADA画面)への統合が可能です。


コード例9: 機械学習ベースの異常検知とアラート

import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
import matplotlib.pyplot as plt

class MLAnomalyDetector:
    """
    機械学習ベースの異常検知システム

    Parameters:
    -----------
    contamination : float
        異常の割合(0.0〜0.5)
    """

    def __init__(self, contamination=0.05):
        self.model = IsolationForest(
            contamination=contamination,
            random_state=42,
            n_estimators=100
        )
        self.scaler = StandardScaler()
        self.is_fitted = False
        self.anomaly_scores = []
        self.predictions = []

    def fit(self, X_normal):
        """
        正常データで学習

        Parameters:
        -----------
        X_normal : array-like, shape (n_samples, n_features)
            正常データ
        """
        X_scaled = self.scaler.fit_transform(X_normal)
        self.model.fit(X_scaled)
        self.is_fitted = True

    def predict(self, X):
        """
        異常検知

        Parameters:
        -----------
        X : array-like, shape (n_samples, n_features)
            データ

        Returns:
        --------
        is_anomaly : array, shape (n_samples,)
            異常フラグ(1: 異常, -1: 正常)
        anomaly_score : array, shape (n_samples,)
            異常スコア(低いほど異常)
        """
        if not self.is_fitted:
            return np.zeros(X.shape[0]), np.zeros(X.shape[0])

        X_scaled = self.scaler.transform(X)
        predictions = self.model.predict(X_scaled)  # 1: normal, -1: anomaly
        anomaly_scores = self.model.score_samples(X_scaled)  # 異常スコア(低いほど異常)

        self.predictions.extend(predictions.tolist())
        self.anomaly_scores.extend(anomaly_scores.tolist())

        return predictions, anomaly_scores


# シミュレーション
np.random.seed(42)

# 正常データで学習
n_train = 500
X_train = np.random.multivariate_normal(
    mean=[180, 3.0, 50],
    cov=[[4, 0.1, 0.2], [0.1, 0.04, 0.05], [0.2, 0.05, 25]],
    size=n_train
)

# 異常検知器の学習
detector = MLAnomalyDetector(contamination=0.05)
detector.fit(X_train)

# テストデータ(正常 + 異常)
n_test = 300
X_test = []
true_labels = []

for i in range(n_test):
    if i % 50 == 49:  # 50サンプルごとに異常を注入
        # 異常データ
        X_test.append([195, 3.8, 35])  # 温度高、圧力高、流量低
        true_labels.append(-1)
    else:
        # 正常データ
        sample = np.random.multivariate_normal(
            mean=[180, 3.0, 50],
            cov=[[4, 0.1, 0.2], [0.1, 0.04, 0.05], [0.2, 0.05, 25]]
        )
        X_test.append(sample)
        true_labels.append(1)

X_test = np.array(X_test)
true_labels = np.array(true_labels)

# リアルタイム異常検知
print("Real-Time ML Anomaly Detection")
print("=" * 70)

detected_anomalies = []
for i in range(n_test):
    X_current = X_test[i:i+1]
    prediction, score = detector.predict(X_current)

    if prediction[0] == -1:
        print(f"Time {i}: ANOMALY DETECTED (Score: {score[0]:.3f})")
        print(f"  Data: T={X_current[0,0]:.2f}°C, P={X_current[0,1]:.2f}bar, F={X_current[0,2]:.2f}L/h")
        detected_anomalies.append(i)

# 可視化
fig, axes = plt.subplots(2, 2, figsize=(16, 12))

# 異常スコアの時系列
axes[0, 0].plot(detector.anomaly_scores, color='#11998e', linewidth=1.5)
axes[0, 0].axhline(y=np.percentile(detector.anomaly_scores, 5), color='red',
                   linestyle='--', linewidth=2, label='Anomaly Threshold (5%ile)')
for idx in detected_anomalies:
    axes[0, 0].axvline(x=idx, color='red', linestyle=':', linewidth=1, alpha=0.5)
axes[0, 0].set_xlabel('Time [samples]')
axes[0, 0].set_ylabel('Anomaly Score')
axes[0, 0].set_title('Anomaly Score Over Time', fontweight='bold')
axes[0, 0].legend()
axes[0, 0].grid(alpha=0.3)

# 温度 vs 圧力(異常の可視化)
normal_idx = np.array(detector.predictions) == 1
anomaly_idx = np.array(detector.predictions) == -1
axes[0, 1].scatter(X_test[normal_idx, 0], X_test[normal_idx, 1],
                   color='green', alpha=0.5, label='Normal', s=30)
axes[0, 1].scatter(X_test[anomaly_idx, 0], X_test[anomaly_idx, 1],
                   color='red', alpha=0.8, label='Anomaly', s=100, marker='x', linewidths=2)
axes[0, 1].set_xlabel('Temperature [°C]')
axes[0, 1].set_ylabel('Pressure [bar]')
axes[0, 1].set_title('Anomalies in Temperature-Pressure Space', fontweight='bold')
axes[0, 1].legend()
axes[0, 1].grid(alpha=0.3)

# 圧力 vs 流量
axes[1, 0].scatter(X_test[normal_idx, 1], X_test[normal_idx, 2],
                   color='green', alpha=0.5, label='Normal', s=30)
axes[1, 0].scatter(X_test[anomaly_idx, 1], X_test[anomaly_idx, 2],
                   color='red', alpha=0.8, label='Anomaly', s=100, marker='x', linewidths=2)
axes[1, 0].set_xlabel('Pressure [bar]')
axes[1, 0].set_ylabel('Flow Rate [L/h]')
axes[1, 0].set_title('Anomalies in Pressure-Flow Space', fontweight='bold')
axes[1, 0].legend()
axes[1, 0].grid(alpha=0.3)

# 異常スコアのヒストグラム
axes[1, 1].hist(np.array(detector.anomaly_scores)[normal_idx], bins=30,
                color='green', alpha=0.6, label='Normal', edgecolor='black')
axes[1, 1].hist(np.array(detector.anomaly_scores)[anomaly_idx], bins=30,
                color='red', alpha=0.6, label='Anomaly', edgecolor='black')
axes[1, 1].set_xlabel('Anomaly Score')
axes[1, 1].set_ylabel('Frequency')
axes[1, 1].set_title('Distribution of Anomaly Scores', fontweight='bold')
axes[1, 1].legend()
axes[1, 1].grid(alpha=0.3)

plt.tight_layout()
plt.show()

# 検知性能の評価
from sklearn.metrics import classification_report

predicted_labels = np.array(detector.predictions)
print("\n\nAnomaly Detection Performance:")
print("=" * 70)
print(classification_report(true_labels, predicted_labels,
                           target_names=['Anomaly', 'Normal']))

解説: 機械学習ベースの異常検知は、ルールベースでは捉えられない複雑な異常パターンを検出できます。Isolation Forestは教師なし学習で、正常データのみから学習し、外れ値を効率的に検出します。プロセスの非線形な関係や多変量相関も考慮できます。


5.7 本番環境への展開

コード例10: 本番監視システムの統合実装

import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from collections import deque
import logging
import threading
import time

# ロギング設定
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('process_monitoring.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger('ProcessMonitor')


class ProductionMonitoringSystem:
    """
    本番環境用プロセス監視システム

    Parameters:
    -----------
    sampling_interval : float
        サンプリング間隔 [秒]
    buffer_size : int
        データバッファサイズ
    alert_threshold : dict
        アラート閾値の辞書
    """

    def __init__(self, sampling_interval=1.0, buffer_size=1000, alert_threshold=None):
        self.sampling_interval = sampling_interval
        self.buffer_size = buffer_size
        self.alert_threshold = alert_threshold or {}

        # データバッファ
        self.data_buffer = {
            'timestamp': deque(maxlen=buffer_size),
            'temperature': deque(maxlen=buffer_size),
            'pressure': deque(maxlen=buffer_size),
            'flow_rate': deque(maxlen=buffer_size)
        }

        # 統計情報
        self.statistics = {}
        self.alert_count = 0
        self.uptime_start = datetime.now()

        # 制御フラグ
        self.is_running = False
        self.monitoring_thread = None

        logger.info("Production Monitoring System initialized")

    def read_sensor_data(self):
        """
        センサーデータを読み取る

        実際の実装では、DCS、PLC、OPC-UAサーバー等から読み取り

        Returns:
        --------
        data : dict
            センサーデータ
        """
        # シミュレーション用のダミーデータ
        # 実環境では、OPC-UA、Modbus、MQTT等で実データを取得
        timestamp = datetime.now()
        temperature = 180 + 5 * np.sin(time.time() * 0.1) + np.random.randn()
        pressure = 3.0 + 0.2 * np.cos(time.time() * 0.08) + np.random.randn() * 0.1
        flow_rate = 50 + 5 * np.random.randn()

        return {
            'timestamp': timestamp,
            'temperature': temperature,
            'pressure': pressure,
            'flow_rate': flow_rate
        }

    def update_buffer(self, data):
        """
        データバッファを更新

        Parameters:
        -----------
        data : dict
            新しいセンサーデータ
        """
        for key, value in data.items():
            self.data_buffer[key].append(value)

    def calculate_statistics(self):
        """
        現在のデータバッファから統計量を計算

        Returns:
        --------
        stats : dict
            統計量の辞書
        """
        stats = {}

        for variable in ['temperature', 'pressure', 'flow_rate']:
            data_array = np.array(self.data_buffer[variable])
            if len(data_array) > 0:
                stats[variable] = {
                    'mean': np.mean(data_array),
                    'std': np.std(data_array),
                    'min': np.min(data_array),
                    'max': np.max(data_array),
                    'latest': data_array[-1]
                }

        self.statistics = stats
        return stats

    def check_alerts(self, data):
        """
        アラート条件をチェック

        Parameters:
        -----------
        data : dict
            現在のセンサーデータ

        Returns:
        --------
        alerts : list
            発生したアラートのリスト
        """
        alerts = []

        # 温度アラート
        if data['temperature'] > self.alert_threshold.get('temperature_high', 185):
            alert_msg = f"HIGH TEMPERATURE: {data['temperature']:.2f}°C"
            alerts.append(alert_msg)
            logger.warning(alert_msg)

        if data['temperature'] < self.alert_threshold.get('temperature_low', 170):
            alert_msg = f"LOW TEMPERATURE: {data['temperature']:.2f}°C"
            alerts.append(alert_msg)
            logger.warning(alert_msg)

        # 圧力アラート
        if data['pressure'] > self.alert_threshold.get('pressure_high', 3.5):
            alert_msg = f"HIGH PRESSURE: {data['pressure']:.2f}bar"
            alerts.append(alert_msg)
            logger.warning(alert_msg)

        if data['pressure'] < self.alert_threshold.get('pressure_low', 2.5):
            alert_msg = f"LOW PRESSURE: {data['pressure']:.2f}bar"
            alerts.append(alert_msg)
            logger.warning(alert_msg)

        # 流量アラート
        if data['flow_rate'] < self.alert_threshold.get('flow_rate_low', 40):
            alert_msg = f"LOW FLOW RATE: {data['flow_rate']:.2f}L/h"
            alerts.append(alert_msg)
            logger.warning(alert_msg)

        self.alert_count += len(alerts)
        return alerts

    def monitoring_loop(self):
        """
        メイン監視ループ
        """
        logger.info("Monitoring loop started")

        while self.is_running:
            try:
                # センサーデータ読み取り
                data = self.read_sensor_data()

                # バッファ更新
                self.update_buffer(data)

                # 統計量計算
                self.calculate_statistics()

                # アラートチェック
                alerts = self.check_alerts(data)

                # データログ(定期的にDBに保存する等)
                if len(self.data_buffer['timestamp']) % 60 == 0:  # 1分ごと
                    logger.info(f"System running normally. "
                               f"T={data['temperature']:.2f}°C, "
                               f"P={data['pressure']:.2f}bar, "
                               f"F={data['flow_rate']:.2f}L/h")

                # 次のサンプリングまで待機
                time.sleep(self.sampling_interval)

            except Exception as e:
                logger.error(f"Error in monitoring loop: {e}", exc_info=True)
                time.sleep(5)  # エラー時は5秒待機して再試行

    def start(self):
        """監視システムを開始"""
        if not self.is_running:
            self.is_running = True
            self.monitoring_thread = threading.Thread(target=self.monitoring_loop, daemon=True)
            self.monitoring_thread.start()
            logger.info("Production monitoring system started")

    def stop(self):
        """監視システムを停止"""
        if self.is_running:
            self.is_running = False
            if self.monitoring_thread:
                self.monitoring_thread.join(timeout=5)
            logger.info("Production monitoring system stopped")

    def get_system_status(self):
        """
        システムステータスを取得

        Returns:
        --------
        status : dict
            システムステータス情報
        """
        uptime = datetime.now() - self.uptime_start

        return {
            'is_running': self.is_running,
            'uptime': str(uptime),
            'buffer_size': len(self.data_buffer['timestamp']),
            'total_alerts': self.alert_count,
            'statistics': self.statistics,
            'latest_data': {
                'temperature': self.data_buffer['temperature'][-1] if len(self.data_buffer['temperature']) > 0 else None,
                'pressure': self.data_buffer['pressure'][-1] if len(self.data_buffer['pressure']) > 0 else None,
                'flow_rate': self.data_buffer['flow_rate'][-1] if len(self.data_buffer['flow_rate']) > 0 else None
            }
        }


# 使用例
if __name__ == "__main__":
    # 監視システムの初期化
    alert_thresholds = {
        'temperature_high': 185,
        'temperature_low': 170,
        'pressure_high': 3.5,
        'pressure_low': 2.5,
        'flow_rate_low': 40
    }

    monitor = ProductionMonitoringSystem(
        sampling_interval=1.0,
        buffer_size=1000,
        alert_threshold=alert_thresholds
    )

    # 監視開始
    monitor.start()

    try:
        # 60秒間実行
        for i in range(60):
            time.sleep(1)

            # 10秒ごとにステータス表示
            if i % 10 == 0:
                status = monitor.get_system_status()
                print(f"\n--- System Status (t={i}s) ---")
                print(f"Running: {status['is_running']}")
                print(f"Uptime: {status['uptime']}")
                print(f"Buffer: {status['buffer_size']}/{monitor.buffer_size}")
                print(f"Alerts: {status['total_alerts']}")
                if status['latest_data']['temperature']:
                    print(f"Latest Data:")
                    print(f"  T: {status['latest_data']['temperature']:.2f}°C")
                    print(f"  P: {status['latest_data']['pressure']:.2f}bar")
                    print(f"  F: {status['latest_data']['flow_rate']:.2f}L/h")

    except KeyboardInterrupt:
        print("\nShutting down...")

    finally:
        # 監視停止
        monitor.stop()

    print("\n" + "=" * 70)
    print("Production Monitoring System - Final Status")
    print("=" * 70)
    final_status = monitor.get_system_status()
    print(f"Total uptime: {final_status['uptime']}")
    print(f"Total alerts: {final_status['total_alerts']}")
    print(f"Total samples: {final_status['buffer_size']}")

解説: 本番環境の監視システムは、堅牢性、ロギング、エラーハンドリング、スレッド管理が重要です。この実装は、DCS/PLCからのデータ取得、リアルタイム解析、アラート生成、ログ記録を統合し、24時間365日の連続運用に対応します。


5.8 本章のまとめ

学んだこと

  1. ストリーミングデータ処理
    • 移動窓(deque)による効率的なバッファリング
    • EWMA(指数移動平均)によるリアルタイム統計監視
  2. オンライン機械学習
    • 増分学習(SGDRegressor)による適応的モデル
    • オンラインPCAによる多変量異常検知
  3. リアルタイム可視化
    • Plotly Dashによるインタラクティブダッシュボード
    • 定期更新と動的グラフ生成
  4. データベース統合
    • InfluxDBによる時系列データストリーミング
    • WebSocketによる低レイテンシ配信
  5. アラートシステム
    • ルールベース異常検知
    • 機械学習ベース異常検知(Isolation Forest)
    • 重要度に応じた通知
  6. 本番システム設計
    • マルチスレッド処理
    • ロギングとエラーハンドリング
    • 連続運用とシステム監視

重要なポイント

さらに学ぶには