Audio, sensor, and laboratory pipelines often need a new sample rate without changing the signal's duration or dominant frequencies. SciPy handles rational sample-rate conversion with scipy.signal.resample_poly(), which combines antialias filtering with upsampling and downsampling.
The conversion ratio comes from the input and target rates after their greatest common divisor is removed. Converting 800 Hz to 1200 Hz reduces to up=3 and down=2, so 200 input samples become 300 output samples over the same quarter-second interval.
The input array must contain evenly spaced samples along the selected axis. The default boundary treatment assumes zeros beyond both ends; non-uniform timestamps need interpolation, while scipy.signal.resample() is the Fourier alternative when a periodic signal must be converted to an arbitrary sample count.
Steps to resample a signal with SciPy:
- Create signal_resample.py with the sample rates and evenly spaced input signal.
- signal_resample.py
from math import ceil, gcd import numpy as np from scipy.fft import rfft, rfftfreq from scipy.signal import resample_poly input_rate = 800 target_rate = 1200 duration = 0.25 time = np.arange(0, duration, 1 / input_rate) samples = ( np.sin(2 * np.pi * 40 * time) + 0.25 * np.sin(2 * np.pi * 120 * time) )
The synthetic input combines a dominant 40 Hz tone with a smaller 120 Hz component, making frequency preservation measurable after conversion.
- Append the reduced rate ratio and polyphase resampling call to signal_resample.py.
common_factor = gcd(input_rate, target_rate) up = target_rate // common_factor down = input_rate // common_factor resampled = resample_poly(samples, up=up, down=down)
resample_poly() applies its low-pass FIR filter during the 3/2 conversion. For a multidimensional array, pass axis= for the dimension that contains consecutive time samples.
- Append fail-capable sample-count, duration, and dominant-frequency checks to signal_resample.py.
def dominant_frequency(values, rate): spectrum = np.abs(rfft(values)) frequencies = rfftfreq(values.size, d=1 / rate) return frequencies[np.argmax(spectrum[1:]) + 1] expected_samples = ceil(samples.size * up / down) input_duration = samples.size / input_rate output_duration = resampled.size / target_rate input_frequency = dominant_frequency(samples, input_rate) output_frequency = dominant_frequency(resampled, target_rate) print(f"input: {samples.size} samples at {input_rate} Hz") print(f"rate ratio: {up}/{down}") print(f"output: {resampled.size} samples at {target_rate} Hz") print(f"sample-count match: {resampled.size == expected_samples}") print( f"duration match: {np.isclose(output_duration, input_duration)} " f"({output_duration:.3f} s)" ) print( f"dominant frequency: {input_frequency:.1f} Hz -> " f"{output_frequency:.1f} Hz" )
- Run signal_resample.py to confirm the rate, duration, and dominant frequency.
$ python3 signal_resample.py input: 200 samples at 800 Hz rate ratio: 3/2 output: 300 samples at 1200 Hz sample-count match: True duration match: True (0.250 s) dominant frequency: 40.0 Hz -> 40.0 Hz
The output contains the expected 300 samples, covers the original 0.250 s interval, and retains the 40.0 Hz dominant component.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.