Repository: IAMconsortium/pyam
Pull request #197 (2019-02-18 14:28:11)

add function to normalize
starting to add a function to normalize all data to a specific time value

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

Coverage decreased (-0.5%) to 83.672% when pulling **f6b59a6292754caecaf4decf609d2645e4609c28 on gidden:normalize** into **a6ac0c574c3d5363fb8dba7d8b42a845fd3dbe87 on IAMconsortium:master**.

gidden: we can probably rework this to normalize to any specific subset of data, just works for time at the moment
gidden: Ok @danielhuppmann all suggested changes made!
danielhuppmann: thanks @gidden, looks great! do you want to add it to the release notes for completeness?
gidden: done!

Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 04c4b10..392a2bb 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,6 +2,7 @@
 # Next Release
 
 - [#199](https://github.com/IAMconsortium/pyam/pull/199) Initializing an `IamDataFrame` accepts kwargs to fill or create from the data any missing required columns
+- [#197](https://github.com/IAMconsortium/pyam/pull/197) Added a `normalize` function that normalizes all data in a data frame to a specific time period.
 - [#195](https://github.com/IAMconsortium/pyam/pull/195) Fix filtering for `time`, `day` and `hour` to use generic `pattern_match()` (if such a column exists) in 'year'-formmatted IamDataFrames
 - [#192](https://github.com/IAMconsortium/pyam/pull/192) Extend `utils.find_depth()` to optionally return depth (as list of ints) rather than assert level tests
 - [#190](https://github.com/IAMconsortium/pyam/pull/190) Add `concat()` function
diff --git a/pyam/core.py b/pyam/core.py
index 1469b16..634d75c 100644
--- a/pyam/core.py
+++ b/pyam/core.py
@@ -620,6 +620,29 @@ class IamDataFrame(object):
         if not inplace:
             return ret
 
+    def normalize(self, inplace=False, **kwargs):
+        """Normalize data to a given value. Currently only supports normalizing to a
+        specific time
+
+        Parameters
+        ----------
+        inplace: bool, default False
+            if True, do operation inplace and return None
+        kwargs: the values on which to normalize (e.g., `year=2005`)
+        """
+        if len(kwargs) > 1 or self.time_col not in kwargs:
+            raise ValueError('Only time(year)-based normalization supported')
+        ret = copy.deepcopy(self) if not inplace else self
+        df = ret.data
+        # change all below if supporting more in the future
+        cols = self.time_col
+        value = kwargs[self.time_col]
+        x = df.set_index(IAMC_IDX)
+        x['value'] /= x[x[cols] == value]['value']
+        ret.data = x.reset_index()
+        if not inplace:
+            return ret
+
     def aggregate(self, variable, components=None, units=None, append=False):
         """Compute the aggregate of timeseries components or sub-categories
 


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 13c98e8..222affe 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -965,3 +965,16 @@ def test_concat(meta_df):
     obs = result.meta.reset_index(drop=True)
     exp = pd.concat([left.meta, right.meta]).reset_index(drop=True)
     pd.testing.assert_frame_equal(obs, exp)
+
+
+def test_normalize(meta_df):
+    exp = meta_df.data.copy().reset_index(drop=True)
+    exp['value'][1::2] /= exp['value'][::2].values
+    exp['value'][::2] /= exp['value'][::2].values
+    obs = meta_df.normalize(year=2005).data.reset_index(drop=True)
+    pd.testing.assert_frame_equal(obs, exp)
+
+
+def test_normalize_not_time(meta_df):
+    pytest.raises(ValueError, meta_df.normalize, variable='foo')
+    pytest.raises(ValueError, meta_df.normalize, year=2015, variable='foo')