How to resample a signal with SciPy

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:

  1. 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.

  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.

  3. 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.

  4. 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.

  5. 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.

  6. Remove the sample script after adapting the pattern.
    $ rm signal_resample.py