Automoción y trabajo con datos

Encontrar la frecuencia dominante de datos sintéticos de vibración con FFT

Usa la FFT de NumPy sobre una señal sintética de vibración con un componente conocido de 2 Hz, inspecciona los bins de frecuencia y verifica que la frecuencia dominante detectada coincida con la entrada. El CSV original permanece sin cambios y el espectro se escribe en una nueva carpeta de salida.

Ver el índice

La traducción se ha realizado con IA. Comprueba el código, las unidades y los valores junto con el original. La revisión por hablantes nativos de cada idioma aún no se ha completado. English

Para quién esEsta guía está dirigida a personas que quieren un ejemplo pequeño y verificable antes de aplicar análisis de frecuencia basado en FFT a datos de vibración automotriz o de sensores.

Preparación
  • Python 3.12 y un comando de terminal que inicie esa versión.
  • NumPy instalado con python -m pip install numpy.
  • Una carpeta de trabajo donde el script pueda crear una nueva carpeta dentro de outputs.
  • Comprensión básica de que una señal muestreada tiene una frecuencia de muestreo y un intervalo de tiempo discreto.

01Empezar con una frecuencia conocida de antemano

El ejercicio inicial más seguro con FFT es una señal cuya frecuencia ya conoces. Este ejemplo utiliza 8 samples por segundo y una señal sintética de vibración pura de 2 Hz. Con 8 samples, el espaciado de frecuencia de la FFT es sampling_rate / sample_count = 8 / 8 = 1 Hz.

Para una entrada de valores reales, rfft de NumPy devuelve únicamente los bins de frecuencia no negativos. Con 8 samples a 8 Hz, esos bins son 0, 1, 2, 3 y 4 Hz. El bin de 4 Hz es la frecuencia de Nyquist.

02Crear el CSV sintético de vibración

El siguiente conjunto de datos es sintético y fue creado específicamente para este artículo. Guárdalo como vibration_data.csv. El intervalo de muestreo es 0.125 seconds, por lo que la frecuencia de muestreo es 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

Los valores se repiten cada cuatro samples: 0, 1, 0, -1. A 8 samples por segundo, cuatro samples corresponden a 0.5 seconds, y un periodo de 0.5 seconds corresponde a 2 Hz. Por tanto, la señal tiene una frecuencia de entrada dominante conocida de 2 Hz.

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

03Predecir el resultado de la FFT antes de ejecutar el código

Como el registro contiene un número entero de ciclos y la señal de 2 Hz cae exactamente sobre un bin de la FFT, el ejemplo sintético es deliberadamente limpio. La magnitude de la FFT sin procesar debería concentrarse en 2 Hz.

Para la secuencia 0, 1, 0, -1, 0, 1, 0, -1, el coefficient de FFT de 2 Hz tiene una magnitude de 4. Los demás bins de frecuencia no negativa tienen magnitude cero en aritmética exacta. En los cálculos informáticos pueden aparecer pequeños errores numéricos de redondeo, por lo que el espectro exportado se redondea a seis decimales.

FrecuenciaMagnitude esperada de la FFT sin procesar
0 Hz0
1 Hz0
2 Hz4
3 Hz0
4 Hz0

El bin de 0 Hz representa la media o componente DC. Esta señal tiene una media de 0. De todos modos, el script resta la media antes de la FFT, algo habitual cuando el objetivo es examinar el contenido oscilatorio en lugar de un offset constante del sensor.

04Calcular el espectro y la frecuencia dominante

Guarda el script siguiente como fft_dominant_frequency.py. Comprueba el espaciado de los timestamps, elimina la media, calcula la FFT real, escribe el espectro de frecuencia en un nuevo CSV y verifica que el bin de frecuencia distinta de cero más intenso sea el esperado de 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()

05Comparar el espectro esperado

El CSV del espectro debería contener cinco filas porque una FFT real de 8-sample devuelve bins desde 0 hasta la frecuencia de Nyquist de 4 Hz. Redondeado a seis decimales, únicamente el bin de 2 Hz debería tener una magnitude distinta de cero.

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

La salida de consola esperada que aparece a continuación se obtuvo manualmente a partir de la señal sintética y del script. No es un registro capturado de ejecución.

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

06Comprobar el resultado y los errores comunes

  • Comprueba manualmente el intervalo de muestreo: 0.125 seconds.
  • Calcula manualmente la frecuencia de muestreo: 1 / 0.125 = 8 Hz.
  • Calcula el espaciado de frecuencia: 8 / 8 = 1 Hz.
  • Confirma que la mayor magnitude de FFT distinta de cero se produce a 2 Hz.
  • Confirma que el archivo vibration_data.csv original no haya cambiado.
  • Ejecuta de nuevo el script sin cambiar OUTPUT_DIR. Debería detenerse con FileExistsError en lugar de sobrescribir el espectro.
ProblemaQué comprobar
La frecuencia dominante es incorrecta por un factor de escalaComprueba la unidad de tiempo y el intervalo de muestreo utilizados en rfftfreq.
0 Hz es el componente más grandeUna media distinta de cero o un offset del sensor puede dominar; decide si es apropiado eliminar la media.
Aparece energía en bins vecinosEs posible que la frecuencia de la señal no caiga exactamente sobre un bin de la FFT o que el registro no contenga un número entero de ciclos.
El resultado de alta frecuencia parece sospechosoComprueba el límite de Nyquist y si la frecuencia de muestreo es suficientemente alta para la señal física.
Distintas ejecuciones sobrescriben los resultadosUtiliza una carpeta de salida nueva; este ejemplo se detiene deliberadamente cuando el destino ya existe.

07Comprender los límites antes de utilizar datos medidos de vibración

Este ejemplo es intencionadamente ideal. Los registros reales de vibración rara vez se alinean perfectamente con los bins de la FFT, por lo que la fuga espectral puede repartir energía entre frecuencias vecinas. Las window functions se utilizan habitualmente para controlar la fuga, pero también modifican la interpretación de la amplitud y requieren sus propias comprobaciones.

La resolución de frecuencia depende de la longitud del registro además de la frecuencia de muestreo. Con 8 samples a 8 Hz, el espaciado es 1 Hz, por lo que este análisis no puede distinguir frecuencias como 2.1 Hz y 2.4 Hz como bins exactos independientes. Un registro más largo proporciona un espaciado de frecuencia más fino cuando la frecuencia de muestreo permanece sin cambios.

Para datos automotrices medidos, considera también el filtrado anti-alias, el ancho de banda del sensor, la sincronización de muestreo, la pérdida de samples, el detrending, la elección de window, el escalado de amplitud y si la señal es suficientemente estacionaria para que una sola FFT tenga sentido. Conserva los datos brutos en el dominio del tiempo y documenta cada paso de preprocesamiento antes de interpretar físicamente los picos.

Registro de ejecución y verificación

2026-09-20 · ejemplo revisado manualmente · objetivo: Python 3.12 · se requiere NumPy · sin ejecución

  • Se comprobó manualmente que los 8 timestamps están separados por 0.125 seconds, lo que da una frecuencia de muestreo de 8 Hz.
  • Se calculó manualmente el espaciado de frecuencia de la FFT como 8 Hz / 8 samples = 1 Hz.
  • Se identificó el patrón repetitivo de cuatro samples 0, 1, 0, -1 como un periodo de 0.5 seconds, correspondiente a 2 Hz.
  • Se determinaron manualmente las magnitudes esperadas de la FFT no negativa como 0, 0, 4, 0, 0 en aritmética exacta.
  • Se revisó el script para las comprobaciones de muestreo constante, eliminación de la media, rfft, rfftfreq, exclusión del bin DC de la selección de frecuencia dominante, protección frente a colisiones de salida y conservación del CSV original.
  • Se obtuvieron manualmente el CSV del espectro esperado y la salida de consola.
Límites de la verificación
  • El código no fue ejecutado por el autor de esta respuesta; aquí no se probaron la salida de la FFT de NumPy ni las escrituras del sistema de archivos.
  • No se probaron fuga espectral, window functions, calibración de amplitud, muestreo irregular, aliasing, ruido de sensores ni conjuntos de datos largos.
  • El pico sintético de 2 Hz es una entrada matemática conocida y no se afirma que represente una resonancia real de vehículo o una frecuencia de fallo.
  • Las URL de la documentación oficial se proporcionaron a partir de ubicaciones de documentación conocidas, pero no se comprobaron en línea.

Criterios de redacción y verificación de todo el sitio

Fuentes de referencia

Las explicaciones y los ejemplos son de elaboración propia. Puedes consultar los comportamientos y conceptos relacionados en las siguientes fuentes oficiales.