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.
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.
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.
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" )
$ 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.