diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2b9d99540..6437a51f1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,6 +12,7 @@ values 'full range', 'interquartile' or 'central 90%'. The previous boolean argu - [#991](https://github.com/IAMconsortium/pyam/pull/991) Support the central 90% as limits in the statistics summary - [#989](https://github.com/IAMconsortium/pyam/pull/989) Add an `IamDataFrame.series` attribute to get timeseries data as **pd.Series** +- [#986](https://github.com/IAMconsortium/pyam/pull/986) Extend `set_meta_from_data()` to apply on different column # Release v3.4.0 diff --git a/pyam/core.py b/pyam/core.py index df681867f..9c63a35f0 100755 --- a/pyam/core.py +++ b/pyam/core.py @@ -946,31 +946,91 @@ def set_meta(self, meta, name=None, index=None): # noqa: C901 self._new_meta_column(name) self.meta[name] = meta[name].combine_first(self.meta[name]) - def set_meta_from_data(self, name, method=None, column="value", **kwargs): + def set_meta_from_data(self, name, method=None, column="value", on=None, **kwargs): """Add meta indicators from downselected timeseries data Parameters ---------- name : str - Column name of the 'meta' table - method : function, optional + Resulting column name in the 'meta' table. + method : function or str, optional Method for aggregation (e.g., :func:`numpy.max `); - required if downselected data do not yield unique values + required if downselected data do not yield unique values. column : str, optional - The column from `data` to be used to derive the indicator + The column from `data` to be used to derive the indicator. + on : str, optional + If given, apply the `method` on this column and use corresponding value from + `column` as meta indicator. **kwargs - Passed to :meth:`slice` for downselected data + Passed to :meth:`slice` for downselection of data. + + Raises + ------ + ValueError + If the resulting meta-indicators are not unique for each element + of the :attr:`index`. + + Examples + -------- + A simple use case for this method is to compute a meta-indicator for the peak + (maximum) temperature from annual temperature timeseries data for each scenario: + + .. code:: python + + df.set_meta_from_data( + name="Climate Assessment|Peak Warming [°C]", + method="max", + variable="Climate Assessment|Surface Temperature", + ) + + Alternatively, the *method* can be applied on a column *on* and the + corresponding value from the *column* is set as meta indicator. This can be + used to set the year when peak-temperature is reached as meta-indicator. + + .. code:: python + + df.set_meta_from_data( + name="Climate Assessment|Year of Peak Warming", + method="max", + column="value", + on="year", + variable="Climate Assessment|Surface Temperature", + ) + """ - values = self._data[self.slice(**kwargs)] - if method is None and column != "value": - values = values.reset_index(column)[column] - elif method is not None: - if column == "value": - values = values.groupby(self.index.names) - else: - values = values.reset_index(column).groupby(self.index.names)[column] - values = values.apply(method) + if on is not None: + + @staticmethod + def apply_method(x): + if callable(method): + value = x[x[on] == method(x[on])][column].unique() + else: + value = x[x[on] == x[on].apply(method)][column].unique() + + if len(value) > 1: + logger.warning(f"Non-unique result from {method} on column {on}.") + + return value[0] + + values = ( + self._data[self.slice(**kwargs)] + .reset_index([col for col in [column, on] if col != "value"]) + .groupby(self.index.names) + .apply(apply_method) + ) + else: + values = self._data[self.slice(**kwargs)] + if method is None and column != "value": + values = values.reset_index(column)[column] + elif method is not None: + if column == "value": + values = values.groupby(self.index.names) + else: + values = values.reset_index(column).groupby(self.index.names)[ + column + ] + values = values.apply(method) self.set_meta(values, name) def categorize( diff --git a/tests/test_feature_set_meta.py b/tests/test_feature_set_meta.py index 07aa27c97..6a967c9cf 100644 --- a/tests/test_feature_set_meta.py +++ b/tests/test_feature_set_meta.py @@ -141,7 +141,7 @@ def test_set_meta_from_data_mean(test_df): pdt.assert_series_equal(test_df["pe_mean"], exp) -def test_set_meta_from_data_method_other_column(test_df): +def test_set_meta_from_data_method_non_default_column(test_df): if "year" in test_df.data.columns: col, value = "year", 2010 else: @@ -158,3 +158,13 @@ def test_set_meta_from_data_nonunique(test_df): pytest.raises( ValueError, test_df.set_meta_from_data, "fail", variable="Primary Energy" ) + + +@pytest.mark.parametrize("method", ("min", np.min)) +def test_set_meta_from_data_method_on_other_column(test_df_year, method): + # get the variable that has the lowest data value for each scenario + test_df_year.set_meta_from_data("foo", method=method, column="variable", on="value") + exp = pd.Series( + data=["Primary Energy|Coal", "Primary Energy"], index=EXP_IDX, name="foo" + ) + pdt.assert_series_equal(test_df_year.meta["foo"], exp)