[MRG + 1] Ensuring that the OneHotEncoder outputs sparse matrix with given dtype #11034
diff --git a/doc/whats_new/v0.20.rst b/doc/whats_new/v0.20.rst
index 0e206878862ae..d735b09d7b7af 100644
--- a/doc/whats_new/v0.20.rst
+++ b/doc/whats_new/v0.20.rst
@@ -496,6 +496,10 @@ Preprocessing
   ``inverse_transform`` on unseen labels. :issue:`9816` by :user:`Charlie Newey
   <newey01c>`.
 
+- Fix bug in :class:`preprocessing.OneHotEncoder` which discarded the ``dtype``
+  when returning a sparse matrix output. :issue:`11042` by :user:`Daniel
+  Morales <DanielMorales9>`.
+
 Feature selection
 
 - Fixed computation of ``n_features_to_compute`` for edge case with tied CV
diff --git a/sklearn/preprocessing/data.py b/sklearn/preprocessing/data.py
index fb8f443e9c7ac..4df7c295bd834 100644
--- a/sklearn/preprocessing/data.py
+++ b/sklearn/preprocessing/data.py
@@ -1825,7 +1825,7 @@ def add_dummy_feature(X, value=1.0):
         return np.hstack((np.ones((n_samples, 1)) * value, X))
 
 
-def _transform_selected(X, transform, selected="all", copy=True):
+def _transform_selected(X, transform, dtype, selected="all", copy=True):
     """Apply a transform function to portion of selected features
 
     Parameters
@@ -1836,6 +1836,9 @@ def _transform_selected(X, transform, selected="all", copy=True):
     transform : callable
         A callable transform(X) -> X_transformed
 
+    dtype : number type
+        Desired dtype of output.
+
     copy : boolean, optional
         Copy X even if it could be avoided.
 
@@ -1869,7 +1872,10 @@ def _transform_selected(X, transform, selected="all", copy=True):
         return transform(X)
     else:
         X_sel = transform(X[:, ind[sel]])
-        X_not_sel = X[:, ind[not_sel]]
+        # The columns of X which are not transformed need
+        # to be casted to the desire dtype before concatenation.
+        # Otherwise, the stacking will cast to the higher-precision dtype.
+        X_not_sel = X[:, ind[not_sel]].astype(dtype)
 
         if sparse.issparse(X_sel) or sparse.issparse(X_not_sel):
             return sparse.hstack((X_sel, X_not_sel))
@@ -2061,7 +2067,7 @@ def fit_transform(self, X, y=None):
         X : array-like, shape [n_samples, n_feature]
             Input array of type int.
         """
-        return _transform_selected(X, self._fit_transform,
+        return _transform_selected(X, self._fit_transform, self.dtype,
                                    self.categorical_features, copy=True)
 
     def _transform(self, X):
@@ -2117,7 +2123,7 @@ def transform(self, X):
         X_out : sparse matrix if sparse=True else a 2-d array, dtype=int
             Transformed input.
         """
-        return _transform_selected(X, self._transform,
+        return _transform_selected(X, self._transform, self.dtype,
                                    self.categorical_features, copy=True)
 
 
diff --git a/sklearn/preprocessing/tests/test_data.py b/sklearn/preprocessing/tests/test_data.py
index e3bf4096750de..e194802ef2fe5 100644
--- a/sklearn/preprocessing/tests/test_data.py
+++ b/sklearn/preprocessing/tests/test_data.py
@@ -1909,40 +1909,45 @@ def test_one_hot_encoder_dense():
                                  [1., 0., 1., 0., 1.]]))
 
 
-def _check_transform_selected(X, X_expected, sel):
+def _check_transform_selected(X, X_expected, dtype, sel):
     for M in (X, sparse.csr_matrix(X)):
-        Xtr = _transform_selected(M, Binarizer().transform, sel)
+        Xtr = _transform_selected(M, Binarizer().transform, dtype, sel)
         assert_array_equal(toarray(Xtr), X_expected)
 
 
-def test_transform_selected():
-    X = [[3, 2, 1], [0, 1, 1]]
+@pytest.mark.parametrize("output_dtype", [np.int32, np.float32, np.float64])
+@pytest.mark.parametrize("input_dtype", [np.int32, np.float32, np.float64])
+def test_transform_selected(output_dtype, input_dtype):
+    X = np.asarray([[3, 2, 1], [0, 1, 1]], dtype=input_dtype)
 
-    X_expected = [[1, 2, 1], [0, 1, 1]]
-    _check_transform_selected(X, X_expected, [0])
-    _check_transform_selected(X, X_expected, [True, False, False])
+    X_expected = np.asarray([[1, 2, 1], [0, 1, 1]], dtype=output_dtype)
+    _check_transform_selected(X, X_expected, output_dtype, [0])
+    _check_transform_selected(X, X_expected, output_dtype,
+                              [True, False, False])
 
-    X_expected = [[1, 1, 1], [0, 1, 1]]
-    _check_transform_selected(X, X_expected, [0, 1, 2])
-    _check_transform_selected(X, X_expected, [True, True, True])
-    _check_transform_selected(X, X_expected, "all")
+    X_expected = np.asarray([[1, 1, 1], [0, 1, 1]], dtype=output_dtype)
+    _check_transform_selected(X, X_expected, output_dtype, [0, 1, 2])
+    _check_transform_selected(X, X_expected, output_dtype, [True, True, True])
+    _check_transform_selected(X, X_expected, output_dtype, "all")
 
-    _check_transform_selected(X, X, [])
-    _check_transform_selected(X, X, [False, False, False])
+    _check_transform_selected(X, X, output_dtype, [])
+    _check_transform_selected(X, X, output_dtype, [False, False, False])
 
 
-def test_transform_selected_copy_arg():
+@pytest.mark.parametrize("output_dtype", [np.int32, np.float32, np.float64])
+@pytest.mark.parametrize("input_dtype", [np.int32, np.float32, np.float64])
+def test_transform_selected_copy_arg(output_dtype, input_dtype):
     # transformer that alters X
     def _mutating_transformer(X):
         X[0, 0] = X[0, 0] + 1
         return X
 
-    original_X = np.asarray([[1, 2], [3, 4]])
-    expected_Xtr = [[2, 2], [3, 4]]
+    original_X = np.asarray([[1, 2], [3, 4]], dtype=input_dtype)
+    expected_Xtr = np.asarray([[2, 2], [3, 4]], dtype=output_dtype)
 
     X = original_X.copy()
-    Xtr = _transform_selected(X, _mutating_transformer, copy=True,
-                              selected='all')
+    Xtr = _transform_selected(X, _mutating_transformer, output_dtype,
+                              copy=True, selected='all')
 
     assert_array_equal(toarray(X), toarray(original_X))
     assert_array_equal(toarray(Xtr), expected_Xtr)
@@ -1987,6 +1992,17 @@ def test_one_hot_encoder_categorical_features():
     _check_one_hot(X, X2, cat, 5)
 
 
+@pytest.mark.parametrize("output_dtype", [np.int32, np.float32, np.float64])
+@pytest.mark.parametrize("input_dtype",  [np.int32, np.float32, np.float64])
+@pytest.mark.parametrize("sparse", [True, False])
+def test_one_hot_encoder_preserve_type(input_dtype, output_dtype, sparse):
+    X = np.array([[0, 1, 0, 0], [1, 2, 0, 0]], dtype=input_dtype)
+    transformer = OneHotEncoder(categorical_features=[0, 1],
+                                dtype=output_dtype, sparse=sparse)
+    X_trans = transformer.fit_transform(X)
+    assert X_trans.dtype == output_dtype
+
+
 def test_one_hot_encoder_unknown_transform():
     X = np.array([[0, 2, 1], [1, 0, 3], [1, 0, 2]])
     y = np.array([[4, 1, 1]])

#### Reference Issues/PRs
Original discussion at #11034

#### What does this implement/fix? Explain your changes.
