Skip to content

Commit

Permalink
Support more dtypes for CFTimeIndex resampling
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverhiggs committed Sep 4, 2024
1 parent 36b8a63 commit 977c7a4
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 19 deletions.
37 changes: 36 additions & 1 deletion xarray/coding/cftime_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -772,11 +772,18 @@ def _emit_freq_deprecation_warning(deprecated_freq):
emit_user_level_warning(message, FutureWarning)


def to_offset(freq: BaseCFTimeOffset | str, warn: bool = True) -> BaseCFTimeOffset:
def to_offset(
freq: BaseCFTimeOffset | str | timedelta | pd.Timedelta | pd.DateOffset,
warn: bool = True,
) -> BaseCFTimeOffset:
"""Convert a frequency string to the appropriate subclass of
BaseCFTimeOffset."""
if isinstance(freq, BaseCFTimeOffset):
return freq
if isinstance(freq, timedelta | pd.Timedelta):
return delta_to_tick(freq)
if isinstance(freq, pd.DateOffset):
freq = freq.freqstr

match = re.match(_PATTERN, freq)
if match is None:
Expand All @@ -791,6 +798,34 @@ def to_offset(freq: BaseCFTimeOffset | str, warn: bool = True) -> BaseCFTimeOffs
return _FREQUENCIES[freq](n=multiples)


def delta_to_tick(delta: timedelta | pd.Timedelta) -> Tick:
"""Adapted from pandas.tslib.delta_to_tick"""
if isinstance(delta, pd.Timedelta) and delta.nanoseconds != 0:
# pandas.Timedelta has nanoseconds, but these are not supported
raise ValueError(
"Unable to convert 'pandas.Timedelta' object with non-zero "
"nanoseconds to 'CFTimeOffset' object"
)
if delta.microseconds == 0:
if delta.seconds == 0:
return Day(n=delta.days)
else:
seconds = delta.days * 86400 + delta.seconds
if seconds % 3600 == 0:
return Hour(n=seconds // 3600)
elif seconds % 60 == 0:
return Minute(n=seconds // 60)
else:
return Second(n=seconds)
else:
# Regardless of the days and seconds this will always be a Millsecond
# or Microsecond object
if delta.microseconds % 1_000 == 0:
return Millisecond(n=delta.microseconds // 1_000)
else:
return Microsecond(n=delta.microseconds)


def to_cftime_datetime(date_str_or_date, calendar=None):
if cftime is None:
raise ModuleNotFoundError("No module named 'cftime'")
Expand Down
6 changes: 5 additions & 1 deletion xarray/core/dataarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -7252,7 +7252,11 @@ def resample(
offset: pd.Timedelta | datetime.timedelta | str | None = None,
origin: str | DatetimeLike = "start_day",
restore_coord_dims: bool | None = None,
**indexer_kwargs: str | Resampler,
**indexer_kwargs: str
| datetime.timedelta
| pd.Timedelta
| pd.DateOffset
| Resampler,
) -> DataArrayResample:
"""Returns a Resample object for performing resampling operations.
Expand Down
6 changes: 5 additions & 1 deletion xarray/core/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -10693,7 +10693,11 @@ def resample(
offset: pd.Timedelta | datetime.timedelta | str | None = None,
origin: str | DatetimeLike = "start_day",
restore_coord_dims: bool | None = None,
**indexer_kwargs: str | Resampler,
**indexer_kwargs: str
| datetime.timedelta
| pd.Timedelta
| pd.DateOffset
| Resampler,
) -> DatasetResample:
"""Returns a Resample object for performing resampling operations.
Expand Down
6 changes: 5 additions & 1 deletion xarray/core/resample_cftime.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ class CFTimeGrouper:

def __init__(
self,
freq: str | BaseCFTimeOffset,
freq: str
| datetime.timedelta
| pd.Timedelta
| pd.DateOffset
| BaseCFTimeOffset,
closed: SideOptions | None = None,
label: SideOptions | None = None,
origin: str | CFTimeDatetime = "start_day",
Expand Down
7 changes: 0 additions & 7 deletions xarray/groupers.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,13 +380,6 @@ def _init_properties(self, group: T_Group) -> None:
if isinstance(group_as_index, CFTimeIndex):
from xarray.core.resample_cftime import CFTimeGrouper

if not isinstance(self.freq, str | BaseCFTimeOffset):
raise ValueError(
"Resample frequency must be a string or 'BaseCFTimeOffset' "
"object when resampling a 'CFTimeIndex'. Received "
f"{type(self.freq)} instead."
)

self.index_grouper = CFTimeGrouper(
freq=self.freq,
closed=self.closed,
Expand Down
37 changes: 29 additions & 8 deletions xarray/tests/test_groupby.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,6 @@ def test_groupby_none_group_name() -> None:


def test_groupby_getitem(dataset) -> None:

assert_identical(dataset.sel(x=["a"]), dataset.groupby("x")["a"])
assert_identical(dataset.sel(z=[1]), dataset.groupby("z")[1])
assert_identical(dataset.foo.sel(x=["a"]), dataset.foo.groupby("x")["a"])
Expand Down Expand Up @@ -1829,13 +1828,12 @@ def test_resample_dtype(self, use_cftime: bool) -> None:
)
],
)
test_resample_freqs = ["10min"]
if not use_cftime:
test_resample_freqs += [
pd.Timedelta(hours=2),
pd.offsets.MonthBegin(),
datetime.timedelta(days=1, hours=6),
]
test_resample_freqs = (
"10min",
pd.Timedelta(hours=2),
pd.offsets.MonthBegin(),
datetime.timedelta(days=1, hours=6),
)
for freq in test_resample_freqs:
array.resample(time=freq)

Expand Down Expand Up @@ -2258,6 +2256,29 @@ def test_resample_and_first(self) -> None:
result = actual.reduce(method)
assert_equal(expected, result)

@pytest.mark.parametrize("use_cftime", [True, False])
def test_resample_dtype(self, use_cftime: bool) -> None:
if use_cftime and not has_cftime:
pytest.skip()
times = xr.date_range(
"2000-01-01", freq="6h", periods=10, use_cftime=use_cftime
)
ds = Dataset(
{
"foo": (["time", "x", "y"], np.random.randn(10, 5, 3)),
"bar": ("time", np.random.randn(10), {"meta": "data"}),
"time": times,
}
)
test_resample_freqs = [
"10min",
pd.Timedelta(hours=2),
pd.offsets.MonthBegin(),
datetime.timedelta(days=1, hours=6),
]
for freq in test_resample_freqs:
ds.resample(time=freq)

def test_resample_min_count(self) -> None:
times = pd.date_range("2000-01-01", freq="6h", periods=10)
ds = Dataset(
Expand Down

0 comments on commit 977c7a4

Please sign in to comment.