Repository: pandas-dev/pandas

Issue:
df.first_valid_index() fails if there is no valid entry.
https://github.com/pandas-dev/pandas/blob/3a7f956c30528736beaae5784f509a76d892e229/pandas/core/frame.py#L3770-L3777

    import pandas as pd
    import numpy as np

    df = pd.DataFrame({'a': [np.nan,np.nan]})
    df.first_valid_index()

This code results in the following error message, which I believe is called because the boolean series returned by self.count(1) > 0 is all False.

    Traceback (most recent call last):
      File "<pyshell#10>", line 1, in <module>
        df.first_valid_index()
      File "C:\Python27\lib\site-packages\pandas\core\frame.py", line 3859, in first_valid_index
        return self.index[self.count(1) > 0][0]
      File "C:\Python27\lib\site-packages\pandas\indexes\base.py", line 1423, in __getitem__
        return getitem(key)
    IndexError: index 0 is out of bounds for axis 0 with size 0

Should there be a check where None is returned if self.count(1) > 0 is all False?
As an aside, this issue also occurs in df.last_valid_index() for the same reason.
df.first_valid_index() fails if there is no valid entry.
https://github.com/pandas-dev/pandas/blob/3a7f956c30528736beaae5784f509a76d892e229/pandas/core/frame.py#L3770-L3777

    import pandas as pd
    import numpy as np

    df = pd.DataFrame({'a': [np.nan,np.nan]})
    df.first_valid_index()

This code results in the following error message, which I believe is called because the boolean series returned by self.count(1) > 0 is all False.

    Traceback (most recent call last):
      File "<pyshell#10>", line 1, in <module>
        df.first_valid_index()
      File "C:\Python27\lib\site-packages\pandas\core\frame.py", line 3859, in first_valid_index
        return self.index[self.count(1) > 0][0]
      File "C:\Python27\lib\site-packages\pandas\indexes\base.py", line 1423, in __getitem__
        return getitem(key)
    IndexError: index 0 is out of bounds for axis 0 with size 0

Should there be a check where None is returned if self.count(1) > 0 is all False?
As an aside, this issue also occurs in df.last_valid_index() for the same reason.


Discussion:
@bjlobo : Thanks for reporting this!

I think returning` None` is fine.  Make sure to update the docs + add test when you do so.
yes this should act similarly to Series in this condition.
@gfyoung I have never modified the code or documentation or written tests before and I am not sure how to start doing this.
@bjlobo : Thanks for reporting this!

I think returning` None` is fine.  Make sure to update the docs + add test when you do so.
yes this should act similarly to Series in this condition.
@gfyoung I have never modified the code or documentation or written tests before and I am not sure how to start doing this.

Resolving patch:
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -4069,23 +4069,27 @@ def update(self, other, join='left', overwrite=True, filter_func=None,
     # ----------------------------------------------------------------------
     # Misc methods
 
+    def _get_valid_indices(self):
+        is_valid = self.count(1) > 0
+        return self.index[is_valid]
+
+    @Appender(_shared_docs['valid_index'] % {
+        'position': 'first', 'klass': 'DataFrame'})
     def first_valid_index(self):
-        """
-        Return label for first non-NA/null value
-        """
         if len(self) == 0:
             return None
 
-        return self.index[self.count(1) > 0][0]
+        valid_indices = self._get_valid_indices()
+        return valid_indices[0] if len(valid_indices) else None
 
+    @Appender(_shared_docs['valid_index'] % {
+        'position': 'first', 'klass': 'DataFrame'})
     def last_valid_index(self):
-        """
-        Return label for last non-NA/null value
-        """
         if len(self) == 0:
             return None
 
-        return self.index[self.count(1) > 0][-1]
+        valid_indices = self._get_valid_indices()
+        return valid_indices[-1] if len(valid_indices) else None
 
     # ----------------------------------------------------------------------
     # Data reshaping
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -6763,6 +6763,22 @@ def transform(self, func, *args, **kwargs):
 
         cls.transform = transform
 
+    # ----------------------------------------------------------------------
+    # Misc methods
+
+    _shared_docs['valid_index'] = """
+        Return index for %(position)s non-NA/null value.
+
+        Notes
+        --------
+        If all elements are non-NA/null, returns None.
+        Also returns None for empty %(klass)s.
+
+        Returns
+        --------
+        scalar : type of index
+        """
+
 
 def _doc_parms(cls):
     """Return a tuple of the doc parms."""
diff --git a/pandas/core/series.py b/pandas/core/series.py
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -2838,10 +2838,9 @@ def dropna(self, axis=0, inplace=False, **kwargs):
     valid = lambda self, inplace=False, **kwargs: self.dropna(inplace=inplace,
                                                               **kwargs)
 
+    @Appender(generic._shared_docs['valid_index'] % {
+        'position': 'first', 'klass': 'Series'})
     def first_valid_index(self):
-        """
-        Return label for first non-NA/null value
-        """
         if len(self) == 0:
             return None
 
@@ -2852,10 +2851,9 @@ def first_valid_index(self):
         else:
             return self.index[i]
 
+    @Appender(generic._shared_docs['valid_index'] % {
+        'position': 'last', 'klass': 'Series'})
     def last_valid_index(self):
-        """
-        Return label for last non-NA/null value
-        """
         if len(self) == 0:
             return None
 


Test patch:
diff --git a/pandas/tests/frame/test_timeseries.py b/pandas/tests/frame/test_timeseries.py
--- a/pandas/tests/frame/test_timeseries.py
+++ b/pandas/tests/frame/test_timeseries.py
@@ -440,6 +440,11 @@ def test_first_last_valid(self):
         assert empty.last_valid_index() is None
         assert empty.first_valid_index() is None
 
+        # GH17400: no valid entries
+        frame[:] = nan
+        assert frame.last_valid_index() is None
+        assert frame.first_valid_index() is None
+
     def test_at_time_frame(self):
         rng = date_range('1/1/2000', '1/5/2000', freq='5min')
         ts = DataFrame(np.random.randn(len(rng), 2), index=rng)