Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ Other API changes
the dtype of the resulting Index (:issue:`60797`)
- :class:`IncompatibleFrequency` now subclasses ``TypeError`` instead of ``ValueError``. As a result, joins with mismatched frequencies now cast to object like other non-comparable joins, and arithmetic with indexes with mismatched frequencies align (:issue:`55782`)
- :class:`Series` "flex" methods like :meth:`Series.add` no longer allow passing a :class:`DataFrame` for ``other``; use the DataFrame reversed method instead (:issue:`46179`)
- :func:`date_range` and :func:`timedelta_range` no longer default to ``unit="ns"``, instead will infer a unit from the ``start``, ``end``, and ``freq`` parameters. Explicitly specify a desired ``unit`` to override these (:issue:`59031`)
- :meth:`CategoricalIndex.append` no longer attempts to cast different-dtype indexes to the caller's dtype (:issue:`41626`)
- :meth:`ExtensionDtype.construct_array_type` is now a regular method instead of a ``classmethod`` (:issue:`58663`)
- Comparison operations between :class:`Index` and :class:`Series` now consistently return :class:`Series` regardless of which object is on the left or right (:issue:`36759`)
Expand Down
55 changes: 52 additions & 3 deletions pandas/core/indexes/datetimes.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
timezones,
to_offset,
)
from pandas._libs.tslibs.offsets import prefix_mapping
from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
from pandas._libs.tslibs.offsets import (
DateOffset,
prefix_mapping,
)
from pandas.errors import Pandas4Warning
from pandas.util._decorators import (
cache_readonly,
Expand Down Expand Up @@ -883,7 +887,7 @@ def date_range(
name: Hashable | None = None,
inclusive: IntervalClosedType = "both",
*,
unit: TimeUnit = "ns",
unit: TimeUnit | None = None,
**kwargs,
) -> DatetimeIndex:
"""
Expand Down Expand Up @@ -922,8 +926,9 @@ def date_range(
Name of the resulting DatetimeIndex.
inclusive : {"both", "neither", "left", "right"}, default "both"
Include boundaries; Whether to set each bound as closed or open.
unit : {'s', 'ms', 'us', 'ns'}, default 'ns'
unit : {'s', 'ms', 'us', 'ns', None}, default None
Specify the desired resolution of the result.
If not specified, this is inferred from the 'start', 'end', and 'freq'

.. versionadded:: 2.0.0
**kwargs
Expand Down Expand Up @@ -1062,6 +1067,50 @@ def date_range(
"""
if freq is None and com.any_none(periods, start, end):
freq = "D"
if freq is not None:
freq = to_offset(freq)

if start is NaT or end is NaT:
# This check needs to come before the `unit = start.unit` line below
raise ValueError("Neither `start` nor `end` can be NaT")

if unit is None:
# Infer the unit based on the inputs

if start is not None and end is not None:
start = Timestamp(start)
end = Timestamp(end)
if abbrev_to_npy_unit(start.unit) > abbrev_to_npy_unit(end.unit):
unit = start.unit
else:
unit = end.unit
elif start is not None:
start = Timestamp(start)
unit = start.unit
elif end is not None:
end = Timestamp(end)
unit = end.unit
else:
raise ValueError(
"Of the four parameters: start, end, periods, "
"and freq, exactly three must be specified"
)

# Last we need to watch out for cases where the 'freq' implies a higher
# unit than either start or end
if freq is not None:
creso = abbrev_to_npy_unit(unit)
if isinstance(freq, Tick):
if freq._creso > creso:
unit = freq.base.freqstr # type: ignore[assignment]
elif hasattr(freq, "offset") and freq.offset is not None:
# e.g. BDay with an offset
td = Timedelta(freq.offset)
if abbrev_to_npy_unit(td.unit) > creso:
unit = td.unit # type: ignore[assignment]
elif type(freq) is DateOffset and getattr(freq, "nanoseconds", 0) != 0:
# e.g. test_freq_dateoffset_with_relateivedelta_nanos
unit = "ns"

dtarr = DatetimeArray._generate_range(
start=start,
Expand Down
49 changes: 45 additions & 4 deletions pandas/core/indexes/timedeltas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

from __future__ import annotations

from typing import TYPE_CHECKING
from typing import (
TYPE_CHECKING,
cast,
)

from pandas._libs import (
index as libindex,
Expand All @@ -13,6 +16,7 @@
Timedelta,
to_offset,
)
from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
from pandas.util._decorators import set_module

from pandas.core.dtypes.common import (
Expand All @@ -33,6 +37,10 @@

if TYPE_CHECKING:
from pandas._libs import NaTType
from pandas._libs.tslibs import (
Day,
Tick,
)
from pandas._typing import (
DtypeObj,
TimeUnit,
Expand Down Expand Up @@ -252,7 +260,7 @@ def timedelta_range(
name=None,
closed=None,
*,
unit: TimeUnit = "ns",
unit: TimeUnit | None = None,
) -> TimedeltaIndex:
"""
Return a fixed frequency TimedeltaIndex with day as the default.
Expand All @@ -272,8 +280,9 @@ def timedelta_range(
closed : str, default None
Make the interval closed with respect to the given frequency to
the 'left', 'right', or both sides (None).
unit : {'s', 'ms', 'us', 'ns'}, default 'ns'
unit : {'s', 'ms', 'us', 'ns', None}, default None
Specify the desired resolution of the result.
If not specified, this is inferred from the 'start', 'end', and 'freq'

.. versionadded:: 2.0.0

Expand Down Expand Up @@ -337,8 +346,40 @@ def timedelta_range(
"""
if freq is None and com.any_none(periods, start, end):
freq = "D"

freq = to_offset(freq)

if com.count_not_none(start, end, periods, freq) != 3:
# This check needs to come before the `unit = start.unit` line below
raise ValueError(
"Of the four parameters: start, end, periods, "
"and freq, exactly three must be specified"
)

if unit is None:
# Infer the unit based on the inputs

if start is not None and end is not None:
start = Timedelta(start)
end = Timedelta(end)
if abbrev_to_npy_unit(start.unit) > abbrev_to_npy_unit(end.unit):
unit = cast("TimeUnit", start.unit)
else:
unit = cast("TimeUnit", end.unit)
elif start is not None:
start = Timedelta(start)
unit = cast("TimeUnit", start.unit)
else:
end = Timedelta(end)
unit = cast("TimeUnit", end.unit)

# Last we need to watch out for cases where the 'freq' implies a higher
# unit than either start or end
if freq is not None:
freq = cast("Tick | Day", freq)
creso = abbrev_to_npy_unit(unit)
if freq._creso > creso:
unit = cast("TimeUnit", freq.base.freqstr)

tdarr = TimedeltaArray._generate_range(
start, end, periods, freq, closed=closed, unit=unit
)
Expand Down
8 changes: 5 additions & 3 deletions pandas/tests/apply/test_frame_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,13 +653,15 @@ def test_apply_dict(df, dicts):

def test_apply_non_numpy_dtype():
# GH 12244
df = DataFrame({"dt": date_range("2015-01-01", periods=3, tz="Europe/Brussels")})
df = DataFrame(
{"dt": date_range("2015-01-01", periods=3, tz="Europe/Brussels", unit="ns")}
)
result = df.apply(lambda x: x)
tm.assert_frame_equal(result, df)

result = df.apply(lambda x: x + pd.Timedelta("1day"))
expected = DataFrame(
{"dt": date_range("2015-01-02", periods=3, tz="Europe/Brussels")}
{"dt": date_range("2015-01-02", periods=3, tz="Europe/Brussels", unit="ns")}
)
tm.assert_frame_equal(result, expected)

Expand Down Expand Up @@ -1425,7 +1427,7 @@ def test_nuiscance_columns():
"A": [1, 2, 3],
"B": [1.0, 2.0, 3.0],
"C": ["foo", "bar", "baz"],
"D": date_range("20130101", periods=3),
"D": date_range("20130101", periods=3, unit="ns"),
}
)

Expand Down
Loading
Loading