Skip to content

Commit 6048356

Browse files
Add a days_in_month accessor to CFTimeIndex (#3935)
* Add days_in_month accessor to CFTimeIndex * Add pull request number * Add version-dependent skip for test * Fix pull request number * Strip outputs from notebook; add documentation update note * typo
1 parent ba9f822 commit 6048356

File tree

6 files changed

+43
-107
lines changed

6 files changed

+43
-107
lines changed

doc/examples/monthly-means.ipynb

Lines changed: 18 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -29,89 +29,9 @@
2929
"import numpy as np\n",
3030
"import pandas as pd\n",
3131
"import xarray as xr\n",
32-
"from netCDF4 import num2date\n",
3332
"import matplotlib.pyplot as plt "
3433
]
3534
},
36-
{
37-
"cell_type": "markdown",
38-
"metadata": {},
39-
"source": [
40-
"#### Some calendar information so we can support any netCDF calendar. "
41-
]
42-
},
43-
{
44-
"cell_type": "code",
45-
"execution_count": null,
46-
"metadata": {
47-
"ExecuteTime": {
48-
"end_time": "2018-11-28T20:51:35.991620Z",
49-
"start_time": "2018-11-28T20:51:35.960336Z"
50-
}
51-
},
52-
"outputs": [],
53-
"source": [
54-
"dpm = {'noleap': [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],\n",
55-
" '365_day': [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],\n",
56-
" 'standard': [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],\n",
57-
" 'gregorian': [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],\n",
58-
" 'proleptic_gregorian': [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],\n",
59-
" 'all_leap': [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],\n",
60-
" '366_day': [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],\n",
61-
" '360_day': [0, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]} "
62-
]
63-
},
64-
{
65-
"cell_type": "markdown",
66-
"metadata": {},
67-
"source": [
68-
"#### A few calendar functions to determine the number of days in each month\n",
69-
"If you were just using the standard calendar, it would be easy to use the `calendar.month_range` function."
70-
]
71-
},
72-
{
73-
"cell_type": "code",
74-
"execution_count": null,
75-
"metadata": {
76-
"ExecuteTime": {
77-
"end_time": "2018-11-28T20:51:36.015151Z",
78-
"start_time": "2018-11-28T20:51:35.994079Z"
79-
}
80-
},
81-
"outputs": [],
82-
"source": [
83-
"def leap_year(year, calendar='standard'):\n",
84-
" \"\"\"Determine if year is a leap year\"\"\"\n",
85-
" leap = False\n",
86-
" if ((calendar in ['standard', 'gregorian',\n",
87-
" 'proleptic_gregorian', 'julian']) and\n",
88-
" (year % 4 == 0)):\n",
89-
" leap = True\n",
90-
" if ((calendar == 'proleptic_gregorian') and\n",
91-
" (year % 100 == 0) and\n",
92-
" (year % 400 != 0)):\n",
93-
" leap = False\n",
94-
" elif ((calendar in ['standard', 'gregorian']) and\n",
95-
" (year % 100 == 0) and (year % 400 != 0) and\n",
96-
" (year < 1583)):\n",
97-
" leap = False\n",
98-
" return leap\n",
99-
"\n",
100-
"def get_dpm(time, calendar='standard'):\n",
101-
" \"\"\"\n",
102-
" return a array of days per month corresponding to the months provided in `months`\n",
103-
" \"\"\"\n",
104-
" month_length = np.zeros(len(time), dtype=np.int)\n",
105-
" \n",
106-
" cal_days = dpm[calendar]\n",
107-
" \n",
108-
" for i, (month, year) in enumerate(zip(time.month, time.year)):\n",
109-
" month_length[i] = cal_days[month]\n",
110-
" if leap_year(year, calendar=calendar) and month == 2:\n",
111-
" month_length[i] += 1\n",
112-
" return month_length"
113-
]
114-
},
11535
{
11636
"cell_type": "markdown",
11737
"metadata": {},
@@ -131,7 +51,7 @@
13151
"outputs": [],
13252
"source": [
13353
"ds = xr.tutorial.open_dataset('rasm').load()\n",
134-
"print(ds)"
54+
"ds"
13555
]
13656
},
13757
{
@@ -143,7 +63,17 @@
14363
"- calculate the month lengths for each monthly data record\n",
14464
"- calculate weights using `groupby('time.season')`\n",
14565
"\n",
146-
"Finally, we just need to multiply our weights by the `Dataset` and sum allong the time dimension. "
66+
"Finally, we just need to multiply our weights by the `Dataset` and sum allong the time dimension. Creating a `DataArray` for the month length is as easy as using the `days_in_month` accessor on the time coordinate. The calendar type, in this case `'noleap'`, is automatically considered in this operation."
67+
]
68+
},
69+
{
70+
"cell_type": "code",
71+
"execution_count": null,
72+
"metadata": {},
73+
"outputs": [],
74+
"source": [
75+
"month_length = ds.time.dt.days_in_month\n",
76+
"month_length"
14777
]
14878
},
14979
{
@@ -157,13 +87,8 @@
15787
},
15888
"outputs": [],
15989
"source": [
160-
"# Make a DataArray with the number of days in each month, size = len(time)\n",
161-
"month_length = xr.DataArray(get_dpm(ds.time.to_index(), calendar='noleap'),\n",
162-
" coords=[ds.time], name='month_length')\n",
163-
"\n",
16490
"# Calculate the weights by grouping by 'time.season'.\n",
165-
"# Conversion to float type ('astype(float)') only necessary for Python 2.x\n",
166-
"weights = month_length.groupby('time.season') / month_length.astype(float).groupby('time.season').sum()\n",
91+
"weights = month_length.groupby('time.season') / month_length.groupby('time.season').sum()\n",
16792
"\n",
16893
"# Test that the sum of the weights for each season is 1.0\n",
16994
"np.testing.assert_allclose(weights.groupby('time.season').sum().values, np.ones(4))\n",
@@ -183,7 +108,7 @@
183108
},
184109
"outputs": [],
185110
"source": [
186-
"print(ds_weighted)"
111+
"ds_weighted"
187112
]
188113
},
189114
{
@@ -262,13 +187,9 @@
262187
"source": [
263188
"# Wrap it into a simple function\n",
264189
"def season_mean(ds, calendar='standard'):\n",
265-
" # Make a DataArray of season/year groups\n",
266-
" year_season = xr.DataArray(ds.time.to_index().to_period(freq='Q-NOV').to_timestamp(how='E'),\n",
267-
" coords=[ds.time], name='year_season')\n",
268-
"\n",
269190
" # Make a DataArray with the number of days in each month, size = len(time)\n",
270-
" month_length = xr.DataArray(get_dpm(ds.time.to_index(), calendar=calendar),\n",
271-
" coords=[ds.time], name='month_length')\n",
191+
" month_length = ds.time.dt.days_in_month\n",
192+
"\n",
272193
" # Calculate the weights by grouping by 'time.season'\n",
273194
" weights = month_length.groupby('time.season') / month_length.groupby('time.season').sum()\n",
274195
"\n",
@@ -278,13 +199,6 @@
278199
" # Calculate the weighted average\n",
279200
" return (ds * weights).groupby('time.season').sum(dim='time')"
280201
]
281-
},
282-
{
283-
"cell_type": "code",
284-
"execution_count": null,
285-
"metadata": {},
286-
"outputs": [],
287-
"source": []
288202
}
289203
],
290204
"metadata": {
@@ -304,7 +218,7 @@
304218
"name": "python",
305219
"nbconvert_exporter": "python",
306220
"pygments_lexer": "ipython3",
307-
"version": "3.6.8"
221+
"version": "3.7.3"
308222
},
309223
"toc": {
310224
"base_numbering": 1,
@@ -321,5 +235,5 @@
321235
}
322236
},
323237
"nbformat": 4,
324-
"nbformat_minor": 2
238+
"nbformat_minor": 4
325239
}

doc/weather-climate.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ For data indexed by a :py:class:`~xarray.CFTimeIndex` xarray currently supports:
9595
9696
- Access of basic datetime components via the ``dt`` accessor (in this case
9797
just "year", "month", "day", "hour", "minute", "second", "microsecond",
98-
"season", "dayofyear", and "dayofweek"):
98+
"season", "dayofyear", "dayofweek", and "days_in_month"):
9999

100100
.. ipython:: python
101101
@@ -104,6 +104,7 @@ For data indexed by a :py:class:`~xarray.CFTimeIndex` xarray currently supports:
104104
da.time.dt.season
105105
da.time.dt.dayofyear
106106
da.time.dt.dayofweek
107+
da.time.dt.days_in_month
107108
108109
- Rounding of datetimes to fixed frequencies via the ``dt`` accessor:
109110

doc/whats-new.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ New Features
4848
By `Todd Jennings <https://github.com/toddrjen>`_
4949
- Allow plotting of boolean arrays. (:pull:`3766`)
5050
By `Marek Jacob <https://github.com/MeraX>`_
51+
- A ``days_in_month`` accessor for :py:class:`xarray.CFTimeIndex`, analogous to
52+
the ``days_in_month`` accessor for a :py:class:`pandas.DatetimeIndex`, which
53+
returns the days in the month each datetime in the index. Now days in month
54+
weights for both standard and non-standard calendars can be obtained using
55+
the :py:class:`~core.accessor_dt.DatetimeAccessor` (:pull:`3935`). This
56+
feature requires cftime version 1.1.0 or greater. By
57+
`Spencer Clark <https://github.com/spencerkclark>`_.
5158

5259
Bug fixes
5360
~~~~~~~~~
@@ -71,7 +78,10 @@ Documentation
7178
:py:meth:`DataArray.diff` so it does document the ``dim``
7279
parameter as required. (:issue:`1040`, :pull:`3909`)
7380
By `Justus Magin <https://github.com/keewis>`_.
74-
81+
- Updated :doc:`Calculating Seasonal Averages from Timeseries of Monthly Means
82+
<examples/monthly-means>` example notebook to take advantage of the new
83+
``days_in_month`` accessor for :py:class:`xarray.CFTimeIndex`
84+
(:pull:`3935`). By `Spencer Clark <https://github.com/spencerkclark>`_.
7585

7686
Internal Changes
7787
~~~~~~~~~~~~~~~~

xarray/coding/cftimeindex.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ class CFTimeIndex(pd.Index):
243243
"dayofyr", "The ordinal day of year of the datetime", "1.0.2.1"
244244
)
245245
dayofweek = _field_accessor("dayofwk", "The day of week of the datetime", "1.0.2.1")
246+
days_in_month = _field_accessor(
247+
"daysinmonth", "The number of days in the month of the datetime", "1.1.0.0"
248+
)
246249
date_type = property(get_date_type)
247250

248251
def __new__(cls, data, name=None):

xarray/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def LooseVersion(vstring):
6262
has_pynio, requires_pynio = _importorskip("Nio")
6363
has_pseudonetcdf, requires_pseudonetcdf = _importorskip("PseudoNetCDF")
6464
has_cftime, requires_cftime = _importorskip("cftime")
65+
has_cftime_1_1_0, requires_cftime_1_1_0 = _importorskip("cftime", minversion="1.1.0.0")
6566
has_dask, requires_dask = _importorskip("dask")
6667
has_bottleneck, requires_bottleneck = _importorskip("bottleneck")
6768
has_nc_time_axis, requires_nc_time_axis = _importorskip("nc_time_axis")

xarray/tests/test_cftimeindex.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
)
1616
from xarray.tests import assert_array_equal, assert_identical
1717

18-
from . import raises_regex, requires_cftime
18+
from . import raises_regex, requires_cftime, requires_cftime_1_1_0
1919
from .test_coding_times import (
2020
_ALL_CALENDARS,
2121
_NON_STANDARD_CALENDARS,
@@ -229,6 +229,13 @@ def test_cftimeindex_dayofweek_accessor(index):
229229
assert_array_equal(result, expected)
230230

231231

232+
@requires_cftime_1_1_0
233+
def test_cftimeindex_days_in_month_accessor(index):
234+
result = index.days_in_month
235+
expected = [date.daysinmonth for date in index]
236+
assert_array_equal(result, expected)
237+
238+
232239
@requires_cftime
233240
@pytest.mark.parametrize(
234241
("string", "date_args", "reso"),

0 commit comments

Comments
 (0)