まず試験 CSV の時間軸と単位を確認する
合成の demonstration data で units、missing values、finite numbers、duplicate または reversed timestamps を確認し、その後 check results と chart を保存します。
既知の 2 Hz 成分を持つ合成振動信号に NumPy FFT を適用し、周波数ビンを確認して、検出された卓越周波数が入力と一致することを検証します。元の CSV は変更せず、スペクトルは新しい出力フォルダに保存します。
この翻訳はAIで作成しました。コード、単位、数値は原文と併せて確認してください。各言語のネイティブ話者による校閲は、まだ完了していません。 English
対象読者自動車の振動データやセンサーデータに FFT ベースの周波数解析を適用する前に、小さく確認可能な例を試したい人向けのガイドです。
最初の 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 のビンがナイキスト周波数です。
以下のデータセットは合成データであり、この記事のために作成したものです。vibration_data.csv として保存してください。サンプリング間隔は 0.125 seconds なので、サンプリング周波数は 1 / 0.125 = 8 Hz です。
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 です。
| 量 | 値 |
|---|---|
| Samples | 8 |
| Sample interval | 0.125 s |
| Sampling frequency | 8 Hz |
| FFT frequency spacing | 1 Hz |
| Known signal frequency | 2 Hz |
| Nyquist frequency | 4 Hz |
記録には整数個の周期が含まれ、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 Hz | 0 |
| 1 Hz | 0 |
| 2 Hz | 4 |
| 3 Hz | 0 |
| 4 Hz | 0 |
0 Hz のビンは平均値、つまり DC component を表します。この信号の平均値は 0 です。スクリプトでは FFT 前に平均値を差し引きます。これは、一定のセンサーオフセットではなく振動成分を確認することが目的の場合によく行われる前処理です。
次のスクリプトを fft_dominant_frequency.py として保存してください。タイムスタンプ間隔を確認し、平均値を除去し、実数 FFT を計算し、周波数スペクトルを新しい CSV に保存し、最も強い非ゼロ周波数ビンが想定した 2 Hz であることを検証します。
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()
8-sample の実数 FFT では 0 から 4 Hz のナイキスト周波数までのビンが返されるため、スペクトル CSV には5行が含まれるはずです。小数点以下6桁に丸めると、2 Hz のビンだけが非ゼロの magnitude を持つはずです。
frequency_hz,fft_magnitude
0.000000,0.000000
1.000000,0.000000
2.000000,4.000000
3.000000,0.000000
4.000000,0.000000
以下の想定コンソール出力は、合成信号とスクリプトから手作業で導出したものです。実際に取得した実行ログではありません。
Samples: 8.
Sampling frequency: 8.0 Hz.
Frequency resolution: 1.0 Hz.
Dominant nonzero frequency: 2.0 Hz.
Output: outputs/fft_result/spectrum.csv| 問題 | 確認すること |
|---|---|
| 卓越周波数が倍率分ずれている | rfftfreq で使用する時間単位とサンプリング間隔を確認します。 |
| 0 Hz が最大成分になっている | 非ゼロ平均またはセンサーオフセットが支配している可能性があります。平均値除去が適切か判断してください。 |
| 隣接ビンにエネルギーが現れる | 信号周波数が FFT ビンに正確に一致していないか、記録に整数個の周期が含まれていない可能性があります。 |
| 高周波の結果が不自然に見える | ナイキスト限界と、物理信号に対してサンプリング周波数が十分高いかを確認します。 |
| 実行ごとに結果が上書きされる | 新しい出力フォルダを使用してください。この例では保存先がすでに存在する場合に意図的に停止します。 |
この例は意図的に理想的な条件です。実際の振動記録が 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 必須 · 未実行
説明と例は独自に作成しました。関連する動作や概念は、以下の公式資料で確認できます。