diff --git a/.gitignore b/.gitignore index 1324af9..6ad77c4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ opengrid/notebooks/index.html dist/ build/ -opengrid.egg-info/ \ No newline at end of file +opengrid.egg-info/ +.DS_Store diff --git a/opengrid/library/analysis.py b/opengrid/library/analysis.py index daea53a..d0f9151 100644 --- a/opengrid/library/analysis.py +++ b/opengrid/library/analysis.py @@ -9,6 +9,7 @@ import datetime as dt import pandas as pd import numpy as np +import numbers from opengrid.library.exceptions import EmptyDataFrame @@ -68,7 +69,7 @@ def do_analysis(self, agg, starttime=dt.time.min, endtime=dt.time.max): self.result = pd.DataFrame() -def standby(df, resolution='d'): +def standby(df, resolution='24h', time_window=None): """ Compute standby power @@ -76,14 +77,72 @@ def standby(df, resolution='d'): ---------- df : Pandas DataFrame Electricity Power - resolution : str + resolution : str, default='d' + Resolution of the computation. Data will be resampled to this resolution (as mean) before computation + of the minimum. + String that can be parsed by the pandas resample function, example ='h', '15min', '6h' + time_window : tuple with start-hour and end-hour, default=None + Specify the start-time and end-time for the analysis. + Only data within this time window will be considered. + Both times have to be specified as string ('01:00', '06:30') or as datetime.time() objects + + Returns + ------- + df : pandas.Series with DateTimeIndex in the given resolution """ + def parse_time(t): + if isinstance(t, numbers.Number): + return pd.Timestamp.utcfromtimestamp(t).time() + else: + return pd.Timestamp(t).time() + if df.empty: raise EmptyDataFrame() + # first filter based on the time-window + if time_window is not None: + t_start = parse_time(time_window[0]) + t_end = parse_time(time_window[1]) + if t_start > t_end: + # start before midnight + df = df[(df.index.time >= t_start) | (df.index.time < t_end)] + else: + df = df[(df.index.time >= t_start) & (df.index.time < t_end)] + return df.resample(resolution).min() +def share_of_standby(df, resolution='24h', time_window=None): + """ + Compute the share of the standby power in the total consumption. + + Parameters + ---------- + df : Pandas DataFrame + Power (typically electricity, can be anything) + resolution : str, default='d' + Resolution of the computation. Data will be resampled to this resolution (as mean) before computation + of the minimum. + String that can be parsed by the pandas resample function, example ='h', '15min', '6h' + time_window : tuple with start-hour and end-hour, default=None + Specify the start-time and end-time for the analysis. + Only data within this time window will be considered. + Both times have to be specified as string ('01:00', '06:30') or as datetime.time() objects + + Returns + ------- + fraction : float between 0-1 with the share of the standby consumption + """ + + df_ = pd.DataFrame(df) + p_sb = standby(df_, resolution, time_window) + df_resampled = df_.resample(resolution).mean() + p_tot = df_resampled.sum() + p_standby = p_sb.sum() + share_standby = p_standby/p_tot + return share_standby.iloc[0] + + def count_peaks(ts): """ Toggle counter for gas boilers @@ -105,3 +164,33 @@ def count_peaks(ts): result = on_toggles & shifted count = result.sum() return count + + +def load_factor(ts, resolution=None, norm=None): + """ + Calculate the ratio of input vs. norm over a given interval. + + Parameters + ---------- + ts : Pandas Series + timeseries + resolution : str, optional + interval over which to calculate the ratio + default: resolution of the input timeseries + norm : int | float, optional + denominator of the ratio + default: the maximum of the input timeseries + + Returns + ------- + Pandas Series + """ + if norm is None: + norm = ts.max() + + if resolution is not None: + ts = ts.resample(rule=resolution).mean() + + lf = ts/norm + + return lf diff --git a/opengrid/library/plotting.py b/opengrid/library/plotting.py index 3908fb7..4136e06 100644 --- a/opengrid/library/plotting.py +++ b/opengrid/library/plotting.py @@ -1,7 +1,10 @@ import os +import os import numpy as np import pandas as pd import matplotlib +import pandas as pd +import numpy as np import matplotlib.cm as cm import matplotlib.pyplot as plt from matplotlib.dates import date2num, num2date, HourLocator, DayLocator, AutoDateLocator, DateFormatter @@ -19,6 +22,12 @@ def plot_style(): matplotlib.style.use('seaborn-deep') plt.rcParams['figure.figsize'] = 16, 6 + + # To overrule the legend style + plt.rcParams['legend.facecolor'] = "#ffffff" + plt.rcParams['legend.frameon'] = True + plt.rcParams['legend.framealpha'] = 1 + return plt @@ -114,3 +123,56 @@ def carpet(timeseries, **kwargs): plt.title(title) return im + + +def boxplot(df, plot_mean=False, plot_ids=None, title=None, xlabel=None, ylabel=None): + """ + Plot boxplots + + Plot the boxplots of a dataframe in time + + Parameters + ---------- + df: Pandas Dataframe + Every collumn is a timeseries + plot_mean: bool + Wether or not to plot the means + plot_ids: [str] + List of id's to plot + + Returns + ------- + matplotlib figure + """ + + df = df.applymap(float) + description = df.apply(pd.DataFrame.describe, axis=1) + + # plot + plt = plot_style() + + plt.boxplot(df) + #plt.setp(bp['boxes'], color='black') + #plt.setp(bp['whiskers'], color='black') + if plot_ids is not None: + for id in plot_ids: + if id in df.columns: + plt.scatter(x=range(1, len(df) + 1), y=df[id], label=str(id)) + + if plot_mean: + plt.scatter(x=range(1, len(df) + 1), y=description['mean'], label="Mean", color='k', s=30, marker='+') + + ax = plt.gca() + ax.set_xticklabels(df.index) + #plt.xticks(rotation=45) + + plt.legend() + if title is not None: + plt.title(title) + if xlabel is not None: + plt.xlabel(xlabel) + if ylabel is not None: + plt.ylabel(ylabel) + + return plt.gcf() + diff --git a/opengrid/tests/test_analyses.py b/opengrid/tests/test_analyses.py index 4fc903e..796eb92 100644 --- a/opengrid/tests/test_analyses.py +++ b/opengrid/tests/test_analyses.py @@ -7,6 +7,7 @@ import unittest import pandas as pd +import numpy as np import opengrid as og from opengrid import datasets @@ -22,12 +23,47 @@ def test_standby(self): self.assertRaises(EmptyDataFrame, og.analysis.standby, pd.DataFrame) + def test_standby_with_time_window(self): + df = datasets.get('elec_power_min_1sensor') + res = og.analysis.standby(df, 'D', time_window=('01:00', '06:00')) + self.assertEqual(res.index.tz.zone, 'Europe/Brussels') + self.assertEqual(res.to_json(), '{"1507327200000":61.739999936,"1507413600000":214.9799999222,"1507500000000":53.0399997951,"1507586400000":55.7399999164,"1507672800000":59.94000006,"1507759200000":69.4800002407,"1507845600000":56.8200000236,"1507932000000":54.1799997864,"1508018400000":54.779999801,"1508104800000":54.7199997772,"1508191200000":98.5199999576,"1508277600000":55.6799999066,"1508364000000":53.9399997052,"1508450400000":109.5599999931,"1508536800000":144.3600001093,"1508623200000":52.7999997279}') + + res = og.analysis.standby(df, 'D', time_window=('22:00', '06:00')) + self.assertEqual(res.index.tz.zone, 'Europe/Brussels') + self.assertEqual(res.to_json(), '{"1507327200000":61.739999936,"1507413600000":119.2800000636,"1507500000000":53.0399997951,"1507586400000":55.7399999164,"1507672800000":59.94000006,"1507759200000":69.4800002407,"1507845600000":56.8200000236,"1507932000000":54.1799997864,"1508018400000":54.779999801,"1508104800000":54.7199997772,"1508191200000":98.5199999576,"1508277600000":55.6799999066,"1508364000000":53.9399997052,"1508450400000":96.3000000408,"1508536800000":133.9200000744,"1508623200000":52.7999997279}') + + def test_share_of_standby_1(self): + df = pd.DataFrame(data={'conso':np.ones(48)}, + index=pd.DatetimeIndex(start=pd.Timestamp('20180304'), periods=48, freq='h')) + share_of_standby = og.analysis.share_of_standby(df, resolution='24h') + self.assertEqual(share_of_standby, 1.0) + + def test_share_of_standby_2(self): + df = pd.DataFrame(data={'conso':np.ones(48)}, + index=pd.DatetimeIndex(start=pd.Timestamp('20180304'), periods=48, freq='h')) + df.iloc[0,0] = 0 + share_of_standby = og.analysis.share_of_standby(df, resolution='24h') + self.assertAlmostEqual(share_of_standby, 0.5106382978723404) + + def test_count_peaks(self): df = datasets.get('gas_dec2016_min') ts = df['313b'].head(100) count = og.analysis.count_peaks(ts) self.assertEqual(count, 13) + def test_load_factor(self): + ts = og.datasets.get('electricity_2016_hour') + ts = ts['e1de'].truncate(after=pd.Timestamp('20160107')) + lf1 = og.analysis.load_factor(ts) + self.assertIsInstance(ts, pd.Series) + self.assertAlmostEqual(ts.iloc[0], (lf1 * ts.max()).iloc[0]) + + lf2 = og.analysis.load_factor(ts, resolution='3h', norm=800) + self.assertIsInstance(ts, pd.Series) + self.assertAlmostEqual(175.0345212009457, (lf2 * 800).iloc[0]) + if __name__ == '__main__': unittest.main() diff --git a/opengrid/tests/test_plotting.py b/opengrid/tests/test_plotting.py index 2cde7a4..fd9b182 100644 --- a/opengrid/tests/test_plotting.py +++ b/opengrid/tests/test_plotting.py @@ -18,13 +18,30 @@ def test_default(self): class CarpetTest(unittest.TestCase): def test_default(self): import numpy as np - index = pd.date_range('2015-1-1', '2015-12-31', freq='h') + index = pd.date_range('2015-1-1', '2015-2-1', freq='h') ser = pd.Series(np.random.normal(size=len(index)), index=index, name='abc') assert plotting.carpet(ser) is not None def test_empty(self): assert plotting.carpet(pd.Series(index=list('abc'))) is None +class BoxplotTest(unittest.TestCase): + def test_default(self): + import numpy as np + import pandas as pd + from opengrid.library import plotting + index = pd.date_range('2015-1-1', '2015-2-1', freq='d') + df = pd.DataFrame(index=index, data=np.random.randint(5, size=(len(index),20))) + plotting.boxplot(df) + + def test_arguments(self): + import numpy as np + import pandas as pd + from opengrid.library import plotting + index = pd.date_range('2015-1-1', '2015-2-1', freq='d') + df = pd.DataFrame(index=index, data=np.random.randint(5, size=(len(index),20))) + plotting.boxplot(df, plot_mean=True, plot_ids=[2, 3], title="Title", xlabel="xlable", ylabel="ylable") + if __name__ == '__main__': unittest.main()