Skip to content

Commit

Permalink
Implemented function to resample time series object (#269)
Browse files Browse the repository at this point in the history
* Implemented function resample_timeseries(self, method) to resample time series object

Time series object is resampled from hourly to quarter hourly resolution.
Three possible methods to fill missing values
* ffill (see: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ffill.html)
* bfill (see: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.bfill.html)
* interpolate (see: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.interpolate.html)

* Added freq parameter

* Added input parameter and wrapper function for `resample_timeseries`

Added input parameter `freq` to the function and added wrapper function in `EDisGo` class

* Added basic tests for function `resample_timeseries()`

* Added downsampling

* Added tests for down-sampling method

* Added documentation and NotImplementedError

* fixed tests for timeseries resampling

* minor doc string change

* debug github

* debug github

* debug github

* debug github

* corrected pandas links in docstrings

* Fix API doc

* Expand and reference docstring

* Fix docstring link

* Add comments

* Expand test

* Fix test

Co-authored-by: Kilian Helfenbein <Kilian.Helfenbein@rl-institut.de>
Co-authored-by: birgits <birgit.schachler@rl-institut.de>
  • Loading branch information
3 people committed Sep 4, 2022
1 parent 1f05023 commit 703507a
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 1 deletion.
46 changes: 46 additions & 0 deletions edisgo/edisgo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2037,6 +2037,52 @@ def check_integrity(self):

logging.info("Integrity check finished. Please pay attention to warnings.")

def resample_timeseries(self, method: str = "ffill", freq: str = "15min"):
"""
Resamples all generator, load and storage time series to a desired resolution.
The following time series are affected by this:
* :attr:`~.network.timeseries.TimeSeries.generators_active_power`
* :attr:`~.network.timeseries.TimeSeries.loads_active_power`
* :attr:`~.network.timeseries.TimeSeries.storage_units_active_power`
* :attr:`~.network.timeseries.TimeSeries.generators_reactive_power`
* :attr:`~.network.timeseries.TimeSeries.loads_reactive_power`
* :attr:`~.network.timeseries.TimeSeries.storage_units_reactive_power`
Both up- and down-sampling methods are possible.
Parameters
----------
method : str, optional
Method to choose from to fill missing values when resampling.
Possible options are:
* 'ffill'
Propagate last valid observation forward to next valid
observation. See :pandas:`pandas.DataFrame.ffill<DataFrame.ffill>`.
* 'bfill'
Use next valid observation to fill gap. See
:pandas:`pandas.DataFrame.bfill<DataFrame.bfill>`.
* 'interpolate'
Fill NaN values using an interpolation method. See
:pandas:`pandas.DataFrame.interpolate<DataFrame.interpolate>`.
Default: 'ffill'.
freq : str, optional
Frequency that time series is resampled to. Offset aliases can be found
here:
https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases.
Default: '15min'.
"""
self.timeseries.resample_timeseries(method=method, freq=freq)


def import_edisgo_from_pickle(filename, path=""):
abs_path = os.path.abspath(path)
Expand Down
86 changes: 85 additions & 1 deletion edisgo/network/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,11 @@ def reset(self):
Resets all time series.
Active and reactive power time series of all loads, generators and storage units
are deleted, as well as everything stored in :py:attr:`~time_series_raw`.
are deleted, as well as timeindex everything stored in
:py:attr:`~time_series_raw`.
"""
self.timeindex = pd.DatetimeIndex([])
self.generators_active_power = None
self.loads_active_power = None
self.storage_units_active_power = None
Expand Down Expand Up @@ -2145,6 +2147,88 @@ def _check_if_components_exist(
return set(component_names) - set(comps_not_in_network)
return component_names

def resample_timeseries(self, method: str = "ffill", freq: str = "15min"):
"""
Resamples all generator, load and storage time series to a desired resolution.
See :attr:`~.EDisGo.resample_timeseries` for more information.
Parameters
----------
method : str, optional
See :attr:`~.EDisGo.resample_timeseries` for more information.
freq : str, optional
See :attr:`~.EDisGo.resample_timeseries` for more information.
"""

# add time step at the end of the time series in case of up-sampling so that
# last time interval in the original time series is still included
attrs = self._attributes
freq_orig = self.timeindex[1] - self.timeindex[0]
df_dict = {}
for attr in attrs:
df_dict[attr] = getattr(self, attr)
if pd.Timedelta(freq) < freq_orig: # up-sampling
new_dates = pd.DatetimeIndex([df_dict[attr].index[-1] + freq_orig])
else: # down-sampling
new_dates = pd.DatetimeIndex([df_dict[attr].index[-1]])
df_dict[attr] = (
df_dict[attr]
.reindex(df_dict[attr].index.union(new_dates).unique().sort_values())
.ffill()
)

# create new index
if pd.Timedelta(freq) < freq_orig: # up-sampling
index = pd.date_range(
self.timeindex[0],
self.timeindex[-1] + freq_orig,
freq=freq,
closed="left",
)
else: # down-sampling
index = pd.date_range(
self.timeindex[0],
self.timeindex[-1],
freq=freq,
)

# set new timeindex
self._timeindex = index

# resample time series
if pd.Timedelta(freq) < freq_orig: # up-sampling
if method == "interpolate":
for attr in attrs:
setattr(
self,
attr,
df_dict[attr].resample(freq, closed="left").interpolate(),
)
elif method == "ffill":
for attr in attrs:
setattr(
self, attr, df_dict[attr].resample(freq, closed="left").ffill()
)
elif method == "bfill":
for attr in attrs:
setattr(
self, attr, df_dict[attr].resample(freq, closed="left").bfill()
)
else:
raise NotImplementedError(
f"Resampling method {method} is not implemented."
)
else: # down-sampling
for attr in attrs:
setattr(
self,
attr,
df_dict[attr].resample(freq).mean(),
)


class TimeSeriesRaw:
"""
Expand Down
66 changes: 66 additions & 0 deletions tests/network/test_timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@ def test_predefined_fluctuating_generators_by_technology(self):
# time series for generators are set for those for which time series are
# provided)
self.edisgo.timeseries.reset()
self.edisgo.timeseries.timeindex = timeindex
self.edisgo.timeseries.predefined_fluctuating_generators_by_technology(
self.edisgo, gens_p
)
Expand Down Expand Up @@ -2310,6 +2311,71 @@ def test_check_if_components_exist(self):
assert len(component_names) == 1
assert "Load_residential_LVGrid_5_3" in component_names

def test_resample_timeseries(self):

self.edisgo.set_time_series_worst_case_analysis()

len_timeindex_orig = len(self.edisgo.timeseries.timeindex)
mean_value_orig = self.edisgo.timeseries.generators_active_power.mean()

# test up-sampling
self.edisgo.timeseries.resample_timeseries()
# check if resampled length of time index is 4 times original length of
# timeindex
assert len(self.edisgo.timeseries.timeindex) == 4 * len_timeindex_orig
# check if mean value of resampled data is the same as mean value of original
# data
assert (
np.isclose(
self.edisgo.timeseries.generators_active_power.mean(),
mean_value_orig,
atol=1e-5,
)
).all()

# same tests for down-sampling
self.edisgo.timeseries.resample_timeseries(freq="2h")
assert len(self.edisgo.timeseries.timeindex) == 0.5 * len_timeindex_orig
assert (
np.isclose(
self.edisgo.timeseries.generators_active_power.mean(),
mean_value_orig,
atol=1e-5,
)
).all()

# test bfill
self.edisgo.timeseries.resample_timeseries(method="bfill")
assert len(self.edisgo.timeseries.timeindex) == 4 * len_timeindex_orig
assert np.isclose(
self.edisgo.timeseries.generators_active_power.iloc[1:, :].loc[
:, "GeneratorFluctuating_3"
],
2.26950,
atol=1e-5,
).all()

# test interpolate
self.edisgo.timeseries.reset()
self.edisgo.set_time_series_worst_case_analysis()
len_timeindex_orig = len(self.edisgo.timeseries.timeindex)
ts_orig = self.edisgo.timeseries.generators_active_power.loc[
:, "GeneratorFluctuating_3"
]
self.edisgo.timeseries.resample_timeseries(method="interpolate")
assert len(self.edisgo.timeseries.timeindex) == 4 * len_timeindex_orig
assert np.isclose(
self.edisgo.timeseries.generators_active_power.at[
pd.Timestamp("1970-01-01 01:30:00"), "GeneratorFluctuating_3"
],
(
ts_orig.at[pd.Timestamp("1970-01-01 01:00:00")]
+ ts_orig.at[pd.Timestamp("1970-01-01 02:00:00")]
)
/ 2,
atol=1e-5,
)


class TestTimeSeriesRaw:
@pytest.fixture(autouse=True)
Expand Down

0 comments on commit 703507a

Please sign in to comment.