Skip to content

Commit 59ad782

Browse files
authored
implement interp_like (#2222)
* implement interp_like * flake8 * interp along datetime * Support datetime coordinate * Using reindex for object coordinate. * Update based on the comments
1 parent 66be9c5 commit 59ad782

File tree

8 files changed

+217
-1
lines changed

8 files changed

+217
-1
lines changed

doc/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ Indexing
111111
Dataset.sel
112112
Dataset.squeeze
113113
Dataset.interp
114+
Dataset.interp_like
114115
Dataset.reindex
115116
Dataset.reindex_like
116117
Dataset.set_index
@@ -265,6 +266,7 @@ Indexing
265266
DataArray.sel
266267
DataArray.squeeze
267268
DataArray.interp
269+
DataArray.interp_like
268270
DataArray.reindex
269271
DataArray.reindex_like
270272
DataArray.set_index

doc/indexing.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ Indexing axes with monotonic decreasing labels also works, as long as the
193193
reversed_da.loc[3.1:0.9]
194194
195195
196+
.. note::
197+
198+
If you want to interpolate along coordinates rather than looking up the
199+
nearest neighbors, use :py:meth:`~xarray.Dataset.interp` and
200+
:py:meth:`~xarray.Dataset.interp_like`.
201+
See :ref:`interpolation <interp>` for the details.
202+
203+
196204
Dataset indexing
197205
----------------
198206

doc/interpolation.rst

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ indexing of a :py:class:`~xarray.DataArray`,
3434
da.sel(time=3)
3535
3636
# interpolation
37-
da.interp(time=3.5)
37+
da.interp(time=2.5)
3838
3939
4040
Similar to the indexing, :py:meth:`~xarray.DataArray.interp` also accepts an
@@ -82,6 +82,31 @@ Array-like coordinates are also accepted:
8282
da.interp(time=[1.5, 2.5], space=[0.15, 0.25])
8383
8484
85+
:py:meth:`~xarray.DataArray.interp_like` method is a useful shortcut. This
86+
method interpolates an xarray object onto the coordinates of another xarray
87+
object. For example, if we want to compute the difference between
88+
two :py:class:`~xarray.DataArray` s (``da`` and ``other``) staying on slightly
89+
different coordinates,
90+
91+
.. ipython:: python
92+
93+
other = xr.DataArray(np.sin(0.4 * np.arange(9).reshape(3, 3)),
94+
[('time', [0.9, 1.9, 2.9]),
95+
('space', [0.15, 0.25, 0.35])])
96+
97+
it might be a good idea to first interpolate ``da`` so that it will stay on the
98+
same coordinates of ``other``, and then subtract it.
99+
:py:meth:`~xarray.DataArray.interp_like` can be used for such a case,
100+
101+
.. ipython:: python
102+
103+
# interpolate da along other's coordinates
104+
interpolated = da.interp_like(other)
105+
interpolated
106+
107+
It is now possible to safely compute the difference ``other - interpolated``.
108+
109+
85110
Interpolation methods
86111
---------------------
87112

doc/whats-new.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ Documentation
3636
Enhancements
3737
~~~~~~~~~~~~
3838

39+
- :py:meth:`~xarray.DataArray.interp_like` and
40+
:py:meth:`~xarray.Dataset.interp_like` methods are newly added.
41+
(:issue:`2218`)
42+
By `Keisuke Fujii <https://github.com/fujiisoup>`_.
43+
44+
3945
Bug fixes
4046
~~~~~~~~~
4147

xarray/core/dataarray.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,54 @@ def interp(self, coords=None, method='linear', assume_sorted=False,
951951
**coords_kwargs)
952952
return self._from_temp_dataset(ds)
953953

954+
def interp_like(self, other, method='linear', assume_sorted=False,
955+
kwargs={}):
956+
"""Interpolate this object onto the coordinates of another object,
957+
filling out of range values with NaN.
958+
959+
Parameters
960+
----------
961+
other : Dataset or DataArray
962+
Object with an 'indexes' attribute giving a mapping from dimension
963+
names to an 1d array-like, which provides coordinates upon
964+
which to index the variables in this dataset.
965+
method: string, optional.
966+
{'linear', 'nearest'} for multidimensional array,
967+
{'linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic'}
968+
for 1-dimensional array. 'linear' is used by default.
969+
assume_sorted: boolean, optional
970+
If False, values of coordinates that are interpolated over can be
971+
in any order and they are sorted first. If True, interpolated
972+
coordinates are assumed to be an array of monotonically increasing
973+
values.
974+
kwargs: dictionary, optional
975+
Additional keyword passed to scipy's interpolator.
976+
977+
Returns
978+
-------
979+
interpolated: xr.DataArray
980+
Another dataarray by interpolating this dataarray's data along the
981+
coordinates of the other object.
982+
983+
Note
984+
----
985+
scipy is required.
986+
If the dataarray has object-type coordinates, reindex is used for these
987+
coordinates instead of the interpolation.
988+
989+
See Also
990+
--------
991+
DataArray.interp
992+
DataArray.reindex_like
993+
"""
994+
if self.dtype.kind not in 'uifc':
995+
raise TypeError('interp only works for a numeric type array. '
996+
'Given {}.'.format(self.dtype))
997+
998+
ds = self._to_temp_dataset().interp_like(
999+
other, method=method, kwargs=kwargs, assume_sorted=assume_sorted)
1000+
return self._from_temp_dataset(ds)
1001+
9541002
def rename(self, new_name_or_name_dict=None, **names):
9551003
"""Returns a new DataArray with renamed coordinates or a new name.
9561004

xarray/core/dataset.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,6 +1893,63 @@ def maybe_variable(obj, k):
18931893
.union(coord_vars))
18941894
return obj._replace_vars_and_dims(variables, coord_names=coord_names)
18951895

1896+
def interp_like(self, other, method='linear', assume_sorted=False,
1897+
kwargs={}):
1898+
"""Interpolate this object onto the coordinates of another object,
1899+
filling the out of range values with NaN.
1900+
1901+
Parameters
1902+
----------
1903+
other : Dataset or DataArray
1904+
Object with an 'indexes' attribute giving a mapping from dimension
1905+
names to an 1d array-like, which provides coordinates upon
1906+
which to index the variables in this dataset.
1907+
method: string, optional.
1908+
{'linear', 'nearest'} for multidimensional array,
1909+
{'linear', 'nearest', 'zero', 'slinear', 'quadratic', 'cubic'}
1910+
for 1-dimensional array. 'linear' is used by default.
1911+
assume_sorted: boolean, optional
1912+
If False, values of coordinates that are interpolated over can be
1913+
in any order and they are sorted first. If True, interpolated
1914+
coordinates are assumed to be an array of monotonically increasing
1915+
values.
1916+
kwargs: dictionary, optional
1917+
Additional keyword passed to scipy's interpolator.
1918+
1919+
Returns
1920+
-------
1921+
interpolated: xr.Dataset
1922+
Another dataset by interpolating this dataset's data along the
1923+
coordinates of the other object.
1924+
1925+
Note
1926+
----
1927+
scipy is required.
1928+
If the dataset has object-type coordinates, reindex is used for these
1929+
coordinates instead of the interpolation.
1930+
1931+
See Also
1932+
--------
1933+
Dataset.interp
1934+
Dataset.reindex_like
1935+
"""
1936+
coords = alignment.reindex_like_indexers(self, other)
1937+
1938+
numeric_coords = OrderedDict()
1939+
object_coords = OrderedDict()
1940+
for k, v in coords.items():
1941+
if v.dtype.kind in 'uifcMm':
1942+
numeric_coords[k] = v
1943+
else:
1944+
object_coords[k] = v
1945+
1946+
ds = self
1947+
if object_coords:
1948+
# We do not support interpolation along object coordinate.
1949+
# reindex instead.
1950+
ds = self.reindex(object_coords)
1951+
return ds.interp(numeric_coords, method, assume_sorted, kwargs)
1952+
18961953
def rename(self, name_dict=None, inplace=False, **names):
18971954
"""Returns a new object with renamed variables and dimensions.
18981955

xarray/core/missing.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,26 @@ def _localize(var, indexes_coords):
395395
return var.isel(**indexes), indexes_coords
396396

397397

398+
def _floatize_x(x, new_x):
399+
""" Make x and new_x float.
400+
This is particulary useful for datetime dtype.
401+
x, new_x: tuple of np.ndarray
402+
"""
403+
x = list(x)
404+
new_x = list(new_x)
405+
for i in range(len(x)):
406+
if x[i].dtype.kind in 'Mm':
407+
# Scipy casts coordinates to np.float64, which is not accurate
408+
# enough for datetime64 (uses 64bit integer).
409+
# We assume that the most of the bits are used to represent the
410+
# offset (min(x)) and the variation (x - min(x)) can be
411+
# represented by float.
412+
xmin = np.min(x[i])
413+
x[i] = (x[i] - xmin).astype(np.float64)
414+
new_x[i] = (new_x[i] - xmin).astype(np.float64)
415+
return x, new_x
416+
417+
398418
def interp(var, indexes_coords, method, **kwargs):
399419
""" Make an interpolation of Variable
400420
@@ -523,6 +543,8 @@ def _interp1d(var, x, new_x, func, kwargs):
523543

524544

525545
def _interpnd(var, x, new_x, func, kwargs):
546+
x, new_x = _floatize_x(x, new_x)
547+
526548
if len(x) == 1:
527549
return _interp1d(var, x, new_x, func, kwargs)
528550

xarray/tests/test_interp.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import, division, print_function
22

33
import numpy as np
4+
import pandas as pd
45
import pytest
56

67
import xarray as xr
@@ -430,3 +431,50 @@ def test_interpolate_dimorder(case):
430431
actual = da.interp(x=0.5, z=new_z).dims
431432
expected = da.sel(x=0.5, z=new_z, method='nearest').dims
432433
assert actual == expected
434+
435+
436+
@requires_scipy
437+
def test_interp_like():
438+
ds = create_test_data()
439+
ds.attrs['foo'] = 'var'
440+
ds['var1'].attrs['buz'] = 'var2'
441+
442+
other = xr.DataArray(np.random.randn(3), dims=['dim2'],
443+
coords={'dim2': [0, 1, 2]})
444+
interpolated = ds.interp_like(other)
445+
446+
assert_allclose(interpolated['var1'],
447+
ds['var1'].interp(dim2=other['dim2']))
448+
assert_allclose(interpolated['var1'],
449+
ds['var1'].interp_like(other))
450+
assert interpolated['var3'].equals(ds['var3'])
451+
452+
# attrs should be kept
453+
assert interpolated.attrs['foo'] == 'var'
454+
assert interpolated['var1'].attrs['buz'] == 'var2'
455+
456+
other = xr.DataArray(np.random.randn(3), dims=['dim3'],
457+
coords={'dim3': ['a', 'b', 'c']})
458+
459+
actual = ds.interp_like(other)
460+
expected = ds.reindex_like(other)
461+
assert_allclose(actual, expected)
462+
463+
464+
@requires_scipy
465+
def test_datetime():
466+
da = xr.DataArray(np.random.randn(24), dims='time',
467+
coords={'time': pd.date_range('2000-01-01', periods=24)})
468+
469+
x_new = pd.date_range('2000-01-02', periods=3)
470+
actual = da.interp(time=x_new)
471+
expected = da.isel(time=[1, 2, 3])
472+
assert_allclose(actual, expected)
473+
474+
x_new = np.array([np.datetime64('2000-01-01T12:00'),
475+
np.datetime64('2000-01-02T12:00')])
476+
actual = da.interp(time=x_new)
477+
assert_allclose(actual.isel(time=0).drop('time'),
478+
0.5 * (da.isel(time=0) + da.isel(time=1)))
479+
assert_allclose(actual.isel(time=1).drop('time'),
480+
0.5 * (da.isel(time=1) + da.isel(time=2)))

0 commit comments

Comments
 (0)