From b84e085f671a4627a3120e1c1796e6c67c0edc71 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Sun, 12 Aug 2018 08:18:35 +0200 Subject: [PATCH 01/15] BUG: don't mangle NaN-float-values and pd.NaT (GH 22295) it is more or less the clean-up after PR #21904 and PR #22207, the underlying hash-map handles all cases correctly out-of-the box and thus no special handling is needed. --- pandas/_libs/hashtable_class_helper.pxi.in | 34 +++------------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/pandas/_libs/hashtable_class_helper.pxi.in b/pandas/_libs/hashtable_class_helper.pxi.in index 550cabd5e3192..3c7a862cf5323 100644 --- a/pandas/_libs/hashtable_class_helper.pxi.in +++ b/pandas/_libs/hashtable_class_helper.pxi.in @@ -470,7 +470,6 @@ cdef class {{name}}HashTable(HashTable): int ret = 0 {{dtype}}_t val khiter_t k - bint seen_na = 0 {{name}}Vector uniques = {{name}}Vector() {{name}}VectorData *ud @@ -479,22 +478,6 @@ cdef class {{name}}HashTable(HashTable): with nogil: for i in range(n): val = values[i] - {{if float_group}} - if val == val: - k = kh_get_{{dtype}}(self.table, val) - if k == self.table.n_buckets: - kh_put_{{dtype}}(self.table, val, &ret) - if needs_resize(ud): - with gil: - uniques.resize() - append_data_{{dtype}}(ud, val) - elif not seen_na: - seen_na = 1 - if needs_resize(ud): - with gil: - uniques.resize() - append_data_{{dtype}}(ud, NAN) - {{else}} k = kh_get_{{dtype}}(self.table, val) if k == self.table.n_buckets: kh_put_{{dtype}}(self.table, val, &ret) @@ -502,7 +485,6 @@ cdef class {{name}}HashTable(HashTable): with gil: uniques.resize() append_data_{{dtype}}(ud, val) - {{endif}} return uniques.to_array() {{endfor}} @@ -854,19 +836,11 @@ cdef class PyObjectHashTable(HashTable): for i in range(n): val = values[i] hash(val) + k = kh_get_pymap(self.table, val) + if k == self.table.n_buckets: + kh_put_pymap(self.table, val, &ret) + uniques.append(val) - # `val is None` below is exception to prevent mangling of None and - # other NA values; note however that other NA values (ex: pd.NaT - # and np.nan) will still get mangled, so many not be a permanent - # solution; see GH 20866 - if not checknull(val) or val is None: - k = kh_get_pymap(self.table, val) - if k == self.table.n_buckets: - kh_put_pymap(self.table, val, &ret) - uniques.append(val) - elif not seen_na: - seen_na = 1 - uniques.append(nan) return uniques.to_array() From 949fdee91a0e5d47decb4a4ea64473f30ef1f81d Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Sun, 12 Aug 2018 08:42:23 +0200 Subject: [PATCH 02/15] adding unit tests --- pandas/tests/test_algos.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 64d2e155aa9a9..0a484743cb516 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -520,6 +520,33 @@ def test_different_nans(self): expected = np.array([np.nan]) tm.assert_numpy_array_equal(result, expected) + def test_first_nan_kept(self): + # GH 22295 + # create different nans from bit-patterns: + bits_for_nan1 = 0xfff8000000000001 + bits_for_nan2 = 0x7ff8000000000001 + NAN1 = struct.unpack("d", struct.pack("=Q", bits_for_nan1))[0] + NAN2 = struct.unpack("d", struct.pack("=Q", bits_for_nan2))[0] + assert NAN1 != NAN1 + assert NAN2 != NAN2 + for el_type in [np.float64, np.object]: + a = np.array([NAN1, NAN2], dtype=el_type) + result = pd.unique(a) + assert result.size == 1 + # use bit patterns to identify which nan was kept: + result_nan_bits = struct.unpack("=Q", + struct.pack("d", result[0]))[0] + assert result_nan_bits == bits_for_nan1 + + def test_do_not_mangle_na_values(self): + # GH 22295 + a = np.array([None, np.nan, pd.NaT], dtype=np.object) + result = pd.unique(a) + assert result.size == 3 + assert a[0] is None + assert np.isnan(a[1]) + assert a[2] is pd.NaT + class TestIsin(object): From 90de15624338264ea66f263e5d8e64e63668458c Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Thu, 16 Aug 2018 20:17:18 +0200 Subject: [PATCH 03/15] don't mangle None, nan and NaT and do not depend on underlying hash-map for doing it --- pandas/core/indexes/base.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index ca381160de0df..487d3975a6219 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -3109,7 +3109,6 @@ def get_loc(self, key, method=None, tolerance=None): return self._engine.get_loc(key) except KeyError: return self._engine.get_loc(self._maybe_cast_indexer(key)) - indexer = self.get_indexer([key], method=method, tolerance=tolerance) if indexer.ndim > 1 or indexer.size > 1: raise TypeError('get_loc requires scalar valued input') @@ -4475,10 +4474,6 @@ def insert(self, loc, item): ------- new_index : Index """ - if is_scalar(item) and isna(item): - # GH 18295 - item = self._na_value - _self = np.asarray(self) item = self._coerce_scalar_to_index(item)._ndarray_values idx = np.concatenate((_self[:loc], item, _self[loc:])) From a99a9d1e16aee7b43ce68235af366abbd27b2efe Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Thu, 16 Aug 2018 20:18:47 +0200 Subject: [PATCH 04/15] be consistent in the hashtable: no mangling of None, nan and NaT in all methods --- pandas/_libs/hashtable_class_helper.pxi.in | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/pandas/_libs/hashtable_class_helper.pxi.in b/pandas/_libs/hashtable_class_helper.pxi.in index 3c7a862cf5323..f294fd141a9f1 100644 --- a/pandas/_libs/hashtable_class_helper.pxi.in +++ b/pandas/_libs/hashtable_class_helper.pxi.in @@ -729,9 +729,6 @@ cdef class StringHashTable(HashTable): return np.asarray(labels) -na_sentinel = object - - cdef class PyObjectHashTable(HashTable): def __init__(self, size_hint=1): @@ -749,8 +746,7 @@ cdef class PyObjectHashTable(HashTable): def __contains__(self, object key): cdef khiter_t k hash(key) - if key != key or key is None: - key = na_sentinel + k = kh_get_pymap(self.table, key) return k != self.table.n_buckets @@ -762,8 +758,7 @@ cdef class PyObjectHashTable(HashTable): cpdef get_item(self, object val): cdef khiter_t k - if val != val or val is None: - val = na_sentinel + k = kh_get_pymap(self.table, val) if k != self.table.n_buckets: return self.table.vals[k] @@ -777,8 +772,7 @@ cdef class PyObjectHashTable(HashTable): char* buf hash(key) - if key != key or key is None: - key = na_sentinel + k = kh_put_pymap(self.table, key, &ret) # self.table.keys[k] = key if kh_exist_pymap(self.table, k): @@ -796,8 +790,6 @@ cdef class PyObjectHashTable(HashTable): for i in range(n): val = values[i] hash(val) - if val != val or val is None: - val = na_sentinel k = kh_put_pymap(self.table, val, &ret) self.table.vals[k] = i @@ -813,8 +805,6 @@ cdef class PyObjectHashTable(HashTable): for i in range(n): val = values[i] hash(val) - if val != val or val is None: - val = na_sentinel k = kh_get_pymap(self.table, val) if k != self.table.n_buckets: @@ -831,7 +821,6 @@ cdef class PyObjectHashTable(HashTable): object val khiter_t k ObjectVector uniques = ObjectVector() - bint seen_na = 0 for i in range(n): val = values[i] @@ -841,7 +830,6 @@ cdef class PyObjectHashTable(HashTable): kh_put_pymap(self.table, val, &ret) uniques.append(val) - return uniques.to_array() def get_labels(self, ndarray[object] values, ObjectVector uniques, From c02d66d8df7787a424f6758eb0ae01f6cc0e3355 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Thu, 16 Aug 2018 21:34:35 +0200 Subject: [PATCH 05/15] fixing/changing unit test, because NA values are no longer mangled in Index.insert() --- pandas/tests/indexes/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 755b3cc7f1dca..9149a30b9aa6b 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -560,8 +560,8 @@ def test_insert(self): tm.assert_index_equal(Index(['a']), null_index.insert(0, 'a')) def test_insert_missing(self, nulls_fixture): - # GH 18295 (test missing) - expected = Index(['a', np.nan, 'b', 'c']) + # test there is no mangling of NA values + expected = Index(['a', nulls_fixture, 'b', 'c']) result = Index(list('abc')).insert(1, nulls_fixture) tm.assert_index_equal(result, expected) From 876fb2b9182ce4a9a8ba52630dec9f7add85d148 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Thu, 16 Aug 2018 21:51:38 +0200 Subject: [PATCH 06/15] when passing None and other NA values to NumericIndex the value should become nan --- pandas/core/indexes/numeric.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py index e0627432cbc2e..ddff22431b1d6 100644 --- a/pandas/core/indexes/numeric.py +++ b/pandas/core/indexes/numeric.py @@ -9,6 +9,7 @@ is_bool, is_bool_dtype, is_scalar) +from pandas.core.dtypes.missing import isna from pandas import compat from pandas.core import algorithms @@ -114,6 +115,12 @@ def is_all_dates(self): """ return False + def insert(self, loc, item): + # treat NA values as nans: + if is_scalar(item) and isna(item): + item = np.nan + return super(NumericIndex, self).insert(loc, item) + _num_index_shared_docs['class_descr'] = """ Immutable ndarray implementing an ordered, sliceable set. The basic object From 622d3cf52c1950c00438ed2faafc6b1294dcb813 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Thu, 16 Aug 2018 20:28:41 +0200 Subject: [PATCH 07/15] adding unit test for GH22332 --- pandas/tests/indexes/test_base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 9149a30b9aa6b..d669cfdd82261 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -1245,6 +1245,19 @@ def test_get_indexer(self): e1 = np.array([1, 3, -1], dtype=np.intp) assert_almost_equal(r1, e1) + def test_get_indexer_with_NA_values(self): + # GH 22332 + # check pairwise, that no pair of na values + # is mangled + na_values = [None, np.nan, pd.NaT] + for f in na_values: + for s in na_values: + if f is not s: # otherwise not unique + arr = np.array([f, s], dtype=np.object) + result = pd.Index(arr, dtype=np.object).get_indexer([f, s, 'Unknown']) + expected = np.array([0, 1, -1], dtype=np.int64) + tm.assert_numpy_array_equal(result, expected) + @pytest.mark.parametrize("reverse", [True, False]) @pytest.mark.parametrize("expected,method", [ (np.array([-1, 0, 0, 1, 1], dtype=np.intp), 'pad'), From 1d7814e235262af591c41736a39474ab92c1b0f1 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Thu, 16 Aug 2018 23:19:34 +0200 Subject: [PATCH 08/15] making PEP8 happy --- pandas/tests/indexes/test_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index d669cfdd82261..96bc07e604b80 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -1254,7 +1254,8 @@ def test_get_indexer_with_NA_values(self): for s in na_values: if f is not s: # otherwise not unique arr = np.array([f, s], dtype=np.object) - result = pd.Index(arr, dtype=np.object).get_indexer([f, s, 'Unknown']) + index = pd.Index(arr, dtype=np.object) + result = index.get_indexer([f, s, 'Unknown']) expected = np.array([0, 1, -1], dtype=np.int64) tm.assert_numpy_array_equal(result, expected) From 0dd2513afc1cbde5305c2ec9a8c9288899a30769 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Wed, 5 Sep 2018 22:33:27 +0200 Subject: [PATCH 09/15] using fixtures for tests in indexes/test_base.py. Introducing unique_nulls_fixture, because otherwise it is just too troublesome to filter out all types of nans --- pandas/conftest.py | 12 ++++++++++++ pandas/tests/indexes/test_base.py | 21 +++++++++++---------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 28c24fc8c0640..621de3ffd4b12 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -286,6 +286,18 @@ def nulls_fixture(request): nulls_fixture2 = nulls_fixture # Generate cartesian product of nulls_fixture +@pytest.fixture(params=[None, np.nan, pd.NaT]) +def unique_nulls_fixture(request): + """ + Fixture for each null type in pandas, each null type exactly once + """ + return request.param + + +# Generate cartesian product of unique_nulls_fixture: +unique_nulls_fixture2 = unique_nulls_fixture + + TIMEZONES = [None, 'UTC', 'US/Eastern', 'Asia/Tokyo', 'dateutil/US/Pacific', 'dateutil/Asia/Singapore'] diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 96bc07e604b80..44b60b009ecfe 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -1245,19 +1245,20 @@ def test_get_indexer(self): e1 = np.array([1, 3, -1], dtype=np.intp) assert_almost_equal(r1, e1) - def test_get_indexer_with_NA_values(self): + def test_get_indexer_with_NA_values(self, unique_nulls_fixture, + unique_nulls_fixture2): # GH 22332 # check pairwise, that no pair of na values # is mangled - na_values = [None, np.nan, pd.NaT] - for f in na_values: - for s in na_values: - if f is not s: # otherwise not unique - arr = np.array([f, s], dtype=np.object) - index = pd.Index(arr, dtype=np.object) - result = index.get_indexer([f, s, 'Unknown']) - expected = np.array([0, 1, -1], dtype=np.int64) - tm.assert_numpy_array_equal(result, expected) + if unique_nulls_fixture is unique_nulls_fixture2: + return # skip it, values are not unique + arr = np.array([unique_nulls_fixture, + unique_nulls_fixture2], dtype=np.object) + index = pd.Index(arr, dtype=np.object) + result = index.get_indexer([unique_nulls_fixture, + unique_nulls_fixture2, 'Unknown']) + expected = np.array([0, 1, -1], dtype=np.int64) + tm.assert_numpy_array_equal(result, expected) @pytest.mark.parametrize("reverse", [True, False]) @pytest.mark.parametrize("expected,method", [ From d5f6457b863998ed46e3b65719c7474fdb98773d Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Wed, 5 Sep 2018 22:40:04 +0200 Subject: [PATCH 10/15] using null fixtures for tests in test_algos.py --- pandas/tests/test_algos.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pandas/tests/test_algos.py b/pandas/tests/test_algos.py index 0a484743cb516..b2ddbf715b480 100644 --- a/pandas/tests/test_algos.py +++ b/pandas/tests/test_algos.py @@ -538,14 +538,17 @@ def test_first_nan_kept(self): struct.pack("d", result[0]))[0] assert result_nan_bits == bits_for_nan1 - def test_do_not_mangle_na_values(self): + def test_do_not_mangle_na_values(self, unique_nulls_fixture, + unique_nulls_fixture2): # GH 22295 - a = np.array([None, np.nan, pd.NaT], dtype=np.object) + if unique_nulls_fixture is unique_nulls_fixture2: + return # skip it, values not unique + a = np.array([unique_nulls_fixture, + unique_nulls_fixture2], dtype=np.object) result = pd.unique(a) - assert result.size == 3 - assert a[0] is None - assert np.isnan(a[1]) - assert a[2] is pd.NaT + assert result.size == 2 + assert a[0] is unique_nulls_fixture + assert a[1] is unique_nulls_fixture2 class TestIsin(object): From 8d97d7fb9d672d1c3eb1ad892cee56db5ba89ca9 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Wed, 5 Sep 2018 22:46:44 +0200 Subject: [PATCH 11/15] moving test case nearer to get_loc test cases --- pandas/tests/indexes/test_base.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index 44b60b009ecfe..d0a48f2fab125 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -1245,21 +1245,6 @@ def test_get_indexer(self): e1 = np.array([1, 3, -1], dtype=np.intp) assert_almost_equal(r1, e1) - def test_get_indexer_with_NA_values(self, unique_nulls_fixture, - unique_nulls_fixture2): - # GH 22332 - # check pairwise, that no pair of na values - # is mangled - if unique_nulls_fixture is unique_nulls_fixture2: - return # skip it, values are not unique - arr = np.array([unique_nulls_fixture, - unique_nulls_fixture2], dtype=np.object) - index = pd.Index(arr, dtype=np.object) - result = index.get_indexer([unique_nulls_fixture, - unique_nulls_fixture2, 'Unknown']) - expected = np.array([0, 1, -1], dtype=np.int64) - tm.assert_numpy_array_equal(result, expected) - @pytest.mark.parametrize("reverse", [True, False]) @pytest.mark.parametrize("expected,method", [ (np.array([-1, 0, 0, 1, 1], dtype=np.intp), 'pad'), @@ -1379,6 +1364,21 @@ def test_get_indexer_numeric_index_boolean_target(self): expected = np.array([-1, -1, -1], dtype=np.intp) tm.assert_numpy_array_equal(result, expected) + def test_get_indexer_with_NA_values(self, unique_nulls_fixture, + unique_nulls_fixture2): + # GH 22332 + # check pairwise, that no pair of na values + # is mangled + if unique_nulls_fixture is unique_nulls_fixture2: + return # skip it, values are not unique + arr = np.array([unique_nulls_fixture, + unique_nulls_fixture2], dtype=np.object) + index = pd.Index(arr, dtype=np.object) + result = index.get_indexer([unique_nulls_fixture, + unique_nulls_fixture2, 'Unknown']) + expected = np.array([0, 1, -1], dtype=np.int64) + tm.assert_numpy_array_equal(result, expected) + @pytest.mark.parametrize("method", [None, 'pad', 'backfill', 'nearest']) def test_get_loc(self, method): index = pd.Index([0, 1, 2]) From 41c1d2d936d9ebf00c3036386f3771e4c2ffee6e Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Wed, 5 Sep 2018 23:00:48 +0200 Subject: [PATCH 12/15] using self._na_value instead of np.nan --- pandas/core/indexes/numeric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py index ddff22431b1d6..11b10410ff230 100644 --- a/pandas/core/indexes/numeric.py +++ b/pandas/core/indexes/numeric.py @@ -118,7 +118,7 @@ def is_all_dates(self): def insert(self, loc, item): # treat NA values as nans: if is_scalar(item) and isna(item): - item = np.nan + item = self._na_value return super(NumericIndex, self).insert(loc, item) From 9cf7180a81848ec7b594454ce6d62e099786ed99 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Fri, 7 Sep 2018 21:51:04 +0200 Subject: [PATCH 13/15] add GH number to test case --- pandas/tests/indexes/test_base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index d0a48f2fab125..eab04419fe939 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -560,6 +560,7 @@ def test_insert(self): tm.assert_index_equal(Index(['a']), null_index.insert(0, 'a')) def test_insert_missing(self, nulls_fixture): + # GH 22295 # test there is no mangling of NA values expected = Index(['a', nulls_fixture, 'b', 'c']) result = Index(list('abc')).insert(1, nulls_fixture) From a3fd2efeef959a57445e6473772f72d73b1bb2e4 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Fri, 7 Sep 2018 21:58:39 +0200 Subject: [PATCH 14/15] adding docstring to NumericIndex.insert (checked in an interactive session that it works) --- pandas/core/indexes/numeric.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/core/indexes/numeric.py b/pandas/core/indexes/numeric.py index 11b10410ff230..8d616468a87d9 100644 --- a/pandas/core/indexes/numeric.py +++ b/pandas/core/indexes/numeric.py @@ -115,6 +115,7 @@ def is_all_dates(self): """ return False + @Appender(Index.insert.__doc__) def insert(self, loc, item): # treat NA values as nans: if is_scalar(item) and isna(item): From 356b8aab754c32e469508e23c447d8c650938843 Mon Sep 17 00:00:00 2001 From: Egor Dranischnikow Date: Sun, 12 Aug 2018 21:43:21 +0200 Subject: [PATCH 15/15] adding whatsnew --- doc/source/whatsnew/v0.24.0.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 649629714c3b1..ff9661bcd37fc 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -720,13 +720,16 @@ Indexing - Bug where mixed indexes wouldn't allow integers for ``.at`` (:issue:`19860`) - ``Float64Index.get_loc`` now raises ``KeyError`` when boolean key passed. (:issue:`19087`) - Bug in :meth:`DataFrame.loc` when indexing with an :class:`IntervalIndex` (:issue:`19977`) +- :class:`Index` no longer mangles ``None``, ``NaN`` and ``NaT``, i.e. they are treated as three different keys. However, for numeric Index all three are still coerced to a ``NaN`` (:issue:`22332`) Missing ^^^^^^^ - Bug in :func:`DataFrame.fillna` where a ``ValueError`` would raise when one column contained a ``datetime64[ns, tz]`` dtype (:issue:`15522`) - Bug in :func:`Series.hasnans` that could be incorrectly cached and return incorrect answers if null elements are introduced after an initial call (:issue:`19700`) -- :func:`Series.isin` now treats all nans as equal also for ``np.object``-dtype. This behavior is consistent with the behavior for float64 (:issue:`22119`) +- :func:`Series.isin` now treats all NaN-floats as equal also for `np.object`-dtype. This behavior is consistent with the behavior for float64 (:issue:`22119`) +- :func:`unique` no longer mangles NaN-floats and the ``NaT``-object for `np.object`-dtype, i.e. ``NaT`` is no longer coerced to a NaN-value and is treated as a different entity. (:issue:`22295`) + MultiIndex ^^^^^^^^^^