How to resample a signal with SciPy

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:

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

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

  3. 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"
    )
  4. 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.