Repository: pandas-dev/pandas

Issue:
BUG: Series.is_unique does not consider single occurrences of NaN unique
#### Code Sample, a copy-pastable example if possible
On `master`:
```python
In [1]: import numpy as np; import pandas as pd; pd.__version__
Out[1]: '0.25.0.dev0+67.gf04bb2fc9'

In [2]: pd.Series(['foo', 'bar', np.nan]).is_unique
Out[2]: False

In [3]: pd.Series([np.nan]).is_unique
Out[3]: False
```

Note that indexes consider the equivalent cases to be unique:
```python
In [4]: pd.Index(['foo', 'bar', np.nan]).is_unique
Out[4]: True

In [5]: pd.Float64Index([np.nan]).is_unique
Out[5]: True
```
#### Problem description
`Series.is_unique` does not consider single occurrences of `NaN` to be unique.

#### Expected Output
I'd expect `[2]` and `[3]` to be `True`.

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: f04bb2fc9c3d0d23e92bbdf09fe6b94e59589cbe
python: 3.6.8.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.25.0.dev0+67.gf04bb2fc9
pytest: 4.0.2
pip: 9.0.1
setuptools: 40.6.3
Cython: 0.28.2
numpy: 1.14.6
scipy: 1.0.0
pyarrow: 0.6.0
xarray: 0.9.6
IPython: 7.2.0
sphinx: 1.8.2
patsy: 0.4.1
dateutil: 2.6.0
pytz: 2017.2
blosc: None
bottleneck: 1.2.1
tables: 3.4.2
numexpr: 2.6.4
feather: 0.4.0
matplotlib: 2.0.2
openpyxl: 2.4.8
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 0.9.8
lxml.etree: 3.8.0
bs4: None
html5lib: 0.999
sqlalchemy: 1.1.13
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
fastparquet: 0.1.5
pandas_gbq: None
pandas_datareader: None
gcsfs: None

</details>



Discussion:

Resolving patch:
diff --git a/pandas/core/base.py b/pandas/core/base.py
--- a/pandas/core/base.py
+++ b/pandas/core/base.py
@@ -1345,7 +1345,7 @@ def is_unique(self):
         -------
         is_unique : boolean
         """
-        return self.nunique() == len(self)
+        return self.nunique(dropna=False) == len(self)
 
     @property
     def is_monotonic(self):


Test patch:
diff --git a/pandas/tests/indexes/common.py b/pandas/tests/indexes/common.py
--- a/pandas/tests/indexes/common.py
+++ b/pandas/tests/indexes/common.py
@@ -913,3 +913,24 @@ def test_astype_category(self, copy, name, ordered):
             result = index.astype('category', copy=copy)
             expected = CategoricalIndex(index.values, name=name)
             tm.assert_index_equal(result, expected)
+
+    def test_is_unique(self):
+        # initialize a unique index
+        index = self.create_index().drop_duplicates()
+        assert index.is_unique is True
+
+        # empty index should be unique
+        index_empty = index[:0]
+        assert index_empty.is_unique is True
+
+        # test basic dupes
+        index_dup = index.insert(0, index[0])
+        assert index_dup.is_unique is False
+
+        # single NA should be unique
+        index_na = index.insert(0, np.nan)
+        assert index_na.is_unique is True
+
+        # multiple NA should not be unique
+        index_na_dup = index_na.insert(0, np.nan)
+        assert index_na_dup.is_unique is False
diff --git a/pandas/tests/indexes/interval/test_interval.py b/pandas/tests/indexes/interval/test_interval.py
--- a/pandas/tests/indexes/interval/test_interval.py
+++ b/pandas/tests/indexes/interval/test_interval.py
@@ -242,12 +242,10 @@ def test_take(self, closed):
             [0, 0, 1], [1, 1, 2], closed=closed)
         tm.assert_index_equal(result, expected)
 
-    def test_unique(self, closed):
-        # unique non-overlapping
-        idx = IntervalIndex.from_tuples(
-            [(0, 1), (2, 3), (4, 5)], closed=closed)
-        assert idx.is_unique is True
-
+    def test_is_unique_interval(self, closed):
+        """
+        Interval specific tests for is_unique in addition to base class tests
+        """
         # unique overlapping - distinct endpoints
         idx = IntervalIndex.from_tuples([(0, 1), (0.5, 1.5)], closed=closed)
         assert idx.is_unique is True
@@ -261,15 +259,6 @@ def test_unique(self, closed):
         idx = IntervalIndex.from_tuples([(-1, 1), (-2, 2)], closed=closed)
         assert idx.is_unique is True
 
-        # duplicate
-        idx = IntervalIndex.from_tuples(
-            [(0, 1), (0, 1), (2, 3)], closed=closed)
-        assert idx.is_unique is False
-
-        # empty
-        idx = IntervalIndex([], closed=closed)
-        assert idx.is_unique is True
-
     def test_monotonic(self, closed):
         # increasing non-overlapping
         idx = IntervalIndex.from_tuples(
diff --git a/pandas/tests/indexes/multi/test_duplicates.py b/pandas/tests/indexes/multi/test_duplicates.py
--- a/pandas/tests/indexes/multi/test_duplicates.py
+++ b/pandas/tests/indexes/multi/test_duplicates.py
@@ -143,6 +143,18 @@ def test_has_duplicates(idx, idx_dup):
     assert mi.is_unique is False
     assert mi.has_duplicates is True
 
+    # single instance of NaN
+    mi_nan = MultiIndex(levels=[['a', 'b'], [0, 1]],
+                        codes=[[-1, 0, 0, 1, 1], [-1, 0, 1, 0, 1]])
+    assert mi_nan.is_unique is True
+    assert mi_nan.has_duplicates is False
+
+    # multiple instances of NaN
+    mi_nan_dup = MultiIndex(levels=[['a', 'b'], [0, 1]],
+                            codes=[[-1, -1, 0, 0, 1, 1], [-1, -1, 0, 1, 0, 1]])
+    assert mi_nan_dup.is_unique is False
+    assert mi_nan_dup.has_duplicates is True
+
 
 def test_has_duplicates_from_tuples():
     # GH 9075
diff --git a/pandas/tests/indexes/period/test_indexing.py b/pandas/tests/indexes/period/test_indexing.py
--- a/pandas/tests/indexes/period/test_indexing.py
+++ b/pandas/tests/indexes/period/test_indexing.py
@@ -441,18 +441,6 @@ def test_is_monotonic_decreasing(self):
         assert idx_dec1.is_monotonic_decreasing is True
         assert idx.is_monotonic_decreasing is False
 
-    def test_is_unique(self):
-        # GH 17717
-        p0 = pd.Period('2017-09-01')
-        p1 = pd.Period('2017-09-02')
-        p2 = pd.Period('2017-09-03')
-
-        idx0 = pd.PeriodIndex([p0, p1, p2])
-        assert idx0.is_unique is True
-
-        idx1 = pd.PeriodIndex([p1, p1, p2])
-        assert idx1.is_unique is False
-
     def test_contains(self):
         # GH 17717
         p0 = pd.Period('2017-09-01')
diff --git a/pandas/tests/indexes/test_category.py b/pandas/tests/indexes/test_category.py
--- a/pandas/tests/indexes/test_category.py
+++ b/pandas/tests/indexes/test_category.py
@@ -611,15 +611,6 @@ def test_is_monotonic(self, data, non_lexsorted_data):
         assert c.is_monotonic_increasing is True
         assert c.is_monotonic_decreasing is False
 
-    @pytest.mark.parametrize('values, expected', [
-        ([1, 2, 3], True),
-        ([1, 3, 1], False),
-        (list('abc'), True),
-        (list('aba'), False)])
-    def test_is_unique(self, values, expected):
-        ci = CategoricalIndex(values)
-        assert ci.is_unique is expected
-
     def test_has_duplicates(self):
 
         idx = CategoricalIndex([0, 0, 0], name='foo')
diff --git a/pandas/tests/series/test_duplicates.py b/pandas/tests/series/test_duplicates.py
--- a/pandas/tests/series/test_duplicates.py
+++ b/pandas/tests/series/test_duplicates.py
@@ -59,12 +59,18 @@ def test_unique_data_ownership():
     Series(Series(["a", "c", "b"]).unique()).sort_values()
 
 
-def test_is_unique():
-    # GH11946
-    s = Series(np.random.randint(0, 10, size=1000))
-    assert s.is_unique is False
-    s = Series(np.arange(1000))
-    assert s.is_unique is True
+@pytest.mark.parametrize('data, expected', [
+    (np.random.randint(0, 10, size=1000), False),
+    (np.arange(1000), True),
+    ([], True),
+    ([np.nan], True),
+    (['foo', 'bar', np.nan], True),
+    (['foo', 'foo', np.nan], False),
+    (['foo', 'bar', np.nan, np.nan], False)])
+def test_is_unique(data, expected):
+    # GH11946 / GH25180
+    s = Series(data)
+    assert s.is_unique is expected
 
 
 def test_is_unique_class_ne(capsys):