diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 37b88e55c19a..9c4f18879ab2 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -1458,6 +1458,65 @@ def tail(self, n=5): return self.iloc[-n:] + def _find_valid_index(self, *, how: str) -> Hashable: + valid = self.notna() + if valid.ndim == 2: + valid = valid.any(axis=1) + valid_index = self.index[valid] + if len(valid_index) == 0: + return None + return valid_index[0] if how == "first" else valid_index[-1] + + @_performance_tracking + def first_valid_index(self) -> Hashable: + """ + Return index for first non-NA value or None, if no non-NA value is found. + + Returns + ------- + type of index + Index of first non-missing value, or None if all entries are + missing or the Series/DataFrame is empty. + + See Also + -------- + Series.last_valid_index : Return index for last non-NA value. + DataFrame.last_valid_index : Return index for last non-NA value. + + Examples + -------- + >>> import cudf + >>> s = cudf.Series([None, 3, 4]) + >>> s.first_valid_index() + np.int64(1) + """ + return self._find_valid_index(how="first") + + @_performance_tracking + def last_valid_index(self) -> Hashable: + """ + Return index for last non-NA value or None, if no non-NA value is found. + + Returns + ------- + type of index + Index of last non-missing value, or None if all entries are + missing or the Series/DataFrame is empty. + + See Also + -------- + Series.first_valid_index : Return index for first non-NA value. + DataFrame.first_valid_index : Return index for first non-NA value. + + Examples + -------- + >>> import cudf + >>> s = cudf.Series([None, 3, 4]) + >>> s.last_valid_index() + np.int64(2) + """ + return self._find_valid_index(how="last") + @_performance_tracking def pipe(self, func, *args, **kwargs): """ diff --git a/python/cudf/cudf/tests/dataframe/methods/test_first_last_valid_index.py b/python/cudf/cudf/tests/dataframe/methods/test_first_last_valid_index.py new file mode 100644 index 000000000000..f45a9cbdde6a --- /dev/null +++ b/python/cudf/cudf/tests/dataframe/methods/test_first_last_valid_index.py @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 + +import pandas as pd +import pytest + +import cudf + + +@pytest.mark.parametrize( + "data,index", + [ + ([None, 3, 4], None), + ([None, None], None), + ([1, 2, 3, 4], None), + ([], None), + ([None, 3, 4], ["x", "y", "z"]), + ], +) +def test_series_first_last_valid_index(data, index): + ps = pd.Series(data, index=index, dtype="float64" if data else "object") + gs = cudf.from_pandas(ps) + + assert gs.first_valid_index() == ps.first_valid_index() + assert gs.last_valid_index() == ps.last_valid_index() + + +@pytest.mark.parametrize( + "data", + [ + {"A": [None, None, 2], "B": [None, 3, 4]}, + {"A": [None, None, None], "B": [None, None, None]}, + {"A": [1, 2, 3], "B": [4, 5, 6]}, + {}, + ], +) +def test_dataframe_first_last_valid_index(data): + pdf = pd.DataFrame(data) + gdf = cudf.from_pandas(pdf) + + assert gdf.first_valid_index() == pdf.first_valid_index() + assert gdf.last_valid_index() == pdf.last_valid_index()