From a37ec55b0f38f6c1a6ebd9e0cf7364070ada52c8 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Sun, 23 Sep 2018 11:40:04 -0400 Subject: [PATCH 1/3] Enable use of cftime.datetime coords with differentiate and interp --- doc/interpolation.rst | 3 ++ doc/time-series.rst | 56 +++++++++++++++++--------- doc/whats-new.rst | 7 ++++ xarray/coding/cftimeindex.py | 29 ++++++++++++++ xarray/core/dataset.py | 32 ++++++++------- xarray/core/missing.py | 5 ++- xarray/core/utils.py | 15 ++++--- xarray/tests/test_cftimeindex.py | 23 ++++++++++- xarray/tests/test_dataset.py | 38 ++++++++++++++++-- xarray/tests/test_interp.py | 67 +++++++++++++++++++++++++++++++- xarray/tests/test_utils.py | 43 +++++++++++++++++++- 11 files changed, 271 insertions(+), 47 deletions(-) diff --git a/doc/interpolation.rst b/doc/interpolation.rst index e5230e95dae..10e46331d0a 100644 --- a/doc/interpolation.rst +++ b/doc/interpolation.rst @@ -63,6 +63,9 @@ by specifing the time periods required. da_dt64.interp(time=pd.date_range('1/1/2000', '1/3/2000', periods=3)) +Interpolation of data indexed by a :py:class:`~xarray.CFTimeIndex` is also +allowed. See :ref:`CFTimeIndex` for examples. + .. note:: Currently, our interpolation only works for regular grids. diff --git a/doc/time-series.rst b/doc/time-series.rst index d99c3218d18..c1a686b409f 100644 --- a/doc/time-series.rst +++ b/doc/time-series.rst @@ -70,9 +70,9 @@ You can manual decode arrays in this form by passing a dataset to One unfortunate limitation of using ``datetime64[ns]`` is that it limits the native representation of dates to those that fall between the years 1678 and 2262. When a netCDF file contains dates outside of these bounds, dates will be -returned as arrays of ``cftime.datetime`` objects and a ``CFTimeIndex`` -can be used for indexing. The ``CFTimeIndex`` enables only a subset of -the indexing functionality of a ``pandas.DatetimeIndex`` and is only enabled +returned as arrays of :py:class:`cftime.datetime` objects and a :py:class:`~xarray.CFTimeIndex` +can be used for indexing. The :py:class:`~xarray.CFTimeIndex` enables only a subset of +the indexing functionality of a :py:class:`pandas.DatetimeIndex` and is only enabled when using the standalone version of ``cftime`` (not the version packaged with earlier versions ``netCDF4``). See :ref:`CFTimeIndex` for more information. @@ -219,12 +219,12 @@ Non-standard calendars and dates outside the Timestamp-valid range ------------------------------------------------------------------ Through the standalone ``cftime`` library and a custom subclass of -``pandas.Index``, xarray supports a subset of the indexing functionality enabled -through the standard ``pandas.DatetimeIndex`` for dates from non-standard -calendars or dates using a standard calendar, but outside the -`Timestamp-valid range`_ (approximately between years 1678 and 2262). This -behavior has not yet been turned on by default; to take advantage of this -functionality, you must have the ``enable_cftimeindex`` option set to +:py:class:`pandas.Index`, xarray supports a subset of the indexing +functionality enabled through the standard :py:class:`pandas.DatetimeIndex` for +dates from non-standard calendars or dates using a standard calendar, but +outside the `Timestamp-valid range`_ (approximately between years 1678 and +2262). This behavior has not yet been turned on by default; to take advantage +of this functionality, you must have the ``enable_cftimeindex`` option set to ``True`` within your context (see :py:func:`~xarray.set_options` for more information). It is expected that this will become the default behavior in xarray version 0.11. @@ -232,7 +232,7 @@ xarray version 0.11. For instance, you can create a DataArray indexed by a time coordinate with a no-leap calendar within a context manager setting the ``enable_cftimeindex`` option, and the time index will be cast to a -``CFTimeIndex``: +:py:class:`~xarray.CFTimeIndex`: .. ipython:: python @@ -247,28 +247,28 @@ coordinate with a no-leap calendar within a context manager setting the .. note:: - With the ``enable_cftimeindex`` option activated, a ``CFTimeIndex`` + With the ``enable_cftimeindex`` option activated, a :py:class:`~xarray.CFTimeIndex` will be used for time indexing if any of the following are true: - The dates are from a non-standard calendar - Any dates are outside the Timestamp-valid range - Otherwise a ``pandas.DatetimeIndex`` will be used. In addition, if any + Otherwise a :py:class:`pandas.DatetimeIndex` will be used. In addition, if any variable (not just an index variable) is encoded using a non-standard - calendar, its times will be decoded into ``cftime.datetime`` objects, + calendar, its times will be decoded into :py:class:`cftime.datetime` objects, regardless of whether or not they can be represented using ``np.datetime64[ns]`` objects. -xarray also includes a :py:func:`cftime_range` function, which enables creating a -``CFTimeIndex`` with regularly-spaced dates. For instance, we can create the -same dates and DataArray we created above using: +xarray also includes a :py:func:`~xarray.cftime_range` function, which enables +creating a :py:class:`~xarray.CFTimeIndex` with regularly-spaced dates. For instance, we can +create the same dates and DataArray we created above using: .. ipython:: python dates = xr.cftime_range(start='0001', periods=24, freq='MS', calendar='noleap') da = xr.DataArray(np.arange(24), coords=[dates], dims=['time'], name='foo') -For data indexed by a ``CFTimeIndex`` xarray currently supports: +For data indexed by a :py:class:`~xarray.CFTimeIndex` xarray currently supports: - `Partial datetime string indexing`_ using strictly `ISO 8601-format`_ partial datetime strings: @@ -294,7 +294,25 @@ For data indexed by a ``CFTimeIndex`` xarray currently supports: .. ipython:: python da.groupby('time.month').sum() - + +- Interpolation using :py:class:`cftime.datetime` objects: + +.. ipython:: python + + da.interp(time=[DatetimeNoLeap(1, 1, 15), DatetimeNoLeap(1, 2, 15)]) + +- Interpolation using datetime strings: + +.. ipython:: python + + da.interp(time=['0001-01-15', '0001-02-15']) + +- Differentiation: + +.. ipython:: python + + da.differentiate('time') + - And serialization: .. ipython:: python @@ -305,7 +323,7 @@ For data indexed by a ``CFTimeIndex`` xarray currently supports: .. note:: Currently resampling along the time dimension for data indexed by a - ``CFTimeIndex`` is not supported. + :py:class:`~xarray.CFTimeIndex` is not supported. .. _Timestamp-valid range: https://pandas.pydata.org/pandas-docs/stable/timeseries.html#timestamp-limitations .. _ISO 8601-format: https://en.wikipedia.org/wiki/ISO_8601 diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 67d0d548ec5..e5f83adf01f 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -36,6 +36,13 @@ Enhancements - Added support for Python 3.7. (:issue:`2271`). By `Joe Hamman `_. +- Added support for using ``cftime.datetime`` coordinates with + :py:meth:`~xarray.DataArray.differentiate`, + :py:meth:`~xarray.Dataset.differentiate`, + :py:meth:`~xarray.DataArray.interp`, and + :py:meth:`~xarray.Dataset.interp`. + By `Spencer Clark `_ + Bug fixes ~~~~~~~~~ diff --git a/xarray/coding/cftimeindex.py b/xarray/coding/cftimeindex.py index ea2bcbc5858..e236dca3693 100644 --- a/xarray/coding/cftimeindex.py +++ b/xarray/coding/cftimeindex.py @@ -314,3 +314,32 @@ def __contains__(self, key): def contains(self, key): """Needed for .loc based partial-string indexing""" return self.__contains__(key) + + +def _parse_iso8601_without_reso(date_type, datetime_str): + date, _ = _parse_iso8601_with_reso(date_type, datetime_str) + return date + + +def _parse_array_of_cftime_strings(strings, date_type): + """Create a numpy array from an array of strings. + + For use in generating dates from strings for use with interp. Assumes the + array is either 0-dimensional or 1-dimensional. + + Parameters + ---------- + strings : array of strings + Strings to convert to dates + date_type : cftime.datetime type + Calendar type to use for dates + + Returns + ------- + np.array + """ + if strings.ndim == 0: + return np.array(_parse_iso8601_without_reso(date_type, strings.item())) + else: + return np.array([_parse_iso8601_without_reso(date_type, s) + for s in strings]) diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 9cf304858a6..7b3bfbd4c80 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -34,6 +34,8 @@ ensure_us_time_resolution, hashable, maybe_wrap_array, to_numeric) from .variable import IndexVariable, Variable, as_variable, broadcast_variables +from ..coding.cftimeindex import _parse_array_of_cftime_strings + # list of attributes of pd.DatetimeIndex that are ndarrays of time info _DATETIMEINDEX_COMPONENTS = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond', 'nanosecond', 'date', @@ -1412,8 +1414,8 @@ def _validate_indexers(self, indexers): """ Here we make sure + indexer has a valid keys + indexer is in a valid data type - * string indexers are cast to datetime64 - if associated index is DatetimeIndex + + string indexers are cast to the appropriate date type if the + associated index is a DatetimeIndex or CFTimeIndex """ from .dataarray import DataArray @@ -1435,10 +1437,12 @@ def _validate_indexers(self, indexers): else: v = np.asarray(v) - if ((v.dtype.kind == 'U' or v.dtype.kind == 'S') - and isinstance(self.coords[k].to_index(), - pd.DatetimeIndex)): - v = v.astype('datetime64[ns]') + if v.dtype.kind == 'U' or v.dtype.kind == 'S': + index = self.indexes[k] + if isinstance(index, pd.DatetimeIndex): + v = v.astype('datetime64[ns]') + elif isinstance(index, xr.CFTimeIndex): + v = _parse_array_of_cftime_strings(v, index.date_type) if v.ndim == 0: v = as_variable(v) @@ -3807,19 +3811,21 @@ def differentiate(self, coord, edge_order=1, datetime_unit=None): ' dimensional'.format(coord, coord_var.ndim)) dim = coord_var.dims[0] - coord_data = coord_var.data - if coord_data.dtype.kind in 'mM': - if datetime_unit is None: - datetime_unit, _ = np.datetime_data(coord_data.dtype) - coord_data = to_numeric(coord_data, datetime_unit=datetime_unit) + if _contains_datetime_like_objects(coord_var): + if coord_var.dtype.kind in 'mM' and datetime_unit is None: + datetime_unit, _ = np.datetime_data(coord_var.dtype) + elif datetime_unit is None: + datetime_unit = 's' # Default to seconds for cftime objects + coord_var = to_numeric(coord_var, datetime_unit=datetime_unit) variables = OrderedDict() for k, v in self.variables.items(): if (k in self.data_vars and dim in v.dims and k not in self.coords): - v = to_numeric(v, datetime_unit=datetime_unit) + if _contains_datetime_like_objects(v): + v = to_numeric(v, datetime_unit=datetime_unit) grad = duck_array_ops.gradient( - v.data, coord_data, edge_order=edge_order, + v.data, coord_var, edge_order=edge_order, axis=v.get_axis_num(dim)) variables[k] = Variable(v.dims, grad) else: diff --git a/xarray/core/missing.py b/xarray/core/missing.py index afb34d99115..e1dea43fea5 100644 --- a/xarray/core/missing.py +++ b/xarray/core/missing.py @@ -9,6 +9,7 @@ import pandas as pd from . import rolling +from .common import _contains_datetime_like_objects from .computation import apply_ufunc from .pycompat import iteritems from .utils import is_scalar, OrderedSet, to_numeric @@ -407,13 +408,13 @@ def _floatize_x(x, new_x): x = list(x) new_x = list(new_x) for i in range(len(x)): - if x[i].dtype.kind in 'Mm': + if _contains_datetime_like_objects(x[i]): # Scipy casts coordinates to np.float64, which is not accurate # enough for datetime64 (uses 64bit integer). # We assume that the most of the bits are used to represent the # offset (min(x)) and the variation (x - min(x)) can be # represented by float. - xmin = np.min(x[i]) + xmin = x[i].min() x[i] = to_numeric(x[i], offset=xmin, dtype=np.float64) new_x[i] = to_numeric(new_x[i], offset=xmin, dtype=np.float64) return x, new_x diff --git a/xarray/core/utils.py b/xarray/core/utils.py index 9d129d5c4f4..150119c7017 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -594,19 +594,24 @@ def __len__(self): def to_numeric(array, offset=None, datetime_unit=None, dtype=float): - """ - Make datetime array float + """Convert an array containing datetime-like data to an array of floats. + Parameters + ---------- + da : array + Input data offset: Scalar with the same type of array or None If None, subtract minimum values to reduce round off error datetime_unit: None or any of {'Y', 'M', 'W', 'D', 'h', 'm', 's', 'ms', 'us', 'ns', 'ps', 'fs', 'as'} dtype: target dtype + + Returns + ------- + array """ - if array.dtype.kind not in ['m', 'M']: - return array.astype(dtype) if offset is None: - offset = np.min(array) + offset = array.min() array = array - offset if datetime_unit: diff --git a/xarray/tests/test_cftimeindex.py b/xarray/tests/test_cftimeindex.py index f72c6904f0e..62a29a15247 100644 --- a/xarray/tests/test_cftimeindex.py +++ b/xarray/tests/test_cftimeindex.py @@ -9,10 +9,11 @@ from datetime import timedelta from xarray.coding.cftimeindex import ( parse_iso8601, CFTimeIndex, assert_all_valid_date_type, - _parsed_string_to_bounds, _parse_iso8601_with_reso) + _parsed_string_to_bounds, _parse_iso8601_with_reso, + _parse_array_of_cftime_strings) from xarray.tests import assert_array_equal, assert_identical -from . import has_cftime, has_cftime_or_netCDF4 +from . import has_cftime, has_cftime_or_netCDF4, requires_cftime from .test_coding_times import _all_cftime_date_types @@ -616,3 +617,21 @@ def test_concat_cftimeindex(date_type, enable_cftimeindex): def test_empty_cftimeindex(): index = CFTimeIndex([]) assert index.date_type is None + + +@requires_cftime +def test_parse_array_of_cftime_strings(): + from cftime import DatetimeNoLeap + + strings = np.array(['2000-01-01', '2000-01-02']) + expected = np.array([DatetimeNoLeap(2000, 1, 1), + DatetimeNoLeap(2000, 1, 2)]) + + result = _parse_array_of_cftime_strings(strings, DatetimeNoLeap) + np.testing.assert_array_equal(result, expected) + + # Test scalar array case + strings = np.array('2000-01-01') + expected = np.array(DatetimeNoLeap(2000, 1, 1)) + result = _parse_array_of_cftime_strings(strings, DatetimeNoLeap) + np.testing.assert_array_equal(result, expected) diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index f8fb9b98ac3..b7953091282 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -22,8 +22,9 @@ from . import ( InaccessibleArray, TestCase, UnexpectedDataAccess, assert_allclose, - assert_array_equal, assert_equal, assert_identical, has_dask, raises_regex, - requires_bottleneck, requires_dask, requires_scipy, source_ndarray) + assert_array_equal, assert_equal, assert_identical, has_cftime, + has_dask, raises_regex, requires_bottleneck, requires_dask, requires_scipy, + source_ndarray) try: import cPickle as pickle @@ -4517,7 +4518,7 @@ def test_raise_no_warning_for_nan_in_binary_ops(): @pytest.mark.parametrize('dask', [True, False]) @pytest.mark.parametrize('edge_order', [1, 2]) -def test_gradient(dask, edge_order): +def test_differentiate(dask, edge_order): rs = np.random.RandomState(42) coord = [0.2, 0.35, 0.4, 0.6, 0.7, 0.75, 0.76, 0.8] @@ -4555,7 +4556,7 @@ def test_gradient(dask, edge_order): @pytest.mark.parametrize('dask', [True, False]) -def test_gradient_datetime(dask): +def test_differentiate_datetime(dask): rs = np.random.RandomState(42) coord = np.array( ['2004-07-13', '2006-01-13', '2010-08-13', '2010-09-13', @@ -4588,3 +4589,32 @@ def test_gradient_datetime(dask): coords={'x': coord}) actual = da.differentiate('x', edge_order=1) assert np.allclose(actual, 1.0) + + +@pytest.mark.skipif(not has_cftime, reason='Test requires cftime.') +@pytest.mark.parametrize('dask', [True, False]) +def test_differentiate_cftime(dask): + rs = np.random.RandomState(42) + coord = xr.cftime_range('2000', periods=8, freq='2M') + + da = xr.DataArray( + rs.randn(8, 6), + coords={'time': coord, 'z': 3, 't2d': (('time', 'y'), rs.randn(8, 6))}, + dims=['time', 'y']) + + if dask and has_dask: + da = da.chunk({'time': 4}) + + actual = da.differentiate('time', edge_order=1, datetime_unit='D') + expected_data = npcompat.gradient( + da, utils.to_numeric(da['time'], datetime_unit='D'), + axis=0, edge_order=1) + expected = xr.DataArray(expected_data, coords=da.coords, dims=da.dims) + assert_equal(expected, actual) + + actual2 = da.differentiate('time', edge_order=1, datetime_unit='h') + assert_allclose(actual, actual2 * 24) + + # Test the differentiation of datetimes themselves + actual = da['time'].differentiate('time', edge_order=1, datetime_unit='D') + assert_allclose(actual, xr.ones_like(da['time']).astype(float)) diff --git a/xarray/tests/test_interp.py b/xarray/tests/test_interp.py index 4a8f4e6eedf..be1f5253781 100644 --- a/xarray/tests/test_interp.py +++ b/xarray/tests/test_interp.py @@ -5,10 +5,13 @@ import pytest import xarray as xr -from xarray.tests import assert_allclose, assert_equal, requires_scipy +from xarray.tests import (assert_allclose, assert_equal, requires_cftime, + requires_scipy) from . import has_dask, has_scipy from .test_dataset import create_test_data +from ..coding.cftimeindex import _parse_array_of_cftime_strings + try: import scipy except ImportError: @@ -490,3 +493,65 @@ def test_datetime_single_string(): expected = xr.DataArray(0.5) assert_allclose(actual.drop('time'), expected) + + +@requires_cftime +@requires_scipy +def test_cftime(): + times = xr.cftime_range('2000', periods=24, freq='D') + da = xr.DataArray(np.arange(24), coords=[times], dims='time') + + times_new = xr.cftime_range('2000-01-01T12:00:00', periods=3, freq='D') + actual = da.interp(time=times_new) + expected = xr.DataArray([0.5, 1.5, 2.5], coords=[times_new], dims=['time']) + + assert_allclose(actual, expected) + + +@requires_cftime +@requires_scipy +def test_cftime_type_error(): + times = xr.cftime_range('2000', periods=24, freq='D') + da = xr.DataArray(np.arange(24), coords=[times], dims='time') + + times_new = xr.cftime_range('2000-01-01T12:00:00', periods=3, freq='D', + calendar='noleap') + with pytest.raises(TypeError): + da.interp(time=times_new) + + +@requires_cftime +@requires_scipy +def test_cftime_list_of_strings(): + from cftime import DatetimeProlepticGregorian + + times = xr.cftime_range('2000', periods=24, freq='D') + da = xr.DataArray(np.arange(24), coords=[times], dims='time') + + times_new = ['2000-01-01T12:00', '2000-01-02T12:00', '2000-01-03T12:00'] + actual = da.interp(time=times_new) + + times_new_array = _parse_array_of_cftime_strings( + np.array(times_new), DatetimeProlepticGregorian) + expected = xr.DataArray([0.5, 1.5, 2.5], coords=[times_new_array], + dims=['time']) + + assert_allclose(actual, expected) + + +@requires_cftime +@requires_scipy +def test_cftime_single_string(): + from cftime import DatetimeProlepticGregorian + + times = xr.cftime_range('2000', periods=24, freq='D') + da = xr.DataArray(np.arange(24), coords=[times], dims='time') + + times_new = '2000-01-01T12:00' + actual = da.interp(time=times_new) + + times_new_array = _parse_array_of_cftime_strings( + np.array(times_new), DatetimeProlepticGregorian) + expected = xr.DataArray(0.5, coords={'time': times_new_array}) + + assert_allclose(actual, expected) diff --git a/xarray/tests/test_utils.py b/xarray/tests/test_utils.py index ed8045b78e4..231f60bd9c7 100644 --- a/xarray/tests/test_utils.py +++ b/xarray/tests/test_utils.py @@ -5,16 +5,18 @@ import numpy as np import pandas as pd import pytest +import xarray as xr from xarray.coding.cftimeindex import CFTimeIndex from xarray.core import duck_array_ops, utils from xarray.core.options import set_options from xarray.core.pycompat import OrderedDict from xarray.core.utils import either_dict_or_kwargs +from xarray.testing import assert_identical from . import ( TestCase, assert_array_equal, has_cftime, has_cftime_or_netCDF4, - requires_dask) + requires_dask, requires_cftime) from .test_coding_times import _all_cftime_date_types @@ -263,3 +265,42 @@ def test_either_dict_or_kwargs(): with pytest.raises(ValueError, match=r'foo'): result = either_dict_or_kwargs(dict(a=1), dict(a=1), 'foo') + + +def test_to_numeric_datetime64(): + times = pd.date_range('2000', periods=5, freq='7D') + da = xr.DataArray(times, coords=[times], dims=['time']) + result = utils.to_numeric(da, datetime_unit='h') + expected = 24 * xr.DataArray(np.arange(0, 35, 7), coords=da.coords) + assert_identical(result, expected) + + offset = da.isel(time=1) + result = utils.to_numeric(da, offset=offset, datetime_unit='h') + expected = 24 * xr.DataArray(np.arange(-7, 28, 7), coords=da.coords) + assert_identical(result, expected) + + dtype = np.float32 + result = utils.to_numeric(da, datetime_unit='h', dtype=dtype) + expected = 24 * xr.DataArray( + np.arange(0, 35, 7), coords=da.coords).astype(dtype) + assert_identical(result, expected) + + +@requires_cftime +def test_to_numeric_cftime(): + times = xr.cftime_range('2000', periods=5, freq='7D') + da = xr.DataArray(times, coords=[times], dims=['time']) + result = utils.to_numeric(da, datetime_unit='h') + expected = 24 * xr.DataArray(np.arange(0, 35, 7), coords=da.coords) + assert_identical(result, expected) + + offset = da.isel(time=1) + result = utils.to_numeric(da, offset=offset, datetime_unit='h') + expected = 24 * xr.DataArray(np.arange(-7, 28, 7), coords=da.coords) + assert_identical(result, expected) + + dtype = np.float32 + result = utils.to_numeric(da, datetime_unit='h', dtype=dtype) + expected = 24 * xr.DataArray( + np.arange(0, 35, 7), coords=da.coords).astype(dtype) + assert_identical(result, expected) From 5377b164fff43b939beb8d9575a9545b72a85989 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Wed, 26 Sep 2018 11:39:29 -0400 Subject: [PATCH 2/3] Raise TypeError for non-datetime x_new --- xarray/core/dataset.py | 17 ++++++++++++++++- xarray/tests/test_interp.py | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 7b3bfbd4c80..673818b8112 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -1984,11 +1984,26 @@ def maybe_variable(obj, k): except KeyError: return as_variable((k, range(obj.dims[k]))) + def _validate_interp_indexer(x, new_x): + # In the case of datetimes, the restrictions placed on indexers + # used with interp are stronger than those which are placed on + # isel, so we need an additional check after _validate_indexers. + if (_contains_datetime_like_objects(x) and + not _contains_datetime_like_objects(new_x)): + raise TypeError('When interpolating over a datetime-like ' + 'coordinate, the coordinates to ' + 'interpolate to must be either datetime ' + 'strings or datetimes. ' + 'Instead got\n{}'.format(new_x)) + else: + return (x, new_x) + variables = OrderedDict() for name, var in iteritems(obj._variables): if name not in indexers: if var.dtype.kind in 'uifc': - var_indexers = {k: (maybe_variable(obj, k), v) for k, v + var_indexers = {k: _validate_interp_indexer( + maybe_variable(obj, k), v) for k, v in indexers.items() if k in var.dims} variables[name] = missing.interp( var, var_indexers, method, **kwargs) diff --git a/xarray/tests/test_interp.py b/xarray/tests/test_interp.py index be1f5253781..0778a1ff128 100644 --- a/xarray/tests/test_interp.py +++ b/xarray/tests/test_interp.py @@ -555,3 +555,21 @@ def test_cftime_single_string(): expected = xr.DataArray(0.5, coords={'time': times_new_array}) assert_allclose(actual, expected) + + +@requires_scipy +def test_datetime_to_non_datetime_error(): + da = xr.DataArray(np.arange(24), dims='time', + coords={'time': pd.date_range('2000-01-01', periods=24)}) + with pytest.raises(TypeError): + da.interp(time=0.5) + + +@requires_cftime +@requires_scipy +def test_cftime_to_non_cftime_error(): + times = xr.cftime_range('2000', periods=24, freq='D') + da = xr.DataArray(np.arange(24), coords=[times], dims='time') + + with pytest.raises(TypeError): + da.interp(time=0.5) From fd8f92f0080cfd954bb9d03faabde3790c323d8c Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Thu, 27 Sep 2018 09:27:23 -0400 Subject: [PATCH 3/3] Rename to_numeric to datetime_to_numeric --- xarray/core/dataset.py | 6 +++--- xarray/core/missing.py | 7 ++++--- xarray/core/utils.py | 2 +- xarray/tests/test_dataset.py | 4 ++-- xarray/tests/test_utils.py | 16 ++++++++-------- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 673818b8112..7202332e0be 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -31,7 +31,7 @@ OrderedDict, basestring, dask_array_type, integer_types, iteritems, range) from .utils import ( Frozen, SortedKeysDict, either_dict_or_kwargs, decode_numpy_dict_values, - ensure_us_time_resolution, hashable, maybe_wrap_array, to_numeric) + ensure_us_time_resolution, hashable, maybe_wrap_array, datetime_to_numeric) from .variable import IndexVariable, Variable, as_variable, broadcast_variables from ..coding.cftimeindex import _parse_array_of_cftime_strings @@ -3831,14 +3831,14 @@ def differentiate(self, coord, edge_order=1, datetime_unit=None): datetime_unit, _ = np.datetime_data(coord_var.dtype) elif datetime_unit is None: datetime_unit = 's' # Default to seconds for cftime objects - coord_var = to_numeric(coord_var, datetime_unit=datetime_unit) + coord_var = datetime_to_numeric(coord_var, datetime_unit=datetime_unit) variables = OrderedDict() for k, v in self.variables.items(): if (k in self.data_vars and dim in v.dims and k not in self.coords): if _contains_datetime_like_objects(v): - v = to_numeric(v, datetime_unit=datetime_unit) + v = datetime_to_numeric(v, datetime_unit=datetime_unit) grad = duck_array_ops.gradient( v.data, coord_var, edge_order=edge_order, axis=v.get_axis_num(dim)) diff --git a/xarray/core/missing.py b/xarray/core/missing.py index e1dea43fea5..0b560c277ae 100644 --- a/xarray/core/missing.py +++ b/xarray/core/missing.py @@ -12,7 +12,7 @@ from .common import _contains_datetime_like_objects from .computation import apply_ufunc from .pycompat import iteritems -from .utils import is_scalar, OrderedSet, to_numeric +from .utils import is_scalar, OrderedSet, datetime_to_numeric from .variable import Variable, broadcast_variables from .duck_array_ops import dask_array_type @@ -415,8 +415,9 @@ def _floatize_x(x, new_x): # offset (min(x)) and the variation (x - min(x)) can be # represented by float. xmin = x[i].min() - x[i] = to_numeric(x[i], offset=xmin, dtype=np.float64) - new_x[i] = to_numeric(new_x[i], offset=xmin, dtype=np.float64) + x[i] = datetime_to_numeric(x[i], offset=xmin, dtype=np.float64) + new_x[i] = datetime_to_numeric( + new_x[i], offset=xmin, dtype=np.float64) return x, new_x diff --git a/xarray/core/utils.py b/xarray/core/utils.py index 150119c7017..c39a07e1b5a 100644 --- a/xarray/core/utils.py +++ b/xarray/core/utils.py @@ -593,7 +593,7 @@ def __len__(self): return len(self._data) - num_hidden -def to_numeric(array, offset=None, datetime_unit=None, dtype=float): +def datetime_to_numeric(array, offset=None, datetime_unit=None, dtype=float): """Convert an array containing datetime-like data to an array of floats. Parameters diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py index b7953091282..2d1dcfbf5ce 100644 --- a/xarray/tests/test_dataset.py +++ b/xarray/tests/test_dataset.py @@ -4573,7 +4573,7 @@ def test_differentiate_datetime(dask): actual = da.differentiate('x', edge_order=1, datetime_unit='D') expected_x = xr.DataArray( npcompat.gradient( - da, utils.to_numeric(da['x'], datetime_unit='D'), + da, utils.datetime_to_numeric(da['x'], datetime_unit='D'), axis=0, edge_order=1), dims=da.dims, coords=da.coords) assert_equal(expected_x, actual) @@ -4607,7 +4607,7 @@ def test_differentiate_cftime(dask): actual = da.differentiate('time', edge_order=1, datetime_unit='D') expected_data = npcompat.gradient( - da, utils.to_numeric(da['time'], datetime_unit='D'), + da, utils.datetime_to_numeric(da['time'], datetime_unit='D'), axis=0, edge_order=1) expected = xr.DataArray(expected_data, coords=da.coords, dims=da.dims) assert_equal(expected, actual) diff --git a/xarray/tests/test_utils.py b/xarray/tests/test_utils.py index 231f60bd9c7..0c0e0f3f744 100644 --- a/xarray/tests/test_utils.py +++ b/xarray/tests/test_utils.py @@ -267,40 +267,40 @@ def test_either_dict_or_kwargs(): result = either_dict_or_kwargs(dict(a=1), dict(a=1), 'foo') -def test_to_numeric_datetime64(): +def test_datetime_to_numeric_datetime64(): times = pd.date_range('2000', periods=5, freq='7D') da = xr.DataArray(times, coords=[times], dims=['time']) - result = utils.to_numeric(da, datetime_unit='h') + result = utils.datetime_to_numeric(da, datetime_unit='h') expected = 24 * xr.DataArray(np.arange(0, 35, 7), coords=da.coords) assert_identical(result, expected) offset = da.isel(time=1) - result = utils.to_numeric(da, offset=offset, datetime_unit='h') + result = utils.datetime_to_numeric(da, offset=offset, datetime_unit='h') expected = 24 * xr.DataArray(np.arange(-7, 28, 7), coords=da.coords) assert_identical(result, expected) dtype = np.float32 - result = utils.to_numeric(da, datetime_unit='h', dtype=dtype) + result = utils.datetime_to_numeric(da, datetime_unit='h', dtype=dtype) expected = 24 * xr.DataArray( np.arange(0, 35, 7), coords=da.coords).astype(dtype) assert_identical(result, expected) @requires_cftime -def test_to_numeric_cftime(): +def test_datetime_to_numeric_cftime(): times = xr.cftime_range('2000', periods=5, freq='7D') da = xr.DataArray(times, coords=[times], dims=['time']) - result = utils.to_numeric(da, datetime_unit='h') + result = utils.datetime_to_numeric(da, datetime_unit='h') expected = 24 * xr.DataArray(np.arange(0, 35, 7), coords=da.coords) assert_identical(result, expected) offset = da.isel(time=1) - result = utils.to_numeric(da, offset=offset, datetime_unit='h') + result = utils.datetime_to_numeric(da, offset=offset, datetime_unit='h') expected = 24 * xr.DataArray(np.arange(-7, 28, 7), coords=da.coords) assert_identical(result, expected) dtype = np.float32 - result = utils.to_numeric(da, datetime_unit='h', dtype=dtype) + result = utils.datetime_to_numeric(da, datetime_unit='h', dtype=dtype) expected = 24 * xr.DataArray( np.arange(0, 35, 7), coords=da.coords).astype(dtype) assert_identical(result, expected)