Sample-rate conversion changes how many measurements represent the same signal interval. In SciPy, scipy.signal.resample_poly() fits measured audio, sensor, and lab data when the new rate can be expressed as an integer up/down ratio.
The polyphase method upsamples the array, applies a zero-phase low-pass FIR filter, and downsamples the filtered result. An 800 Hz signal converted to 1200 Hz uses up=3 and down=2, so the output should contain one and a half times as many samples while covering the same duration.
Use evenly spaced samples along the axis being converted. For non-uniform timestamps, use an interpolation method instead; for a truly periodic signal that must be converted to an arbitrary target count, scipy.signal.resample() may fit better than rational sample-rate conversion.
Steps to resample a signal with SciPy:
- Create a script named signal_resample.py.
- signal_resample.py
from math import gcd import numpy as np from scipy.fft import rfft, rfftfreq from scipy.signal import resample_poly sample_rate = 800 target_rate = 1200 seconds = 0.25 sample_count = int(sample_rate * seconds) t = np.arange(sample_count) / sample_rate signal = np.sin(2 * np.pi * 40 * t) + 0.25 * np.sin(2 * np.pi * 120 * t) factor = gcd(sample_rate, target_rate) up = target_rate // factor down = sample_rate // factor resampled = resample_poly(signal, up=up, down=down) t_resampled = np.arange(resampled.size) / target_rate 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 = int(np.ceil(signal.size * up / down)) print(f"input samples: {signal.size} at {sample_rate} Hz") print(f"output samples: {resampled.size} at {target_rate} Hz") print(f"input duration: {signal.size / sample_rate:.3f} s") print(f"output duration: {resampled.size / target_rate:.3f} s") print(f"dominant frequency before: {dominant_frequency(signal, sample_rate):.1f} Hz") print(f"dominant frequency after: {dominant_frequency(resampled, target_rate):.1f} Hz") print("first five resampled values:", np.round(resampled[:5], 4).tolist()) print("length check:", resampled.size == expected_samples) print("duration check:", np.isclose(t_resampled[-1] + 1 / target_rate, seconds))
The gcd() call reduces the sample-rate ratio. For 1200 Hz divided by 800 Hz, resample_poly() receives up=3 and down=2.
- Run the script and confirm the new sample count.
$ python3 signal_resample.py input samples: 200 at 800 Hz output samples: 300 at 1200 Hz input duration: 0.250 s output duration: 0.250 s dominant frequency before: 40.0 Hz dominant frequency after: 40.0 Hz first five resampled values: [0.0, 0.3178, 0.6685, 0.826, 0.8795] length check: True duration check: True
The 300 output samples at 1200 Hz cover the same 0.250 s interval as 200 samples at 800 Hz. The dominant frequency remains 40.0 Hz after the rate conversion.
- Set the sample rates for the measured data.
sample_rate = 48000 target_rate = 16000
For a 48 kHz to 16 kHz conversion, the reduced ratio becomes up=1 and down=3.
- Pass the measured array to resample_poly() on the sample axis.
sample_axis = 0 resampled = resample_poly(signal, up=up, down=down, axis=sample_axis)
Keep axis=0 when each row is one time sample. Change sample_axis when the samples run across columns or another array dimension.
- Build the new time base from the target sample rate.
t_resampled = np.arange(resampled.shape[sample_axis]) / target_rate
For one-dimensional arrays, resampled.size and resampled.shape[sample_axis] are the same.
- Remove the sample script after adapting the pattern.
$ rm signal_resample.py
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.