diff --git a/doc/source/whatsnew/v2.2.2.rst b/doc/source/whatsnew/v2.2.2.rst index 0dac3660c76b2..9c62d2be0f1fa 100644 --- a/doc/source/whatsnew/v2.2.2.rst +++ b/doc/source/whatsnew/v2.2.2.rst @@ -16,6 +16,7 @@ Fixed regressions - :meth:`DataFrame.__dataframe__` was producing incorrect data buffers when the a column's type was a pandas nullable on with missing values (:issue:`56702`) - :meth:`DataFrame.__dataframe__` was producing incorrect data buffers when the a column's type was a pyarrow nullable on with missing values (:issue:`57664`) - Avoid issuing a spurious ``DeprecationWarning`` when a custom :class:`DataFrame` or :class:`Series` subclass method is called (:issue:`57553`) +- Fixed regression in :meth:`Index.map` that would not change the dtype when the provided mapping would change data from tz-aware to tz-agnostic or tz-agnostic to tz-aware (:issue:`57192`) - Fixed regression in precision of :func:`to_datetime` with string and ``unit`` input (:issue:`57051`) .. --------------------------------------------------------------------------- diff --git a/pandas/_libs/lib.pyi b/pandas/_libs/lib.pyi index b39d32d069619..2b6fd5288450a 100644 --- a/pandas/_libs/lib.pyi +++ b/pandas/_libs/lib.pyi @@ -54,6 +54,7 @@ def is_timedelta_or_timedelta64_array( values: np.ndarray, skipna: bool = True ) -> bool: ... def is_datetime_with_singletz_array(values: np.ndarray) -> bool: ... +def is_datetime_naive_array(values: np.ndarray) -> bool: ... def is_time_array(values: np.ndarray, skipna: bool = ...): ... def is_date_array(values: np.ndarray, skipna: bool = ...): ... def is_datetime_array(values: np.ndarray, skipna: bool = ...): ... diff --git a/pandas/_libs/lib.pyx b/pandas/_libs/lib.pyx index a2205454a5a46..407b26b790128 100644 --- a/pandas/_libs/lib.pyx +++ b/pandas/_libs/lib.pyx @@ -2068,6 +2068,26 @@ def is_datetime_with_singletz_array(values: ndarray) -> bool: return True +def is_datetime_naive_array(values: ndarray) -> bool: + """ + Check values have are datetime naive. + Doesn't check values are datetime-like types. + """ + cdef: + Py_ssize_t j, n = len(values) + object tz + + if n == 0: + return False + + for j in range(n): + tz = getattr(values[j], "tzinfo", None) + if tz is not None: + return False + + return True + + @cython.internal cdef class TimedeltaValidator(TemporalValidator): cdef bint is_value_typed(self, object value) except -1: diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index d446407ec3d01..313eb2046a84f 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -290,9 +290,15 @@ def _scalar_type(self) -> type[Timestamp]: @classmethod def _from_scalars(cls, scalars, *, dtype: DtypeObj) -> Self: - if lib.infer_dtype(scalars, skipna=True) not in ["datetime", "datetime64"]: - # TODO: require any NAs be valid-for-DTA - # TODO: if dtype is passed, check for tzawareness compat? + # TODO: require any NAs be valid-for-DTA + # TODO: if dtype is passed, check for tzawareness compat? + if not lib.is_datetime64_array(scalars): + raise ValueError + elif isinstance( + dtype, DatetimeTZDtype + ) and not lib.is_datetime_with_singletz_array(scalars): + raise ValueError + elif isinstance(dtype, np.dtype) and not lib.is_datetime_naive_array(scalars): raise ValueError return cls._from_sequence(scalars, dtype=dtype) diff --git a/pandas/tests/indexes/datetimes/methods/test_map.py b/pandas/tests/indexes/datetimes/methods/test_map.py index f35f07bd32068..b012c0b79b68e 100644 --- a/pandas/tests/indexes/datetimes/methods/test_map.py +++ b/pandas/tests/indexes/datetimes/methods/test_map.py @@ -45,3 +45,16 @@ def test_index_map(self, name): ) exp_index = MultiIndex.from_product(((2018,), range(1, 7)), names=[name, name]) tm.assert_index_equal(index, exp_index) + + @pytest.mark.parametrize("input_tz", ["UTC", None]) + @pytest.mark.parametrize("output_tz", ["UTC", None]) + def test_mapping_tz_to_tz_agnostic(self, input_tz, output_tz): + # GH#57192 + index = date_range("2018-01-01", periods=6, freq="ME", tz=input_tz) + expected = date_range("2018-01-01", periods=6, freq="ME", tz=output_tz) + if input_tz == "UTC" and output_tz == "UTC": + method = "tz_convert" + else: + method = "tz_localize" + result = index.map(lambda x: getattr(x, method)(output_tz)) + tm.assert_index_equal(result, expected)