自動車・データ実務

移動平均で合成センサーデータを平滑化し、遅延を確認する

合成加速度データに3サンプルの後方移動平均を適用し、すべてのフィルタ値を手計算で確認し、フィルタによる1サンプルのアライメント遅延を明示します。元のCSVは変更せず、処理済みデータは新しいフォルダに書き出します。

目次を表示

この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English

対象読者このガイドは、サンプリングされた自動車または試験用センサーデータに対して簡単な初期平滑化手法を必要とし、フィルタによって生じる遅延も理解したい人向けです。

準備するもの
  • Python 3.12と、そのバージョンを起動できるターミナルコマンド。
  • UTF-8のCSVファイルとPythonファイルを保存できるテキストエディタ。
  • センサー値が離散的な時刻ごとに記録されるという基本的な理解。
  • 必要なのはPython標準ライブラリのみです: csv と pathlib。

01移動平均とその時間関係を定義する

この例では、ウィンドウ幅が3サンプルの後方移動平均を使用します。サンプル n のフィルタ値は、現在のサンプルと直前の2サンプルの平均です: ma[n] = (x[n] + x[n-1] + x[n-2]) / 3。したがって、最初の2サンプルには完全な3サンプルウィンドウがありません。

移動平均はサンプル間の急激な変動を抑えますが、時間関係も変化させます。0.2、0.3、0.4秒の時刻を含む3サンプルウィンドウの中心は0.3秒ですが、因果的な後方実装では0.4秒のサンプルが到着した時点で結果が生成されます。サンプル間隔が0.1秒の場合、この中心時刻と出力時刻のアライメント差は0.1秒です。

02小さな合成加速度信号を作成する

以下のデータセットは合成データで、この資料のために作成したものです。sensor_data.csv として保存してください。サンプル間隔は0.1秒です。信号は0付近から始まり、その後9 m/s^2付近まで上昇し、両方のレベルの周辺に意図的な変動を加えています。

csv
time_s,accel_m_s2
0.0,0
0.1,3
0.2,-3
0.3,0
0.4,9
0.5,12
0.6,6
0.7,9

サンプルは8個あります。0.4秒より前の生データ値は0、3、-3、0です。0.4秒以降の値は9、12、6、9です。このデータは意図的に単純化されており、特定の車両センサーやノイズ分布を表しているとは主張しません。

03フィルタ値を手計算する

最初の完全なウィンドウは0.2秒で終了します: (0 + 3 - 3) / 3 = 0。次のウィンドウは (3 - 3 + 0) / 3 = 0 です。生信号が0.4秒で初めて9に達したとき、ウィンドウ内の3つの値は-3、0、9なので、フィルタ結果は9ではなく2になります。

出力時刻3つのサンプルMA3ウィンドウ中心アライメント遅延
0.2 s0, 3, -30.0000.1 s0.1 s
0.3 s3, -3, 00.0000.2 s0.1 s
0.4 s-3, 0, 92.0000.3 s0.1 s
0.5 s0, 9, 127.0000.4 s0.1 s
0.6 s9, 12, 69.0000.5 s0.1 s
0.7 s12, 6, 99.0000.6 s0.1 s

変化部分のフィルタ系列は0から9へ直接跳ぶのではなく、0、2、7、9となり、上昇が複数の出力に広がっていることが分かります。これが平滑化の効果であり、フィルタ済み信号を他のチャンネルと比較する際に時間関係を考慮する必要がある理由も示しています。

04フィルタを適用し、アライメント遅延を記録する

以下のスクリプトを moving_average_filter.py として保存してください。タイムスタンプが一定の0.1秒間隔で増加していることを確認し、3サンプルの後方平均を計算して、新しいCSVを書き出します。既存の出力フォルダは再利用しません。

python
import csv
from pathlib import Path

SOURCE = Path("sensor_data.csv")
OUTPUT_DIR = Path("outputs") / "moving_average_result"
OUTPUT = OUTPUT_DIR / "filtered_sensor_data.csv"
WINDOW = 3
EXPECTED_DT = 0.1
TOLERANCE = 1e-9


def main() -> None:
    if not SOURCE.is_file():
        raise FileNotFoundError(f"Source CSV not found: {SOURCE}")
    if OUTPUT_DIR.exists():
        raise FileExistsError(f"Output folder already exists: {OUTPUT_DIR}")
    if WINDOW < 1 or WINDOW % 2 == 0:
        raise ValueError("WINDOW must be a positive odd integer.")

    samples = []
    with SOURCE.open("r", encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream)
        required = {"time_s", "accel_m_s2"}
        if reader.fieldnames is None or not required.issubset(reader.fieldnames):
            raise ValueError("CSV is missing a required column.")

        for row in reader:
            samples.append((float(row["time_s"]), float(row["accel_m_s2"])))

    if len(samples) < WINDOW:
        raise ValueError("Not enough samples for the moving-average window.")

    for index in range(1, len(samples)):
        dt = samples[index][0] - samples[index - 1][0]
        if abs(dt - EXPECTED_DT) > TOLERANCE:
            raise ValueError(f"Unexpected sample interval at row {index + 2}: {dt}")

    half_window = WINDOW // 2
    output_rows = []

    for index, (time_s, raw_value) in enumerate(samples):
        if index < WINDOW - 1:
            average_text = ""
            center_text = ""
            delay_text = ""
        else:
            window_values = [value for _, value in samples[index - WINDOW + 1:index + 1]]
            average = sum(window_values) / WINDOW
            center_time = samples[index - half_window][0]
            alignment_delay = time_s - center_time
            average_text = f"{average:.3f}"
            center_text = f"{center_time:.1f}"
            delay_text = f"{alignment_delay:.1f}"

        output_rows.append({
            "time_s": f"{time_s:.1f}",
            "raw_accel_m_s2": f"{raw_value:g}",
            "ma3_accel_m_s2": average_text,
            "window_center_time_s": center_text,
            "alignment_delay_s": delay_text,
        })

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()
    fields = [
        "time_s", "raw_accel_m_s2", "ma3_accel_m_s2",
        "window_center_time_s", "alignment_delay_s"
    ]
    with OUTPUT.open("x", encoding="utf-8", newline="") as stream:
        writer = csv.DictWriter(stream, fieldnames=fields)
        writer.writeheader()
        writer.writerows(output_rows)

    valid_outputs = len(samples) - WINDOW + 1
    print(f"Input samples: {len(samples)}.")
    print(f"MA{WINDOW} outputs with full windows: {valid_outputs}.")
    print(f"Nominal alignment delay: {half_window * EXPECTED_DT:.1f} s.")
    print(f"Output: {OUTPUT.as_posix()}")


if __name__ == "__main__":
    main()

05期待結果と比較する

最初の2つのフィルタ済みセルは、完全な3サンプルウィンドウが利用できないため空欄になるはずです。残り6つのフィルタ値は0.000、0.000、2.000、7.000、9.000、9.000になるはずです。

csv
time_s,raw_accel_m_s2,ma3_accel_m_s2,window_center_time_s,alignment_delay_s
0.0,0,,,
0.1,3,,,
0.2,-3,0.000,0.1,0.1
0.3,0,0.000,0.2,0.1
0.4,9,2.000,0.3,0.1
0.5,12,7.000,0.4,0.1
0.6,6,9.000,0.5,0.1
0.7,9,9.000,0.6,0.1

以下の期待されるコンソール出力は、合成データとコードから手作業で導出したものです。実際の実行ログではありません。

text
Input samples: 8.
MA3 outputs with full windows: 6.
Nominal alignment delay: 0.1 s.
Output: outputs/moving_average_result/filtered_sensor_data.csv

06結果と一般的なエラーを確認する

  • 最初の完全な平均を手作業で確認します: (0 + 3 - 3) / 3 = 0。
  • 0.4秒での遷移値を確認します: (-3 + 0 + 9) / 3 = 2。
  • 8サンプルにウィンドウ幅3を適用すると、完全なウィンドウの値が6個生成されることを確認します。
  • 報告されるすべてのウィンドウ中心との差が0.1秒であることを確認します。
  • OUTPUT_DIR を変更せずにスクリプトをもう一度実行します。結果を上書きせず、FileExistsError で停止するはずです。
問題確認すること
予期しないサンプル間隔欠落サンプル、重複タイムスタンプ、または誤った EXPECTED_DT 値を確認します。
フィルタ済み信号が遅れて見える後方ウィンドウは出力タイムスタンプより前を中心とするデータを要約していることを考慮します。
重要なピークが小さくなる移動平均は短時間のイベントを隣接サンプルに広げます。
平滑化が強すぎるウィンドウ長を短くし、ノイズ低減と時間特性のバランスを再評価します。
平滑化が弱すぎるより長いウィンドウが有効な場合もありますが、時間方向のぼやけと公称遅延も増えます。

07試験データに使用する前に限界を理解する

移動平均は単純なローパス平滑化手法であり、あらゆるセンサーに適用できる万能フィルタではありません。急激な変動を低減できますが、ピークを弱めたり、短時間イベントの開始点や終了点をぼかしたりすることもあります。これは振動、衝撃、故障検出、しきい値ベースの解析で重要です。

ここで報告する0.1秒の遅延は、0.1秒ごとにサンプリングされた3サンプルウィンドウについて、後方ウィンドウの中心時刻と結果が利用可能になるタイムスタンプとの差です。すべての信号特徴やすべてのフィルタ実装に共通する遅延として扱うべきではありません。

実際の自動車データには、不均一サンプリング、欠落サンプル、センサーバイアス、飽和、複数の同期チャンネル、またはサンプリング周波数と帯域要件から設計したフィルタを必要とする周波数成分が含まれる場合があります。工学的な結論を導く前に、必ず生信号を保存し、フィルタリング方法を記録してください。

実行・検証の記録

2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · 標準ライブラリ: csv, pathlib · 実行なし

  • 8個の合成加速度サンプルと一定の0.1秒間隔を手作業で確認しました。
  • 6個の完全な3サンプル平均を0、0、2、7、9、9として手計算しました。
  • 3サンプルウィンドウの中心が後方出力タイムスタンプの1サンプル前にあることを手作業で確認しました。
  • 公称の中心時刻から出力時刻までのアライメント差を 1 sample × 0.1 s = 0.1 s と計算しました。
  • 入力列チェック、サンプル間隔チェック、出力競合保護、元CSVの保持についてスクリプトを確認しました。
  • 期待される出力CSVとコンソールテキストを手作業で導出しました。
検証範囲の限界
  • この回答の作成者はコードを実行しておらず、フィルタ済みCSVは作成していません。
  • 合成信号は特定の車両、加速度計、または実測ノイズ分布を表すものではありません。
  • 周波数応答、任意周波数に対する位相応答、不規則サンプリング、欠落サンプル、大規模データセットはテストしていません。
  • 公式ドキュメントのURLは既知のドキュメント位置に基づいて記載しましたが、リアルタイムでは確認していません。

サイト全体の執筆・検証方針

参考資料

説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。