Repository: pydata/xarray

Issue:
.sel method gives error with float32 values

`.sel` method gives error when it is used to select `float32` values however it works fine for  `float64`
Example:
```python
import xarray as xr
import numpy as np

a = np.asarray([0.    , 0.111, 0.222, 0.333], dtype='float32')
ds = xr.Dataset(coords={'a': (['a'],a )})
ds.a.sel({'a': 0.111})
```
Error Traceback:
```python
KeyError                                  Traceback (most recent call last)
~/anaconda3/envs/check3/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2656             try:
-> 2657                 return self._engine.get_loc(key)
   2658             except KeyError:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Float64HashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Float64HashTable.get_item()

KeyError: 0.111

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-7-b4190c1d9580> in <module>
      4 a = np.asarray([0.    , 0.111, 0.222, 0.333], dtype='float32')
      5 ds = xr.Dataset(coords={'a': (['a'],a )})
----> 6 ds.a.sel({'a': 0.111})
      7 ds.a.sel({'a': 0.111}, method= 'nearest')

~/anaconda3/envs/check3/lib/python3.7/site-packages/xarray/core/dataarray.py in sel(self, indexers, method, tolerance, drop, **indexers_kwargs)
    853         ds = self._to_temp_dataset().sel(
    854             indexers=indexers, drop=drop, method=method, tolerance=tolerance,
--> 855             **indexers_kwargs)
    856         return self._from_temp_dataset(ds)
    857 

~/anaconda3/envs/check3/lib/python3.7/site-packages/xarray/core/dataset.py in sel(self, indexers, method, tolerance, drop, **indexers_kwargs)
   1729         indexers = either_dict_or_kwargs(indexers, indexers_kwargs, 'sel')
   1730         pos_indexers, new_indexes = remap_label_indexers(
-> 1731             self, indexers=indexers, method=method, tolerance=tolerance)
   1732         result = self.isel(indexers=pos_indexers, drop=drop)
   1733         return result._overwrite_indexes(new_indexes)

~/anaconda3/envs/check3/lib/python3.7/site-packages/xarray/core/coordinates.py in remap_label_indexers(obj, indexers, method, tolerance, **indexers_kwargs)
    315 
    316     pos_indexers, new_indexes = indexing.remap_label_indexers(
--> 317         obj, v_indexers, method=method, tolerance=tolerance
    318     )
    319     # attach indexer's coordinate to pos_indexers

~/anaconda3/envs/check3/lib/python3.7/site-packages/xarray/core/indexing.py in remap_label_indexers(data_obj, indexers, method, tolerance)
    250         else:
    251             idxr, new_idx = convert_label_indexer(index, label,
--> 252                                                   dim, method, tolerance)
    253             pos_indexers[dim] = idxr
    254             if new_idx is not None:

~/anaconda3/envs/check3/lib/python3.7/site-packages/xarray/core/indexing.py in convert_label_indexer(index, label, index_name, method, tolerance)
    179                 indexer, new_index = index.get_loc_level(label.item(), level=0)
    180             else:
--> 181                 indexer = get_loc(index, label.item(), method, tolerance)
    182         elif label.dtype.kind == 'b':
    183             indexer = label

~/anaconda3/envs/check3/lib/python3.7/site-packages/xarray/core/indexing.py in get_loc(index, label, method, tolerance)
    106 def get_loc(index, label, method=None, tolerance=None):
    107     kwargs = _index_method_kwargs(method, tolerance)
--> 108     return index.get_loc(label, **kwargs)
    109 
    110 

~/anaconda3/envs/check3/lib/python3.7/site-packages/pandas/core/indexes/numeric.py in get_loc(self, key, method, tolerance)
    434             pass
    435         return super(Float64Index, self).get_loc(key, method=method,
--> 436                                                  tolerance=tolerance)
    437 
    438     @cache_readonly

~/anaconda3/envs/check3/lib/python3.7/site-packages/pandas/core/indexes/base.py in get_loc(self, key, method, tolerance)
   2657                 return self._engine.get_loc(key)
   2658             except KeyError:
-> 2659                 return self._engine.get_loc(self._maybe_cast_indexer(key))
   2660         indexer = self.get_indexer([key], method=method, tolerance=tolerance)
   2661         if indexer.ndim > 1 or indexer.size > 1:

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/index.pyx in pandas._libs.index.IndexEngine.get_loc()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Float64HashTable.get_item()

pandas/_libs/hashtable_class_helper.pxi in pandas._libs.hashtable.Float64HashTable.get_item()

KeyError: 0.111
```
xarray version: 0.12.1
numpy version: 1.16.3

Discussion:
pep8speaks: Hello @HasanAhmadQ7! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:

* In the file [`xarray/core/dataset.py`](https://github.com/pydata/xarray/blob/34d4115cb40c6cb1c0edebe3a939984674e0ab52/xarray/core/dataset.py):

> [Line 2330:31](https://github.com/pydata/xarray/blob/34d4115cb40c6cb1c0edebe3a939984674e0ab52/xarray/core/dataset.py#L2330): [E271](https://duckduckgo.com/?q=pep8%20E271) multiple spaces after keyword
> [Line 2336:80](https://github.com/pydata/xarray/blob/34d4115cb40c6cb1c0edebe3a939984674e0ab52/xarray/core/dataset.py#L2336): [E501](https://duckduckgo.com/?q=pep8%20E501) line too long (85 > 79 characters)




max-sixty: Hi @HasanAhmadQ7 thanks for the PR - I saw your comment here: https://github.com/pydata/xarray/issues/3137#issuecomment-515626172

I see the tests are failing, but the build fails before it tests these changes - could you try merging master and pushing? That'll give them another chance to run.
shoyer: @HasanAhmadQ7 thanks for looking into this!

I think this could be solved a little more cleanly at a lower level of xarray's indexing logic. In particular, all of our indexing calls go through [`convert_label_indexer`](https://github.com/pydata/xarray/blob/1d7bcbdc75b6d556c04e2c7d7a042e4379e15303/xarray/core/indexing.py#L121), which in turn uses `get_loc` and `get_indexer_nd` helper functions to wrap the calls into `pandas.Index` methods:
https://github.com/pydata/xarray/blob/1d7bcbdc75b6d556c04e2c7d7a042e4379e15303/xarray/core/indexing.py#L106-L118

These helper function would be a good place to implement this casting logic -- and you might even consider trying to fix it upstream in pandas as well.
HasanAhmadQ7: @shoyer @max-sixty really appreciate your time and feedback. I will work on it within a week.
HasanAhmadQ7: @shoyer A fix in pandas that I can imagine is to modify the pd.Index to retain the dtype similar to the PandasIndexAdapter in xarray/core/indexing. However, it seems to me that their design it to coerce any float to float64, so I did not feel they would consider this to be a bug. I would try to dig into such solution more if you recommend so.

I added a test case in which the coords are float16 to show that casting to the coords type is required.

The lowest level in which I can access the coords type is in the indexing/remap_label_indexers where I could get the coords from the data_obj. In the latest code, casting is done just before calling convert_label_indexer

Sorry if I am missing the point, and thank you in advance for any further feedback.
shoyer: > A fix in pandas that I can imagine is to modify the pd.Index to retain the dtype similar to the PandasIndexAdapter in xarray/core/indexing. However, it seems to me that their design it to coerce any float to float64, so I did not feel they would consider this to be a bug. I would try to dig into such solution more if you recommend so.

Yes, after thinking a little bit more about this I think you're totally right.

Pandas only has `Float64Index`. When xarray makes an index for float32 data, we use a Float64Index under the hood for look-ups, but the fact that index represents float32 data is only known to xarray, not pandas.
shoyer: I'm going to merge this shortly assuming tests pass...

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 15f2803c..02226d80 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,10 +10,10 @@ repos:
     rev: v2.2.3
     hooks:
       - id: flake8
--   repo: https://github.com/pre-commit/mirrors-mypy
+  - repo: https://github.com/pre-commit/mirrors-mypy
     rev: v0.720
     hooks:
-    -   id: mypy
+      - id: mypy
   # run these occasionally, ref discussion https://github.com/pydata/xarray/pull/3194
   # - repo: https://github.com/asottile/pyupgrade
   #   rev: v1.22.1
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index 9e6fb954..646be295 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -62,6 +62,9 @@ Bug fixes
 - XFAIL several tests which are expected to fail on ARM systems
   due to a ``datetime`` issue in NumPy (:issue:`2334`).
   By `Graham Inggs <https://github.com/ginggs>`_.
+- Fix KeyError that arises when using .sel method with float values
+  different from coords float type (:issue:`3137`).
+  By `Hasan Ahmad <https://github.com/HasanAhmadQ7>`_.
 - Fixed bug in ``combine_by_coords()`` causing a `ValueError` if the input had
   an unused dimension with coordinates which were not monotonic (:issue:`3150`).
   By `Tom Nicholas <http://github.com/TomNicholas>`_.
diff --git a/xarray/core/indexing.py b/xarray/core/indexing.py
index a6edb0a9..c5c3cadf 100644
--- a/xarray/core/indexing.py
+++ b/xarray/core/indexing.py
@@ -11,7 +11,7 @@ import pandas as pd
 from . import duck_array_ops, nputils, utils
 from .npcompat import DTypeLike
 from .pycompat import dask_array_type, integer_types
-from .utils import is_dict_like
+from .utils import is_dict_like, maybe_cast_to_coords_dtype
 
 
 def expanded_indexer(key, ndim):
@@ -269,6 +269,8 @@ def remap_label_indexers(data_obj, indexers, method=None, tolerance=None):
                 )
             pos_indexers[dim] = label
         else:
+            coords_dtype = data_obj.coords[dim].dtype
+            label = maybe_cast_to_coords_dtype(label, coords_dtype)
             idxr, new_idx = convert_label_indexer(index, label, dim, method, tolerance)
             pos_indexers[dim] = idxr
             if new_idx is not None:
diff --git a/xarray/core/utils.py b/xarray/core/utils.py
index 4541049d..66a7adfc 100644
--- a/xarray/core/utils.py
+++ b/xarray/core/utils.py
@@ -85,6 +85,12 @@ def _maybe_cast_to_cftimeindex(index: pd.Index) -> pd.Index:
         return index
 
 
+def maybe_cast_to_coords_dtype(label, coords_dtype):
+    if coords_dtype.kind == "f" and not isinstance(label, slice):
+        label = np.asarray(label, dtype=coords_dtype)
+    return label
+
+
 def safe_cast_to_index(array: Any) -> pd.Index:
     """Given an array, safely cast it to a pandas.Index.
 


Test patch:
diff --git a/xarray/tests/test_dataarray.py b/xarray/tests/test_dataarray.py
index 8b63b650..71734f09 100644
--- a/xarray/tests/test_dataarray.py
+++ b/xarray/tests/test_dataarray.py
@@ -928,6 +928,33 @@ class TestDataArray:
         result = array.sel(delta=slice(array.delta[0], array.delta[-1]))
         assert_equal(result, array)
 
+    def test_sel_float(self):
+        data_values = np.arange(4)
+
+        # case coords are float32 and label is list of floats
+        float_values = [0.0, 0.111, 0.222, 0.333]
+        coord_values = np.asarray(float_values, dtype="float32")
+        array = DataArray(data_values, [("float32_coord", coord_values)])
+        expected = DataArray(data_values[1:3], [("float32_coord", coord_values[1:3])])
+        actual = array.sel(float32_coord=float_values[1:3])
+        # case coords are float16 and label is list of floats
+        coord_values_16 = np.asarray(float_values, dtype="float16")
+        expected_16 = DataArray(
+            data_values[1:3], [("float16_coord", coord_values_16[1:3])]
+        )
+        array_16 = DataArray(data_values, [("float16_coord", coord_values_16)])
+        actual_16 = array_16.sel(float16_coord=float_values[1:3])
+
+        # case coord, label are scalars
+        expected_scalar = DataArray(
+            data_values[2], coords={"float32_coord": coord_values[2]}
+        )
+        actual_scalar = array.sel(float32_coord=float_values[2])
+
+        assert_equal(expected, actual)
+        assert_equal(expected_scalar, actual_scalar)
+        assert_equal(expected_16, actual_16)
+
     def test_sel_no_index(self):
         array = DataArray(np.arange(10), dims="x")
         assert_identical(array[0], array.sel(x=0))