Repository: Ouranosinc/xclim

Issue:
Implement missing check for specific periods (e.g. season=djf)
At the moment the missing function checks for all dates in the `freq`, even though we might only be interested in a given month. 

Discussion:

Patch:
diff --git a/setup.cfg b/setup.cfg
index 78d6b84c..c8c7e55b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -3,13 +3,13 @@ current_version = 0.9-beta
 commit = True
 tag = True
 parse = (?P<major>\d+)\.(?P<minor>\d+)(\-(?P<release>[a-z]+))?
-serialize = 
+serialize =
 	{major}.{minor}-{release}
 	{major}.{minor}
 
 [bumpversion:part:release]
 optional_value = gamma
-values = 
+values =
 	beta
 	gamma
 
@@ -25,13 +25,14 @@ replace = __version__ = '{new_version}'
 universal = 1
 
 [flake8]
-exclude = 
+exclude =
 	.git,
 	docs,
-	build
+	build,
+    .eggs,
 max-line-length = 120
 max-complexity = 10
-ignore = 
+ignore =
 	F401
 	F403
 
@@ -41,6 +42,6 @@ test = pytest
 [tool:pytest]
 collect_ignore = ['setup.py']
 addopts = --verbose
-filterwarnings = 
+filterwarnings =
 	ignore::UserWarning
 
diff --git a/xclim/checks.py b/xclim/checks.py
index ec10cf38..212b6eec 100644
--- a/xclim/checks.py
+++ b/xclim/checks.py
@@ -147,7 +147,8 @@ def check_is_dataarray(comp):
     return func
 
 
-def missing_any(da, freq, **kwds):
+# This function can probably be made simpler once CFPeriodIndex is implemented.
+def missing_any(da, freq, **indexer):
     r"""Return a boolean DataArray indicating whether there are missing days in the resampled array.
 
     Parameters
@@ -156,19 +157,28 @@ def missing_any(da, freq, **kwds):
       Input array at daily frequency.
     freq : str
       Resampling frequency.
+    **indexer : {dim: indexer, }, optional
+      Time attribute and values over which to subset the array. For example, use season='DJF' to select winter values,
+      month=1 to select January, or month=[6,7,8] to select summer months. If not indexer is given, all values are
+      considered.
 
     Returns
     -------
     out : DataArray
       A boolean array set to True if any month or year has missing values.
     """
-    c = da.notnull().resample(time=freq).sum(dim='time')
+    from . import generic
 
     if '-' in freq:
         pfreq, anchor = freq.split('-')
     else:
         pfreq = freq
 
+    # Compute the number of days in the time series during each period at the given frequency.
+    selected = generic.select_time(da, **indexer)
+    c = selected.notnull().resample(time=freq).sum(dim='time')
+
+    # Otherwise simply use the start and end dates to find the expected number of days.
     if pfreq.endswith('S'):
         start_time = c.indexes['time']
         end_time = start_time.shift(1, freq=freq)
@@ -176,6 +186,22 @@ def missing_any(da, freq, **kwds):
         end_time = c.indexes['time']
         start_time = end_time.shift(-1, freq=freq)
 
+    if indexer:
+        # Create a full synthetic time series and compare the number of days with the original series.
+        t0 = str(start_time[0].date())
+        t1 = str(end_time[-1].date())
+        cal = da.time.encoding.get('calendar')
+        if cal:
+            t = xr.cftime_range(t0, t1, freq='D', calendar=cal)
+        else:
+            t = pd.date_range(t0, t1, freq='D')
+
+        sda = xr.DataArray(data=np.empty(len(t)), coords={'time': t}, dims=('time', ))
+        st = generic.select_time(sda, **indexer)
+        sn = st.notnull().resample(time=freq).sum(dim='time')
+        miss = sn != c
+        return miss
+
     n = (end_time - start_time).days
     nda = xr.DataArray(n.values, coords={'time': c.time}, dims='time')
     return c != nda
diff --git a/xclim/streamflow.py b/xclim/streamflow.py
index c176da23..34b6185e 100644
--- a/xclim/streamflow.py
+++ b/xclim/streamflow.py
@@ -30,7 +30,7 @@ class Stats(Streamflow):
         indexer = kwds['indexer']
         freq = kwds['freq'] or generic.default_freq(**indexer)
 
-        miss = (checks.missing_any(generic.select_time(da, **indexer), freq) for da in args)
+        miss = (checks.missing_any(da, freq, **indexer) for da in args)
         return reduce(np.logical_or, miss)
 
 


Test patch:
diff --git a/tests/test_checks.py b/tests/test_checks.py
index a0be5edb..9556a265 100644
--- a/tests/test_checks.py
+++ b/tests/test_checks.py
@@ -91,3 +91,29 @@ class TestMissingAnyFills:
         ts = tasmin_series(a)
         miss = checks.missing_any(ts, freq='A-JUN')
         np.testing.assert_equal(miss, [False])
+
+    def test_month(self, tasmin_series):
+        ts = tasmin_series(np.zeros(36))
+        miss = checks.missing_any(ts, freq='YS', month=7)
+        np.testing.assert_equal(miss, [False])
+
+        miss = checks.missing_any(ts, freq='YS', month=8)
+        np.testing.assert_equal(miss, [True])
+
+        miss = checks.missing_any(ts, freq='YS', month=[7, 8])
+        np.testing.assert_equal(miss, [True])
+
+        ts = tasmin_series(np.zeros(76))
+        miss = checks.missing_any(ts, freq='YS', month=[7, 8])
+        np.testing.assert_equal(miss, [False])
+
+    def test_season(self, tasmin_series):
+        ts = tasmin_series(np.zeros(360))
+        miss = checks.missing_any(ts, freq='YS', season='MAM')
+        np.testing.assert_equal(miss, [False])
+
+        miss = checks.missing_any(ts, freq='YS', season='JJA')
+        np.testing.assert_array_equal(miss, [True, True])
+
+        miss = checks.missing_any(ts, freq='YS', season='SON')
+        np.testing.assert_equal(miss, [False])
diff --git a/tests/test_streamflow.py b/tests/test_streamflow.py
index ddbec90e..7005c3e3 100644
--- a/tests/test_streamflow.py
+++ b/tests/test_streamflow.py
@@ -25,7 +25,6 @@ class TestStats():
         out = streamflow.stats(ndq_series, freq='YS', op='min', season='MAM')
         assert out.attrs['units'] == 'm^3 s-1'
 
-    @pytest.mark.skip()
     def test_missing(self, ndq_series):
         a = ndq_series
         a = ndq_series.where(~((a.time.dt.dayofyear == 5) * (a.time.dt.year == 1902)))