-
Notifications
You must be signed in to change notification settings - Fork 120
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
27d0fac
commit 8dc1d27
Showing
10 changed files
with
208 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.. currentmodule:: pyam | ||
|
||
Computing indicators | ||
==================== | ||
|
||
.. autoclass:: IamComputeAccessor | ||
:members: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import math | ||
import pandas as pd | ||
from pyam.utils import remove_from_list | ||
|
||
|
||
class IamComputeAccessor: | ||
"""Perform computations on the timeseries data of an IamDataFrame | ||
An :class:`IamDataFrame` has a module for computation of (advanced) indicators | ||
from the timeseries data. | ||
The methods in this module can be accessed via | ||
.. code-block:: python | ||
IamDataFrame.compute.<method>(*args, **kwargs) | ||
""" | ||
|
||
def __init__(self, df): | ||
self._df = df | ||
|
||
def learning_rate(self, name, performance, experience, append=False): | ||
"""Compute the implicit learning rate from timeseries data | ||
Experience curves are based on the concept that a technology's performance | ||
improves as experience with this technology grows. | ||
The "learning rate" indicates the performance improvement (e.g., cost reduction) | ||
for each doubling of the accumulated experience (e.g., cumulative capacity). | ||
The experience curve parameter *b* is equivalent to the (linear) slope when | ||
plotting performance and experience timeseries on double-logarithmic scales. | ||
The learning rate can be computed from the experience curve parameter as | ||
:math:`1 - 2^{b}`. | ||
The learning rate parameter in period *t* is computed based on the changes | ||
to the subsequent period, i.e., from period *t* to period *t+1*. | ||
Parameters | ||
---------- | ||
name : str | ||
Variable name of the computed timeseries data. | ||
performance : str | ||
Variable of the "performance" timeseries (e.g., specific investment costs). | ||
experience : str | ||
Variable of the "experience" timeseries (e.g., installed capacity). | ||
append : bool, optional | ||
Whether to append computed timeseries data to this instance. | ||
Returns | ||
------- | ||
:class:`IamDataFrame` or **None** | ||
Computed timeseries data or None if `append=True`. | ||
""" | ||
value = ( | ||
self._df._data[self._df._apply_filters(variable=[performance, experience])] | ||
.groupby( | ||
remove_from_list(self._df.dimensions, ["variable", "year", "unit"]) | ||
) | ||
.apply(_compute_learning_rate, performance, experience) | ||
) | ||
|
||
return self._df._finalize(value, append=append, variable=name, unit="") | ||
|
||
|
||
def _compute_learning_rate(x, performance, experience): | ||
"""Internal implementation for computing implicit learning rate from timeseries data | ||
Parameters | ||
---------- | ||
x : :class:`pandas.Series` | ||
Timeseries data of the *performance* and *experience* variables | ||
indexed over the time domain. | ||
performance : str | ||
Variable of the "performance" timeseries (e.g., specific investment costs). | ||
experience : str | ||
Variable of the "experience" timeseries (e.g., cumulative installed capacity). | ||
Returns | ||
------- | ||
Indexed :class:`pandas.Series` of implicit learning rates | ||
""" | ||
# drop all index dimensions other than "variable" and "year" | ||
x.index = x.index.droplevel( | ||
[i for i in x.index.names if i not in ["variable", "year"]] | ||
) | ||
|
||
# apply log, dropping all values that are zero or negative | ||
x = x[x > 0].apply(math.log10) | ||
|
||
# return empty pd.Series if not all relevant variables exist | ||
if not all([v in x.index for v in [performance, experience]]): | ||
names = remove_from_list(x.index.names, "variable") | ||
empty_list = [[]] * len(names) | ||
return pd.Series( | ||
index=pd.MultiIndex(levels=empty_list, codes=empty_list, names=names), | ||
dtype="float64", | ||
) | ||
|
||
# compute the "experience parameter" (slope of experience curve on double-log scale) | ||
b = (x[performance] - x[performance].shift(periods=-1)) / ( | ||
x[experience] - x[experience].shift(periods=-1) | ||
) | ||
|
||
# translate to "learning rate" (e.g., cost reduction per doubling of capacity) | ||
return b.apply(lambda y: 1 - math.pow(2, y)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import pandas as pd | ||
from pyam import IamDataFrame, IAMC_IDX | ||
from pyam.testing import assert_iamframe_equal | ||
import pytest | ||
|
||
|
||
TEST_DF = IamDataFrame( | ||
pd.DataFrame( | ||
[ | ||
["model_a", "scen_a", "World", "Cap", "GW", 1, 2], | ||
["model_a", "scen_a", "World", "Cost", "US$2010/kW", 1, 0.5], | ||
["model_a", "scen_b", "World", "Cap", "GW", 0.1, 0.2], | ||
["model_a", "scen_b", "World", "Cost", "US$2010/kW", 1, 0.5], | ||
["model_a", "scen_c", "World", "Cap", "GW", 10, 20], | ||
["model_a", "scen_c", "World", "Cost", "US$2010/kW", 1, 0.5], | ||
["model_a", "scen_d", "World", "Cap", "GW", 1, 2], | ||
["model_a", "scen_d", "World", "Cost", "US$2010/kW", 1, 0.75], | ||
["model_a", "scen_e", "World", "Cap", "GW", 1, 2], | ||
["model_a", "scen_e", "World", "Cost", "US$2010/kW", 1, 0.25], | ||
], | ||
columns=IAMC_IDX + [2005, 2010], | ||
) | ||
) | ||
|
||
EXP_DF = IamDataFrame( | ||
pd.DataFrame( | ||
[ | ||
["model_a", "scen_a", "World", "Learning Rate", "", 0.5], | ||
["model_a", "scen_b", "World", "Learning Rate", "", 0.5], | ||
["model_a", "scen_c", "World", "Learning Rate", "", 0.5], | ||
["model_a", "scen_d", "World", "Learning Rate", "", 0.25], | ||
["model_a", "scen_e", "World", "Learning Rate", "", 0.75], | ||
], | ||
columns=IAMC_IDX + [2005], | ||
) | ||
) | ||
|
||
|
||
@pytest.mark.parametrize("append", (False, True)) | ||
def test_learning_rate(append): | ||
"""Check computing the learning rate""" | ||
|
||
if append: | ||
obs = TEST_DF.copy() | ||
obs.compute.learning_rate("Learning Rate", "Cost", "Cap", append=True) | ||
assert_iamframe_equal(TEST_DF.append(EXP_DF), obs) | ||
else: | ||
obs = TEST_DF.compute.learning_rate("Learning Rate", "Cost", "Cap") | ||
assert_iamframe_equal(EXP_DF, obs) | ||
|
||
|
||
@pytest.mark.parametrize("append", (False, True)) | ||
def test_learning_rate_empty(append): | ||
"""Assert that computing the learning rate with invalid variables returns empty""" | ||
|
||
if append: | ||
obs = TEST_DF.copy() | ||
obs.compute.learning_rate("Learning Rate", "foo", "Cap", append=True) | ||
assert_iamframe_equal(TEST_DF, obs) # assert that no data was added | ||
else: | ||
obs = TEST_DF.compute.learning_rate("Learning Rate", "foo", "Cap") | ||
assert obs.empty |