Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CFTimeIndex Resampling #2593

Merged
merged 72 commits into from
Feb 3, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
daa3a71
First implementation of resampling for CFTimeIndex.
jwenfai Nov 9, 2018
f9f3347
First implementation of resampling for CFTimeIndex, cleaned.
jwenfai Nov 9, 2018
0950505
First implementation of resampling for CFTimeIndex, cleaned.
jwenfai Nov 9, 2018
89f418a
First implementation of resampling for CFTimeIndex, cleaned.
jwenfai Nov 9, 2018
39c9d11
First implementation of resampling for CFTimeIndex.
jwenfai Nov 12, 2018
073b8e0
First implementation of resampling for CFTimeIndex,
jwenfai Nov 12, 2018
193c4c4
First implementation of resampling for CFTimeIndex, test file written.
jwenfai Nov 15, 2018
2c97738
First implementation of resampling for CFTimeIndex, test file written…
jwenfai Nov 15, 2018
9993ed9
First implementation of resampling for CFTimeIndex, test file written…
jwenfai Nov 15, 2018
f01745c
First implementation of resampling for CFTimeIndex, test file written…
jwenfai Nov 27, 2018
ffbf265
First implementation of resampling for CFTimeIndex, test file written…
jwenfai Dec 5, 2018
e64fedb
Merge pull request #1 from jwenfai/resample-v2-clean
jwenfai Dec 5, 2018
2850dd5
Docstrings for resample_cftime.py written. Upsample still not fixed.
jwenfai Dec 8, 2018
770b778
Fixed PEP8 and test parametrization.
jwenfai Dec 8, 2018
181e82c
PEP8
Zeitsperre Dec 12, 2018
5a41ee2
Merge pull request #3 from Ouranosinc/PEP8
Zeitsperre Dec 12, 2018
6b948c5
Test file fixes and other optimizations (2018-12-16 @spencerclark and…
jwenfai Dec 18, 2018
97c0948
Merge pull request #1 from Ouranosinc/master
jwenfai Dec 18, 2018
63d25ab
Merge remote-tracking branch 'origin/resample-v2-clean' into resample…
jwenfai Dec 18, 2018
05af869
Test file fixes and other optimizations (2018-12-16 @spencerclark and…
jwenfai Dec 18, 2018
85f1a84
Merge branch 'resample-v2-upsample' into resample-v2-clean
jwenfai Dec 18, 2018
2e8ced3
Merge pull request #4 from jwenfai/resample-v2-clean
Zeitsperre Dec 18, 2018
4317c69
Merge branch 'master' into master
jwenfai Dec 19, 2018
e7deeb2
Merge pull request #2 from Ouranosinc/master
jwenfai Jan 9, 2019
f9ac1a1
Merge pull request #3 from Ouranosinc/master
jwenfai Jan 9, 2019
8505ca9
_get_range_edges logic changed to emulate latest version of pandas.
jwenfai Jan 9, 2019
a495c2d
Simplified resampling logic (errors persist). Pre-cleaning.
jwenfai Jan 12, 2019
ad65ef0
Simplified resampling logic (error persists). Cleaned.
jwenfai Jan 12, 2019
5775e11
Simplified resampling logic (error persists). Fixed first_items.dropn…
jwenfai Jan 13, 2019
e1902fe
Simplified resampling logic (error persists). Logic slightly altered …
jwenfai Jan 16, 2019
f82500c
Simplified resampling logic (error persists). Logic slightly altered …
jwenfai Jan 16, 2019
80914e0
Merge pull request #5 from jwenfai/resample-v2-upsample
jwenfai Jan 16, 2019
1b3f41a
Simplified resampling logic (error persists). Cleaned. Merged with la…
jwenfai Jan 18, 2019
bc95f55
Precise cftime arithmetic. Reduced overall test time. Added test for …
jwenfai Jan 19, 2019
9fa4d51
Merge remote-tracking branch 'origin/master'
jwenfai Jan 19, 2019
5227480
Merge pull request #6 from jwenfai/master
jwenfai Jan 19, 2019
77bb2aa
Added default values for closed and label args of resample function i…
jwenfai Jan 19, 2019
be2e657
Merge pull request #7 from jwenfai/master
jwenfai Jan 19, 2019
a18161d
Added back replace['dayofwk'] = -1 to cftime_offsets.py and cftimeind…
jwenfai Jan 20, 2019
9582fbf
Merge pull request #8 from jwenfai/master
jwenfai Jan 20, 2019
5737546
Optimizations as per https://github.com/pydata/xarray/pull/2593/#pull…
jwenfai Jan 20, 2019
3268fc4
Merge pull request #9 from jwenfai/master
jwenfai Jan 20, 2019
6f38935
Simple test for non-standard calendars added and documentation updated.
jwenfai Jan 21, 2019
e7986c5
Simple test for non-standard calendars added and documentation updated.
jwenfai Jan 21, 2019
c64265f
Merge pull request #10 from jwenfai/master
jwenfai Jan 21, 2019
71f98db
Merge branch 'master' into master
jwenfai Jan 21, 2019
ec4e460
Added loffset support to CFTimeIndex resampling. Better adherence to …
jwenfai Jan 22, 2019
47b0eaa
Added loffset support to CFTimeIndex resampling. Better adherence to …
jwenfai Jan 22, 2019
f2ecaf6
Merge pull request #11 from jwenfai/master
jwenfai Jan 22, 2019
cd266c2
Support datetime.timedelta objects for loffset. Improved test coverage.
jwenfai Jan 22, 2019
41783cb
Merge pull request #12 from jwenfai/master
jwenfai Jan 22, 2019
5435910
Removed support for Python 2 compatibility.
jwenfai Jan 27, 2019
35b40fb
Merge pull request #13 from jwenfai/master
jwenfai Jan 27, 2019
814a04d
Updated pandas minversion to 0.24 as 0.24 is officially out.
jwenfai Jan 27, 2019
2a9402e
Merge branch 'pydata-master'
jwenfai Jan 27, 2019
505a0fa
Removed Python 2 support from test_cftimeindex_resample.py.
jwenfai Jan 27, 2019
9fbb016
Merge branch 'master' into master
jwenfai Jan 27, 2019
0820c3b
Merge pull request #14 from jwenfai/master
jwenfai Jan 27, 2019
89bc708
Moved full_index and first_items generation logic to a helper functio…
jwenfai Jan 29, 2019
31ccebf
Merge remote-tracking branch 'origin/master'
jwenfai Jan 29, 2019
8ac6f76
Merge pull request #15 from jwenfai/master
jwenfai Jan 29, 2019
8dbee52
Merge branch 'master' into master
shoyer Feb 1, 2019
afad30d
In groupby.py, moved s to _get_index_and_items helper function.
jwenfai Feb 1, 2019
1381dab
Removed redundant code from test_formatting.py due to bad merge.
jwenfai Feb 2, 2019
6074548
Merge pull request #16 from jwenfai/master
jwenfai Feb 2, 2019
1010264
Merge branch 'master' into master
jwenfai Feb 2, 2019
6c4b609
Merge branch 'pydata-master'
jwenfai Feb 2, 2019
59f1f94
Removed redundant test and simplify code now that dropna is implemented.
jwenfai Feb 2, 2019
db62a96
Merge branch 'master' into master
jwenfai Feb 2, 2019
6edb45a
Merge pull request #17 from jwenfai/master
jwenfai Feb 2, 2019
f7f2c38
delete unnecessary test
shoyer Feb 2, 2019
ef68960
eliminate some repetition
shoyer Feb 2, 2019
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
2 changes: 1 addition & 1 deletion xarray/backends/zarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ..core import indexing
from ..core.pycompat import OrderedDict, integer_types, iteritems
from ..core.utils import FrozenOrderedDict, HiddenKeyDict
from .common import AbstractWritableDataStore, ArrayWriter, BackendArray
from .common import AbstractWritableDataStore, BackendArray

# need some special secret attributes to tell us the dimensions
_DIMENSION_KEY = '_ARRAY_DIMENSIONS'
Expand Down
40 changes: 28 additions & 12 deletions xarray/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ def resample(self, freq=None, dim=None, how=None, skipna=None,
"""
# TODO support non-string indexer after removing the old API.

from ..coding.cftime_offsets import cftime_range
from .dataarray import DataArray
from .resample import RESAMPLE_DIM
from ..coding.cftimeindex import CFTimeIndex
Expand Down Expand Up @@ -690,20 +691,35 @@ def resample(self, freq=None, dim=None, how=None, skipna=None,
"was passed %r" % dim)

if isinstance(self.indexes[dim_name], CFTimeIndex):
raise NotImplementedError(
'Resample is currently not supported along a dimension '
'indexed by a CFTimeIndex. For certain kinds of downsampling '
'it may be possible to work around this by converting your '
'time index to a DatetimeIndex using '
'CFTimeIndex.to_datetimeindex. Use caution when doing this '
'however, because switching to a DatetimeIndex from a '
'CFTimeIndex with a non-standard calendar entails a change '
'in the calendar type, which could lead to subtle and silent '
'errors.'
)
from ..coding.cftime_offsets import to_offset
from .resample_cftime import (_get_time_bins, _offset_timedelta,
_adjust_binner_for_upsample)
offset = to_offset(freq)
times = self.indexes[dim_name]
binner, labels = _get_time_bins(self.indexes[dim_name],
offset,
closed, label, base)
if times.size > labels.size:
# if we're downsampling CFTimeIndex, do this:
if closed == 'right':
fill_method = 'bfill'
else:
fill_method = 'ffill'
binner = (pd.Series(binner, index=binner)
.reindex(times, method=fill_method))
bin_actual = np.unique(binner.values)
label_dict = dict(zip(bin_actual, labels.values))
# np.unique returns --sorted-- unique values
binner = binner.map(label_dict)
grouper = ('downsampling', pd.Index(labels), binner)
else:
# if we're upsampling CFTimeIndex, do this:
binner = _adjust_binner_for_upsample(binner, closed)
grouper = ('upsampling', pd.Index(labels), binner, closed)
else:
grouper = pd.Grouper(freq=freq, closed=closed, label=label, base=base)

group = DataArray(dim, [(dim.dims, dim)], name=RESAMPLE_DIM)
grouper = pd.Grouper(freq=freq, closed=closed, label=label, base=base)
resampler = self._resample_cls(self, group=group, dim=dim_name,
grouper=grouper,
resample_dim=RESAMPLE_DIM)
Expand Down
2 changes: 1 addition & 1 deletion xarray/core/dataarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
assert_coordinate_consistent, remap_label_indexers)
from .dataset import Dataset, merge_indexes, split_indexes
from .formatting import format_item
from .options import OPTIONS, _get_keep_attrs
from .options import OPTIONS
from .pycompat import OrderedDict, basestring, iteritems, range, zip
from .utils import (
decode_numpy_dict_values, either_dict_or_kwargs, ensure_us_time_resolution)
Expand Down
24 changes: 23 additions & 1 deletion xarray/core/groupby.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,29 @@ def __init__(self, obj, group, squeeze=False, grouper=None, bins=None,
# TODO: sort instead of raising an error
raise ValueError('index must be monotonic for resampling')
s = pd.Series(np.arange(index.size), index)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make this object s inside the helper function instead? It's not needed outside here

first_items = s.groupby(grouper).first()
if isinstance(grouper, tuple):
Copy link
Member

@spencerkclark spencerkclark Dec 29, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this logic over the last few days. With some minor modifications to _get_time_bins, I think there may be a simpler way of determining this first_items Series without distinguishing between downsampling and upsampling. I'll try to post a gist and some more comments later today or tomorrow.

if grouper[0] == 'downsampling':
# if we're downsampling CFTimeIndex, do this:
labels = grouper[1]
binner = grouper[2]
first_items = s.groupby(binner).first().reindex(labels)
# reindex(grouper[1]) adds empty np.nan bins to
# emulate pandas behavior
elif grouper[0] == 'upsampling':
# if we're upsampling CFTimeIndex, do this:
labels = grouper[1]
binner = grouper[2]
closed = grouper[3]
if closed == 'right':
first_items = s.reindex(pd.Index(binner),
method='nearest')
first_items.index = labels
else:
first_items = s.reindex(pd.Index(binner),
method='bfill')
first_items.index = labels
else:
first_items = s.groupby(grouper).first()
full_index = first_items.index
if first_items.isnull().any():
first_items = first_items.dropna()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if dropna() worked for a Series indexed by a CFTimeIndex. I'll see if I can sort that out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that dropna() for CFTimeIndex indices are working (#2734), the logic here will be simplified once #2734 is merged with master.

Expand Down
3 changes: 2 additions & 1 deletion xarray/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def _get_keep_attrs(default):
elif global_choice in [True, False]:
return global_choice
else:
raise ValueError("The global option keep_attrs must be one of True, False or 'default'.")
raise ValueError("The global option keep_attrs must be one of"
" True, False or 'default'.")


class set_options(object):
Expand Down
25 changes: 23 additions & 2 deletions xarray/core/resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,35 @@ def _interpolate(self, kind='linear'):
.format(self._obj.data.name)
)

x = self._obj[self._dim].astype('float')
# from ..coding.cftimeindex import CFTimeIndex
import cftime as cf
import numpy as np
if isinstance(self._obj[self._dim].values[0], cf.datetime):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something like if isinstance(self._obj.indexes[self._dim], CFTimeIndex) might be cleaner here.

t = self._obj[self._dim]
x = np.insert([td.total_seconds() for td in
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just make use of xarray.core.utils.datetime_to_numeric here, e.g.

x = datetime_to_numeric(t, datetime_unit='s')

t[1:].values - t[:-1].values], 0, 0).cumsum()
# calling total_seconds is potentially bad for performance
x = x.round()
Copy link
Member

@spencerkclark spencerkclark Jan 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other minor thing -- I'm inclined not to round here. I realize that in the climate community, with non-standard calendars, it would be pretty unusual to encounter dates with a time resolution finer than one second. However in xarray my sense is we should avoid making assumptions about this.

Fortunately with recent updates in cftime, this is becoming less and less of an issue. See the difference in behavior here between cftime 1.0.0 and cftime 1.0.3.4 (the latest version):

cftime 1.0.0

Here differences in datetime arithmetic precision lead to small differences in the interpolation results between data indexed by a CFTimeIndex and data indexed by a DatetimeIndex.

In [1]: import numpy as np

In [2]: import xarray as xr

In [3]: times = xr.cftime_range('1892-01-01T12:00:00', periods=15, freq='5256113T')

In [4]: da_cftime = xr.DataArray(range(15), [('time', times)])

In [5]: da_pandas = xr.DataArray(range(15), [('time', times.to_datetimeindex())])

In [6]: new_times = xr.cftime_range('1892', periods=10, freq='6AS-JUN')

In [7]: da_cftime_interp = da_cftime.interp(time=new_times)

In [8]: da_pandas_interp = da_pandas.interp(time=new_times.to_datetimeindex())

In [9]: np.testing.assert_array_equal(da_cftime_interp.values, da_pandas_interp.values)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-9-9bc960239b46> in <module>
----> 1 np.testing.assert_array_equal(da_cftime_interp.values, da_pandas_interp.values)

//anaconda/envs/xarray-dev-37/lib/python3.6/site-packages/numpy/testing/_private/utils.py in assert_array_equal(x, y, err_msg, verbose)
    863     __tracebackhide__ = True  # Hide traceback for py.test
    864     assert_array_compare(operator.__eq__, x, y, err_msg=err_msg,
--> 865                          verbose=verbose, header='Arrays are not equal')
    866
    867

//anaconda/envs/xarray-dev-37/lib/python3.6/site-packages/numpy/testing/_private/utils.py in assert_array_compare(comparison, x, y, err_msg, verbose, header, precision, equal_nan, equal_inf)
    787                                 verbose=verbose, header=header,
    788                                 names=('x', 'y'), precision=precision)
--> 789             raise AssertionError(msg)
    790     except ValueError:
    791         import traceback

AssertionError:
Arrays are not equal

(mismatch 60.0%)
 x: array([0.041506, 0.641767, 1.242028, 1.842289, 2.442824, 3.043085,
       3.64362 , 4.243881, 4.844416, 5.444677])
 y: array([0.041506, 0.641767, 1.242028, 1.842289, 2.442824, 3.043085,
       3.64362 , 4.243881, 4.844416, 5.444677])

Note that we can use assert_allclose with the default tolerances here:

In [10]: np.testing.assert_allclose(da_cftime_interp.values, da_pandas_interp.values)

cftime 1.0.3.4

For this example at least, the issues with datetime arithmetic precision have been resolved.

In [1]: import numpy as np

In [2]: import xarray as xr

In [3]: times = xr.cftime_range('1892-01-01T12:00:00', periods=15, freq='5256113T')

In [4]: da_cftime = xr.DataArray(range(15), [('time', times)])

In [5]: da_pandas = xr.DataArray(range(15), [('time', times.to_datetimeindex())])

In [6]: new_times = xr.cftime_range('1892', periods=10, freq='6AS-JUN')

In [7]: da_cftime_interp = da_cftime.interp(time=new_times)

In [8]: da_pandas_interp = da_pandas.interp(time=new_times.to_datetimeindex())

In [9]: np.testing.assert_array_equal(da_cftime_interp.values, da_pandas_interp.values)

Any errors in interpolation due to microsecond noise in the ordinal coordinate would be expected to be small (I think we could account for this in the tests by using xarray.testing.assert_allclose, which checks that arrays are equal up to a specified tolerance).

# Rounding fixes erroneous microsecond offsets in timedelta
# (fault of CFTime), but destroys microsecond resolution data
else:
x = self._obj[self._dim].astype('float')
y = self._obj.data

axis = self._obj.get_axis_num(self._dim)

f = interp1d(x, y, kind=kind, axis=axis, bounds_error=True,
assume_sorted=True)
new_x = self._full_index.values.astype('float')
if isinstance(self._full_index.values[0], cf.datetime):
t = self._full_index
new_x = np.insert([td.total_seconds() for td in
t[1:].values - t[:-1].values], 0, 0).cumsum()
# calling total_seconds is potentially bad for performance
new_x = new_x.round()
# Rounding fixes erroneous microsecond offsets in timedelta
# (fault of CFTime), but destroys microsecond resolution data
else:
new_x = self._full_index.values.astype('float')

# construct new up-sampled DataArray
dummy = self._obj.copy()
Expand Down
151 changes: 151 additions & 0 deletions xarray/core/resample_cftime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think following what we've done in similar situations, we might want to add a copy of the pandas license at the top of this file (see here, for example, following #2301 (comment)).

CFTimeIndex port of pandas resampling
(pandas/pandas/core/resample.py)
Does not support non-integer freq
"""
from __future__ import absolute_import, division, print_function
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another small thing -- since xarray is no longer worried about Python 2 compatibility, we can remove these imports (xref: #2645).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the import statements. Same failures as before.


import datetime
from ..coding.cftimeindex import CFTimeIndex
from ..coding.cftime_offsets import (cftime_range, normalize_date,
Day, Hour, Minute, Second)


def _get_time_bins(index, freq, closed, label, base):
# This portion of code comes from TimeGrouper __init__ #
end_types = {'M', 'A'}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it might be cleaner to separate some of this logic into another method. For example define:

def _default_closed_or_label(freq):
    if freq._freq in {'M', 'A'}:
        return 'right'
    else:
        return 'left'

Then within _get_time_bins you could use something like this:

if closed is None:
    closed = _default_closed_or_label(freq)

if label is None:
    label = _default_closed_or_label(freq)

if freq._freq in end_types:
if closed is None:
closed = 'right'
if label is None:
label = 'right'
else:
if closed is None:
closed = 'left'
if label is None:
label = 'left'
# This portion of code comes from TimeGrouper __init__ #

if not isinstance(index, CFTimeIndex):
raise TypeError('index must be a CFTimeIndex, but got '
'an instance of %r' % type(index).__name__)
if len(index) == 0:
binner = labels = CFTimeIndex(data=[], name=index.name)
return binner, [], labels

first, last = _get_range_edges(index.min(), index.max(), freq,
closed=closed,
base=base)
binner = labels = cftime_range(freq=freq,
start=first,
end=last,
name=index.name)

if len(binner) > 1 and binner[-1] < last:
extra_date_range = cftime_range(binner[-1], last + freq,
freq=freq, name=index.name)
binner = labels = CFTimeIndex(binner.append(extra_date_range[1:]))

trimmed = False
if len(binner) > 2 and binner[-2] == last and closed == 'right':
binner = binner[:-1]
trimmed = True

if closed == 'right':
spencerkclark marked this conversation as resolved.
Show resolved Hide resolved
labels = binner
if label == 'right':
labels = labels[1:]
elif not trimmed:
labels = labels[:-1]
else:
if label == 'right':
labels = labels[1:]
elif not trimmed:
labels = labels[:-1]
return binner, labels


def _adjust_bin_edges(binner, ax_values, freq):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method does not appear to be used in this implementation. Should it be?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also noticed I've been too slavishly copying pandas logic. This and some other unnecessary code have been/will be removed.

# Some hacks for > daily data, see #1471, #1458, #1483
if freq._freq not in ['D', 'H', 'T', 'min', 'S']:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worth adding a CFTIME_TICKS variable in cftime_offsets.py that we could import and use for these instance checks.

# pandas defines these offsets as "Tick" objects, which for instance have 
# distinct behavior from monthly or longer frequencies in resample.
CFTIME_TICKS = (Day, Hour, Minute, Second)

Then here and in other methods this check could just be something like if not isinstance(freq, CFTIME_TICKS) or if isinstance(freq, CFTIME_TICKS).

# intraday values on last day
if binner[-2] > ax_values.max():
binner = binner[:-1]
return binner


def _get_range_edges(first, last, offset, closed='left', base=0):
if offset._freq in ['D', 'H', 'T', 'min', 'S']:
is_day = isinstance(offset, Day)
if (is_day and offset.n == 1) or not is_day:
return _adjust_dates_anchored(first, last, offset,
closed=closed, base=base)
else:
first = normalize_date(first)
last = normalize_date(last)

if closed == 'left':
first = offset.rollback(first)
else:
first = first - offset

last = last + offset
return first, last


def _adjust_dates_anchored(first, last, offset, closed='right', base=0):
base = base % offset.n
start_day = normalize_date(first)
base_td = datetime.timedelta(0)
if offset._freq == 'D':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we defined an as_timedelta method for cftime offsets (as described below), we could instead replace this conditional block with one line:

base_td = type(offset)(n=base).as_timedelta()

base_td = datetime.timedelta(days=base)
elif offset._freq == 'H':
base_td = datetime.timedelta(hours=base)
elif offset._freq in ['T', 'min']:
base_td = datetime.timedelta(minutes=base)
elif offset._freq == 'S':
base_td = datetime.timedelta(seconds=base)
offset_td = _offset_timedelta(offset)
start_day += base_td
foffset = (first - start_day) % offset_td
loffset = (last - start_day) % offset_td
if closed == 'right':
if foffset.total_seconds() > 0:
fresult = first - foffset
else:
fresult = first - offset_td

if loffset.total_seconds() > 0:
lresult = last + (offset_td - loffset)
else:
lresult = last
else:
if foffset.total_seconds() > 0:
fresult = first - foffset
else:
fresult = first

if loffset.total_seconds() > 0:
lresult = last + (offset_td - loffset)
else:
lresult = last + offset_td
return fresult, lresult


def _offset_timedelta(offset):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to add an as_timedelta method (where applicable) to the cftime offset objects. For example:

class Day(BaseCFTimeOffset):
    _freq = 'D'

    def as_timedelta(self):
        return timedelta(days=self.n)

    def __apply__(self, other):
        return other + self.as_timedelta()

That way we would not need this method to do the translation.

if isinstance(offset, Day):
return datetime.timedelta(days=offset.n)
elif isinstance(offset, Hour):
return datetime.timedelta(hours=offset.n)
elif isinstance(offset, Minute):
return datetime.timedelta(minutes=offset.n)
elif isinstance(offset, Second):
return datetime.timedelta(seconds=offset.n)


def _adjust_binner_for_upsample(binner, closed):
spencerkclark marked this conversation as resolved.
Show resolved Hide resolved
if closed == 'right':
binner = binner[1:]
else:
binner = binner[:-1]
return binner
3 changes: 1 addition & 2 deletions xarray/plot/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import numpy as np
import pandas as pd

from xarray.core.alignment import align
# from xarray.core.alignment import align
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a reason for commenting out this line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't recall working on the plot.py file. It seems to be an unused import statement so that might've been why it's been commented out.

from xarray.core.common import contains_cftime_datetimes
from xarray.core.pycompat import basestring

Expand Down Expand Up @@ -255,7 +255,6 @@ def _infer_line_data(darray, x, y, hue):
huelabel = label_from_attrs(darray[huename])
hueplt = darray[huename]


xlabel = label_from_attrs(xplt)
ylabel = label_from_attrs(yplt)

Expand Down
Loading