Repository: IAMconsortium/pyam
Pull request #334 (2020-02-17 14:02:55)

Interpolate date
# Please confirm that this PR has done the following:

- [x] Tests Added
- [x] Documentation Added
- [x] Description in RELEASE_NOTES.md Added

# Description of PR
Lets `interpolate` work on datetimes as well as integer years. Makes clear that `fill_series` works on both too. 



coveralls: 
[![Coverage Status](https://coveralls.io/builds/28775584/badge)](https://coveralls.io/builds/28775584)

Coverage decreased (-0.2%) to 85.416% when pulling **86eba2e342b8113e268c9402c76d21dce733558f on Rlamboll:interpolate_date** into **7d46b2c1c3a46d799c1b09dbabc9dbe88e80ee07 on IAMconsortium:master**.

danielhuppmann: @gidden, over to you for a final look-over and merge!
gidden: Looks great everybody. Thanks a bunch @Rlamboll !!

Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 5a3c54b..4585829 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,13 @@
 # Next release
 
+## API changes
+
+PR [#334](https://github.com/IAMconsortium/pyam/pull/334) Changes the arguments of `IamDataFrame.interpolate()` and `pyam.fill_series()` to `time`. It can still be an integer (i.e., year). 
+
+## Individual Updates
+
+- [#334](https://github.com/IAMconsortium/pyam/pull/334) Enable interpolate to work on datetimes.
+
 # Release v0.4.1
 
 This is a patch release to enable compatibility with `pandas v1.0`.
diff --git a/pyam/core.py b/pyam/core.py
index 49d2826..0baf6aa 100755
--- a/pyam/core.py
+++ b/pyam/core.py
@@ -343,24 +343,28 @@ class IamDataFrame(object):
                             aggfunc=aggfunc, fill_value=fill_value)
         return df
 
-    def interpolate(self, year):
+    def interpolate(self, time):
         """Interpolate missing values in timeseries (linear interpolation)
 
         Parameters
         ----------
-        year: int
-             year to be interpolated
+        time: int, datetime
+             Time or year to be interpolated. This must match the
+             date-time/year style of self.
         """
-        df = self.pivot_table(index=IAMC_IDX, columns=['year'],
+        if self.time_col == 'year' and not isinstance(time, int):
+            raise ValueError(
+                'The `time` argument `{}` is not an integer'.format(time)
+            )
+        df = self.pivot_table(index=IAMC_IDX, columns=[self.time_col],
                               values='value', aggfunc=np.sum)
-        # drop year-rows where values are already defined
-        if year in df.columns:
-            df = df[np.isnan(df[year])]
-        fill_values = df.apply(fill_series,
-                               raw=False, axis=1, year=year)
+        # drop time-rows where values are already defined
+        if time in df.columns:
+            df = df[np.isnan(df[time])]
+        fill_values = df.apply(fill_series, raw=False, axis=1, time=time)
         fill_values = fill_values.dropna().reset_index()
         fill_values = fill_values.rename(columns={0: "value"})
-        fill_values['year'] = year
+        fill_values[self.time_col] = time
         self.data = self.data.append(fill_values, ignore_index=True)
 
     def swap_time_for_year(self, inplace=False):
diff --git a/pyam/timeseries.py b/pyam/timeseries.py
index 83f724f..a80fe71 100644
--- a/pyam/timeseries.py
+++ b/pyam/timeseries.py
@@ -8,27 +8,27 @@ logger = logging.getLogger(__name__)
 # %%
 
 
-def fill_series(x, year):
-    """Returns the value of a timeseries (indexed over years) for a year
-    by linear interpolation.
+def fill_series(x, time):
+    """Returns the value of a timeseries (indexed over years) for a year or
+    datetime by linear interpolation.
 
     Parameters
     ----------
     x: pandas.Series
         a timeseries to be interpolated
-    year: int
-        year of interpolation
+    time: int
+        year or datetime to interpolate
     """
     x = x.dropna()
-    if year in x.index and not np.isnan(x[year]):
-        return x[year]
+    if time in x.index and not np.isnan(x[time]):
+        return x[time]
     else:
-        prev = [i for i in x.index if i < year]
-        nxt = [i for i in x.index if i > year]
+        prev = [i for i in x.index if i < time]
+        nxt = [i for i in x.index if i > time]
         if prev and nxt:
             p = max(prev)
             n = min(nxt)
-            return ((n - year) * x[p] + (year - p) * x[n]) / (n - p)
+            return ((n - time) * x[p] + (time - p) * x[n]) / (n - p)
         else:
             return np.nan
 


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index dde16c8..c44f9ad 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -668,6 +668,22 @@ def test_interpolate(test_df_year):
     assert not test_df_year.filter().data.duplicated().any()
 
 
+def test_interpolate_datetimes(test_df):
+    # test that interpolation also works with date-times.
+    some_date = datetime.datetime(2007, 7, 1)
+    if test_df.time_col == "year":
+        pytest.raises(ValueError, test_df.interpolate, time=some_date)
+    else:
+        test_df.interpolate(some_date)
+        obs = test_df.filter(time=some_date).data['value']\
+            .reset_index(drop=True)
+        exp = pd.Series([3, 1.5, 4], name='value')
+        pd.testing.assert_series_equal(obs, exp, check_less_precise=True)
+        # redo the interpolation and check that no duplicates are added
+        test_df.interpolate(some_date)
+        assert not test_df.filter().data.duplicated().any()
+
+
 def test_filter_by_bool(test_df):
     test_df.set_meta([True, False], name='exclude')
     obs = test_df.filter(exclude=True)
diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py
index 90262b3..e80bb67 100644
--- a/tests/test_timeseries.py
+++ b/tests/test_timeseries.py
@@ -1,5 +1,6 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
+import datetime
 import logging
 
 import numpy as np
@@ -14,6 +15,15 @@ def test_fill_series():
     assert fill_series(y, 2006) == 3.
 
 
+def test_fill_series_datetime():
+    # note that the series is not order and the index is defined as float
+    y = pd.Series(
+        data=[3, 1],
+        index=[datetime.datetime(2001, 1, 1), datetime.datetime(2003, 1, 1)]
+    )
+    assert fill_series(y, datetime.datetime(2002, 1, 1)) == 2.
+
+
 def test_fill_series_out_of_range():
     y = pd.Series(data=[np.nan, 1, 3, 1], index=[2002., 2005., 2007., 2013.])
     assert fill_series(y, 2001) is np.nan