Skip to content

Commit 175fb17

Browse files
Fix to_datetime nanosecond inference and DatetimeIndex tz wall-time interpretation (#23141)
- `to_datetime` infers nanosecond precision for strings with more than 6 fractional-second digits, matching pandas. - `DatetimeIndex` with a timezone dtype treats timezone-naive strings/datetimes as wall time in the target timezone, while numeric data stays interpreted as UTC epoch values. - Removes 13 now-passing entries from the pandas-testing plugin. - Split out from #23138. Authors: - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Matthew Roeschke (https://github.com/mroeschke) URL: #23141
1 parent 0039691 commit 175fb17

4 files changed

Lines changed: 49 additions & 32 deletions

File tree

python/cudf/cudf/core/index.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3399,7 +3399,18 @@ def __init__(
33993399
if dtype.kind != "M":
34003400
raise TypeError("dtype must be a datetime type")
34013401
elif not isinstance(data.dtype, pd.DatetimeTZDtype):
3402-
data = data.astype(dtype)
3402+
if (
3403+
isinstance(dtype, pd.DatetimeTZDtype)
3404+
and data.dtype.kind not in "iuf"
3405+
):
3406+
# pandas interprets timezone-naive strings/datetimes as
3407+
# wall time in the target timezone (numeric data stays
3408+
# interpreted as UTC epoch values)
3409+
data = data.astype(
3410+
np.dtype(f"datetime64[{dtype.unit}]")
3411+
).tz_localize(str(dtype.tz))
3412+
else:
3413+
data = data.astype(dtype)
34033414
elif data.dtype.kind != "M":
34043415
if is_dtype_obj_string(data.dtype):
34053416
# Pandas's array_to_datetime falls back to [s] when no

python/cudf/cudf/core/tools/datetimes.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION.
1+
# SPDX-FileCopyrightText: Copyright (c) 2019-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
from __future__ import annotations
44

@@ -494,9 +494,16 @@ def _process_col(
494494
element=col.element_indexing(0),
495495
dayfirst=dayfirst,
496496
)
497+
target_unit = unit
498+
if unit is None:
499+
# pandas infers nanosecond precision from strings with
500+
# more than 6 fractional-second digits
501+
subsecond = re.search(r"%(\d)f", format)
502+
if subsecond is not None and int(subsecond.group(1)) > 6:
503+
target_unit = "ns"
497504
col = col.strptime(
498505
dtype=np.dtype(
499-
_unit_dtype_map.get(unit, _unit_dtype_map["us"]) # type: ignore[arg-type]
506+
_unit_dtype_map.get(target_unit, _unit_dtype_map["us"]) # type: ignore[arg-type]
500507
),
501508
format=format,
502509
)

python/cudf/cudf/pandas/scripts/pandas-testing-plugin.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2320,14 +2320,8 @@ def pytest_unconfigure(config):
23202320
"tests/indexes/datetimes/test_arithmetic.py::TestDatetimeIndexArithmetic::test_add_dti_day": "TypeError: unsupported operand type(s) for +: 'DatetimeArray' and 'DateOffset'",
23212321
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_coverage": "TODO: Add a reason for failure",
23222322
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_datetime64_tzformat[W-SUN]": "TODO: Add a reason for failure",
2323-
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_dtype": "AssertionError: Index are different",
23242323
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_dtype_tz_mismatch_raises": "Failed: DID NOT RAISE <class 'ValueError'>",
2325-
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_start_end_with_tz[America/Los_Angeles]": "NotImplementedError: freq is not implemented yet",
2326-
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_start_end_with_tz[None]": "NotImplementedError: freq is not implemented yet",
2327-
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_start_end_with_tz[tz2]": "NotImplementedError: freq is not implemented yet",
2328-
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_constructor_start_end_with_tz[tz3]": "NotImplementedError: freq is not implemented yet",
23292324
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_dti_from_tzaware_datetime[tz1]": "AssertionError: assert False",
2330-
"tests/indexes/datetimes/test_constructors.py::TestDatetimeIndex::test_integer_values_and_tz_interpreted_as_utc": "AssertionError: Index are different",
23312325
"tests/indexes/datetimes/test_constructors.py::TestTimeSeries::test_constructor_int64_nocopy": "TODO: Add a reason for failure",
23322326
"tests/indexes/datetimes/test_constructors.py::TestTimeSeries::test_dti_constructor_small_int[int16]": "AssertionError: Index are different",
23332327
"tests/indexes/datetimes/test_constructors.py::TestTimeSeries::test_dti_constructor_small_int[int32]": "AssertionError: Index are different",
@@ -2402,8 +2396,6 @@ def pytest_unconfigure(config):
24022396
"tests/indexes/datetimes/test_partial_slicing.py::TestSlicing::test_partial_slice_minutely": "TODO: Add a reason for failure",
24032397
"tests/indexes/datetimes/test_partial_slicing.py::TestSlicing::test_partial_slicing_dataframe": "TODO: Add a reason for failure",
24042398
"tests/indexes/datetimes/test_partial_slicing.py::TestSlicing::test_return_type_doesnt_depend_on_monotonicity": "TODO: Add a reason for failure",
2405-
"tests/indexes/datetimes/test_scalar_compat.py::TestDatetimeIndexOps::test_dti_time2[datetime64[ns, CET]]": "AssertionError: numpy array are different",
2406-
"tests/indexes/datetimes/test_scalar_compat.py::TestDatetimeIndexOps::test_dti_time2[datetime64[ns, EST]]": "AssertionError: numpy array are different",
24072399
"tests/indexes/datetimes/test_setops.py::TestDatetimeIndexSetOps::test_difference_freq[False]": "TODO: Add a reason for failure",
24082400
"tests/indexes/datetimes/test_setops.py::TestDatetimeIndexSetOps::test_difference_freq[None]": "TODO: Add a reason for failure",
24092401
"tests/indexes/datetimes/test_setops.py::TestDatetimeIndexSetOps::test_intersection[False-Asia/Tokyo]": "TODO: Add a reason for failure",
@@ -2424,10 +2416,6 @@ def pytest_unconfigure(config):
24242416
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_date_range_localize2[ns]": "assert nan == 3",
24252417
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_date_range_localize2[s]": "assert nan == 3",
24262418
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_date_range_localize2[us]": "assert nan == 3",
2427-
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_date_range_localize[ms]": "AssertionError: Index are different",
2428-
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_date_range_localize[ns]": "AssertionError: Index are different",
2429-
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_date_range_localize[s]": "AssertionError: Index are different",
2430-
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_date_range_localize[us]": "AssertionError: Index are different",
24312419
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_dti_convert_tz_aware_datetime_datetime[tz0]": "AssertionError: assert zoneinfo.ZoneInfo(key='US/Eastern') is datetime.timezone.utc",
24322420
"tests/indexes/datetimes/test_timezones.py::TestDatetimeIndexTimezones::test_dti_convert_tz_aware_datetime_datetime[tz1]": "AssertionError: assert False",
24332421
"tests/indexes/interval/test_astype.py::TestDatetimelikeSubtype::test_subtype_integer[index0-int64]": "TODO: Add a reason for failure",
@@ -3418,7 +3406,6 @@ def pytest_unconfigure(config):
34183406
"tests/reshape/concat/test_concat.py::test_concat_ignore_empty_object_float[object-datetime64[ns]]": "AssertionError: Attributes of DataFrame.iloc[:, 0] (column name='foo') are different",
34193407
"tests/reshape/concat/test_concat.py::test_concat_ignore_empty_object_float[object-float64]": "AssertionError: Attributes of DataFrame.iloc[:, 0] (column name='foo') are different",
34203408
"tests/reshape/concat/test_concat.py::test_concat_ignore_empty_object_float[object-int64]": "TODO: Add a reason for failure",
3421-
"tests/reshape/concat/test_datetimes.py::TestTimezoneConcat::test_concat_multiindex_with_tz": "AssertionError: DataFrame.index level [0] are different",
34223409
"tests/reshape/concat/test_datetimes.py::test_concat_float_datetime64": "TODO: Add a reason for failure",
34233410
"tests/reshape/concat/test_empty.py::TestEmptyConcat::test_concat_empty_dataframe_dtypes": "TODO: Add a reason for failure",
34243411
"tests/reshape/concat/test_empty.py::TestEmptyConcat::test_concat_empty_df_object_dtype": "TODO: Add a reason for failure",
@@ -3704,14 +3691,6 @@ def pytest_unconfigure(config):
37043691
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_list_like_values[True-list]": "AssertionError: Attributes of DataFrame.iloc[:, 0] (column name='('baz', 'A')') are different",
37053692
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_list_like_values_nans[False-values0]": "TODO: Add a reason for failure",
37063693
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_list_like_values_nans[True-values0]": "TODO: Add a reason for failure",
3707-
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_tz[ms-False]": "AssertionError: DataFrame.index are different",
3708-
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_tz[ms-True]": "AssertionError: DataFrame.index are different",
3709-
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_tz[ns-False]": "AssertionError: DataFrame.index are different",
3710-
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_tz[ns-True]": "AssertionError: DataFrame.index are different",
3711-
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_tz[s-False]": "AssertionError: DataFrame.index are different",
3712-
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_tz[s-True]": "AssertionError: DataFrame.index are different",
3713-
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_tz[us-False]": "AssertionError: DataFrame.index are different",
3714-
"tests/reshape/test_pivot.py::TestPivotTable::test_pivot_with_tz[us-True]": "AssertionError: DataFrame.index are different",
37153694
"tests/reshape/test_pivot_multilevel.py::test_pivot_list_like_columns[input_index1-input_columns1-values-expected_values1-expected_columns1-expected_index1]": "TODO: Add a reason for failure",
37163695
"tests/reshape/test_pivot_multilevel.py::test_pivot_list_like_columns[input_index3-input_columns3-values-expected_values3-expected_columns3-expected_index3]": "TODO: Add a reason for failure",
37173696
"tests/reshape/test_pivot_multilevel.py::test_pivot_list_like_columns[lev4-input_columns0-values-expected_values0-expected_columns0-expected_index0]": "TODO: Add a reason for failure",
@@ -4347,12 +4326,7 @@ def pytest_unconfigure(config):
43474326
"tests/test_algos.py::TestFactorize::test_factorize[tuples-False]": "AssertionError: Index are different",
43484327
"tests/test_algos.py::TestFactorize::test_factorize[tuples-True]": "AssertionError: Index are different",
43494328
"tests/test_algos.py::TestFactorize::test_int_factorize_use_na_sentinel_false[data0-expected_codes0-expected_uniques0]": "TODO: Add a reason for failure",
4350-
"tests/test_algos.py::TestUnique::test_datetime64_dtype_array_returned": "TODO: Add a reason for failure",
43514329
"tests/test_algos.py::TestUnique::test_factorize_multiindex_empty": "AssertionError: Index are different",
4352-
"tests/test_algos.py::TestUnique::test_order_of_appearance_dt64tz[ms]": "AssertionError: Index are different",
4353-
"tests/test_algos.py::TestUnique::test_order_of_appearance_dt64tz[ns]": "AssertionError: Index are different",
4354-
"tests/test_algos.py::TestUnique::test_order_of_appearance_dt64tz[s]": "AssertionError: Index are different",
4355-
"tests/test_algos.py::TestUnique::test_order_of_appearance_dt64tz[us]": "AssertionError: Index are different",
43564330
"tests/test_algos.py::TestValueCounts::test_value_counts_dropna": "TODO: Add a reason for failure",
43574331
"tests/test_algos.py::TestValueCounts::test_value_counts_stability": "AssertionError: Series.index are different",
43584332
"tests/test_col.py::test_cached_property": "AssertionError: assert False",
@@ -4466,8 +4440,6 @@ def pytest_unconfigure(config):
44664440
"tests/tools/test_to_datetime.py::TestOrigin::test_julian": "AssertionError: Attributes of Series are different",
44674441
"tests/tools/test_to_datetime.py::TestOrigin::test_to_datetime_out_of_bounds_with_format_arg[%Y-%d-%m %H:%M:%S-None]": "TODO: Add a reason for failure",
44684442
"tests/tools/test_to_datetime.py::TestOrigin::test_to_datetime_out_of_bounds_with_format_arg[%Y-%m-%d %H:%M:%S-None]": "TODO: Add a reason for failure",
4469-
"tests/tools/test_to_datetime.py::TestTimeConversionFormats::test_parse_nanoseconds_with_formula[False-2012-01-01 09:00:00.000000001]": "cudf strptime drops trailing nanosecond precision",
4470-
"tests/tools/test_to_datetime.py::TestTimeConversionFormats::test_parse_nanoseconds_with_formula[True-2012-01-01 09:00:00.000000001]": "cudf strptime drops trailing nanosecond precision",
44714443
"tests/tools/test_to_datetime.py::TestTimeConversionFormats::test_to_datetime_format_weeks[False-2013020-%Y%U%w-2013-01-13]": "AssertionError: assert Timestamp('2013-01-19 00:00:00') == Timestamp('2013-01-13 00:00:00')",
44724444
"tests/tools/test_to_datetime.py::TestTimeConversionFormats::test_to_datetime_format_weeks[True-2013020-%Y%U%w-2013-01-13]": "AssertionError: assert Timestamp('2013-01-19 00:00:00') == Timestamp('2013-01-13 00:00:00')",
44734445
"tests/tools/test_to_datetime.py::TestToDatetime::test_mixed_offsets_with_native_datetime_utc_false_raises": "assert False",

python/cudf/cudf/tests/general_functions/test_to_datetime.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33

44

@@ -407,3 +407,30 @@ def test_to_datetime_dataframe_with_ns_field_widens_to_ns():
407407
actual = cudf.to_datetime(cudf.from_pandas(df))
408408
assert actual.dtype == expected.dtype
409409
assert_eq(actual, expected)
410+
411+
412+
def test_to_datetime_nanosecond_precision_strings():
413+
# pandas infers nanosecond precision from strings with more than 6
414+
# fractional-second digits.
415+
data = ["2015-01-03T00:00:00.000000000", "2015-01-01T00:00:00.000000000"]
416+
assert_eq(pd.to_datetime(data), cudf.to_datetime(data))
417+
data_us = ["2015-01-03T00:00:00.000000", "2015-01-01T00:00:00.000000"]
418+
assert_eq(pd.to_datetime(data_us), cudf.to_datetime(data_us))
419+
420+
421+
@pytest.mark.parametrize(
422+
"data",
423+
[
424+
["2016-01-01 00:00:00"],
425+
[1451606400, 1451610000],
426+
],
427+
)
428+
def test_datetime_index_tz_dtype_wall_time(data):
429+
# Timezone-naive string data with a tz-aware dtype is interpreted as
430+
# wall time in the target timezone, while integers are interpreted as
431+
# epoch time (UTC), matching pandas.
432+
result = cudf.DatetimeIndex(
433+
data, dtype=pd.DatetimeTZDtype("s", "US/Eastern")
434+
)
435+
expected = pd.DatetimeIndex(data, dtype="datetime64[s, US/Eastern]")
436+
assert_eq(result, expected)

0 commit comments

Comments
 (0)