Find the dominant frequency of synthetic vibration data with FFT
Use NumPy FFT on a synthetic vibration signal with a known 2 Hz component, inspect the frequency bins, and verify that the detected dominant frequency matches the input. The original CSV remains unchanged and the spectrum is written to a new output folder.
Content checked 2026.09.20Hand-checked example
Show contents
Who this is forThis guide is for people who want a small checkable example before applying FFT-based frequency analysis to automotive vibration or sensor data.
What you need
Python 3.12 and a terminal command that starts that version.
NumPy installed with python -m pip install numpy.
A working folder where the script can create a new folder beneath outputs.
Basic understanding that a sampled signal has a sampling frequency and discrete time interval.
01Start with a frequency that is known in advance
The safest first FFT exercise is a signal whose frequency you already know. This example uses 8 samples per second and a pure 2 Hz synthetic vibration signal. With 8 samples, the FFT frequency spacing is sampling_rate / sample_count = 8 / 8 = 1 Hz.
For real-valued input, NumPy's rfft returns the non-negative frequency bins only. With 8 samples at 8 Hz, those bins are 0, 1, 2, 3, and 4 Hz. The 4 Hz bin is the Nyquist frequency.
02Create the synthetic vibration CSV
The following dataset is synthetic and was written specifically for this article. Save it as vibration_data.csv. The sampling interval is 0.125 seconds, so the sampling frequency is 1 / 0.125 = 8 Hz.
The values repeat every four samples: 0, 1, 0, -1. At 8 samples per second, four samples correspond to 0.5 seconds, and a period of 0.5 seconds corresponds to 2 Hz. The signal therefore has a known dominant input frequency of 2 Hz.
Quantity
Value
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
03Predict the FFT result before running code
Because the record contains an integer number of cycles and the 2 Hz signal falls exactly on an FFT bin, the synthetic example is deliberately clean. The raw FFT magnitude should be concentrated at 2 Hz.
For the sequence 0, 1, 0, -1, 0, 1, 0, -1, the 2 Hz FFT coefficient has magnitude 4. The other non-negative bins have zero magnitude in exact arithmetic. Small numerical roundoff can appear in computer calculations, so the exported spectrum is rounded to six decimal places.
Frequency
Expected raw FFT magnitude
0 Hz
0
1 Hz
0
2 Hz
4
3 Hz
0
4 Hz
0
The 0 Hz bin represents the mean or DC component. This signal has mean 0. The script subtracts the mean before the FFT anyway, which is a common preparation step when the goal is to inspect oscillatory content rather than a constant sensor offset.
04Calculate the spectrum and dominant frequency
Save the following script as fft_dominant_frequency.py. It checks the timestamp spacing, removes the mean, calculates the real FFT, writes the frequency spectrum to a new CSV, and verifies that the strongest nonzero-frequency bin is the expected 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()
05Compare the expected spectrum
The spectrum CSV should contain five rows because an 8-sample real FFT returns bins from 0 through the 4 Hz Nyquist frequency. Rounded to six decimal places, only the 2 Hz bin should have a nonzero magnitude.
The expected console output below was derived by hand from the synthetic signal and script. It is not a captured execution log.
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
06Check the result and common mistakes
Check the sample interval manually: 0.125 seconds.
Calculate the sampling frequency manually: 1 / 0.125 = 8 Hz.
Calculate the frequency spacing: 8 / 8 = 1 Hz.
Confirm that the largest nonzero FFT magnitude occurs at 2 Hz.
Confirm that the original vibration_data.csv is unchanged.
Run the script again without changing OUTPUT_DIR. It should stop with FileExistsError instead of overwriting the spectrum.
Problem
What to check
Dominant frequency is wrong by a scale factor
Check the time unit and sampling interval used in rfftfreq.
0 Hz is the largest component
A nonzero mean or sensor offset may dominate; decide whether mean removal is appropriate.
Energy appears in neighboring bins
The signal frequency may not fall exactly on an FFT bin, or the record may not contain an integer number of cycles.
High-frequency result looks suspicious
Check the Nyquist limit and whether the sampling rate is high enough for the physical signal.
Different runs overwrite results
Use a fresh output folder; this example deliberately stops when the destination already exists.
07Understand the limits before using measured vibration data
This example is intentionally ideal. Real vibration records rarely align perfectly with FFT bins, so spectral leakage can spread energy into neighboring frequencies. Window functions are commonly used to manage leakage, but they also change amplitude interpretation and require their own checks.
Frequency resolution depends on record length as well as sampling rate. With 8 samples at 8 Hz, the spacing is 1 Hz, so this analysis cannot distinguish frequencies such as 2.1 Hz and 2.4 Hz as separate exact bins. A longer record gives finer frequency spacing when the sampling rate is unchanged.
For measured automotive data, also consider anti-alias filtering, sensor bandwidth, sampling synchronization, dropped samples, detrending, window choice, amplitude scaling, and whether the signal is stationary enough for one FFT to be meaningful. Preserve the raw time-domain data and document every preprocessing step before interpreting peaks physically.
Execution and verification record
2026-09-20 · hand-checked example · target: Python 3.12 · NumPy required · no execution
Manually checked that the 8 timestamps are separated by 0.125 seconds, giving an 8 Hz sampling frequency.
Manually calculated the FFT frequency spacing as 8 Hz / 8 samples = 1 Hz.
Identified the repeating four-sample pattern 0, 1, 0, -1 as a 0.5-second period, corresponding to 2 Hz.
Manually determined the expected non-negative FFT magnitudes as 0, 0, 4, 0, 0 in exact arithmetic.
Inspected the script for constant-sampling checks, mean removal, rfft, rfftfreq, exclusion of the DC bin from dominant-frequency selection, output collision protection, and preservation of the original CSV.
Derived the expected spectrum CSV and console output by hand.
Verification limits
The code was not executed by the author of this response; NumPy FFT output and filesystem writes were not tested here.
Spectral leakage, window functions, amplitude calibration, irregular sampling, aliasing, sensor noise, and long datasets were not tested.
The synthetic peak at 2 Hz is a known mathematical input and is not claimed to represent a real vehicle resonance or fault frequency.
The official documentation URLs were provided from known documentation locations but were not checked live.
Apply a three-sample trailing moving average to synthetic acceleration data, verify every filtered value by hand, and make the filter's one-sample alignment delay explicit. The original CSV remains unchanged and the processed data is written to a new folder.
Convert synthetic vehicle speed and acceleration data into consistent SI units, then convert the results back to check the arithmetic. The script preserves the original CSV and writes the converted values to a new output folder.
Check units, missing values, finite numbers, and duplicate or reversed timestamps in synthetic demonstration data, then save the check results and a chart.