The world’s leading publication for data science, AI, and ML professionals.

Understanding Predictive Maintenance - Wave Data: Feature Engineering (Part 2 Spectral)

Feature Engineering of spectral data

Photo by Evie S. on Unsplash
Photo by Evie S. on Unsplash

Article Purpose

This is the second part of the article about Wave Data Feature Engineering. We are going to focus on the spectral features. Have you got any thoughts to add? Feel free to share!

This article is part of the series Understanding Predictive Maintenance.

Check the whole series in this link. Ensure you don’t miss out on new articles by following me. All images without captions were created by me.

Frequency-Domain Features

Transitioning to the frequency domain, we employ techniques like the Fast Fourier Transform (FFT) to convert time-domain signals. Extracted features include dominant frequency, spectral entropy, and spectral kurtosis. Power Spectral Density (PSD) and Harmonic Ratios offer insights into power distribution and harmonic relationships.

  • FFT (Fast Fourier Transform) Convert the time-domain signal to the frequency domain. Extract features from the resulting spectrum, such as dominant frequency, spectral entropy, and spectral kurtosis.
  • Power Spectral Density (PSD) Describes how the power of a signal is distributed over frequency.

Plan for the next article

  • Wavelet Transform
  • Demodulation
  • Recurrence Quantification Analysis (RQA)

Create the signal for experiments

I will use exactly the same as in the previous part:

Understanding Predictive Maintenance – Wave Data: Feature Engineering (Part 1)

Let’s generate the signal:

# Parameters
duration = 20         # seconds
sampling_rate = 20    # Hz
frequency = 5         # Hz (vibration frequency)
amplitude = 1.0       # Min Max range
noise_level = 0.3     # Noise factor to increase reality
max_wear = 1          # Maximum wear before reset
wear_threshold = 0.5  # Wear threshold for reset

# Generate synthetic vibration signal with wear and threshold
time, vibration_signal = generate_vibration_signal(duration, sampling_rate,
 frequency, amplitude, noise_level, max_wear, wear_threshold)

Fast Fourier Transform (FFT) and Short-Time Fourier Transform (STFT)

FFT equation (Latex compiled)
FFT equation (Latex compiled)

Signal Representation

Let’s start with our signal, which is essentially a series of data points representing how the signal changes over time. This could be a sound wave, a sequence of numbers, or any other data that varies with time.

Discrete Fourier Transform (DFT)

The FFT is a more efficient way of computing something called the Discrete Fourier Transform (DFT). The DFT takes our signal and expresses it as a sum of sinusoidal functions, each representing a different frequency component. This is where the magic happens.

Divide and Conquer

Instead of directly computing the DFT for the entire signal, FFT takes advantage of the fact that a DFT of any composite signal can be expressed as the combination of DFTs of its subparts. It divides the signal into smaller sections, computes the DFT for each section, and then combines them.

Butterfly Operation

The magic of FFT lies in a process called the butterfly operation. It’s like a dance move where the computed frequencies are paired up and combined in a specific way. This happens recursively until we have the final frequency components of the entire signal.

Efficiency Boost

The key to FFT's speed is its ability to dramatically reduce the number of computations needed compared to the straightforward DFT approach. By exploiting the symmetries and patterns in the signal, FFT efficiently calculates the frequency components.

Time for code

Now we can apply the theory into practice in simple code lines:

import numpy as np

# Apply FFT to the signal
fft_result = np.fft.fft(vibration_signal)

# This very important part, let`s investigate it more in depth
frequencies = np.fft.fftfreq(len(fft_result), 1/sampling_rate)

len(fft_result) This is the length of the FFT result, which is essentially the number of points in the frequency domain. The FFT operation transforms a time-domain signal into a frequency-domain signal and len(fft_result) gives you the number of frequency bins.

1/sampling_rate This is the inverse of the sampling rate sampling_rate, representing the time interval between samples in the original time-domain signal. The sampling rate is the number of samples per second.

np.fft.fftfreq() This function generates the frequencies corresponding to the FFT result. It takes two parameters. The first one is the length of the result len(fft_result), and the second one is the sampling interval 1/sampling_rate. It returns an array of frequencies.

But do not worry. Using these two lines, the whole ‘magic’ will happen

# Plot the time-domain signal
plt.subplot(2, 1, 1)
plt.plot(t, vibration_signal)
plt.title('Vibration Signal with Wear')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')

# Plot the frequency-domain signal (FFT)
plt.subplot(2, 1, 2)
plt.plot(frequencies, np.abs(fft_result))
plt.title('Frequency Domain Signal (FFT)')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude Spectrum')

plt.tight_layout()
plt.show()
Vibration signal and their FFT representation (code output)
Vibration signal and their FFT representation (code output)

In the plot, we notice a central signal at 0, flanked by two mirrored symmetrical signals on the positive and negative sides. This indicates that our signal is comprised of a single distinct wave. Before delving into experiments, let’s first explore the concept of symmetry, and afterward, we can conduct experiments with various signals.

Behind the scenes – FFT symmetry

What is happening on the backstage? We have some math and theoretical concepts. Let’s make it easy.

In many real-world scenarios, signals are composed of real numbers. In the time domain, these signals can be represented as a sequence of values. When you take the FFT of a real-valued signal, the resulting frequency spectrum is symmetric.

Complex conjugate pairs the symmetry comes from the fact that the FFT involves complex numbers. For every positive frequency component, there is a corresponding negative frequency component with the same magnitude. These pairs of frequencies are complex conjugates of each other.

Mirrored information The positive frequencies represent the information about how the signal oscillates in one direction, while the negative frequencies represent the same information but in the opposite direction. The FFT captures both directions, and that’s why the plot looks symmetrical

In summary, the symmetry in the FFT is a consequence of the mathematical properties of real-valued signals and complex numbers in the context of the FFT.


Let’s start the next experiments

Now we understand the basic concepts of the FFT. Let’s simulate 2 signals with different parameters connected together

We will generate a second similar signal we will only focus on Frequency and amplitude:

# First Signal
frequency = 10  
amplitude = 1

#Second Signal
frequency = 20  
amplitude = 1

Now let’s create a combined signal and plot:

t2, vibration_signal_2 = generate_vibration_signal(duration, sampling_rate,
   frequency, amplitude, noise_level, max_wear, wear_threshold)

# Combine the signals just simply add them :) 
combined_signal = vibration_signal + vibration_signal_2

plt.plot(t1, combined_signal, label='Signal 1')
plt.title('Combined Signals')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.show()
Combined signal (code output)
Combined signal (code output)

Now let’s make FFT and plot results:

# Apply FFT to the combined signal
fft_result = np.fft.fft(combined_signal)
frequencies = np.fft.fftfreq(len(fft_result), 1/sampling_rate)

# Plot the frequency-domain signal (FFT)
plt.plot(frequencies, np.abs(fft_result))
plt.title('Frequency Domain Signal (FFT)')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude Spectrum')
plt.show()
FFT representation of combined_signal (code output)
FFT representation of combined_signal (code output)

Now you can see we have the same amplitude height but 2 more signals equally offset. First signal = 10Hz Second signal = 20Hz

The X-axis position is adjusted for signal frequency. Let`s introduce the third signal = 100 Hz and plot.

frequency = 100  
amplitude = 1
t3, vibration_signal_3 = generate_vibration_signal(duration, sampling_rate,
   frequency, amplitude, noise_level, max_wear, wear_threshold)

# Combine the signals just simply add them :)
combined_signal = vibration_signal + vibration_signal_2 + vibration_signal_3

frequency = 150 # Just for make offset, now you know how it works  
amplitude = 2
t4, vibration_signal_4 = generate_vibration_signal(duration, sampling_rate,
    frequency, amplitude, noise_level, max_wear, wear_threshold)

# Combine the signals
combined_signal = vibration_signal + vibration_signal_2 +
    vibration_signal_3 +vibration_signal_4

# Apply FFT to the combined signal
fft_result = np.fft.fft(combined_signal)
frequencies = np.fft.fftfreq(len(fft_result), 1/sampling_rate)

plt.plot(frequencies, np.abs(fft_result))
plt.title('Frequency Domain Signal (FFT)')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude Spectrum')

plt.tight_layout()
plt.show()
Combined_signal and additional 3rd signal component (code output)
Combined_signal and additional 3rd signal component (code output)

As we can see our "new" signal is now much more offset due to the higher frequency value

What will happen if we add a fourth signal with a different amplitude

Make a new signal and plot it together

frequency = 150 # Just for make offset, now you know how it works  
amplitude = 2
t4, vibration_signal_4 = generate_vibration_signal(duration, sampling_rate,
    frequency, amplitude, noise_level, max_wear, wear_threshold)

# Combine the signals
combined_signal = vibration_signal + vibration_signal_2 +
    vibration_signal_3 + vibration_signal_4

frequency = 150 # Just for make offset, now you know how it works  
amplitude = 2
t4, vibration_signal_4 = generate_vibration_signal(duration, sampling_rate,
    frequency, amplitude, noise_level, max_wear, wear_threshold)

# Combine the signals
combined_signal = vibration_signal + vibration_signal_2 +
    vibration_signal_3 + vibration_signal_4

# Apply FFT to the combined signal
fft_result = np.fft.fft(combined_signal)
frequencies = np.fft.fftfreq(len(fft_result), 1/sampling_rate)

plt.plot(frequencies, np.abs(fft_result))
plt.title('Frequency Domain Signal (FFT)')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude Spectrum')

plt.tight_layout()
plt.show()
Combined_signal and additional 4th signal component (code output)
Combined_signal and additional 4th signal component (code output)

Now you can see our Amplitude spectrum is twice higher due to amplitude values 1 and 2


Power Spectral Density (PSD)

Power Spectral Density equation (Latex compiled)
Power Spectral Density equation (Latex compiled)

PSD is like taking a special snapshot of a signal and understanding how much power it has at different frequencies. It helps us see the energy distribution across the musical or vibration spectrum. We have 2 main PSD estimation methods, Welch and Barlett.

For the experiments, we will create a new combined signal just for the sake of clarity and improved visualization (not close frequency to see clear peaks)

frequency = 100  
amplitude = 1
t5, vibration_signal_5 = generate_vibration_signal(duration, sampling_rate,
       frequency, amplitude, noise_level, max_wear, wear_threshold)

frequency = 200
amplitude = 1
t6, vibration_signal_6 = generate_vibration_signal(duration, sampling_rate, 
       frequency, amplitude, noise_level, max_wear, wear_threshold)

frequency = 400
amplitude = 3
t7, vibration_signal_7 = generate_vibration_signal(duration, sampling_rate,
       frequency, amplitude, noise_level, max_wear, wear_threshold)

combined_signal2 = vibration_signal_5 + vibration_signal_6 + vibration_signal_7

Welch Method

PSD Welch method equation (Latex compiled)
PSD Welch method equation (Latex compiled)
def welch_method(signal, segment_size=128, overlap=64):
    f, Pxx = plt.psd(signal, NFFT=segment_size, Fs=sampling_rate, noverlap=overlap)
    plt.title('Welch Method')
    plt.xlabel('Frequency (Hz)')
    plt.ylabel('Power/Frequency (dB)')
    plt.show()
    return f, Pxx

freq_welch, P_welch = welch_method(combined_signal2)

The Welch method divides the signal into overlapping segments and averages the periodograms. This approach provides a trade-off between accuracy and computational complexity. However, it may sacrifice frequency resolution for improved variance properties.

Bartlett Method

PSD Bartlett estimation equation (Latex compiled)
PSD Bartlett estimation equation (Latex compiled)

Bartlett method is a specific case of the Welch method with no overlap between segments. While offering simplicity and reduced computational load, it shares a similar trade-off between accuracy and frequency resolution with the Welch method.

def bartlett_method(signal, segment_size=128):
    f, Pxx = plt.psd(signal, NFFT=segment_size, Fs=sampling_rate, window=np.bartlett(segment_size))
    plt.title('Bartlett Method')
    plt.xlabel('Frequency (Hz)')
    plt.ylabel('Power/Frequency (dB)')
    plt.show()
    return f, Pxx

freq_bartlett, P_bartlett = bartlett_method(combined_signal2)

The graphical explanation about the differences

image source and my recommendation: https://www.researchgate.net/figure/Welchs-and-Bartletts-methods-for-power-spectral-density-estimation-The-Bartletts_fig1_349283231
image source and my recommendation: https://www.researchgate.net/figure/Welchs-and-Bartletts-methods-for-power-spectral-density-estimation-The-Bartletts_fig1_349283231

Short-Time Fourier Transform (STFT) – windowing + FFT

Short-Time Fourier Transform equation (Latex compiled)
Short-Time Fourier Transform equation (Latex compiled)

Why it is useful in predictive maintenance?

STFT analysis enables engineers to scrutinize the frequency components of signals, such as vibrations or acoustic emissions from machinery. The distinct frequency patterns associated with various faults or abnormalities allow for the early detection of potential issues, contributing to minimizing downtime and maintenance costs.

One of the primary advantages of STFT in predictive maintenance is its ability to identify fault signatures early on. This is particularly crucial as STFT can detect low-amplitude vibrations or subtle changes in signals, providing an early warning system that allows practitioners to address issues before they escalate.

Furthermore, STFT plays a pivotal role in distinguishing between normal and anomalous frequency patterns. By establishing baseline frequency spectra through a comparison of healthy and faulty equipment, engineers can efficiently identify deviations and signal impending problems, forming the foundation for effective predictive maintenance strategies.

Let’s make some code

from scipy.signal import spectrogram

# Apply Short-Time Fourier Transform (STFT) to the combined signal
frequencies, times, Sxx = spectrogram(combined_signal, fs=sampling_rate,
nperseg=256, noverlap=128)

Woooh, just "one" line! Power of python. In this function, we have to play with 2 important parameters with funny names nperseg and noverlap.

nperseg (Number of Points per Segment). This parameter determines the size of each time window or segment. A larger nperseg one results in better frequency resolution but poorer time resolution, and a smaller nperseg one provides better time resolution but poorer frequency resolution. In other words, it affects the trade-off between time and frequency resolution. In the example, nperseg=256 it means that each time window is 256 points long.

noverlap (Number of Overlapping Points). This parameter controls the overlap between consecutive time windows. Overlapping windows help in capturing the dynamic changes in the signal over time. If noverlap is set to a value less than nperseg, the windows overlap. In the example, noverlap=128 means that each time window overlaps with the previous one by 128 points.

Let’s plot it

plt.pcolormesh(times, frequencies, 10 * np.log10(Sxx), shading='auto')
plt.title('Spectrogram - Combined Signal')
plt.xlabel('Time (s)')
plt.ylabel('Frequency (Hz)')
plt.show()
STFT results spectrogram of combined_signal (code output)
STFT results spectrogram of combined_signal (code output)

We have to use Sxx with basically is spectrogram output(Sxx[i, j]) We can easily spot our 4 signals combined together. The thicker the ribbon the higher the amplitude we have

Spectogram Grid (Sxx) Imagine a big grid like a piece of graph paper.

Grid Rows (up and down) Each row in the grid represents a different sound frequency, like high or low tones. Higher rows might represent high-pitched sounds, and lower rows could be for low-pitched sounds.

Grid Columns (left to right) Each column in the grid represents a different moment in time, like a snapshot. As you move from left to right, you’re looking at how the sound changes over time.

Colors in the Grid The colors in the grid tell you how loud or strong each frequency is at each moment. Bright colors might mean loud sounds and dull colors might mean quieter sounds.

What will happen if one of our signals will be much stronger?

Let’s reframe the explanation in the context of predictive maintenance and simulate a scenario where we modify the fourth signal with a significantly higher amplitude, resembling a potential fault or anomaly in the machinery.

amplitude = 20 # Increase it 
t4, vibration_signal_4 = generate_vibration_signal(duration, sampling_rate,
  frequency, amplitude, noise_level, max_wear, wear_threshold)

# Combine the signals
combined_signal = vibration_signal + vibration_signal_2 + 
  vibration_signal_3 + vibration_signal_4

# Apply FFT to the combined signal
fft_result = np.fft.fft(combined_signal)
frequencies = np.fft.fftfreq(len(fft_result), 1/sampling_rate)

plt.plot(frequencies, np.abs(fft_result))
plt.title('Frequency Domain Signal (FFT)')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude Spectrum')

plt.tight_layout()
plt.show()
FFT results of combined_signal with component created using significantly higher amplitude (code output)
FFT results of combined_signal with component created using significantly higher amplitude (code output)

Now, observe our new signal, which is significantly dominant. This doesn’t imply that the other signals have vanished, but their presence is overshadowed by the amplitude of the prominent signal, making them appear more like noise in comparison.

from scipy.signal import spectrogram

# Apply Short-Time Fourier Transform (STFT) to the combined signal
frequencies, times, Sxx = spectrogram(combined_signal, fs=sampling_rate, nperseg=256, noverlap=128)

plt.pcolormesh(times, frequencies, 10 * np.log10(Sxx), shading='auto')
plt.title('Spectrogram - Combined Signal')
plt.xlabel('Time (s)')
plt.ylabel('Frequency (Hz)')
plt.show()
STFT results spectrogram of combined_signal with component created using significantly higher amplitude (code output)
STFT results spectrogram of combined_signal with component created using significantly higher amplitude (code output)

In the spectrogram results, a conspicuous and intense line emerges, indicating a potential precursor to future machinery failure. In normal operational conditions, our machinery produces a background spectrum. However, as wear and tear progress or a component is on the verge of failure, deviations from this normal state become noticeable. To delve deeper into this, we might explore training a Convolutional Neural Network (CNN) to analyze these spectral patterns and identify distinctive signatures associated with impending failures. After the articles about Feature Engineering, I am going to start the modeling series.


This is the end of part 2 of the wave data feature engineering. In the next article, I am going to cover.

  • Wavelet Transform
  • Demodulation
  • Recurrence Quantification Analysis (RQA)

This article is part of the series Understanding Predictive Maintenance. Due to your great feedback and recommendation, I have a plan to make the book. If there is something that is worth extending or including just let me know. I am considering all your feedback.

Check the whole series in this link. Ensure you don’t miss out on new articles by following me.


Related Articles