Welch's method is a way to get better power spectral density (PSD) estimations than simple, naive periodograms. It has two main components:
- Cutting the input into many segments and average their individual PSD to reduce variance.
- Windowing each segment to reduce leakage from one frequency bin to the other.
My question relates to the first point: when we cut the input signal into many components, we essentially estimate the PSD of a shorter signal, which means that the frequency bins are wider. How do we then interpret the value at the Nyquist frequency? (and at the 0 frequency as well).
In the example below, I start with a perfectly flat one-sided PSD and compute a corresponding signal assuming a random phase for each bin (except for 0 and Nyquist because those are always real values for real signals). I then estimate the PSD of this signal using Welch's method with a lot of short segments. As you can see on the plot, the estimate is good except for the 0 and Nyquist frequency where it is half the expected valued. Why is that? How do I interpret these values? I can see why segmenting the full signal would yield a different DC component for each segment and therefor alter the average DC component, but what is happening to the Nyquist frequency?
This is not implementation-specific, it seems to be a fundamental side-effect of segmenting the input signal, as I have tried other windows as well as my own custom implementation of the method. I've included in the plot an average of 512 PSD across 512 independent signals sharing the same original PSD (in blue). This clearly doesn't drop to half the average at 0 and the Nyquist frequency.
corresponding python code:
import matplotlib.pyplot as plt
import numpy as np
import scipy.signal as signal
N = 8192
freq = np.fft.rfftfreq(N)
def get_signal(spectrum: np.ndarray) -> np.ndarray:
spectrum = init_spectrum.copy() + 0j
spectrum[1:-1] /= 2 # 1-sided PSD -> pseudo-2-sided PSD
spectrum = np.sqrt(spectrum) # Power -> amplitude
spectrum[1:-1] = np.exp(np.random.rand(N // 2 - 1) 2j * np.pi) # random phase
return np.sqrt(N) * np.fft.irfft(spectrum)
np.random.seed(1)
init_spectrum = np.ones(N // 2 + 1)
spec_full = []
for _ in range(512):
full_signal = get_signal(init_spectrum)
f_full, spec = signal.welch(full_signal, nperseg=N, detrend=False)
spec_full.append(spec)
spec_full = np.mean(spec_full, axis=0)
f_welch, spectrum = signal.welch(full_signal, nperseg=N // 64, noverlap=N // 128, detrend=False)
plt.plot(f_full, spec_full, label="windowed, no segmentation")
plt.plot(f_welch, spectrum, label="Welch's method")
plt.plot(freq, init_spectrum, label="initial PSD")
plt.xlabel("frequency")
plt.ylabel("spectral intensity")
plt.legend()
plt.show()

