Repository: scikit-learn/scikit-learn
Pull request: [MRG+1] Fixes #10458: Make LabelEncoder work with empty list/array
<!--
Thanks for contributing a pull request! Please ensure you have taken a look at
the contribution guidelines: https://github.com/scikit-learn/scikit-learn/blob/master/CONTRIBUTING.md#pull-request-checklist
-->

#### Reference Issues/PRs
Fixes #10458 
<!--
Example: Fixes #1234. See also #3456.
Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests
-->


#### What does this implement/fix? Explain your changes.
- [x] LabelEncoder transforms empty list without raising error
- [x] LabelEncoder inverse transforms empty list without raising error
- [x] test cases to test the changes

#### Any other comments?
The original issue reported transform only. The same issue was with `inverse_transform` calling `inverse_transform([])` used to return a strange error which is now fixed. 

<!--
Please be aware that we are a loose team of volunteers so patience is
necessary; assistance handling other issues is very welcome. We value
all user contributions, no matter how minor they are. If we are slow to
review, either the pull request needs some benchmarking, tinkering,
convincing, etc. or more likely the reviewers are simply busy. In either
case, we ask for your understanding during the review process.
For more information, see our FAQ on this topic:
http://scikit-learn.org/dev/faq.html#why-is-my-pull-request-not-getting-any-attention.

Thanks for contributing!
-->


Problem statement:
LabelEncoder transform fails for empty lists (for certain inputs)
Python 3.6.3, scikit_learn 0.19.1

Depending on which datatypes were used to fit the LabelEncoder, transforming empty lists works or not. Expected behavior would be that empty arrays are returned in both cases.

```python
>>> from sklearn.preprocessing import LabelEncoder
>>> le = LabelEncoder()
>>> le.fit([1,2])
LabelEncoder()
>>> le.transform([])
array([], dtype=int64)
>>> le.fit(["a","b"])
LabelEncoder()
>>> le.transform([])
Traceback (most recent call last):
  File "[...]\Python36\lib\site-packages\numpy\core\fromnumeric.py", line 57, in _wrapfunc
    return getattr(obj, method)(*args, **kwds)
TypeError: Cannot cast array data from dtype('float64') to dtype('<U32') according to the rule 'safe'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "[...]\Python36\lib\site-packages\sklearn\preprocessing\label.py", line 134, in transform
    return np.searchsorted(self.classes_, y)
  File "[...]\Python36\lib\site-packages\numpy\core\fromnumeric.py", line 1075, in searchsorted
    return _wrapfunc(a, 'searchsorted', v, side=side, sorter=sorter)
  File "[...]\Python36\lib\site-packages\numpy\core\fromnumeric.py", line 67, in _wrapfunc
    return _wrapit(obj, method, *args, **kwds)
  File "[...]\Python36\lib\site-packages\numpy\core\fromnumeric.py", line 47, in _wrapit
    result = getattr(asarray(obj), method)(*args, **kwds)
TypeError: Cannot cast array data from dtype('float64') to dtype('<U32') according to the rule 'safe'
```

Hints from issue discussion:
`le.transform([])` will trigger an numpy array of `dtype=np.float64` and you fit something which was some string.

```python
from sklearn.preprocessing import LabelEncoder                                       
import numpy as np                                                                   
                                                                                     
le = LabelEncoder()                                                                  
X = np.array(["a", "b"])                                                             
le.fit(X)                                                                            
X_trans = le.transform(np.array([], dtype=X.dtype))
X_trans
array([], dtype=int64)
```
I would like to take it up. 
Hey @maykulkarni go ahead with PR. Sorry, please don't mind my referenced commit, I don't intend to send in a PR.

I would be happy to have a look over your PR once you send in (not that my review would matter much) :)

Commit message: Revert previously deleted test

Patch to review:
diff --git a/doc/whats_new/v0.20.rst b/doc/whats_new/v0.20.rst
index 5b15eddbf514a..71568755d3e90 100644
--- a/doc/whats_new/v0.20.rst
+++ b/doc/whats_new/v0.20.rst
@@ -276,6 +276,12 @@ Feature Extraction
   (words or n-grams). :issue:`9147` by :user:`Claes-Fredrik Mannby <mannby>`
   and `Roman Yurchak`_.
 
+Preprocessing
+
+- Fixed bugs in :class:`preprocessing.LabelEncoder` which would sometimes throw
+  errors when `transform` or `inverse_transform` was called with empty arrays
+  :issue:`10458` by :user:`Mayur Kulkarni <maykulkarni>`.
+
 API changes summary
 -------------------
 
diff --git a/sklearn/preprocessing/label.py b/sklearn/preprocessing/label.py
index 61b8d4a21af30..dc53d7f997413 100644
--- a/sklearn/preprocessing/label.py
+++ b/sklearn/preprocessing/label.py
@@ -125,6 +125,8 @@ def transform(self, y):
         y : array-like of shape [n_samples]
         """
         check_is_fitted(self, 'classes_')
+        if _num_samples(y) == 0:
+            return np.array([])
         y = column_or_1d(y, warn=True)
 
         classes = np.unique(y)
@@ -147,6 +149,9 @@ def inverse_transform(self, y):
         y : numpy array of shape [n_samples]
         """
         check_is_fitted(self, 'classes_')
+        # inverse transform of empty array is empty array
+        if _num_samples(y) == 0:
+            return np.array([])
 
         diff = np.setdiff1d(y, np.arange(len(self.classes_)))
         if len(diff):
diff --git a/sklearn/preprocessing/tests/test_label.py b/sklearn/preprocessing/tests/test_label.py
index 4f64fc6b4638c..fedea9a9b7c1b 100644
--- a/sklearn/preprocessing/tests/test_label.py
+++ b/sklearn/preprocessing/tests/test_label.py
@@ -513,3 +513,14 @@ def test_inverse_binarize_multiclass():
                                                    [0, 0, 0]]),
                                        np.arange(3))
     assert_array_equal(got, np.array([1, 1, 0]))
+
+
+def test_empty_labels():
+    le = LabelEncoder()
+    le.fit(np.array(["1", "2", "1", "2", "2"], dtype=np.float64))
+    # test empty transform
+    transformed = le.transform([])
+    assert_array_equal(np.array([]), transformed)
+    # test empty inverse transform
+    inverse_transformed = le.inverse_transform([])
+    assert_array_equal(np.array([]), inverse_transformed)


Code review comments:
diff_hunk: @@ -125,6 +125,8 @@ def transform(self, y):
         y : array-like of shape [n_samples]
         """
         check_is_fitted(self, 'classes_')
+        if _num_samples(y) == 0:
+            return np.array([])
path: sklearn/preprocessing/label.py
text: @user1:
I think it might be better to put it after `column_or_1d` (first do input validation), WDYT?

@author:
hmm that seems correct

diff_hunk: @@ -513,3 +513,14 @@ def test_inverse_binarize_multiclass():
                                                    [0, 0, 0]]),
                                        np.arange(3))
     assert_array_equal(got, np.array([1, 1, 0]))
+
+
+def test_empty_labels():
path: sklearn/preprocessing/tests/test_label.py
text: @user1:
please use a meaningful name, e.g., test_label_encoder_empty_array and move it above (where tests for LabelEncoder exists).