自動車・データ実務

FFT で合成振動データの卓越周波数を求める

既知の 2 Hz 成分を持つ合成振動信号に NumPy FFT を適用し、周波数ビンを確認して、検出された卓越周波数が入力と一致することを検証します。元の CSV は変更せず、スペクトルは新しい出力フォルダに保存します。

目次を表示

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

対象読者自動車の振動データやセンサーデータに FFT ベースの周波数解析を適用する前に、小さく確認可能な例を試したい人向けのガイドです。

準備するもの
  • Python 3.12 と、そのバージョンを起動できるターミナルコマンド。
  • python -m pip install numpy で NumPy をインストール済みであること。
  • スクリプトが outputs の下に新しいフォルダを作成できる作業フォルダ。
  • サンプリングされた信号にはサンプリング周波数と離散時間間隔があるという基本的な理解。

01あらかじめ分かっている周波数から始める

最初の FFT 演習として最も安全なのは、周波数があらかじめ分かっている信号です。この例では、1秒あたり 8 samples と純粋な 2 Hz の合成振動信号を使います。8 samples の場合、FFT の周波数間隔は sampling_rate / sample_count = 8 / 8 = 1 Hz です。

実数値入力に対して、NumPy の rfft は非負の周波数ビンだけを返します。8 samples を 8 Hz で取得した場合、ビンは 0, 1, 2, 3, 4 Hz です。4 Hz のビンがナイキスト周波数です。

02合成振動 CSV を作成する

以下のデータセットは合成データであり、この記事のために作成したものです。vibration_data.csv として保存してください。サンプリング間隔は 0.125 seconds なので、サンプリング周波数は 1 / 0.125 = 8 Hz です。

csv
time_s,accel_m_s2
0.000,0
0.125,1
0.250,0
0.375,-1
0.500,0
0.625,1
0.750,0
0.875,-1

値は4 samples ごとに 0, 1, 0, -1 と繰り返します。1秒あたり 8 samples では、4 samples は 0.5 seconds に相当し、周期 0.5 seconds は 2 Hz に相当します。したがって、この信号の既知の卓越入力周波数は 2 Hz です。

Samples8
Sample interval0.125 s
Sampling frequency8 Hz
FFT frequency spacing1 Hz
Known signal frequency2 Hz
Nyquist frequency4 Hz

03コードを実行する前に FFT の結果を予測する

記録には整数個の周期が含まれ、2 Hz の信号が FFT ビンに正確に一致しているため、この合成例は意図的に単純です。生の FFT magnitude は 2 Hz に集中するはずです。

0, 1, 0, -1, 0, 1, 0, -1 という系列では、2 Hz の FFT coefficient の magnitude は 4 です。他の非負周波数ビンは、厳密な算術では magnitude が 0 です。コンピューター計算では小さな数値丸め誤差が現れる可能性があるため、出力スペクトルは小数点以下6桁に丸めます。

周波数想定される raw FFT magnitude
0 Hz0
1 Hz0
2 Hz4
3 Hz0
4 Hz0

0 Hz のビンは平均値、つまり DC component を表します。この信号の平均値は 0 です。スクリプトでは FFT 前に平均値を差し引きます。これは、一定のセンサーオフセットではなく振動成分を確認することが目的の場合によく行われる前処理です。

04スペクトルと卓越周波数を計算する

次のスクリプトを fft_dominant_frequency.py として保存してください。タイムスタンプ間隔を確認し、平均値を除去し、実数 FFT を計算し、周波数スペクトルを新しい CSV に保存し、最も強い非ゼロ周波数ビンが想定した 2 Hz であることを検証します。

python
import csv
from pathlib import Path
import numpy as np

SOURCE = Path("vibration_data.csv")
OUTPUT_DIR = Path("outputs") / "fft_result"
OUTPUT = OUTPUT_DIR / "spectrum.csv"
EXPECTED_DOMINANT_HZ = 2.0
DT_TOLERANCE = 1e-12
FREQUENCY_TOLERANCE = 1e-12


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}")

    times = []
    values = []
    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:
            times.append(float(row["time_s"]))
            values.append(float(row["accel_m_s2"]))

    if len(times) < 2:
        raise ValueError("At least two samples are required.")

    time_array = np.asarray(times, dtype=float)
    signal = np.asarray(values, dtype=float)
    intervals = np.diff(time_array)
    dt = intervals[0]

    if dt <= 0:
        raise ValueError("Sample interval must be positive.")
    if not np.allclose(intervals, dt, rtol=0.0, atol=DT_TOLERANCE):
        raise ValueError("Sampling interval is not constant.")

    sample_rate = 1.0 / dt
    centered = signal - signal.mean()
    spectrum = np.fft.rfft(centered)
    frequencies = np.fft.rfftfreq(len(centered), d=dt)
    magnitudes = np.abs(spectrum)

    if len(frequencies) < 2:
        raise ValueError("No nonzero frequency bin is available.")

    dominant_index = 1 + int(np.argmax(magnitudes[1:]))
    dominant_frequency = float(frequencies[dominant_index])

    if abs(dominant_frequency - EXPECTED_DOMINANT_HZ) > FREQUENCY_TOLERANCE:
        raise RuntimeError(
            f"Expected {EXPECTED_DOMINANT_HZ} Hz, got {dominant_frequency} Hz."
        )

    OUTPUT_DIR.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT_DIR.mkdir()
    with OUTPUT.open("x", encoding="utf-8", newline="") as stream:
        writer = csv.writer(stream)
        writer.writerow(["frequency_hz", "fft_magnitude"])
        for frequency, magnitude in zip(frequencies, magnitudes):
            writer.writerow([f"{frequency:.6f}", f"{magnitude:.6f}"])

    resolution = sample_rate / len(centered)
    print(f"Samples: {len(centered)}.")
    print(f"Sampling frequency: {sample_rate:.1f} Hz.")
    print(f"Frequency resolution: {resolution:.1f} Hz.")
    print(f"Dominant nonzero frequency: {dominant_frequency:.1f} Hz.")
    print(f"Output: {OUTPUT.as_posix()}")


if __name__ == "__main__":
    main()

05想定されるスペクトルを比較する

8-sample の実数 FFT では 0 から 4 Hz のナイキスト周波数までのビンが返されるため、スペクトル CSV には5行が含まれるはずです。小数点以下6桁に丸めると、2 Hz のビンだけが非ゼロの magnitude を持つはずです。

csv
frequency_hz,fft_magnitude
0.000000,0.000000
1.000000,0.000000
2.000000,4.000000
3.000000,0.000000
4.000000,0.000000

以下の想定コンソール出力は、合成信号とスクリプトから手作業で導出したものです。実際に取得した実行ログではありません。

text
Samples: 8.
Sampling frequency: 8.0 Hz.
Frequency resolution: 1.0 Hz.
Dominant nonzero frequency: 2.0 Hz.
Output: outputs/fft_result/spectrum.csv

06結果とよくある間違いを確認する

  • サンプリング間隔を手作業で確認します: 0.125 seconds.
  • サンプリング周波数を手作業で計算します: 1 / 0.125 = 8 Hz.
  • 周波数間隔を計算します: 8 / 8 = 1 Hz.
  • 最大の非ゼロ FFT magnitude が 2 Hz で発生していることを確認します。
  • 元の vibration_data.csv が変更されていないことを確認します。
  • OUTPUT_DIR を変更せずにスクリプトを再実行します。スペクトルを上書きするのではなく FileExistsError で停止するはずです。
問題確認すること
卓越周波数が倍率分ずれているrfftfreq で使用する時間単位とサンプリング間隔を確認します。
0 Hz が最大成分になっている非ゼロ平均またはセンサーオフセットが支配している可能性があります。平均値除去が適切か判断してください。
隣接ビンにエネルギーが現れる信号周波数が FFT ビンに正確に一致していないか、記録に整数個の周期が含まれていない可能性があります。
高周波の結果が不自然に見えるナイキスト限界と、物理信号に対してサンプリング周波数が十分高いかを確認します。
実行ごとに結果が上書きされる新しい出力フォルダを使用してください。この例では保存先がすでに存在する場合に意図的に停止します。

07実測振動データに使う前に制限を理解する

この例は意図的に理想的な条件です。実際の振動記録が FFT ビンに完全一致することは少なく、スペクトルリーケージによって隣接周波数へエネルギーが広がることがあります。リーケージへの対処には window functions がよく使われますが、振幅の解釈も変わるため、別途確認が必要です。

周波数分解能はサンプリング周波数だけでなく記録長にも依存します。8 samples を 8 Hz で取得した場合、間隔は 1 Hz なので、この解析では 2.1 Hz と 2.4 Hz のような周波数を別々の正確なビンとして区別できません。サンプリング周波数を変えずに記録を長くすると、より細かい周波数間隔が得られます。

実測の自動車データでは、アンチエイリアスフィルタリング、センサー帯域幅、サンプリング同期、欠落サンプル、デトレンド、window の選択、振幅スケーリング、1回の FFT で意味のある解析ができるほど信号が定常かどうかも考慮してください。ピークを物理的に解釈する前に、生の時間領域データを保持し、すべての前処理手順を文書化してください。

実行・検証の記録

2026-09-20 · 手作業で確認した例 · 対象: Python 3.12 · NumPy 必須 · 未実行

  • 8つのタイムスタンプが 0.125 seconds 間隔であることを手作業で確認し、サンプリング周波数が 8 Hz になることを確認しました。
  • FFT の周波数間隔を 8 Hz / 8 samples = 1 Hz と手作業で計算しました。
  • 0, 1, 0, -1 の4-sample 繰り返しパターンを周期 0.5 seconds と特定し、2 Hz に相当することを確認しました。
  • 非負周波数の想定 FFT magnitudes が、厳密な算術で 0, 0, 4, 0, 0 になることを手作業で求めました。
  • 一定サンプリングのチェック、平均値除去、rfft, rfftfreq、卓越周波数選択時の DC bin 除外、出力衝突防止、元 CSV の保持についてスクリプトを確認しました。
  • 想定されるスペクトル CSV とコンソール出力を手作業で導出しました。
検証範囲の限界
  • この回答の作成者はコードを実行しておらず、NumPy FFT の出力やファイルシステムへの書き込みはここではテストしていません。
  • スペクトルリーケージ、window functions、振幅校正、不規則サンプリング、aliasing、センサーノイズ、長時間データはテストしていません。
  • 2 Hz の合成ピークは既知の数学的入力であり、実際の車両共振や故障周波数を表すものとは主張していません。
  • 公式ドキュメントの URL は既知のドキュメント所在地に基づいて記載していますが、ライブでは確認していません。

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

参考資料

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