From 602c55d5a64e57581e21f7cf2a3f08c0e2a7bd81 Mon Sep 17 00:00:00 2001 From: Stuart Berg Date: Mon, 12 Aug 2019 14:14:44 -0400 Subject: [PATCH 1/2] API: unique() should preserve the dtype of the input --- doc/source/whatsnew/v1.0.0.rst | 1 + pandas/core/algorithms.py | 2 +- pandas/tests/test_base.py | 27 +++++++++++++++++++++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 53041441ba040..4816e6a519884 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -151,6 +151,7 @@ Other API changes - :meth:`pandas.api.types.infer_dtype` will now return "integer-na" for integer and ``np.nan`` mix (:issue:`27283`) - :meth:`MultiIndex.from_arrays` will no longer infer names from arrays if ``names=None`` is explicitly provided (:issue:`27292`) +- The returned dtype of ::func:`pd.unique` now matches the input dtype. (:issue:`27874`) - .. _whatsnew_1000.api.documentation: diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index 4073ede84c6f6..5a64a912dfaae 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -396,7 +396,7 @@ def unique(values): table = htable(len(values)) uniques = table.unique(values) - uniques = _reconstruct_data(uniques, dtype, original) + uniques = _reconstruct_data(uniques, original.dtype, original) return uniques diff --git a/pandas/tests/test_base.py b/pandas/tests/test_base.py index c760c75e44f6b..483122a0eeaba 100644 --- a/pandas/tests/test_base.py +++ b/pandas/tests/test_base.py @@ -159,8 +159,8 @@ def test_memory_usage(self): class Ops: def _allow_na_ops(self, obj): """Whether to skip test cases including NaN""" - if isinstance(obj, Index) and (obj.is_boolean() or not obj._can_hold_na): - # don't test boolean / int64 index + if (isinstance(obj, Index) and obj.is_boolean()) or not obj._can_hold_na: + # don't test boolean / integer dtypes return False return True @@ -187,7 +187,24 @@ def setup_method(self, method): types = ["bool", "int", "float", "dt", "dt_tz", "period", "string", "unicode"] self.indexes = [getattr(self, "{}_index".format(t)) for t in types] self.series = [getattr(self, "{}_series".format(t)) for t in types] - self.objs = self.indexes + self.series + + # To test narrow dtypes, we use narrower *data* elements, not *index* elements + index = self.int_index + self.float32_series = Series(arr.astype(np.float32), index=index, name="a") + + arr_int = np.random.choice(10, size=10, replace=False) + self.int8_series = Series(arr_int.astype(np.int8), index=index, name="a") + self.int16_series = Series(arr_int.astype(np.int16), index=index, name="a") + self.int32_series = Series(arr_int.astype(np.int32), index=index, name="a") + + self.uint8_series = Series(arr_int.astype(np.uint8), index=index, name="a") + self.uint16_series = Series(arr_int.astype(np.uint16), index=index, name="a") + self.uint32_series = Series(arr_int.astype(np.uint32), index=index, name="a") + + nrw_types = ["float32", "int8", "int16", "int32", "uint8", "uint16", "uint32"] + self.narrow_series = [getattr(self, "{}_series".format(t)) for t in nrw_types] + + self.objs = self.indexes + self.series + self.narrow_series def check_ops_properties(self, props, filter=None, ignore_failures=False): for op in props: @@ -385,6 +402,7 @@ def test_value_counts_unique_nunique(self): if isinstance(o, Index): assert isinstance(result, o.__class__) tm.assert_index_equal(result, orig) + assert result.dtype == orig.dtype elif is_datetime64tz_dtype(o): # datetimetz Series returns array of Timestamp assert result[0] == orig[0] @@ -396,6 +414,7 @@ def test_value_counts_unique_nunique(self): ) else: tm.assert_numpy_array_equal(result, orig.values) + assert result.dtype == orig.dtype assert o.nunique() == len(np.unique(o.values)) @@ -904,7 +923,7 @@ def test_fillna(self): expected = [fill_value] * 2 + list(values[2:]) - expected = klass(expected) + expected = klass(expected, dtype=orig.dtype) o = klass(values) # check values has the same dtype as the original From 1aec96090d3f8641d994e0e0393ad4ddd592cf9e Mon Sep 17 00:00:00 2001 From: Stuart Berg Date: Sun, 8 Sep 2019 17:43:12 -0400 Subject: [PATCH 2/2] _reconstruct_data(): Use copy=False when calling astype() --- pandas/core/algorithms.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/algorithms.py b/pandas/core/algorithms.py index 5a64a912dfaae..2e5ab0d182aff 100644 --- a/pandas/core/algorithms.py +++ b/pandas/core/algorithms.py @@ -180,13 +180,13 @@ def _reconstruct_data(values, dtype, original): if is_extension_array_dtype(dtype): values = dtype.construct_array_type()._from_sequence(values) elif is_bool_dtype(dtype): - values = values.astype(dtype) + values = values.astype(dtype, copy=False) # we only support object dtypes bool Index if isinstance(original, ABCIndexClass): - values = values.astype(object) + values = values.astype(object, copy=False) elif dtype is not None: - values = values.astype(dtype) + values = values.astype(dtype, copy=False) return values