From 65b47e99de4f4b7c70f969dfc70127003a16b604 Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 01:58:46 +0530 Subject: [PATCH 01/21] Add between_time for Series/DataFrame --- python/cudf/cudf/core/indexed_frame.py | 85 ++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 37b88e55c19a..a8f497137cbb 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -5,6 +5,7 @@ from __future__ import annotations import copy +import operator import itertools import textwrap import warnings @@ -2421,6 +2422,90 @@ def truncate(self, before=None, after=None, axis=0, copy=True): slicer = [slice(None, None)] * self.ndim slicer[axis] = slice(before, after) return self.loc[tuple(slicer)].copy() + + @_performance_tracking + def between_time( + self, + start_time, + end_time, + include_start: bool = True, + include_end: bool = True, + ) -> Self: + """ + Select values between particular times of the day (e.g., 9:00-9:30 AM). + + By setting ``start_time`` to be later than ``end_time``, you can get + the times that are *not* between the two times. + + Parameters + ---------- + start_time : datetime.time or str + Initial time as a time filter limit. + end_time : datetime.time or str + End time as a time filter limit. + include_start : bool, default True + Whether the start time needs to be included in the result. + include_end : bool, default True + Whether the end time needs to be included in the result. + + Returns + ------- + Series or DataFrame + Data from the original object filtered to the specified + time range. + + Raises + ------ + TypeError + If the index is not a :class:`~cudf.DatetimeIndex`. + + Examples + -------- + >>> import cudf + >>> i = cudf.date_range('2018-04-09', periods=4, freq='1D20min') + >>> ts = cudf.DataFrame({'A': [1, 2, 3, 4]}, index=i) + >>> ts + A + 2018-04-09 00:00:00 1 + 2018-04-10 00:20:00 2 + 2018-04-11 00:40:00 3 + 2018-04-12 01:00:00 4 + >>> ts.between_time('0:15', '0:45') + A + 2018-04-10 00:20:00 2 + 2018-04-11 00:40:00 3 + """ + from pandas.core.tools.times import to_time + + if not isinstance(self.index, cudf.DatetimeIndex): + raise TypeError("Index must be DatetimeIndex") + + start_time = to_time(start_time) + end_time = to_time(end_time) + + def _time_to_seconds(t): + return t.hour * 3600 + t.minute * 60 + t.second + + start_secs = _time_to_seconds(start_time) + end_secs = _time_to_seconds(end_time) + + idx = self.index + row_secs = idx.hour * 3600 + idx.minute * 60 + idx.second + + left_op = operator.ge if include_start else operator.gt + right_op = operator.le if include_end else operator.lt + + if start_secs <= end_secs: + mask = left_op(row_secs, start_secs) & right_op( + row_secs, end_secs + ) + else: + mask = left_op(row_secs, start_secs) | right_op( + row_secs, end_secs + ) + + return self[mask] + @property def loc(self): From 1b7e890eef82f624ca800ad01aaa9270ddf8aa50 Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 02:11:59 +0530 Subject: [PATCH 02/21] Add between_time and at_time methods --- python/cudf/cudf/core/indexed_frame.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index a8f497137cbb..b45fe2d22b14 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2422,14 +2422,13 @@ def truncate(self, before=None, after=None, axis=0, copy=True): slicer = [slice(None, None)] * self.ndim slicer[axis] = slice(before, after) return self.loc[tuple(slicer)].copy() - + @_performance_tracking def between_time( self, start_time, end_time, - include_start: bool = True, - include_end: bool = True, + inclusive: str = "both", ) -> Self: """ Select values between particular times of the day (e.g., 9:00-9:30 AM). @@ -2443,10 +2442,8 @@ def between_time( Initial time as a time filter limit. end_time : datetime.time or str End time as a time filter limit. - include_start : bool, default True - Whether the start time needs to be included in the result. - include_end : bool, default True - Whether the end time needs to be included in the result. + inclusive : {"both", "neither", "left", "right"}, default "both" + Include boundaries; whether to set each bound as closed or open. Returns ------- @@ -2480,6 +2477,14 @@ def between_time( if not isinstance(self.index, cudf.DatetimeIndex): raise TypeError("Index must be DatetimeIndex") + if inclusive not in {"both", "neither", "left", "right"}: + raise ValueError( + "Inclusive has to be either 'both', 'neither', " + "'left' or 'right'" + ) + include_start = inclusive in {"both", "left"} + include_end = inclusive in {"both", "right"} + start_time = to_time(start_time) end_time = to_time(end_time) From 684efcc897f9efb8f64d82cdfdbcca35c9aa78ce Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 02:25:04 +0530 Subject: [PATCH 03/21] Add at_time method --- python/cudf/cudf/core/indexed_frame.py | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index b45fe2d22b14..47de721cfba5 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2510,6 +2510,57 @@ def _time_to_seconds(t): ) return self[mask] + + + @_performance_tracking + def at_time(self, time, axis: int = 0) -> Self: + """ + Select values at particular time of day (e.g., 9:30AM). + + Parameters + ---------- + time : datetime.time or str + axis : {0 or 'index', 1 or 'columns'}, default 0 + + Returns + ------- + Series or DataFrame + + Raises + ------ + TypeError + If the index is not a :class:`~cudf.DatetimeIndex`. + + Examples + -------- + >>> import cudf + >>> i = cudf.date_range('2018-04-09', periods=4, freq='12h') + >>> ts = cudf.DataFrame({'A': [1, 2, 3, 4]}, index=i) + >>> ts + A + 2018-04-09 00:00:00 1 + 2018-04-09 12:00:00 2 + 2018-04-10 00:00:00 3 + 2018-04-10 12:00:00 4 + >>> ts.at_time('12:00') + A + 2018-04-09 12:00:00 2 + 2018-04-10 12:00:00 4 + """ + from pandas.core.tools.times import to_time + + if not isinstance(self.index, cudf.DatetimeIndex): + raise TypeError("Index must be DatetimeIndex") + if self._get_axis_from_axis_arg(axis) != 0: + raise NotImplementedError("Only axis=0 is supported.") + + time = to_time(time) + target_secs = time.hour * 3600 + time.minute * 60 + time.second + + idx = self.index + row_secs = idx.hour * 3600 + idx.minute * 60 + idx.second + + return self[row_secs == target_secs] @property From 71a301eecaff96f5c6a9516013d200f26cda1226 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:13:03 +0000 Subject: [PATCH 04/21] [pre-commit.ci] auto code formatting --- python/cudf/cudf/core/indexed_frame.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 47de721cfba5..c70e1db78397 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -5,8 +5,8 @@ from __future__ import annotations import copy -import operator import itertools +import operator import textwrap import warnings from collections import Counter @@ -2501,17 +2501,12 @@ def _time_to_seconds(t): right_op = operator.le if include_end else operator.lt if start_secs <= end_secs: - mask = left_op(row_secs, start_secs) & right_op( - row_secs, end_secs - ) + mask = left_op(row_secs, start_secs) & right_op(row_secs, end_secs) else: - mask = left_op(row_secs, start_secs) | right_op( - row_secs, end_secs - ) + mask = left_op(row_secs, start_secs) | right_op(row_secs, end_secs) return self[mask] - @_performance_tracking def at_time(self, time, axis: int = 0) -> Self: """ @@ -2561,7 +2556,6 @@ def at_time(self, time, axis: int = 0) -> Self: row_secs = idx.hour * 3600 + idx.minute * 60 + idx.second return self[row_secs == target_secs] - @property def loc(self): From 355ccdb4d5cde08f14202a433b9a59571b4eb6da Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 12:13:33 +0530 Subject: [PATCH 05/21] Fix int16 overflow in between_time/at_time row_secs calculation Cast idx.hour/minute/second to int32 before multiplying, since cuDF returns int16 for these which overflows for times after ~9:06 AM (12:00 -> 43200 seconds exceeds int16 max of 32767). Verified fix against real cuDF DatetimeIndex in Colab; between_time and at_time now match pandas expected output including the 12:00 edge case. --- python/cudf/cudf/core/indexed_frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index c70e1db78397..478ea96b83d3 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2495,7 +2495,7 @@ def _time_to_seconds(t): end_secs = _time_to_seconds(end_time) idx = self.index - row_secs = idx.hour * 3600 + idx.minute * 60 + idx.second + row_secs = idx.hour.astype('int32') * 3600 + idx.minute.astype('int32') * 60 + idx.second.astype('int32') left_op = operator.ge if include_start else operator.gt right_op = operator.le if include_end else operator.lt @@ -2553,7 +2553,7 @@ def at_time(self, time, axis: int = 0) -> Self: target_secs = time.hour * 3600 + time.minute * 60 + time.second idx = self.index - row_secs = idx.hour * 3600 + idx.minute * 60 + idx.second + row_secs = idx.hour.astype('int32') * 3600 + idx.minute.astype('int32') * 60 + idx.second.astype('int32') return self[row_secs == target_secs] From f58617c0dd82aa6d268571c58203adb80fb04983 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 06:48:48 +0000 Subject: [PATCH 06/21] [pre-commit.ci] auto code formatting --- python/cudf/cudf/core/indexed_frame.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 478ea96b83d3..87e1f401d1a8 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2495,7 +2495,11 @@ def _time_to_seconds(t): end_secs = _time_to_seconds(end_time) idx = self.index - row_secs = idx.hour.astype('int32') * 3600 + idx.minute.astype('int32') * 60 + idx.second.astype('int32') + row_secs = ( + idx.hour.astype("int32") * 3600 + + idx.minute.astype("int32") * 60 + + idx.second.astype("int32") + ) left_op = operator.ge if include_start else operator.gt right_op = operator.le if include_end else operator.lt @@ -2553,7 +2557,11 @@ def at_time(self, time, axis: int = 0) -> Self: target_secs = time.hour * 3600 + time.minute * 60 + time.second idx = self.index - row_secs = idx.hour.astype('int32') * 3600 + idx.minute.astype('int32') * 60 + idx.second.astype('int32') + row_secs = ( + idx.hour.astype("int32") * 3600 + + idx.minute.astype("int32") * 60 + + idx.second.astype("int32") + ) return self[row_secs == target_secs] From 4c86d6a95983747f6ff89f40abf46c4d656fef60 Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 13:52:30 +0530 Subject: [PATCH 07/21] Fix fractional-second precision in between_time/at_time; add axis=1 guard in at_time --- python/cudf/cudf/core/indexed_frame.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 87e1f401d1a8..c7038e4f64d0 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2489,16 +2489,15 @@ def between_time( end_time = to_time(end_time) def _time_to_seconds(t): - return t.hour * 3600 + t.minute * 60 + t.second + return (t.hour * 3600 + t.minute * 60 + t.second) * 1_000_000 + t.microsecond start_secs = _time_to_seconds(start_time) end_secs = _time_to_seconds(end_time) idx = self.index row_secs = ( - idx.hour.astype("int32") * 3600 - + idx.minute.astype("int32") * 60 - + idx.second.astype("int32") + (idx.hour.astype('int64') * 3600 + idx.minute.astype('int64') * 60 + idx.second.astype('int64')) * 1_000_000 + + idx.microsecond.astype('int64') ) left_op = operator.ge if include_start else operator.gt @@ -2550,17 +2549,27 @@ def at_time(self, time, axis: int = 0) -> Self: if not isinstance(self.index, cudf.DatetimeIndex): raise TypeError("Index must be DatetimeIndex") + if axis in (1, "columns"): + raise NotImplementedError("Only axis=0 is supported.") if self._get_axis_from_axis_arg(axis) != 0: raise NotImplementedError("Only axis=0 is supported.") + time = to_time(time) - target_secs = time.hour * 3600 + time.minute * 60 + time.second + target_secs = ( + time.hour * 3600 + time.minute * 60 + time.second + ) * 1_000_000 + time.microsecond idx = self.index row_secs = ( +<<<<<<< HEAD idx.hour.astype("int32") * 3600 + idx.minute.astype("int32") * 60 + idx.second.astype("int32") +======= + (idx.hour.astype('int64') * 3600 + idx.minute.astype('int64') * 60 + idx.second.astype('int64')) * 1_000_000 + + idx.microsecond.astype('int64') +>>>>>>> 059fe66b31 (Fix fractional-second precision in between_time/at_time; add axis=1 guard in at_time) ) return self[row_secs == target_secs] From c42cadfb18b0b99b84ed841086b427db7c99a966 Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 13:57:30 +0530 Subject: [PATCH 08/21] Remove leftover merge conflict markers --- python/cudf/cudf/core/indexed_frame.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index c7038e4f64d0..257c0ebe3ff6 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2562,14 +2562,8 @@ def at_time(self, time, axis: int = 0) -> Self: idx = self.index row_secs = ( -<<<<<<< HEAD - idx.hour.astype("int32") * 3600 - + idx.minute.astype("int32") * 60 - + idx.second.astype("int32") -======= (idx.hour.astype('int64') * 3600 + idx.minute.astype('int64') * 60 + idx.second.astype('int64')) * 1_000_000 + idx.microsecond.astype('int64') ->>>>>>> 059fe66b31 (Fix fractional-second precision in between_time/at_time; add axis=1 guard in at_time) ) return self[row_secs == target_secs] From bb4fddad1fa6c57c60dc26090058fc0a5628118b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:31:06 +0000 Subject: [PATCH 09/21] [pre-commit.ci] auto code formatting --- python/cudf/cudf/core/indexed_frame.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 257c0ebe3ff6..331f75080242 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2489,16 +2489,19 @@ def between_time( end_time = to_time(end_time) def _time_to_seconds(t): - return (t.hour * 3600 + t.minute * 60 + t.second) * 1_000_000 + t.microsecond + return ( + t.hour * 3600 + t.minute * 60 + t.second + ) * 1_000_000 + t.microsecond start_secs = _time_to_seconds(start_time) end_secs = _time_to_seconds(end_time) idx = self.index row_secs = ( - (idx.hour.astype('int64') * 3600 + idx.minute.astype('int64') * 60 + idx.second.astype('int64')) * 1_000_000 - + idx.microsecond.astype('int64') - ) + idx.hour.astype("int64") * 3600 + + idx.minute.astype("int64") * 60 + + idx.second.astype("int64") + ) * 1_000_000 + idx.microsecond.astype("int64") left_op = operator.ge if include_start else operator.gt right_op = operator.le if include_end else operator.lt @@ -2554,7 +2557,6 @@ def at_time(self, time, axis: int = 0) -> Self: if self._get_axis_from_axis_arg(axis) != 0: raise NotImplementedError("Only axis=0 is supported.") - time = to_time(time) target_secs = ( time.hour * 3600 + time.minute * 60 + time.second @@ -2562,9 +2564,10 @@ def at_time(self, time, axis: int = 0) -> Self: idx = self.index row_secs = ( - (idx.hour.astype('int64') * 3600 + idx.minute.astype('int64') * 60 + idx.second.astype('int64')) * 1_000_000 - + idx.microsecond.astype('int64') - ) + idx.hour.astype("int64") * 3600 + + idx.minute.astype("int64") * 60 + + idx.second.astype("int64") + ) * 1_000_000 + idx.microsecond.astype("int64") return self[row_secs == target_secs] From a73cf2a7ed13af3ec831448696e8c377e0954bf2 Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 14:17:52 +0530 Subject: [PATCH 10/21] Add unit tests for between_time and at_time --- .../methods/test_between_time_at_time.py | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py diff --git a/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py b/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py new file mode 100644 index 000000000000..c50afb1ce78a --- /dev/null +++ b/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py @@ -0,0 +1,94 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +import cudf +from cudf.testing import assert_eq + + +def _make_frame(): + dates = cudf.date_range("2018-04-09", periods=4, freq="1D20min") + df = cudf.DataFrame({"A": [1, 2, 3, 4]}, index=dates) + return df, df.to_pandas() + + +def test_between_time_basic(): + gdf, pdf = _make_frame() + expected = pdf.between_time("0:15", "0:45") + actual = gdf.between_time("0:15", "0:45") + assert_eq(actual, expected) + + +@pytest.mark.parametrize("inclusive", ["both", "neither", "left", "right"]) +def test_between_time_inclusive_modes(inclusive): + gdf, pdf = _make_frame() + expected = pdf.between_time("0:15", "0:45", inclusive=inclusive) + actual = gdf.between_time("0:15", "0:45", inclusive=inclusive) + assert_eq(actual, expected) + + +def test_between_time_wraparound(): + gdf, pdf = _make_frame() + expected = pdf.between_time("0:45", "0:15") + actual = gdf.between_time("0:45", "0:15") + assert_eq(actual, expected) + + +def test_between_time_fractional_seconds(): + dates = cudf.date_range( + "2021-01-01 00:00:00", periods=5, freq="500ms" + ) + gdf = cudf.DataFrame({"A": range(5)}, index=dates) + pdf = gdf.to_pandas() + + expected = pdf.between_time("00:00:00.250", "00:00:01.750") + actual = gdf.between_time("00:00:00.250", "00:00:01.750") + assert_eq(actual, expected) + + +def test_between_time_invalid_index(): + gdf = cudf.DataFrame({"A": [1, 2, 3]}) + with pytest.raises(TypeError): + gdf.between_time("0:15", "0:45") + + +def test_between_time_invalid_inclusive(): + gdf, _ = _make_frame() + with pytest.raises(ValueError): + gdf.between_time("0:15", "0:45", inclusive="oops") + + +def test_at_time_basic(): + dates = cudf.date_range("2018-04-09", periods=4, freq="12h") + gdf = cudf.DataFrame({"A": [1, 2, 3, 4]}, index=dates) + pdf = gdf.to_pandas() + + expected = pdf.at_time("12:00") + actual = gdf.at_time("12:00") + assert_eq(actual, expected) + + +def test_at_time_fractional_seconds(): + dates = cudf.date_range( + "2021-01-01 00:00:00", periods=5, freq="500ms" + ) + gdf = cudf.DataFrame({"A": range(5)}, index=dates) + pdf = gdf.to_pandas() + + expected = pdf.at_time("00:00:01") + actual = gdf.at_time("00:00:01") + assert_eq(actual, expected) + + +def test_at_time_invalid_index(): + gdf = cudf.DataFrame({"A": [1, 2, 3]}) + with pytest.raises(TypeError): + gdf.at_time("12:00") + + +def test_at_time_invalid_axis(): + dates = cudf.date_range("2018-04-09", periods=4, freq="12h") + gdf = cudf.DataFrame({"A": [1, 2, 3, 4]}, index=dates) + with pytest.raises(NotImplementedError): + gdf.at_time("12:00", axis=1) \ No newline at end of file From e1f60269ec673c625c068da25a75def154c2e19e Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 14:24:02 +0530 Subject: [PATCH 11/21] Fill NaT rows with sentinel value to fix between_time null handling --- python/cudf/cudf/core/indexed_frame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 331f75080242..bafe4acdafe8 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2502,6 +2502,7 @@ def _time_to_seconds(t): + idx.minute.astype("int64") * 60 + idx.second.astype("int64") ) * 1_000_000 + idx.microsecond.astype("int64") + row_secs = row_secs.fillna(-1) left_op = operator.ge if include_start else operator.gt right_op = operator.le if include_end else operator.lt From ab70d06d8c6e48128f022a7d47a8aa5d42bbac75 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:57:06 +0000 Subject: [PATCH 12/21] [pre-commit.ci] auto code formatting --- .../dataframe/methods/test_between_time_at_time.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py b/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py index c50afb1ce78a..edfb79c34140 100644 --- a/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py +++ b/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import pytest @@ -36,9 +36,7 @@ def test_between_time_wraparound(): def test_between_time_fractional_seconds(): - dates = cudf.date_range( - "2021-01-01 00:00:00", periods=5, freq="500ms" - ) + dates = cudf.date_range("2021-01-01 00:00:00", periods=5, freq="500ms") gdf = cudf.DataFrame({"A": range(5)}, index=dates) pdf = gdf.to_pandas() @@ -70,9 +68,7 @@ def test_at_time_basic(): def test_at_time_fractional_seconds(): - dates = cudf.date_range( - "2021-01-01 00:00:00", periods=5, freq="500ms" - ) + dates = cudf.date_range("2021-01-01 00:00:00", periods=5, freq="500ms") gdf = cudf.DataFrame({"A": range(5)}, index=dates) pdf = gdf.to_pandas() @@ -91,4 +87,4 @@ def test_at_time_invalid_axis(): dates = cudf.date_range("2018-04-09", periods=4, freq="12h") gdf = cudf.DataFrame({"A": [1, 2, 3, 4]}, index=dates) with pytest.raises(NotImplementedError): - gdf.at_time("12:00", axis=1) \ No newline at end of file + gdf.at_time("12:00", axis=1) From 7f088590f850090a03c70ab5f368e09ce43e66f5 Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 23:39:43 +0530 Subject: [PATCH 13/21] Reuse indexer_between_time; support axis=None per pandas signature --- python/cudf/cudf/core/indexed_frame.py | 190 +++++++++++-------------- 1 file changed, 81 insertions(+), 109 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index bafe4acdafe8..c38f4eb0d613 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2423,106 +2423,90 @@ def truncate(self, before=None, after=None, axis=0, copy=True): slicer[axis] = slice(before, after) return self.loc[tuple(slicer)].copy() - @_performance_tracking - def between_time( - self, - start_time, - end_time, - inclusive: str = "both", - ) -> Self: - """ - Select values between particular times of the day (e.g., 9:00-9:30 AM). - - By setting ``start_time`` to be later than ``end_time``, you can get - the times that are *not* between the two times. - - Parameters - ---------- - start_time : datetime.time or str - Initial time as a time filter limit. - end_time : datetime.time or str - End time as a time filter limit. - inclusive : {"both", "neither", "left", "right"}, default "both" - Include boundaries; whether to set each bound as closed or open. - - Returns - ------- - Series or DataFrame - Data from the original object filtered to the specified - time range. - - Raises - ------ - TypeError - If the index is not a :class:`~cudf.DatetimeIndex`. - - Examples - -------- - >>> import cudf - >>> i = cudf.date_range('2018-04-09', periods=4, freq='1D20min') - >>> ts = cudf.DataFrame({'A': [1, 2, 3, 4]}, index=i) - >>> ts - A - 2018-04-09 00:00:00 1 - 2018-04-10 00:20:00 2 - 2018-04-11 00:40:00 3 - 2018-04-12 01:00:00 4 - >>> ts.between_time('0:15', '0:45') - A - 2018-04-10 00:20:00 2 - 2018-04-11 00:40:00 3 - """ - from pandas.core.tools.times import to_time - - if not isinstance(self.index, cudf.DatetimeIndex): - raise TypeError("Index must be DatetimeIndex") - - if inclusive not in {"both", "neither", "left", "right"}: - raise ValueError( - "Inclusive has to be either 'both', 'neither', " - "'left' or 'right'" + @_performance_tracking + def between_time( + self, + start_time, + end_time, + inclusive: str = "both", + axis: Axis | None = None, + ) -> Self: + """ + Select values between particular times of the day (e.g., 9:00-9:30 AM). + + By setting ``start_time`` to be later than ``end_time``, you can get + the times that are *not* between the two times. + + Parameters + ---------- + start_time : datetime.time or str + Initial time as a time filter limit. + end_time : datetime.time or str + End time as a time filter limit. + inclusive : {"both", "neither", "left", "right"}, default "both" + Include boundaries; whether to set each bound as closed or open. + axis : {0 or 'index'}, None, default None + Axis on which to select. Only axis=0/'index' (rows) is supported. + + Returns + ------- + Series or DataFrame + Data from the original object filtered to the specified + time range. + + Raises + ------ + TypeError + If the index is not a :class:`~cudf.DatetimeIndex`. + + Examples + -------- + >>> import cudf + >>> i = cudf.date_range('2018-04-09', periods=4, freq='1D20min') + >>> ts = cudf.DataFrame({'A': [1, 2, 3, 4]}, index=i) + >>> ts + A + 2018-04-09 00:00:00 1 + 2018-04-10 00:20:00 2 + 2018-04-11 00:40:00 3 + 2018-04-12 01:00:00 4 + >>> ts.between_time('0:15', '0:45') + A + 2018-04-10 00:20:00 2 + 2018-04-11 00:40:00 3 + """ + if axis is not None and axis not in (0, "index"): + raise NotImplementedError("Only axis=0 is supported.") + + if not isinstance(self.index, cudf.DatetimeIndex): + raise TypeError("Index must be DatetimeIndex") + + if inclusive not in {"both", "neither", "left", "right"}: + raise ValueError( + "Inclusive has to be either 'both', 'neither', " + "'left' or 'right'" + ) + include_start = inclusive in {"both", "left"} + include_end = inclusive in {"both", "right"} + + indexer = self.index.indexer_between_time( + start_time, + end_time, + include_start=include_start, + include_end=include_end, ) - include_start = inclusive in {"both", "left"} - include_end = inclusive in {"both", "right"} - - start_time = to_time(start_time) - end_time = to_time(end_time) - - def _time_to_seconds(t): - return ( - t.hour * 3600 + t.minute * 60 + t.second - ) * 1_000_000 + t.microsecond - - start_secs = _time_to_seconds(start_time) - end_secs = _time_to_seconds(end_time) - - idx = self.index - row_secs = ( - idx.hour.astype("int64") * 3600 - + idx.minute.astype("int64") * 60 - + idx.second.astype("int64") - ) * 1_000_000 + idx.microsecond.astype("int64") - row_secs = row_secs.fillna(-1) - - left_op = operator.ge if include_start else operator.gt - right_op = operator.le if include_end else operator.lt - - if start_secs <= end_secs: - mask = left_op(row_secs, start_secs) & right_op(row_secs, end_secs) - else: - mask = left_op(row_secs, start_secs) | right_op(row_secs, end_secs) - - return self[mask] + return self.iloc[indexer] @_performance_tracking - def at_time(self, time, axis: int = 0) -> Self: + def at_time(self, time, axis: Axis | None = None) -> Self: """ Select values at particular time of day (e.g., 9:30AM). Parameters ---------- time : datetime.time or str - axis : {0 or 'index', 1 or 'columns'}, default 0 + axis : {0 or 'index'}, None, default None + Axis on which to select. Only axis=0/'index' (rows) is supported. Returns ------- @@ -2549,28 +2533,16 @@ def at_time(self, time, axis: int = 0) -> Self: 2018-04-09 12:00:00 2 2018-04-10 12:00:00 4 """ - from pandas.core.tools.times import to_time + if axis is not None and axis not in (0, "index"): + raise NotImplementedError("Only axis=0 is supported.") if not isinstance(self.index, cudf.DatetimeIndex): raise TypeError("Index must be DatetimeIndex") - if axis in (1, "columns"): - raise NotImplementedError("Only axis=0 is supported.") - if self._get_axis_from_axis_arg(axis) != 0: - raise NotImplementedError("Only axis=0 is supported.") - - time = to_time(time) - target_secs = ( - time.hour * 3600 + time.minute * 60 + time.second - ) * 1_000_000 + time.microsecond - idx = self.index - row_secs = ( - idx.hour.astype("int64") * 3600 - + idx.minute.astype("int64") * 60 - + idx.second.astype("int64") - ) * 1_000_000 + idx.microsecond.astype("int64") - - return self[row_secs == target_secs] + indexer = self.index.indexer_between_time( + time, time, include_start=True, include_end=True + ) + return self.iloc[indexer] @property def loc(self): From b2e2494b6e4bd5de332b7795e6cdde7fab952f16 Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Wed, 2 Sep 2026 23:47:47 +0530 Subject: [PATCH 14/21] Remove obsolete between_time skip entries now that it's implemented --- python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py b/python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py index f9249b5acef9..8be9cfd75125 100644 --- a/python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py +++ b/python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py @@ -550,8 +550,6 @@ def pytest_unconfigure(config): "tests/copy_view/test_methods.py::test_align_with_series_copy_false": "TODO: Add a reason for failure", "tests/copy_view/test_methods.py::test_assign_drop_duplicates[assign]": "TODO: Add a reason for failure", "tests/copy_view/test_methods.py::test_assign_drop_duplicates[drop_duplicates]": "TODO: Add a reason for failure", - "tests/copy_view/test_methods.py::test_between_time[obj0]": "TODO: Add a reason for failure", - "tests/copy_view/test_methods.py::test_between_time[obj1]": "TODO: Add a reason for failure", "tests/copy_view/test_methods.py::test_chained_methods[reset_index]": "TODO: Add a reason for failure", "tests/copy_view/test_methods.py::test_chained_where_mask[mask]": "TODO: Add a reason for failure", "tests/copy_view/test_methods.py::test_chained_where_mask[where]": "TODO: Add a reason for failure", From 17d685786609b9aa03e3bb09cfc155148ac20a7f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 18:27:27 +0000 Subject: [PATCH 15/21] [pre-commit.ci] auto code formatting --- python/cudf/cudf/core/indexed_frame.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index c38f4eb0d613..6e55ed135f23 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -6,7 +6,6 @@ import copy import itertools -import operator import textwrap import warnings from collections import Counter From 063cc4a9b84963e554a87c49d9bd66b1789f06dc Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Fri, 4 Sep 2026 00:18:25 +0530 Subject: [PATCH 16/21] Fix indentation bug in between_time method --- python/cudf/cudf/core/indexed_frame.py | 142 ++++++++++++------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 6e55ed135f23..0fe2b563b05a 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2422,79 +2422,79 @@ def truncate(self, before=None, after=None, axis=0, copy=True): slicer[axis] = slice(before, after) return self.loc[tuple(slicer)].copy() - @_performance_tracking - def between_time( - self, + @_performance_tracking + def between_time( + self, + start_time, + end_time, + inclusive: str = "both", + axis: Axis | None = None, + ) -> Self: + """ + Select values between particular times of the day (e.g., 9:00-9:30 AM). + + By setting ``start_time`` to be later than ``end_time``, you can get + the times that are *not* between the two times. + + Parameters + ---------- + start_time : datetime.time or str + Initial time as a time filter limit. + end_time : datetime.time or str + End time as a time filter limit. + inclusive : {"both", "neither", "left", "right"}, default "both" + Include boundaries; whether to set each bound as closed or open. + axis : {0 or 'index'}, None, default None + Axis on which to select. Only axis=0/'index' (rows) is supported. + + Returns + ------- + Series or DataFrame + Data from the original object filtered to the specified + time range. + + Raises + ------ + TypeError + If the index is not a :class:`~cudf.DatetimeIndex`. + + Examples + -------- + >>> import cudf + >>> i = cudf.date_range('2018-04-09', periods=4, freq='1D20min') + >>> ts = cudf.DataFrame({'A': [1, 2, 3, 4]}, index=i) + >>> ts + A + 2018-04-09 00:00:00 1 + 2018-04-10 00:20:00 2 + 2018-04-11 00:40:00 3 + 2018-04-12 01:00:00 4 + >>> ts.between_time('0:15', '0:45') + A + 2018-04-10 00:20:00 2 + 2018-04-11 00:40:00 3 + """ + if axis is not None and axis not in (0, "index"): + raise NotImplementedError("Only axis=0 is supported.") + + if not isinstance(self.index, cudf.DatetimeIndex): + raise TypeError("Index must be DatetimeIndex") + + if inclusive not in {"both", "neither", "left", "right"}: + raise ValueError( + "Inclusive has to be either 'both', 'neither', " + "'left' or 'right'" + ) + include_start = inclusive in {"both", "left"} + include_end = inclusive in {"both", "right"} + + indexer = self.index.indexer_between_time( start_time, end_time, - inclusive: str = "both", - axis: Axis | None = None, - ) -> Self: - """ - Select values between particular times of the day (e.g., 9:00-9:30 AM). - - By setting ``start_time`` to be later than ``end_time``, you can get - the times that are *not* between the two times. - - Parameters - ---------- - start_time : datetime.time or str - Initial time as a time filter limit. - end_time : datetime.time or str - End time as a time filter limit. - inclusive : {"both", "neither", "left", "right"}, default "both" - Include boundaries; whether to set each bound as closed or open. - axis : {0 or 'index'}, None, default None - Axis on which to select. Only axis=0/'index' (rows) is supported. - - Returns - ------- - Series or DataFrame - Data from the original object filtered to the specified - time range. - - Raises - ------ - TypeError - If the index is not a :class:`~cudf.DatetimeIndex`. - - Examples - -------- - >>> import cudf - >>> i = cudf.date_range('2018-04-09', periods=4, freq='1D20min') - >>> ts = cudf.DataFrame({'A': [1, 2, 3, 4]}, index=i) - >>> ts - A - 2018-04-09 00:00:00 1 - 2018-04-10 00:20:00 2 - 2018-04-11 00:40:00 3 - 2018-04-12 01:00:00 4 - >>> ts.between_time('0:15', '0:45') - A - 2018-04-10 00:20:00 2 - 2018-04-11 00:40:00 3 - """ - if axis is not None and axis not in (0, "index"): - raise NotImplementedError("Only axis=0 is supported.") - - if not isinstance(self.index, cudf.DatetimeIndex): - raise TypeError("Index must be DatetimeIndex") - - if inclusive not in {"both", "neither", "left", "right"}: - raise ValueError( - "Inclusive has to be either 'both', 'neither', " - "'left' or 'right'" - ) - include_start = inclusive in {"both", "left"} - include_end = inclusive in {"both", "right"} - - indexer = self.index.indexer_between_time( - start_time, - end_time, - include_start=include_start, - include_end=include_end, - ) - return self.iloc[indexer] + include_start=include_start, + include_end=include_end, + ) + return self.iloc[indexer] @_performance_tracking def at_time(self, time, axis: Axis | None = None) -> Self: From 07c7f4b228f024629bfb1d3e7de08f5d54a8aceb Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Fri, 4 Sep 2026 00:29:10 +0530 Subject: [PATCH 17/21] Add Series tests for between_time/at_time; convert DataFrame test helper to pytest fixture --- .../methods/test_between_time_at_time.py | 27 ++++++++-------- .../methods/test_between_time_at_time.py | 31 +++++++++++++++++++ 2 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 python/cudf/cudf/tests/series/methods/test_between_time_at_time.py diff --git a/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py b/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py index edfb79c34140..1082cfa01bca 100644 --- a/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py +++ b/python/cudf/cudf/tests/dataframe/methods/test_between_time_at_time.py @@ -7,29 +7,30 @@ from cudf.testing import assert_eq -def _make_frame(): +@pytest.fixture +def gdf_pdf(): dates = cudf.date_range("2018-04-09", periods=4, freq="1D20min") df = cudf.DataFrame({"A": [1, 2, 3, 4]}, index=dates) return df, df.to_pandas() -def test_between_time_basic(): - gdf, pdf = _make_frame() +def test_between_time_basic(gdf_pdf): + gdf, pdf = gdf_pdf expected = pdf.between_time("0:15", "0:45") actual = gdf.between_time("0:15", "0:45") assert_eq(actual, expected) @pytest.mark.parametrize("inclusive", ["both", "neither", "left", "right"]) -def test_between_time_inclusive_modes(inclusive): - gdf, pdf = _make_frame() - expected = pdf.between_time("0:15", "0:45", inclusive=inclusive) - actual = gdf.between_time("0:15", "0:45", inclusive=inclusive) +def test_between_time_inclusive_modes(gdf_pdf, inclusive): + gdf, pdf = gdf_pdf + expected = pdf.between_time("0:20", "0:40", inclusive=inclusive) + actual = gdf.between_time("0:20", "0:40", inclusive=inclusive) assert_eq(actual, expected) -def test_between_time_wraparound(): - gdf, pdf = _make_frame() +def test_between_time_wraparound(gdf_pdf): + gdf, pdf = gdf_pdf expected = pdf.between_time("0:45", "0:15") actual = gdf.between_time("0:45", "0:15") assert_eq(actual, expected) @@ -51,8 +52,8 @@ def test_between_time_invalid_index(): gdf.between_time("0:15", "0:45") -def test_between_time_invalid_inclusive(): - gdf, _ = _make_frame() +def test_between_time_invalid_inclusive(gdf_pdf): + gdf, _ = gdf_pdf with pytest.raises(ValueError): gdf.between_time("0:15", "0:45", inclusive="oops") @@ -72,8 +73,8 @@ def test_at_time_fractional_seconds(): gdf = cudf.DataFrame({"A": range(5)}, index=dates) pdf = gdf.to_pandas() - expected = pdf.at_time("00:00:01") - actual = gdf.at_time("00:00:01") + expected = pdf.at_time("00:00:01.500") + actual = gdf.at_time("00:00:01.500") assert_eq(actual, expected) diff --git a/python/cudf/cudf/tests/series/methods/test_between_time_at_time.py b/python/cudf/cudf/tests/series/methods/test_between_time_at_time.py new file mode 100644 index 000000000000..e3a9b602f340 --- /dev/null +++ b/python/cudf/cudf/tests/series/methods/test_between_time_at_time.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +import cudf +from cudf.testing import assert_eq + + +@pytest.fixture +def gsr_psr(): + dates = cudf.date_range("2018-04-09", periods=4, freq="1D20min") + gsr = cudf.Series([1, 2, 3, 4], index=dates) + return gsr, gsr.to_pandas() + + +def test_series_between_time_basic(gsr_psr): + gsr, psr = gsr_psr + expected = psr.between_time("0:20", "0:40") + actual = gsr.between_time("0:20", "0:40") + assert_eq(actual, expected) + + +def test_series_at_time_basic(): + dates = cudf.date_range("2018-04-09", periods=4, freq="12h") + gsr = cudf.Series([1, 2, 3, 4], index=dates) + psr = gsr.to_pandas() + + expected = psr.at_time("12:00") + actual = gsr.at_time("12:00") + assert_eq(actual, expected) \ No newline at end of file From fa5feb3d044e033c46f9fab94409e2102e70f54e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 19:04:15 +0000 Subject: [PATCH 18/21] [pre-commit.ci] auto code formatting --- .../cudf/cudf/tests/series/methods/test_between_time_at_time.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cudf/cudf/tests/series/methods/test_between_time_at_time.py b/python/cudf/cudf/tests/series/methods/test_between_time_at_time.py index e3a9b602f340..9de1962f37c0 100644 --- a/python/cudf/cudf/tests/series/methods/test_between_time_at_time.py +++ b/python/cudf/cudf/tests/series/methods/test_between_time_at_time.py @@ -28,4 +28,4 @@ def test_series_at_time_basic(): expected = psr.at_time("12:00") actual = gsr.at_time("12:00") - assert_eq(actual, expected) \ No newline at end of file + assert_eq(actual, expected) From b298c1e3d09fcfb5c1ee688b007b5e10625061e5 Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Fri, 4 Sep 2026 11:50:20 +0530 Subject: [PATCH 19/21] Clear DatetimeIndex freq after between_time/at_time filtering to match pandas --- python/cudf/cudf/core/indexed_frame.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 0fe2b563b05a..2227a475959d 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2494,7 +2494,10 @@ def between_time( include_start=include_start, include_end=include_end, ) - return self.iloc[indexer] + result = self.iloc[indexer] + if isinstance(result.index, cudf.DatetimeIndex): + result.index._freq = None + return result @_performance_tracking def at_time(self, time, axis: Axis | None = None) -> Self: @@ -2541,7 +2544,10 @@ def at_time(self, time, axis: Axis | None = None) -> Self: indexer = self.index.indexer_between_time( time, time, include_start=True, include_end=True ) - return self.iloc[indexer] + result = self.iloc[indexer] + if isinstance(result.index, cudf.DatetimeIndex): + result.index._freq = None + return result @property def loc(self): From b459edd86e8643ee082a0bdcb9e3f3c28612548a Mon Sep 17 00:00:00 2001 From: Darshan R Chavan <2848darshanrc@gmail.com> Date: Mon, 7 Sep 2026 09:19:51 +0530 Subject: [PATCH 20/21] Fix freq calculation after between_time/at_time to match pandas step-based logic --- python/cudf/cudf/core/indexed_frame.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 2227a475959d..885b9720fa2c 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -184,6 +184,24 @@ ) +def _freq_after_take(original_freq, indexer): + """Return the freq that should apply after gathering rows by `indexer`. + + Matches pandas: if the positions in `indexer` form a constant step, + the new freq is step * original_freq; otherwise the new freq is None. + A result with 0 or 1 rows keeps the original freq. + """ + if original_freq is None: + return None + positions = cp.asarray(indexer) + if len(positions) <= 1: + return original_freq + diffs = cp.diff(positions) + if cp.all(diffs == diffs[0]): + return int(diffs[0]) * original_freq + return None + + def _get_unique_drop_labels(array): """Return labels to be dropped for IndexFrame.drop.""" if isinstance(array, (cudf.Series, cudf.Index, ColumnBase)): @@ -2419,7 +2437,7 @@ def truncate(self, before=None, after=None, axis=0, copy=True): before, after = after, before slicer = [slice(None, None)] * self.ndim - slicer[axis] = slice(before, after) + slicer[axis] = slice(before, after) return self.loc[tuple(slicer)].copy() @_performance_tracking @@ -2496,7 +2514,7 @@ def between_time( ) result = self.iloc[indexer] if isinstance(result.index, cudf.DatetimeIndex): - result.index._freq = None + result.index._freq = _freq_after_take(self.index._freq, indexer) return result @_performance_tracking @@ -2546,7 +2564,7 @@ def at_time(self, time, axis: Axis | None = None) -> Self: ) result = self.iloc[indexer] if isinstance(result.index, cudf.DatetimeIndex): - result.index._freq = None + result.index._freq = _freq_after_take(self.index._freq, indexer) return result @property From 8ab92e6b2d7f01823d2439a953e63cc23ddb8221 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:54:50 +0000 Subject: [PATCH 21/21] [pre-commit.ci] auto code formatting --- python/cudf/cudf/core/indexed_frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py index 885b9720fa2c..f72ac492b336 100644 --- a/python/cudf/cudf/core/indexed_frame.py +++ b/python/cudf/cudf/core/indexed_frame.py @@ -2437,7 +2437,7 @@ def truncate(self, before=None, after=None, axis=0, copy=True): before, after = after, before slicer = [slice(None, None)] * self.ndim - slicer[axis] = slice(before, after) + slicer[axis] = slice(before, after) return self.loc[tuple(slicer)].copy() @_performance_tracking