diff --git a/doc/source/whatsnew/v0.18.1.txt b/doc/source/whatsnew/v0.18.1.txt index 1a79601bee384..21d8746abdd0f 100644 --- a/doc/source/whatsnew/v0.18.1.txt +++ b/doc/source/whatsnew/v0.18.1.txt @@ -512,6 +512,7 @@ Other API changes - Provide a proper ``__name__`` and ``__qualname__`` attributes for generic functions (:issue:`12021`) - ``pd.concat(ignore_index=True)`` now uses ``RangeIndex`` as default (:issue:`12695`) - ``pd.merge()`` and ``DataFrame.join()`` will show a ``UserWarning`` when merging/joining a single- with a multi-leveled dataframe (:issue:`9455`, :issue:`12219`) +- Compat with SciPy > 0.17 for deprecated ``piecewise_polynomial`` interpolation method (:issue:`12887`) .. _whatsnew_0181.deprecations: diff --git a/pandas/core/generic.py b/pandas/core/generic.py index b1b38d659b55c..b372e0b79ae99 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -3452,8 +3452,8 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None, ---------- method : {'linear', 'time', 'index', 'values', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'barycentric', 'krogh', - 'polynomial', 'spline' 'piecewise_polynomial', 'pchip', - 'akima'} + 'polynomial', 'spline', 'piecewise_polynomial', + 'from_derivatives', 'pchip', 'akima'} * 'linear': ignore the index and treat the values as equally spaced. This is the only method supported on MultiIndexes. @@ -3473,9 +3473,14 @@ def replace(self, to_replace=None, value=None, inplace=False, limit=None, the scipy documentation for more on their behavior `here `__ # noqa `and here `__ # noqa + * 'from_derivatives' refers to BPoly.from_derivatives which + replaces 'piecewise_polynomial' interpolation method in scipy 0.18 .. versionadded:: 0.18.1 Added support for the 'akima' method + Added interpolate method 'from_derivatives' which replaces + 'piecewise_polynomial' in scipy 0.18; backwards-compatible with + scipy < 0.18 axis : {0, 1}, default 0 * 0: fill column-by-column diff --git a/pandas/core/missing.py b/pandas/core/missing.py index 634a04bbc2cdb..1b92c2a2d3b79 100644 --- a/pandas/core/missing.py +++ b/pandas/core/missing.py @@ -3,6 +3,7 @@ """ import numpy as np +from distutils.version import LooseVersion import pandas.core.common as com import pandas.algos as algos @@ -85,13 +86,15 @@ def clean_interp_method(method, **kwargs): order = kwargs.get('order') valid = ['linear', 'time', 'index', 'values', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'barycentric', 'polynomial', 'krogh', - 'piecewise_polynomial', 'pchip', 'akima', 'spline'] + 'piecewise_polynomial', 'pchip', 'akima', 'spline', + 'from_derivatives'] if method in ('spline', 'polynomial') and order is None: raise ValueError("You must specify the order of the spline or " "polynomial.") if method not in valid: raise ValueError("method must be one of {0}." "Got '{1}' instead.".format(valid, method)) + return method @@ -191,7 +194,8 @@ def _interp_limit(invalid, fw_limit, bw_limit): sp_methods = ['nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'barycentric', 'krogh', 'spline', 'polynomial', - 'piecewise_polynomial', 'pchip', 'akima'] + 'from_derivatives', 'piecewise_polynomial', 'pchip', 'akima'] + if method in sp_methods: inds = np.asarray(xvalues) # hack for DatetimeIndex, #1646 @@ -216,6 +220,7 @@ def _interpolate_scipy_wrapper(x, y, new_x, method, fill_value=None, the list in _clean_interp_method """ try: + import scipy from scipy import interpolate # TODO: Why is DatetimeIndex being imported here? from pandas import DatetimeIndex # noqa @@ -228,7 +233,8 @@ def _interpolate_scipy_wrapper(x, y, new_x, method, fill_value=None, alt_methods = { 'barycentric': interpolate.barycentric_interpolate, 'krogh': interpolate.krogh_interpolate, - 'piecewise_polynomial': interpolate.piecewise_polynomial_interpolate, + 'from_derivatives': _from_derivatives, + 'piecewise_polynomial': _from_derivatives, } if getattr(x, 'is_all_dates', False): @@ -248,6 +254,13 @@ def _interpolate_scipy_wrapper(x, y, new_x, method, fill_value=None, except ImportError: raise ImportError("Your version of Scipy does not support " "Akima interpolation.") + elif method == 'piecewise_polynomial': + """ return the method for compat with scipy version """ + if LooseVersion(scipy.__version__) <= LooseVersion('0.17'): + f = 'piecewise_polynomial_interpolate' + else: + f = 'from_derivatives' + alt_methods['piecewise_polynomial'] = getattr(interpolate, f) interp1d_methods = ['nearest', 'zero', 'slinear', 'quadratic', 'cubic', 'polynomial'] @@ -277,6 +290,60 @@ def _interpolate_scipy_wrapper(x, y, new_x, method, fill_value=None, return new_y +def _from_derivatives(xi, yi, x, order=None, der=0, extrapolate=False): + """ + Convenience function for interpolate.BPoly.from_derivatives + + Construct a piecewise polynomial in the Bernstein basis, compatible + with the specified values and derivatives at breakpoints. + + Parameters + ---------- + xi : array_like + sorted 1D array of x-coordinates + yi : array_like or list of array-likes + yi[i][j] is the j-th derivative known at xi[i] + orders : None or int or array_like of ints. Default: None. + Specifies the degree of local polynomials. If not None, some + derivatives are ignored. + der : int or list + How many derivatives to extract; None for all potentially nonzero + derivatives (that is a number equal to the number of points), or a + list of derivatives to extract. This numberincludes the function + value as 0th derivative. + extrapolate : bool, optional + Whether to extrapolate to ouf-of-bounds points based on first and last + intervals, or to return NaNs. Default: True. + + See Also + -------- + scipy.interpolate.BPoly.from_derivatives + + Returns + ------- + y : scalar or array_like + The result, of length R or length M or M by R, + + """ + import scipy + from scipy import interpolate + + if LooseVersion(scipy.__version__) < '0.18.0': + try: + method = interpolate.piecewise_polynomial_interpolate + return method(xi, yi.reshape(-1, 1), x, + orders=order, der=der) + except AttributeError: + pass + + # return the method for compat with scipy version & backwards compat + method = interpolate.BPoly.from_derivatives + m = method(xi, yi.reshape(-1, 1), + orders=order, extrapolate=extrapolate) + + return m(x) + + def _akima_interpolate(xi, yi, x, der=0, axis=0): """ Convenience function for akima interpolation. diff --git a/pandas/tests/series/test_missing.py b/pandas/tests/series/test_missing.py index eb144df0192ec..dec4f878d7d56 100644 --- a/pandas/tests/series/test_missing.py +++ b/pandas/tests/series/test_missing.py @@ -533,6 +533,36 @@ def test_interpolate_akima(self): interp_s = ser.reindex(new_index).interpolate(method='akima') assert_series_equal(interp_s[1:3], expected) + def test_interpolate_piecewise_polynomial(self): + tm._skip_if_no_scipy() + + ser = Series([10, 11, 12, 13]) + + expected = Series([11.00, 11.25, 11.50, 11.75, + 12.00, 12.25, 12.50, 12.75, 13.00], + index=Index([1.0, 1.25, 1.5, 1.75, + 2.0, 2.25, 2.5, 2.75, 3.0])) + # interpolate at new_index + new_index = ser.index.union(Index([1.25, 1.5, 1.75, 2.25, 2.5, 2.75])) + interp_s = ser.reindex(new_index).interpolate( + method='piecewise_polynomial') + assert_series_equal(interp_s[1:3], expected) + + def test_interpolate_from_derivatives(self): + tm._skip_if_no_scipy() + + ser = Series([10, 11, 12, 13]) + + expected = Series([11.00, 11.25, 11.50, 11.75, + 12.00, 12.25, 12.50, 12.75, 13.00], + index=Index([1.0, 1.25, 1.5, 1.75, + 2.0, 2.25, 2.5, 2.75, 3.0])) + # interpolate at new_index + new_index = ser.index.union(Index([1.25, 1.5, 1.75, 2.25, 2.5, 2.75])) + interp_s = ser.reindex(new_index).interpolate( + method='from_derivatives') + assert_series_equal(interp_s[1:3], expected) + def test_interpolate_corners(self): s = Series([np.nan, np.nan]) assert_series_equal(s.interpolate(), s)