Repository: AI4S2S/s2spy

Issue:
Dealing with mismatch between calendar map and data time extent
PR #60 changes resample so that it takes a mapped calendar and resamples the input data based on those intervals.

However, this will require some extra checks and data handling to prevent user error;

- [ ] Add check to see if the data can fully cover the calendar, and warn the user if it cannot.
- [ ] Ensure that if Pandas data is insufficient to cover the calendar, NaNs will be used for the empty intervals
- [ ] Add tests to ensure that both the warning & the NaN filling goes correctly

The second task already works for the xarray implementation, but not yet for Pandas data.

Related to #26




Discussion:

Patch:
diff --git a/notebooks/tutorial_time.ipynb b/notebooks/tutorial_time.ipynb
index 149fa99..4cdcf45 100644
--- a/notebooks/tutorial_time.ipynb
+++ b/notebooks/tutorial_time.ipynb
@@ -27,7 +27,7 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "AdventCalendar(month=12, day=31, freq=7d, n_targets=1, max_lag=None)\n"
+      "AdventCalendar(month=12, day=31, freq=7d, n_targets=1)\n"
      ]
     }
    ],
@@ -52,7 +52,7 @@
     {
      "data": {
       "text/plain": [
-       "AdventCalendar(month=11, day=30, freq=90d, n_targets=1, max_lag=None)"
+       "AdventCalendar(month=11, day=30, freq=90d, n_targets=1)"
       ]
      },
      "execution_count": 3,
@@ -284,7 +284,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Map the calendar to the input data."
+    "Set the maximum lag"
    ]
   },
   {
@@ -316,14 +316,12 @@
        "      <th>(target) 0</th>\n",
        "      <th>1</th>\n",
        "      <th>2</th>\n",
-       "      <th>3</th>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>anchor_year</th>\n",
        "      <th></th>\n",
        "      <th></th>\n",
        "      <th></th>\n",
-       "      <th></th>\n",
        "    </tr>\n",
        "  </thead>\n",
        "  <tbody>\n",
@@ -332,21 +330,18 @@
        "      <td>(2022-09-01, 2022-11-30]</td>\n",
        "      <td>(2022-06-03, 2022-09-01]</td>\n",
        "      <td>(2022-03-05, 2022-06-03]</td>\n",
-       "      <td>(2021-12-05, 2022-03-05]</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>2021</th>\n",
        "      <td>(2021-09-01, 2021-11-30]</td>\n",
        "      <td>(2021-06-03, 2021-09-01]</td>\n",
        "      <td>(2021-03-05, 2021-06-03]</td>\n",
-       "      <td>(2020-12-05, 2021-03-05]</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>2020</th>\n",
        "      <td>(2020-09-01, 2020-11-30]</td>\n",
        "      <td>(2020-06-03, 2020-09-01]</td>\n",
        "      <td>(2020-03-05, 2020-06-03]</td>\n",
-       "      <td>(2019-12-06, 2020-03-05]</td>\n",
        "    </tr>\n",
        "  </tbody>\n",
        "</table>\n",
@@ -359,11 +354,11 @@
        "2021         (2021-09-01, 2021-11-30]  (2021-06-03, 2021-09-01]   \n",
        "2020         (2020-09-01, 2020-11-30]  (2020-06-03, 2020-09-01]   \n",
        "\n",
-       "i_interval                          2                         3  \n",
-       "anchor_year                                                      \n",
-       "2022         (2022-03-05, 2022-06-03]  (2021-12-05, 2022-03-05]  \n",
-       "2021         (2021-03-05, 2021-06-03]  (2020-12-05, 2021-03-05]  \n",
-       "2020         (2020-03-05, 2020-06-03]  (2019-12-06, 2020-03-05]  "
+       "i_interval                          2  \n",
+       "anchor_year                            \n",
+       "2022         (2022-03-05, 2022-06-03]  \n",
+       "2021         (2021-03-05, 2021-06-03]  \n",
+       "2020         (2020-03-05, 2020-06-03]  "
       ]
      },
      "execution_count": 7,
@@ -371,6 +366,105 @@
      "output_type": "execute_result"
     }
    ],
+   "source": [
+    "calendar.set_max_lag(2)\n",
+    "calendar.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Map the calendar to the input data."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th>i_interval</th>\n",
+       "      <th>(target) 0</th>\n",
+       "      <th>1</th>\n",
+       "      <th>2</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>anchor_year</th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>2021</th>\n",
+       "      <td>(2021-09-01, 2021-11-30]</td>\n",
+       "      <td>(2021-06-03, 2021-09-01]</td>\n",
+       "      <td>(2021-03-05, 2021-06-03]</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2020</th>\n",
+       "      <td>(2020-09-01, 2020-11-30]</td>\n",
+       "      <td>(2020-06-03, 2020-09-01]</td>\n",
+       "      <td>(2020-03-05, 2020-06-03]</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2019</th>\n",
+       "      <td>(2019-09-01, 2019-11-30]</td>\n",
+       "      <td>(2019-06-03, 2019-09-01]</td>\n",
+       "      <td>(2019-03-05, 2019-06-03]</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2018</th>\n",
+       "      <td>(2018-09-01, 2018-11-30]</td>\n",
+       "      <td>(2018-06-03, 2018-09-01]</td>\n",
+       "      <td>(2018-03-05, 2018-06-03]</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "i_interval                 (target) 0                         1  \\\n",
+       "anchor_year                                                       \n",
+       "2021         (2021-09-01, 2021-11-30]  (2021-06-03, 2021-09-01]   \n",
+       "2020         (2020-09-01, 2020-11-30]  (2020-06-03, 2020-09-01]   \n",
+       "2019         (2019-09-01, 2019-11-30]  (2019-06-03, 2019-09-01]   \n",
+       "2018         (2018-09-01, 2018-11-30]  (2018-06-03, 2018-09-01]   \n",
+       "\n",
+       "i_interval                          2  \n",
+       "anchor_year                            \n",
+       "2021         (2021-03-05, 2021-06-03]  \n",
+       "2020         (2020-03-05, 2020-06-03]  \n",
+       "2019         (2019-03-05, 2019-06-03]  \n",
+       "2018         (2018-03-05, 2018-06-03]  "
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
    "source": [
     "# create dummy data for testing\n",
     "time_index = pd.date_range('20171110', '20211211', freq='10d')\n",
diff --git a/s2spy/_base_calendar.py b/s2spy/_base_calendar.py
index 4f1f880..27e5896 100644
--- a/s2spy/_base_calendar.py
+++ b/s2spy/_base_calendar.py
@@ -23,13 +23,20 @@ class BaseCalendar(ABC):
     _mapping = None
 
     @abstractmethod
-    def __init__(self, anchor, freq, n_targets: int = 1, max_lag: int = None):
-        """For initializing calendars, the following four variables will be required."""
+    def __init__(
+        self,
+        anchor,
+        freq,
+        n_targets: int = 1,
+    ):
+        """For initializing calendars, the following five variables will be required."""
         self.n_targets = n_targets
-        self.max_lag = max_lag
         self.anchor = anchor
         self.freq = freq
 
+        self._max_lag = 0
+        self._allow_overlap = False
+
     @abstractmethod
     def _get_anchor(self, year: int) -> pd.Timestamp:
         """Method to generate an anchor timestamp for your specific calendar.
@@ -46,6 +53,31 @@ class BaseCalendar(ABC):
         """
         return pd.Timestamp()
 
+    def set_max_lag(self, max_lag: int, allow_overlap: bool = False) -> None:
+        """Set the maximum lag of a calendar.
+
+        Sets the maximum number of lag periods after the target period. If `0`,
+        the maximum lag will be determined by how many fit in each anchor year.
+        If a maximum lag is provided, the intervals can either only cover part
+        of the year, or extend over multiple years. In case of a large max_lag
+        number where the intervals extend over multiple years, anchor years will
+        be skipped to avoid overlapping intervals. To allow overlapping
+        intervals, use the `allow_overlap` kwarg.
+
+        Args:
+            max_lag: Maximum number of lag periods after the target period.
+            allow_overlap: Allows intervals to overlap between anchor years, if the
+                max_lag is set to a high enough number that intervals extend over
+                multiple years. `False` by default, to avoid train/test information
+                leakage.
+        """
+        if (max_lag < 0) or (max_lag % 1 > 0):
+            raise ValueError("Max lag should be an integer with a value of 0 or greater"
+                             f", not {max_lag} of type {type(max_lag)}.")
+
+        self._max_lag = max_lag
+        self._allow_overlap = allow_overlap
+
     def _map_year(self, year: int) -> pd.Series:
         """Internal routine to return a concrete IntervalIndex for the given year.
 
@@ -81,7 +113,7 @@ class BaseCalendar(ABC):
         """
         periods_per_year = pd.Timedelta("365days") / pd.to_timedelta(self.freq)
         return (
-            (self.max_lag + self.n_targets) if self.max_lag else int(periods_per_year)
+            (self._max_lag + self.n_targets) if self._max_lag > 0 else int(periods_per_year)
         )
 
     def _get_skip_nyears(self) -> int:
@@ -96,9 +128,9 @@ class BaseCalendar(ABC):
         periods_per_year = pd.Timedelta("365days") / pd.to_timedelta(self.freq)
 
         return (
-            (np.ceil(nintervals / periods_per_year).astype(int) - 1)
-            if self.max_lag
-            else 0
+            0
+            if self._max_lag > 0 and self._allow_overlap
+            else int(np.ceil(nintervals / periods_per_year).astype(int) - 1)
         )
 
     def map_years(self, start: int, end: int):
@@ -114,6 +146,9 @@ class BaseCalendar(ABC):
         Returns:
             The calendar mapped to the input start and end year.
         """
+        if start > end:
+            raise ValueError("The start year cannot be greater than the end year")
+
         self._first_year = start
         self._last_year = end
         self._mapping = "years"
@@ -151,21 +186,21 @@ class BaseCalendar(ABC):
         return self
 
     def _set_year_range_from_timestamps(self):
-        map_first_year = self._first_timestamp.year
-        map_last_year = self._last_timestamp.year
+        min_year = self._first_timestamp.year
+        max_year = self._last_timestamp.year
 
         # ensure that the input data could always cover the advent calendar
         # last date check
-        if self._map_year(map_last_year).iloc[0].right > self._last_timestamp:
-            map_last_year -= 1
+        if self._map_year(max_year).iloc[0].right > self._last_timestamp:
+            max_year -= 1
         # first date check
-        if self._map_year(map_first_year).iloc[-1].left < self._first_timestamp:
-            map_first_year += 1
+        while self._map_year(min_year).iloc[-1].right <= self._first_timestamp:
+            min_year += 1
 
         # map year(s) and generate year realized advent calendar
-        if map_last_year >= map_first_year:
-            self._first_year = map_first_year
-            self._last_year = map_last_year
+        if max_year >= min_year:
+            self._first_year = min_year
+            self._last_year = max_year
         else:
             raise ValueError(
                 "The input data could not cover the target advent calendar."
diff --git a/s2spy/_resample.py b/s2spy/_resample.py
index b61130f..646d9b5 100644
--- a/s2spy/_resample.py
+++ b/s2spy/_resample.py
@@ -1,4 +1,3 @@
-import warnings
 from typing import Union
 import numpy as np
 import pandas as pd
@@ -97,8 +96,8 @@ def resample_pandas(
     interval_groups = interval_index.get_indexer(input_data.index)
     interval_means = input_data.groupby(interval_groups).mean()
 
-    # drop the -1 index, as it represents data outside of all intervals
-    interval_means = interval_means.loc[0:]
+    # Reindex the intervals. Empty intervals will contain NaN values.
+    interval_means = interval_means.reindex(np.arange(len(interval_index)))
 
     if isinstance(input_data, pd.DataFrame):
         for name in input_data.keys():
@@ -220,24 +219,14 @@ def resample(
         raise ValueError("Generate a calendar map before calling resample")
 
     utils.check_timeseries(input_data)
+    utils.check_input_frequency(mapped_calendar, input_data)
 
     if isinstance(input_data, PandasData):
-        # raise a warning for upscaling
-        # target frequency must be larger than the (absolute) input frequency
-        if input_data.index.freq:
-            input_freq = input_data.index.freq
-            input_freq = input_freq if input_freq.n > 0 else -input_freq
-            if pd.Timedelta(mapped_calendar.freq) < input_freq:
-                warnings.warn(
-                    """Target frequency is smaller than the original frequency.
-                    The resampled data will contain NaN values, as there is no data
-                    available within all intervals."""
-                )
-
         resampled_data = resample_pandas(mapped_calendar, input_data)
-
     else:
         resampled_data = resample_xarray(mapped_calendar, input_data)
 
+    utils.check_empty_intervals(resampled_data)
+
     # mark target periods before returning the resampled data
     return mark_target_period(mapped_calendar, resampled_data)
diff --git a/s2spy/time.py b/s2spy/time.py
index 288c4d5..667f611 100644
--- a/s2spy/time.py
+++ b/s2spy/time.py
@@ -14,7 +14,7 @@ Example:
     >>> # Countdown the weeks until New Year's Eve
     >>> calendar = s2spy.time.AdventCalendar(anchor=(12, 31), freq="7d")
     >>> calendar
-    AdventCalendar(month=12, day=31, freq=7d, n_targets=1, max_lag=None)
+    AdventCalendar(month=12, day=31, freq=7d, n_targets=1)
 
     >>> # Get the 180-day periods leading up to New Year's eve for the year 2020
     >>> calendar = s2spy.time.AdventCalendar(anchor=(12, 31), freq='180d')
@@ -49,7 +49,6 @@ Example:
 """
 import calendar as pycalendar
 import re
-from typing import Optional
 from typing import Tuple
 import numpy as np
 import pandas as pd
@@ -75,7 +74,6 @@ class AdventCalendar(BaseCalendar):
         anchor: Tuple[int, int] = (11, 30),
         freq: str = "7d",
         n_targets: int = 1,
-        max_lag: Optional[int] = None,
     ) -> None:
         """Instantiate a basic calendar with minimal configuration.
 
@@ -88,12 +86,6 @@ class AdventCalendar(BaseCalendar):
                 of the calendar. It will countdown until this date.
             freq: Frequency of the calendar.
             n_targets: integer specifying the number of target intervals in a period.
-            max_lag: Maximum number of lag periods after the target period. If `None`,
-                the maximum lag will be determined by how many fit in each anchor year.
-                If a maximum lag is provided, the intervals can either only cover part
-                of the year, or extend over multiple years. In case of a large max_lag
-                number where the intervals extend over multiple years, anchor years will
-                be skipped to avoid overlapping intervals.
 
         Example:
             Instantiate a calendar counting down the weeks until new-year's
@@ -102,7 +94,7 @@ class AdventCalendar(BaseCalendar):
             >>> import s2spy.time
             >>> calendar = s2spy.time.AdventCalendar(anchor=(12, 31), freq="7d")
             >>> calendar
-            AdventCalendar(month=12, day=31, freq=7d, n_targets=1, max_lag=None)
+            AdventCalendar(month=12, day=31, freq=7d, n_targets=1)
 
         """
         if not re.fullmatch(r"\d*d", freq):
@@ -111,7 +103,9 @@ class AdventCalendar(BaseCalendar):
         self.day = anchor[1]
         self.freq = freq
         self.n_targets = n_targets
-        self.max_lag = max_lag
+
+        self._max_lag:int = 0
+        self._allow_overlap: bool = False
 
     def _get_anchor(self, year: int) -> pd.Timestamp:
         """Generates a timestamp for the end of interval 0 in year.
@@ -133,7 +127,6 @@ class MonthlyCalendar(BaseCalendar):
         anchor: str = "Dec",
         freq: str = "1M",
         n_targets: int = 1,
-        max_lag: Optional[int] = None,
     ) -> None:
         """Instantiate a basic monthly calendar with minimal configuration.
 
@@ -146,12 +139,6 @@ class MonthlyCalendar(BaseCalendar):
                 of the calendar. It will countdown up to this month.
             freq: Frequency of the calendar, in the form '1M', '2M', etc.
             n_targets: integer specifying the number of target intervals in a period.
-            max_lag: Maximum number of lag periods after the target period. If `None`,
-                the maximum lag will be determined by how many fit in each anchor year.
-                If a maximum lag is provided, the intervals can either only cover part
-                of the year, or extend over multiple years. In case of a large max_lag
-                number where the intervals extend over multiple years, anchor years will
-                be skipped to avoid overlapping intervals.
 
         Example:
             Instantiate a calendar counting down the quarters (3 month periods) until
@@ -160,15 +147,17 @@ class MonthlyCalendar(BaseCalendar):
             >>> import s2spy.time
             >>> calendar = s2spy.time.MonthlyCalendar(anchor='Dec', freq="3M")
             >>> calendar
-            MonthlyCalendar(month=12, freq=3M, n_targets=1, max_lag=None)
+            MonthlyCalendar(month=12, freq=3M, n_targets=1)
 
         """
         if not re.fullmatch(r"\d*M", freq):
             raise ValueError("Please input a frequency in the form of '2M'")
+
         self.month = month_mapping_dict[anchor.upper()]
         self.freq = freq
         self.n_targets = n_targets
-        self.max_lag = max_lag
+        self._max_lag = 0
+        self._allow_overlap = False
 
     def _get_anchor(self, year: int) -> pd.Timestamp:
         """Generates a timestamp for the end of interval 0 in year.
@@ -189,7 +178,7 @@ class MonthlyCalendar(BaseCalendar):
         """
         periods_per_year = 12 / int(self.freq.replace("M", ""))
         return (
-            (self.max_lag + self.n_targets) if self.max_lag else int(periods_per_year)
+            (self._max_lag + self.n_targets) if self._max_lag > 0 else int(periods_per_year)
         )
 
     def _get_skip_nyears(self) -> int:
@@ -201,7 +190,11 @@ class MonthlyCalendar(BaseCalendar):
             int: Number of years that need to be skipped.
         """
         nmonths = int(self.freq.replace("M", ""))
-        return (np.ceil(nmonths / 12) - 1) if self.max_lag else 0
+        return (
+            0
+            if self._max_lag > 0 and self._allow_overlap
+            else int(np.ceil(nmonths / 12) - 1)
+        )
 
     def _interval_as_month(self, interval):
         """Turns an interval with pandas Timestamp values to a formatted string.
@@ -239,7 +232,6 @@ class WeeklyCalendar(BaseCalendar):
         anchor: int,
         freq: str = "1W",
         n_targets: int = 1,
-        max_lag: Optional[int] = None,
     ) -> None:
         """Instantiate a basic week number calendar with minimal configuration.
 
@@ -254,12 +246,6 @@ class WeeklyCalendar(BaseCalendar):
                 It will countdown until this week.
             freq: Frequency of the calendar, e.g. '2W'.
             n_targets: integer specifying the number of target intervals in a period.
-            max_lag: Maximum number of lag periods after the target period. If `None`,
-                the maximum lag will be determined by how many fit in each anchor year.
-                If a maximum lag is provided, the intervals can either only cover part
-                of the year, or extend over multiple years. In case of a large max_lag
-                number where the intervals extend over multiple years, anchor years will
-                be skipped to avoid overlapping intervals.
 
         Example:
             Instantiate a calendar counting down the weeks until week number 40.
@@ -267,7 +253,7 @@ class WeeklyCalendar(BaseCalendar):
             >>> import s2spy.time
             >>> calendar = s2spy.time.WeeklyCalendar(anchor=40, freq="1W")
             >>> calendar
-            WeeklyCalendar(week=40, freq=1W, n_targets=1, max_lag=None)
+            WeeklyCalendar(week=40, freq=1W, n_targets=1)
 
         """
         if not re.fullmatch(r"\d*W", freq):
@@ -276,7 +262,9 @@ class WeeklyCalendar(BaseCalendar):
         self.week = anchor
         self.freq = freq
         self.n_targets = n_targets
-        self.max_lag = max_lag
+
+        self._max_lag: int = 0
+        self._allow_overlap: bool = False
 
     def _get_anchor(self, year: int) -> pd.Timestamp:
         """Generates a timestamp for the end of interval 0 in year.
diff --git a/s2spy/utils.py b/s2spy/utils.py
index 4f88764..e090f19 100644
--- a/s2spy/utils.py
+++ b/s2spy/utils.py
@@ -1,3 +1,7 @@
+import re
+import warnings
+from typing import Union
+import numpy as np
 import pandas as pd
 import xarray as xr
 
@@ -36,3 +40,63 @@ def check_time_dim_pandas(data) -> None:
     """Utility function to check if pandas data has an index with time data."""
     if not isinstance(data.index, pd.DatetimeIndex):
         raise ValueError("The input data does not have a datetime index.")
+
+
+def check_empty_intervals(data: Union[pd.DataFrame, xr.Dataset]) -> None:
+    """Utility to check for empty intervals in data.
+
+    Note: For the Dataset, all values within a certain interval, anchor_year combination
+    have to be NaN, to allow for, e.g., empty gridcells in a latitude/longitude grid.
+
+    Args:
+        data (Union[pd.DataFrame, xr.Dataset]): Data that should be checked for empty
+            intervals. Should be done after resampling the data.
+
+    Raises:
+        UserWarning: If the data is insufficient.
+
+    Returns:
+        None
+    """
+    if isinstance(data, pd.DataFrame) and not np.any(np.isnan(data.iloc[:, 3:])):
+        return None
+    if isinstance(data, xr.Dataset) and not any(
+        data[var].isnull().any(dim=["i_interval", "anchor_year"]).all()
+        for var in data.data_vars
+    ):
+        return None
+
+    warnings.warn(
+        "The input data could not fully cover the calendar's intervals. "
+        "Intervals without available data will contain NaN values."
+    )
+    return None
+
+
+def check_input_frequency(calendar, data):
+    """Checks the frequency of (input) data.
+
+    Note: Pandas and xarray have the builtin function `infer_freq`, but this function is
+    not robust enough for our purpose, so we have to manually infer the frequency if the
+    builtin one fails.
+    """
+    if isinstance(data, PandasData):
+        data_freq = pd.infer_freq(data.index)
+        if data_freq is None: # Manually infer the frequency
+            data_freq = np.min(data.index.values[1:] - data.index.values[:-1])
+    else:
+        data_freq = xr.infer_freq(data.time)
+        if data_freq is None: # Manually infer the frequency
+            data_freq = (data.time.values[1:] - data.time.values[:-1]).min()
+
+    if isinstance(data_freq, str):
+        data_freq.replace("-", "")
+        if not re.match(r'\d+\D', data_freq):
+            data_freq = '1' + data_freq
+
+    if pd.Timedelta(calendar.freq) < pd.Timedelta(data_freq):
+        warnings.warn(
+            """Target frequency is smaller than the original frequency.
+            The resampled data will contain NaN values, as there is no data
+            available within all intervals."""
+        )


Test patch:
diff --git a/tests/test_resample.py b/tests/test_resample.py
new file mode 100644
index 0000000..8a811a3
--- /dev/null
+++ b/tests/test_resample.py
@@ -0,0 +1,155 @@
+"""Tests for s2spy.time's resample module.
+"""
+import numpy as np
+import pandas as pd
+import pytest
+from s2spy.time import AdventCalendar
+from s2spy.time import resample
+
+class TestResample:
+    """Test resample methods."""
+
+    # Define all required inputs as fixtures:
+    @pytest.fixture(autouse=True)
+    def dummy_calendar(self):
+        return AdventCalendar(anchor=(10, 15), freq="180d")
+
+    @pytest.fixture(autouse=True, params=[1, 2, 3])
+    def dummy_calendar_targets(self, request):
+        return AdventCalendar(anchor=(5, 10), freq="100d", n_targets=request.param)
+
+    @pytest.fixture(params=["20151020", "20191015"])
+    def dummy_series(self, request):
+        time_index = pd.date_range(request.param, "20211001", freq="60d")
+        test_data = np.random.random(len(time_index))
+        expected = np.array([test_data[4:7].mean(), test_data[1:4].mean()])
+        series = pd.Series(test_data, index=time_index, name="data1")
+        return series, expected
+
+    @pytest.fixture
+    def dummy_dataframe(self, dummy_series):
+        series, expected = dummy_series
+        return pd.DataFrame(series), expected
+
+    @pytest.fixture
+    def dummy_dataarray(self, dummy_series):
+        series, expected = dummy_series
+        dataarray = series.to_xarray()
+        dataarray = dataarray.rename({"index": "time"})
+        return dataarray, expected
+
+    @pytest.fixture
+    def dummy_dataset(self, dummy_dataframe):
+        dataframe, expected = dummy_dataframe
+        dataset = dataframe.to_xarray().rename({"index": "time"})
+        return dataset, expected
+
+    # Tests start here:
+    def test_non_mapped_calendar(self, dummy_calendar):
+        with pytest.raises(ValueError):
+            resample(dummy_calendar, None)
+
+    def test_nontime_index(self, dummy_calendar, dummy_series):
+        series, _ = dummy_series
+        cal = dummy_calendar.map_to_data(series)
+        series = series.reset_index()
+        with pytest.raises(ValueError):
+            resample(cal, series)
+
+    def test_series(self, dummy_calendar, dummy_series):
+        series, expected = dummy_series
+        cal = dummy_calendar.map_to_data(series)
+        resampled_data = resample(cal, series)
+        np.testing.assert_allclose(resampled_data["data1"].iloc[:2], expected)
+
+    def test_unnamed_series(self, dummy_calendar, dummy_series):
+        series, expected = dummy_series
+        series.name = None
+        cal = dummy_calendar.map_to_data(series)
+        resampled_data = resample(cal, series)
+        np.testing.assert_allclose(resampled_data["mean_data"].iloc[:2], expected)
+
+    def test_dataframe(self, dummy_calendar, dummy_dataframe):
+        dataframe, expected = dummy_dataframe
+        cal = dummy_calendar.map_to_data(dataframe)
+        resampled_data = resample(cal, dataframe)
+        np.testing.assert_allclose(resampled_data["data1"].iloc[:2], expected)
+
+    def test_dataarray(self, dummy_calendar, dummy_dataarray):
+        dataarray, expected = dummy_dataarray
+        cal = dummy_calendar.map_to_data(dataarray)
+        resampled_data = resample(cal, dataarray)
+        testing_vals = resampled_data["data1"].isel(anchor_year=0)
+        np.testing.assert_allclose(testing_vals, expected)
+
+    def test_dataset(self, dummy_calendar, dummy_dataset):
+        dataset, expected = dummy_dataset
+        cal = dummy_calendar.map_to_data(dataset)
+        resampled_data = resample(cal, dataset)
+        testing_vals = resampled_data["data1"].isel(anchor_year=0)
+        np.testing.assert_allclose(testing_vals, expected)
+
+    def test_target_period_dataframe(self, dummy_calendar_targets, dummy_dataframe):
+        df, _ = dummy_dataframe
+        calendar = dummy_calendar_targets.map_to_data(df)
+        resampled_data = resample(calendar, df)
+        expected = np.zeros(resampled_data.index.size, dtype=bool)
+        for i in range(calendar.n_targets):
+            expected[i::3] = True
+        np.testing.assert_array_equal(resampled_data["target"].values, expected)
+
+    def test_target_period_dataset(self, dummy_calendar_targets, dummy_dataset):
+        ds, _ = dummy_dataset
+        calendar = dummy_calendar_targets.map_to_data(ds)
+        resampled_data = resample(calendar, ds)
+        expected = np.zeros(3, dtype=bool)
+        expected[: dummy_calendar_targets.n_targets] = True
+        np.testing.assert_array_equal(resampled_data["target"].values, expected)
+
+    def test_allow_overlap_dataframe(self):
+        calendar = AdventCalendar(anchor=(10, 15), freq="100d")
+        calendar.set_max_lag(5, allow_overlap=True)
+        time_index = pd.date_range("20151101", "20211101", freq="50d")
+        test_data = np.random.random(len(time_index))
+        series = pd.Series(test_data, index=time_index)
+        calendar.map_to_data(series)
+        intervals = calendar.get_intervals()
+        # 4 anchor years are expected if overlap is allowed
+        assert len(intervals.index) == 4
+
+    # Test data for missing intervals, too low frequency.
+    def test_missing_intervals_dataframe(self, dummy_calendar, dummy_dataframe):
+        dataframe, _ = dummy_dataframe
+        cal = dummy_calendar.map_years(2020, 2025)
+        with pytest.warns(UserWarning):
+            resample(cal, dataframe)
+
+    def test_missing_intervals_dataset(self, dummy_calendar, dummy_dataset):
+        dataset, _ = dummy_dataset
+        cal = dummy_calendar.map_years(2020, 2025)
+        with pytest.warns(UserWarning):
+            resample(cal, dataset)
+
+    def test_low_freq_dataframe(self, dummy_dataframe):
+        cal = AdventCalendar(anchor=(10, 15), freq="1d")
+        dataframe, _ = dummy_dataframe
+        cal = cal.map_to_data(dataframe)
+        with pytest.warns(UserWarning):
+            resample(cal, dataframe)
+
+    def test_low_freq_dataset(self, dummy_dataset):
+        cal = AdventCalendar(anchor=(10, 15), freq="1d")
+        dataset, _ = dummy_dataset
+        cal = cal.map_to_data(dataset)
+        with pytest.warns(UserWarning):
+            resample(cal, dataset)
+
+    def test_1day_freq_dataframe(self):
+        # Will test the regular expression match and pre-pending of '1' in the
+        # check_input_frequency utility function
+        calendar = AdventCalendar(anchor=(10, 15), freq="1d")
+        time_index = pd.date_range("20191101", "20211101", freq="1d")
+        test_data = np.random.random(len(time_index))
+        series = pd.Series(test_data, index=time_index, name="data1")
+        calendar.map_to_data(series)
+        calendar.get_intervals()
diff --git a/tests/test_time.py b/tests/test_time.py
index 7690288..6d2c083 100644
--- a/tests/test_time.py
+++ b/tests/test_time.py
@@ -7,7 +7,6 @@ import xarray as xr
 from s2spy.time import AdventCalendar
 from s2spy.time import MonthlyCalendar
 from s2spy.time import WeeklyCalendar
-from s2spy.time import resample
 
 
 def interval(start, end):
@@ -31,7 +30,7 @@ class TestAdventCalendar:
     def test_repr(self):
         cal = AdventCalendar()
         assert repr(cal) == (
-            "AdventCalendar(month=11, day=30, freq=7d, n_targets=1, max_lag=None)"
+            "AdventCalendar(month=11, day=30, freq=7d, n_targets=1)"
         )
 
     def test_show(self, dummy_calendar):
@@ -56,6 +55,16 @@ class TestAdventCalendar:
         with pytest.raises(ValueError):
             AdventCalendar(freq='2W')
 
+    def test_set_max_lag(self):
+        cal = AdventCalendar()
+        cal.set_max_lag(max_lag=5)
+
+    def test_set_max_lag_incorrect_val(self):
+        cal = AdventCalendar()
+        with pytest.raises(ValueError):
+            cal.set_max_lag(-1)
+
+
 class TestMonthlyCalendar:
     """Test MonthlyCalendar methods."""
 
@@ -72,7 +81,7 @@ class TestMonthlyCalendar:
     def test_repr(self):
         cal = MonthlyCalendar(anchor='Dec', freq="2M")
         assert repr(cal) == (
-            "MonthlyCalendar(month=12, freq=2M, n_targets=1, max_lag=None)"
+            "MonthlyCalendar(month=12, freq=2M, n_targets=1)"
         )
 
     def test_show(self, dummy_calendar):
@@ -111,7 +120,7 @@ class TestWeeklyCalendar:
     def test_repr(self):
         cal = WeeklyCalendar(anchor=48, freq="30W")
         assert repr(cal) == (
-            "WeeklyCalendar(week=48, freq=30W, n_targets=1, max_lag=None)"
+            "WeeklyCalendar(week=48, freq=30W, n_targets=1)"
         )
 
     def test_show(self, dummy_calendar):
@@ -299,111 +308,11 @@ class TestMap:
     # and where the max_lag just causes the calendar to skip a year
     @pytest.mark.parametrize("max_lag,expected_index,expected_size", max_lag_edge_cases)
     def test_max_lag_skip_years(self, max_lag, expected_index, expected_size):
-        calendar = AdventCalendar(anchor=(12, 31), freq="5d", max_lag=max_lag)
+        calendar = AdventCalendar(anchor=(12, 31), freq="5d")
+        calendar.set_max_lag(max_lag)
         calendar = calendar.map_years(2018, 2019)
 
         np.testing.assert_array_equal(
             calendar.get_intervals().index.values, expected_index
         )
         assert calendar.get_intervals().iloc[0].size == expected_size
-
-
-class TestResample:
-    """Test resample methods."""
-
-    # Define all required inputs as fixtures:
-    @pytest.fixture(autouse=True)
-    def dummy_calendar(self):
-        return AdventCalendar(anchor=(10, 15), freq="180d")
-
-    @pytest.fixture(autouse=True, params=[1, 2, 3])
-    def dummy_calendar_targets(self, request):
-        return AdventCalendar(anchor=(5, 10), freq="100d", n_targets=request.param)
-
-    @pytest.fixture(params=["20151020", "20191020"])
-    def dummy_series(self, request):
-        time_index = pd.date_range(request.param, "20211001", freq="60d")
-        test_data = np.random.random(len(time_index))
-        expected = np.array([test_data[4:7].mean(), test_data[1:4].mean()])
-        series = pd.Series(test_data, index=time_index, name="data1")
-        return series, expected
-
-    @pytest.fixture
-    def dummy_dataframe(self, dummy_series):
-        series, expected = dummy_series
-        return pd.DataFrame(series), expected
-
-    @pytest.fixture
-    def dummy_dataarray(self, dummy_series):
-        series, expected = dummy_series
-        dataarray = series.to_xarray()
-        dataarray = dataarray.rename({"index": "time"})
-        return dataarray, expected
-
-    @pytest.fixture
-    def dummy_dataset(self, dummy_dataframe):
-        dataframe, expected = dummy_dataframe
-        dataset = dataframe.to_xarray().rename({"index": "time"})
-        return dataset, expected
-
-    # Tests start here:
-    def test_non_mapped_calendar(self, dummy_calendar):
-        with pytest.raises(ValueError):
-            resample(dummy_calendar, None)
-
-    def test_nontime_index(self, dummy_calendar, dummy_series):
-        series, _ = dummy_series
-        cal = dummy_calendar.map_to_data(series)
-        series = series.reset_index()
-        with pytest.raises(ValueError):
-            resample(cal, series)
-
-    def test_series(self, dummy_calendar, dummy_series):
-        series, expected = dummy_series
-        cal = dummy_calendar.map_to_data(series)
-        resampled_data = resample(cal, series)
-        np.testing.assert_allclose(resampled_data["data1"].iloc[:2], expected)
-
-    def test_unnamed_series(self, dummy_calendar, dummy_series):
-        series, expected = dummy_series
-        series.name = None
-        cal = dummy_calendar.map_to_data(series)
-        resampled_data = resample(cal, series)
-        np.testing.assert_allclose(resampled_data["mean_data"].iloc[:2], expected)
-
-    def test_dataframe(self, dummy_calendar, dummy_dataframe):
-        dataframe, expected = dummy_dataframe
-        cal = dummy_calendar.map_to_data(dataframe)
-        resampled_data = resample(cal, dataframe)
-        np.testing.assert_allclose(resampled_data["data1"].iloc[:2], expected)
-
-    def test_dataarray(self, dummy_calendar, dummy_dataarray):
-        dataarray, expected = dummy_dataarray
-        cal = dummy_calendar.map_to_data(dataarray)
-        resampled_data = resample(cal, dataarray)
-        testing_vals = resampled_data["data1"].isel(anchor_year=0)
-        np.testing.assert_allclose(testing_vals, expected)
-
-    def test_dataset(self, dummy_calendar, dummy_dataset):
-        dataset, expected = dummy_dataset
-        cal = dummy_calendar.map_to_data(dataset)
-        resampled_data = resample(cal, dataset)
-        testing_vals = resampled_data["data1"].isel(anchor_year=0)
-        np.testing.assert_allclose(testing_vals, expected)
-
-    def test_target_period_dataframe(self, dummy_calendar_targets, dummy_dataframe):
-        df, _ = dummy_dataframe
-        calendar = dummy_calendar_targets.map_to_data(df)
-        resampled_data = resample(calendar, df)
-        expected = np.zeros(resampled_data.index.size, dtype=bool)
-        for i in range(calendar.n_targets):
-            expected[i::3] = True
-        np.testing.assert_array_equal(resampled_data["target"].values, expected)
-
-    def test_target_period_dataset(self, dummy_calendar_targets, dummy_dataset):
-        ds, _ = dummy_dataset
-        calendar = dummy_calendar_targets.map_to_data(ds)
-        resampled_data = resample(calendar, ds)
-        expected = np.zeros(3, dtype=bool)
-        expected[: dummy_calendar_targets.n_targets] = True
-        np.testing.assert_array_equal(resampled_data["target"].values, expected)