Repository: obspy/obspy

Issue:
Filter frequencies << sampling frequency are unstable
Hi,

I just found a possible question about butterworth-bandpass filtering. I download a seismogram from IRIS and try to filter it with a butterworth-bandpass filter from 0.02hz to 0.2hz, but found the result is incorrect with some extremely large number after filtering. I guess it might be caused by some kind of unstability of filtering algorithm. Here is my code:

``` python
from obspy.fdsn import Client
from obspy import UTCDateTime
from obspy.taup.taup import getTravelTimes
from obspy.core import AttribDict
client = Client('iris')
starttime = UTCDateTime('2014-08-24T10:20:44.0600')
p_arrival_time = 376.945875099  # p phase arrival time
st = client.get_waveforms(network='AK', station='BPAW', location='*', channel='BHZ', 
                          starttime=starttime+p_arrival_time-60, endtime=starttime+p_arrival_time+180,
                          attach_response=True)
st.remove_response(output='disp')  # remove the instrument response
st.detrend(type='demean')  # remove the mean
st.detrend(type='linear')  # remove the linear trend
st.taper(type='hann', max_percentage=0.05)  # taper
# st.resample(20)  # the decreasing samping rate can make filtering work well
st.filter('bandpass',freqmin=0.02, freqmax=0.2, zerophase=True)  # filter
st.write('tmp.sac',format='sac')
```

BTW, when I decrease the sampling rate from original 50hz to 20hz, the filtering will be good. I wonder whether it might also be related to sampling rate.

I also find a similar question about scipy:
http://stackoverflow.com/questions/21862777/bandpass-butterworth-filter-frequencies-in-scipy
Since the filtering in obspy is actually based on scipy, they possibly point to one same question....

Thx



Discussion:
Based on [this answer](http://stackoverflow.com/a/23878820), I think we should a) add a warning if the filtering frequencies are very small compared to the sampling rate, and b) switch to [`sosfilt`](http://docs.scipy.org/doc/scipy-dev/reference/generated/scipy.signal.sosfilt.html) once a new scipy is available.

I think we should backport the second-order filter sections from scipy. This has been an annoying issue for a long time and we cannot simply depend on scipy 0.16 once available.

That should be fairly quick to do and incredibly useful.

It looks like we'd need `sosfilt`, `zpk2sos` and all filters need to support `output='zpk'`. I'm not sure when zpk output was added? Also, `zpk2sos` is pretty long...


Resolving patch:
diff --git a/obspy/signal/_sosfilt.py b/obspy/signal/_sosfilt.py
new file mode 100644
--- /dev/null
+++ b/obspy/signal/_sosfilt.py
@@ -0,0 +1,509 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# ----------------------------------------------------------------------
+# Filename: _sosfilt.py
+#  Purpose: Backport of Second-Order Section Filtering from SciPy 0.16.0
+#   Author: Elliott Sales de Andrade + SciPy authors
+# ---------------------------------------------------------------------
+"""
+Backport of Second-Order Section Filtering from SciPy 0.16.0
+
+:copyright:
+    The ObsPy Development Team (devs@obspy.org)
+:license:
+    GNU Lesser General Public License, Version 3
+    (http://www.gnu.org/copyleft/lesser.html)
+"""
+from __future__ import (absolute_import, division, print_function,
+                        unicode_literals)
+from future.builtins import *  # NOQA
+
+import numpy as np
+from scipy.signal import lfilter, zpk2tf
+
+
+def _cplxreal(z, tol=None):
+    """
+    Split into complex and real parts, combining conjugate pairs.
+
+    The 1D input vector `z` is split up into its complex (`zc`) and real (`zr`)
+    elements.  Every complex element must be part of a complex-conjugate pair,
+    which are combined into a single number (with positive imaginary part) in
+    the output.  Two complex numbers are considered a conjugate pair if their
+    real and imaginary parts differ in magnitude by less than ``tol * abs(z)``.
+
+    Parameters
+    ----------
+    z : array_like
+        Vector of complex numbers to be sorted and split
+    tol : float, optional
+        Relative tolerance for testing realness and conjugate equality.
+        Default is ``100 * spacing(1)`` of `z`'s data type (i.e. 2e-14 for
+        float64)
+
+    Returns
+    -------
+    zc : ndarray
+        Complex elements of `z`, with each pair represented by a single value
+        having positive imaginary part, sorted first by real part, and then
+        by magnitude of imaginary part.  The pairs are averaged when combined
+        to reduce error.
+    zr : ndarray
+        Real elements of `z` (those having imaginary part less than
+        `tol` times their magnitude), sorted by value.
+
+    Raises
+    ------
+    ValueError
+        If there are any complex numbers in `z` for which a conjugate
+        cannot be found.
+
+    See Also
+    --------
+    _cplxpair
+
+    Examples
+    --------
+    >>> a = [4, 3, 1, 2-2j, 2+2j, 2-1j, 2+1j, 2-1j, 2+1j, 1+1j, 1-1j]
+    >>> zc, zr = _cplxreal(a)
+    >>> print(zc)
+    [ 1.+1.j  2.+1.j  2.+1.j  2.+2.j]
+    >>> print(zr)
+    [ 1.  3.  4.]
+    """
+
+    z = np.atleast_1d(z)
+    if z.size == 0:
+        return z, z
+    elif z.ndim != 1:
+        raise ValueError('_cplxreal only accepts 1D input')
+
+    if tol is None:
+        # Get tolerance from dtype of input
+        tol = 100 * np.finfo((1.0 * z).dtype).eps
+
+    # Sort by real part, magnitude of imaginary part (speed up further sorting)
+    z = z[np.lexsort((abs(z.imag), z.real))]
+
+    # Split reals from conjugate pairs
+    real_indices = abs(z.imag) <= tol * abs(z)
+    zr = z[real_indices].real
+
+    if len(zr) == len(z):
+        # Input is entirely real
+        return np.array([]), zr
+
+    # Split positive and negative halves of conjugates
+    z = z[~real_indices]
+    zp = z[z.imag > 0]
+    zn = z[z.imag < 0]
+
+    if len(zp) != len(zn):
+        raise ValueError('Array contains complex value with no matching '
+                         'conjugate.')
+
+    # Find runs of (approximately) the same real part
+    same_real = np.diff(zp.real) <= tol * abs(zp[:-1])
+    diffs = np.diff(np.concatenate(([0], same_real, [0])))
+    run_starts = np.where(diffs > 0)[0]
+    run_stops = np.where(diffs < 0)[0]
+
+    # Sort each run by their imaginary parts
+    for i in range(len(run_starts)):
+        start = run_starts[i]
+        stop = run_stops[i] + 1
+        for chunk in (zp[start:stop], zn[start:stop]):
+            chunk[...] = chunk[np.lexsort([abs(chunk.imag)])]
+
+    # Check that negatives match positives
+    if any(abs(zp - zn.conj()) > tol * abs(zn)):
+        raise ValueError('Array contains complex value with no matching '
+                         'conjugate.')
+
+    # Average out numerical inaccuracy in real vs imag parts of pairs
+    zc = (zp + zn.conj()) / 2
+
+    return zc, zr
+
+
+def _nearest_real_complex_idx(fro, to, which):
+    """Get the next closest real or complex element based on distance"""
+    assert which in ('real', 'complex')
+    order = np.argsort(np.abs(fro - to))
+    mask = np.isreal(fro[order])
+    if which == 'complex':
+        mask = ~mask
+    return order[np.where(mask)[0][0]]
+
+
+def _zpk2sos(z, p, k, pairing='nearest'):
+    """
+    Return second-order sections from zeros, poles, and gain of a system
+
+    Parameters
+    ----------
+    z : array_like
+        Zeros of the transfer function.
+    p : array_like
+        Poles of the transfer function.
+    k : float
+        System gain.
+    pairing : {'nearest', 'keep_odd'}, optional
+        The method to use to combine pairs of poles and zeros into sections.
+        See Notes below.
+
+    Returns
+    -------
+    sos : ndarray
+        Array of second-order filter coefficients, with shape
+        ``(n_sections, 6)``. See `sosfilt` for the SOS filter format
+        specification.
+
+    See Also
+    --------
+    sosfilt
+
+    Notes
+    -----
+    The algorithm used to convert ZPK to SOS format is designed to
+    minimize errors due to numerical precision issues. The pairing
+    algorithm attempts to minimize the peak gain of each biquadratic
+    section. This is done by pairing poles with the nearest zeros, starting
+    with the poles closest to the unit circle.
+
+    *Algorithms*
+
+    The current algorithms are designed specifically for use with digital
+    filters. Although they can operate on analog filters, the results may
+    be sub-optimal.
+
+    The steps in the ``pairing='nearest'`` and ``pairing='keep_odd'``
+    algorithms are mostly shared. The ``nearest`` algorithm attempts to
+    minimize the peak gain, while ``'keep_odd'`` minimizes peak gain under
+    the constraint that odd-order systems should retain one section
+    as first order. The algorithm steps and are as follows:
+
+    As a pre-processing step, add poles or zeros to the origin as
+    necessary to obtain the same number of poles and zeros for pairing.
+    If ``pairing == 'nearest'`` and there are an odd number of poles,
+    add an additional pole and a zero at the origin.
+
+    The following steps are then iterated over until no more poles or
+    zeros remain:
+
+    1. Take the (next remaining) pole (complex or real) closest to the
+       unit circle to begin a new filter section.
+
+    2. If the pole is real and there are no other remaining real poles [#]_,
+       add the closest real zero to the section and leave it as a first
+       order section. Note that after this step we are guaranteed to be
+       left with an even number of real poles, complex poles, real zeros,
+       and complex zeros for subsequent pairing iterations.
+
+    3. Else:
+
+        1. If the pole is complex and the zero is the only remaining real
+           zero*, then pair the pole with the *next* closest zero
+           (guaranteed to be complex). This is necessary to ensure that
+           there will be a real zero remaining to eventually create a
+           first-order section (thus keeping the odd order).
+
+        2. Else pair the pole with the closest remaining zero (complex or
+           real).
+
+        3. Proceed to complete the second-order section by adding another
+           pole and zero to the current pole and zero in the section:
+
+            1. If the current pole and zero are both complex, add their
+               conjugates.
+
+            2. Else if the pole is complex and the zero is real, add the
+               conjugate pole and the next closest real zero.
+
+            3. Else if the pole is real and the zero is complex, add the
+               conjugate zero and the real pole closest to those zeros.
+
+            4. Else (we must have a real pole and real zero) add the next
+               real pole closest to the unit circle, and then add the real
+               zero closest to that pole.
+
+    .. [#] This conditional can only be met for specific odd-order inputs
+           with the ``pairing == 'keep_odd'`` method.
+
+    Examples
+    --------
+
+    Design a 6th order low-pass elliptic digital filter for a system with a
+    sampling rate of 8000 Hz that has a pass-band corner frequency of
+    1000 Hz.  The ripple in the pass-band should not exceed 0.087 dB, and
+    the attenuation in the stop-band should be at least 90 dB.
+
+    In the following call to `signal.ellip`, we could use ``output='sos'``,
+    but for this example, we'll use ``output='zpk'``, and then convert to SOS
+    format with `zpk2sos`:
+
+    >>> from scipy import signal
+    >>> z, p, k = signal.ellip(6, 0.087, 90, 1000/(0.5*8000), output='zpk')
+
+    Now convert to SOS format.
+
+    >>> sos = _zpk2sos(z, p, k)
+
+    The coefficents of the numerators of the sections:
+
+    >>> sos[:, :3]
+    array([[ 0.0014154 ,  0.00248707,  0.0014154 ],
+           [ 1.        ,  0.72965193,  1.        ],
+           [ 1.        ,  0.17594966,  1.        ]])
+
+    The symmetry in the coefficients occurs because all the zeros are on the
+    unit circle.
+
+    The coefficients of the denominators of the sections:
+
+    >>> sos[:, 3:]
+    array([[ 1.        , -1.32543251,  0.46989499],
+           [ 1.        , -1.26117915,  0.6262586 ],
+           [ 1.        , -1.25707217,  0.86199667]])
+
+    The next example shows the effect of the `pairing` option.  We have a
+    system with three poles and three zeros, so the SOS array will have
+    shape (2, 6).  The means there is, in effect, an extra pole and an extra
+    zero at the origin in the SOS representation.
+
+    >>> z1 = np.array([-1, -0.5-0.5j, -0.5+0.5j])
+    >>> p1 = np.array([0.75, 0.8+0.1j, 0.8-0.1j])
+
+    With ``pairing='nearest'`` (the default), we obtain
+
+    >>> _zpk2sos(z1, p1, 1)
+    array([[ 1.  ,  1.  ,  0.5 ,  1.  , -0.75,  0.  ],
+           [ 1.  ,  1.  ,  0.  ,  1.  , -1.6 ,  0.65]])
+
+    The first section has the zeros {-0.5-0.05j, -0.5+0.5j} and the poles
+    {0, 0.75}, and the second section has the zeros {-1, 0} and poles
+    {0.8+0.1j, 0.8-0.1j}.  Note that the extra pole and zero at the origin
+    have been assigned to different sections.
+
+    With ``pairing='keep_odd'``, we obtain:
+
+    >>> _zpk2sos(z1, p1, 1, pairing='keep_odd')
+    array([[ 1.  ,  1.  ,  0.  ,  1.  , -0.75,  0.  ],
+           [ 1.  ,  1.  ,  0.5 ,  1.  , -1.6 ,  0.65]])
+
+    The extra pole and zero at the origin are in the same section.
+    The first section is, in effect, a first-order section.
+
+    """
+    # TODO in the near future:
+    # 1. Add SOS capability to `filtfilt`, `freqz`, etc. somehow (#3259).
+    # 2. Make `decimate` use `sosfilt` instead of `lfilter`.
+    # 3. Make sosfilt automatically simplify sections to first order
+    #    when possible. Note this might make `sosfiltfilt` a bit harder (ICs).
+    # 4. Further optimizations of the section ordering / pole-zero pairing.
+    # See the wiki for other potential issues.
+
+    valid_pairings = ['nearest', 'keep_odd']
+    if pairing not in valid_pairings:
+        raise ValueError('pairing must be one of %s, not %s'
+                         % (valid_pairings, pairing))
+    if len(z) == len(p) == 0:
+        return np.array([[k, 0., 0., 1., 0., 0.]])
+
+    # ensure we have the same number of poles and zeros, and make copies
+    p = np.concatenate((p, np.zeros(max(len(z) - len(p), 0))))
+    z = np.concatenate((z, np.zeros(max(len(p) - len(z), 0))))
+    n_sections = (max(len(p), len(z)) + 1) // 2
+    sos = np.zeros((n_sections, 6))
+
+    if len(p) % 2 == 1 and pairing == 'nearest':
+        p = np.concatenate((p, [0.]))
+        z = np.concatenate((z, [0.]))
+    assert len(p) == len(z)
+
+    # Ensure we have complex conjugate pairs
+    # (note that _cplxreal only gives us one element of each complex pair):
+    z = np.concatenate(_cplxreal(z))
+    p = np.concatenate(_cplxreal(p))
+
+    p_sos = np.zeros((n_sections, 2), np.complex128)
+    z_sos = np.zeros_like(p_sos)
+    for si in range(n_sections):
+        # Select the next "worst" pole
+        p1_idx = np.argmin(np.abs(1 - np.abs(p)))
+        p1 = p[p1_idx]
+        p = np.delete(p, p1_idx)
+
+        # Pair that pole with a zero
+
+        if np.isreal(p1) and np.isreal(p).sum() == 0:
+            # Special case to set a first-order section
+            z1_idx = _nearest_real_complex_idx(z, p1, 'real')
+            z1 = z[z1_idx]
+            z = np.delete(z, z1_idx)
+            p2 = z2 = 0
+        else:
+            if not np.isreal(p1) and np.isreal(z).sum() == 1:
+                # Special case to ensure we choose a complex zero to pair
+                # with so later (setting up a first-order section)
+                z1_idx = _nearest_real_complex_idx(z, p1, 'complex')
+                assert not np.isreal(z[z1_idx])
+            else:
+                # Pair the pole with the closest zero (real or complex)
+                z1_idx = np.argmin(np.abs(p1 - z))
+            z1 = z[z1_idx]
+            z = np.delete(z, z1_idx)
+
+            # Now that we have p1 and z1, figure out what p2 and z2 need to be
+            if not np.isreal(p1):
+                if not np.isreal(z1):  # complex pole, complex zero
+                    p2 = p1.conj()
+                    z2 = z1.conj()
+                else:  # complex pole, real zero
+                    p2 = p1.conj()
+                    z2_idx = _nearest_real_complex_idx(z, p1, 'real')
+                    z2 = z[z2_idx]
+                    assert np.isreal(z2)
+                    z = np.delete(z, z2_idx)
+            else:
+                if not np.isreal(z1):  # real pole, complex zero
+                    z2 = z1.conj()
+                    p2_idx = _nearest_real_complex_idx(p, z1, 'real')
+                    p2 = p[p2_idx]
+                    assert np.isreal(p2)
+                else:  # real pole, real zero
+                    # pick the next "worst" pole to use
+                    idx = np.where(np.isreal(p))[0]
+                    assert len(idx) > 0
+                    p2_idx = idx[np.argmin(np.abs(np.abs(p[idx]) - 1))]
+                    p2 = p[p2_idx]
+                    # find a real zero to match the added pole
+                    assert np.isreal(p2)
+                    z2_idx = _nearest_real_complex_idx(z, p2, 'real')
+                    z2 = z[z2_idx]
+                    assert np.isreal(z2)
+                    z = np.delete(z, z2_idx)
+                p = np.delete(p, p2_idx)
+        p_sos[si] = [p1, p2]
+        z_sos[si] = [z1, z2]
+    assert len(p) == len(z) == 0  # we've consumed all poles and zeros
+    del p, z
+
+    # Construct the system, reversing order so the "worst" are last
+    p_sos = np.reshape(p_sos[::-1], (n_sections, 2))
+    z_sos = np.reshape(z_sos[::-1], (n_sections, 2))
+    gains = np.ones(n_sections)
+    gains[0] = k
+    for si in range(n_sections):
+        x = zpk2tf(z_sos[si], p_sos[si], gains[si])
+        sos[si] = np.concatenate(x)
+    return sos
+
+
+def _sosfilt(sos, x, axis=-1, zi=None):
+    """
+    Filter data along one dimension using cascaded second-order sections
+    Filter a data sequence, `x`, using a digital IIR filter defined by
+    `sos`. This is implemented by performing `lfilter` for each
+    second-order section.  See `lfilter` for details.
+
+    Parameters
+    ----------
+    sos : array_like
+        Array of second-order filter coefficients, must have shape
+        ``(n_sections, 6)``. Each row corresponds to a second-order
+        section, with the first three columns providing the numerator
+        coefficients and the last three providing the denominator
+        coefficients.
+    x : array_like
+        An N-dimensional input array.
+    axis : int, optional
+        The axis of the input data array along which to apply the
+        linear filter. The filter is applied to each subarray along
+        this axis.  Default is -1.
+    zi : array_like, optional
+        Initial conditions for the cascaded filter delays.  It is a (at
+        least 2D) vector of shape ``(n_sections, ..., 2, ...)``, where
+        ``..., 2, ...`` denotes the shape of `x`, but with ``x.shape[axis]``
+        replaced by 2.  If `zi` is None or is not given then initial rest
+        (i.e. all zeros) is assumed.
+        Note that these initial conditions are *not* the same as the initial
+        conditions given by `lfiltic` or `lfilter_zi`.
+
+    Returns
+    -------
+    y : ndarray
+        The output of the digital filter.
+    zf : ndarray, optional
+        If `zi` is None, this is not returned, otherwise, `zf` holds the
+        final filter delay values.
+
+    See Also
+    --------
+    zpk2sos, sos2zpk, sosfilt_zi
+
+    Notes
+    -----
+    The filter function is implemented as a series of second-order filters
+    with direct-form II transposed structure. It is designed to minimize
+    numerical precision errors for high-order filters.
+
+    Examples
+    --------
+    Plot a 13th-order filter's impulse response using both `lfilter` and
+    `sosfilt`, showing the instability that results from trying to do a
+    13th-order filter in a single stage (the numerical error pushes some poles
+    outside of the unit circle):
+    >>> import matplotlib.pyplot as plt
+    >>> from scipy import signal
+    >>> b, a = signal.ellip(13, 0.009, 80, 0.05, output='ba')
+    >>> z, p, k = signal.ellip(13, 0.009, 80, 0.05, output='zpk')
+    >>> sos = _zpk2sos(z, p, k)
+    >>> x = np.zeros(700)
+    >>> x[0] = 1.
+    >>> y_tf = signal.lfilter(b, a, x)
+    >>> y_sos = _sosfilt(sos, x)
+    >>> plt.plot(y_tf, 'r', label='TF')  # doctest: +ELLIPSIS
+    [<matplotlib.lines.Line2D object at ...>]
+    >>> plt.plot(y_sos, 'k', label='SOS')  # doctest: +ELLIPSIS
+    [<matplotlib.lines.Line2D object at ...>]
+    >>> plt.legend(loc='best')  # doctest: +ELLIPSIS
+    <matplotlib.legend.Legend object at ...>
+    >>> plt.show()
+    """
+    x = np.asarray(x)
+
+    sos = np.atleast_2d(sos)
+    if sos.ndim != 2:
+        raise ValueError('sos array must be 2D')
+
+    n_sections, m = sos.shape
+    if m != 6:
+        raise ValueError('sos array must be shape (n_sections, 6)')
+
+    use_zi = zi is not None
+    if use_zi:
+        zi = np.asarray(zi)
+        x_zi_shape = list(x.shape)
+        x_zi_shape[axis] = 2
+        x_zi_shape = tuple([n_sections] + x_zi_shape)
+        if zi.shape != x_zi_shape:
+            raise ValueError('Invalid zi shape.  With axis=%r, an input with '
+                             'shape %r, and an sos array with %d sections, zi '
+                             'must have shape %r.' %
+                             (axis, x.shape, n_sections, x_zi_shape))
+        zf = zeros_like(zi)
+
+    for section in range(n_sections):
+        if use_zi:
+            x, zf[section] = lfilter(sos[section, :3], sos[section, 3:],
+                                     x, axis, zi=zi[section])
+        else:
+            x = lfilter(sos[section, :3], sos[section, 3:], x, axis)
+    out = (x, zf) if use_zi else x
+    return out
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod(exclude_empty=True)
diff --git a/obspy/signal/filter.py b/obspy/signal/filter.py
--- a/obspy/signal/filter.py
+++ b/obspy/signal/filter.py
@@ -26,7 +26,14 @@
 import numpy as np
 from scipy.fftpack import hilbert
 from scipy.signal import (cheb2ord, cheby2, convolve, get_window, iirfilter,
-                          lfilter, remez)
+                          remez)
+
+try:
+    from scipy.signal import sosfilt
+    from scipy.signal import zpk2sos
+except ImportError:
+    from ._sosfilt import _sosfilt as sosfilt
+    from ._sosfilt import _zpk2sos as zpk2sos
 
 
 def bandpass(data, freqmin, freqmax, df, corners=4, zerophase=False):
@@ -36,8 +43,7 @@ def bandpass(data, freqmin, freqmax, df, corners=4, zerophase=False):
     Filter data from ``freqmin`` to ``freqmax`` using ``corners``
     corners.
     The filter uses :func:`scipy.signal.iirfilter` (for design)
-    and :func:`scipy.signal.lfilter` (for applying the filter).
-
+    and :func:`scipy.signal.sosfilt` (for applying the filter).
 
     :type data: numpy.ndarray
     :param data: Data to filter.
@@ -62,13 +68,14 @@ def bandpass(data, freqmin, freqmax, df, corners=4, zerophase=False):
     if low > 1:
         msg = "Selected low corner frequency is above Nyquist."
         raise ValueError(msg)
-    [b, a] = iirfilter(corners, [low, high], btype='band',
-                       ftype='butter', output='ba')
+    z, p, k = iirfilter(corners, [low, high], btype='band',
+                        ftype='butter', output='zpk')
+    sos = zpk2sos(z, p, k)
     if zerophase:
-        firstpass = lfilter(b, a, data)
-        return lfilter(b, a, firstpass[::-1])[::-1]
+        firstpass = sosfilt(sos, data)
+        return sosfilt(sos, firstpass[::-1])[::-1]
     else:
-        return lfilter(b, a, data)
+        return sosfilt(sos, data)
 
 
 def bandstop(data, freqmin, freqmax, df, corners=4, zerophase=False):
@@ -78,7 +85,7 @@ def bandstop(data, freqmin, freqmax, df, corners=4, zerophase=False):
     Filter data removing data between frequencies ``freqmin`` and ``freqmax``
     using ``corners`` corners.
     The filter uses :func:`scipy.signal.iirfilter` (for design)
-    and :func:`scipy.signal.lfilter` (for applying the filter).
+    and :func:`scipy.signal.sosfilt` (for applying the filter).
 
     :type data: numpy.ndarray
     :param data: Data to filter.
@@ -103,13 +110,14 @@ def bandstop(data, freqmin, freqmax, df, corners=4, zerophase=False):
     if low > 1:
         msg = "Selected low corner frequency is above Nyquist."
         raise ValueError(msg)
-    [b, a] = iirfilter(corners, [low, high],
-                       btype='bandstop', ftype='butter', output='ba')
+    z, p, k = iirfilter(corners, [low, high],
+                        btype='bandstop', ftype='butter', output='zpk')
+    sos = zpk2sos(z, p, k)
     if zerophase:
-        firstpass = lfilter(b, a, data)
-        return lfilter(b, a, firstpass[::-1])[::-1]
+        firstpass = sosfilt(sos, data)
+        return sosfilt(sos, firstpass[::-1])[::-1]
     else:
-        return lfilter(b, a, data)
+        return sosfilt(sos, data)
 
 
 def lowpass(data, freq, df, corners=4, zerophase=False):
@@ -119,7 +127,7 @@ def lowpass(data, freq, df, corners=4, zerophase=False):
     Filter data removing data over certain frequency ``freq`` using ``corners``
     corners.
     The filter uses :func:`scipy.signal.iirfilter` (for design)
-    and :func:`scipy.signal.lfilter` (for applying the filter).
+    and :func:`scipy.signal.sosfilt` (for applying the filter).
 
     :type data: numpy.ndarray
     :param data: Data to filter.
@@ -139,13 +147,14 @@ def lowpass(data, freq, df, corners=4, zerophase=False):
         msg = "Selected corner frequency is above Nyquist. " + \
               "Setting Nyquist as high corner."
         warnings.warn(msg)
-    [b, a] = iirfilter(corners, f, btype='lowpass', ftype='butter',
-                       output='ba')
+    z, p, k = iirfilter(corners, f, btype='lowpass', ftype='butter',
+                        output='zpk')
+    sos = zpk2sos(z, p, k)
     if zerophase:
-        firstpass = lfilter(b, a, data)
-        return lfilter(b, a, firstpass[::-1])[::-1]
+        firstpass = sosfilt(sos, data)
+        return sosfilt(sos, firstpass[::-1])[::-1]
     else:
-        return lfilter(b, a, data)
+        return sosfilt(sos, data)
 
 
 def highpass(data, freq, df, corners=4, zerophase=False):
@@ -155,7 +164,7 @@ def highpass(data, freq, df, corners=4, zerophase=False):
     Filter data removing data below certain frequency ``freq`` using
     ``corners`` corners.
     The filter uses :func:`scipy.signal.iirfilter` (for design)
-    and :func:`scipy.signal.lfilter` (for applying the filter).
+    and :func:`scipy.signal.sosfilt` (for applying the filter).
 
     :type data: numpy.ndarray
     :param data: Data to filter.
@@ -173,13 +182,14 @@ def highpass(data, freq, df, corners=4, zerophase=False):
     if f > 1:
         msg = "Selected corner frequency is above Nyquist."
         raise ValueError(msg)
-    [b, a] = iirfilter(corners, f, btype='highpass', ftype='butter',
-                       output='ba')
+    z, p, k = iirfilter(corners, f, btype='highpass', ftype='butter',
+                        output='zpk')
+    sos = zpk2sos(z, p, k)
     if zerophase:
-        firstpass = lfilter(b, a, data)
-        return lfilter(b, a, firstpass[::-1])[::-1]
+        firstpass = sosfilt(sos, data)
+        return sosfilt(sos, firstpass[::-1])[::-1]
     else:
-        return lfilter(b, a, data)
+        return sosfilt(sos, data)
 
 
 def envelope(data):
@@ -379,12 +389,13 @@ def lowpass_cheby_2(data, freq, df, maxorder=12, ba=False,
             break
         wp = wp * 0.99
         order, wn = cheb2ord(wp, ws, rp, rs, analog=0)
-    b, a = cheby2(order, rs, wn, btype='low', analog=0, output='ba')
     if ba:
-        return b, a
+        return cheby2(order, rs, wn, btype='low', analog=0, output='ba')
+    z, p, k = cheby2(order, rs, wn, btype='low', analog=0, output='zpk')
+    sos = zpk2sos(z, p, k)
     if freq_passband:
-        return lfilter(b, a, data), wp * nyquist
-    return lfilter(b, a, data)
+        return sosfilt(sos, data), wp * nyquist
+    return sosfilt(sos, data)
 
 
 if __name__ == '__main__':


Test patch: