diff --git a/.travis.yml b/.travis.yml index 31c8b7e..fe4b70c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ before_install: - chmod +x miniconda.sh - "./miniconda.sh -b -p /home/travis/mc" - export PATH=/home/travis/mc/bin:$PATH +- export MPLBACKEND='Agg' install: - conda update --yes conda - conda config --add channels soft-matter diff --git a/opengrid/__init__.py b/opengrid/__init__.py index 469f06b..6b71eb0 100644 --- a/opengrid/__init__.py +++ b/opengrid/__init__.py @@ -1,5 +1,5 @@ from opengrid.__about__ import __version__ -import opengrid.library.analysis as analysis +from opengrid.library import analysis, plotting from opengrid.library.regression import MultiVarLinReg -import opengrid.datasets as datasets +from opengrid import datasets from opengrid.library.plotting import plot_style diff --git a/opengrid/library/plotting.py b/opengrid/library/plotting.py index 722d65c..3908fb7 100644 --- a/opengrid/library/plotting.py +++ b/opengrid/library/plotting.py @@ -1,12 +1,18 @@ -import matplotlib.pyplot as plt +import os +import numpy as np +import pandas as pd import matplotlib +import matplotlib.cm as cm +import matplotlib.pyplot as plt +from matplotlib.dates import date2num, num2date, HourLocator, DayLocator, AutoDateLocator, DateFormatter +from matplotlib.colors import LogNorm def plot_style(): - try: + # matplotlib inline, only when jupyter notebook + # try-except causes problems in Pycharm Console + if 'JPY_PARENT_PID' in os.environ: get_ipython().run_line_magic('matplotlib', 'inline') - except NameError: - pass matplotlib.style.use('seaborn-talk') matplotlib.style.use('seaborn-whitegrid') @@ -14,3 +20,97 @@ def plot_style(): plt.rcParams['figure.figsize'] = 16, 6 return plt + + +def carpet(timeseries, **kwargs): + """ + Draw a carpet plot of a pandas timeseries. + + The carpet plot reads like a letter. Every day one line is added to the + bottom of the figure, minute for minute moving from left (morning) to right + (evening). + The color denotes the level of consumption and is scaled logarithmically. + If vmin and vmax are not provided as inputs, the minimum and maximum of the + colorbar represent the minimum and maximum of the (resampled) timeseries. + + Parameters + ---------- + timeseries : pandas.Series + vmin, vmax : If not None, either or both of these values determine the range + of the z axis. If None, the range is given by the minimum and/or maximum + of the (resampled) timeseries. + zlabel, title : If not None, these determine the labels of z axis and/or + title. If None, the name of the timeseries is used if defined. + cmap : matplotlib.cm instance, default coolwarm + + Examples + -------- + >>> import numpy as np + >>> import pandas as pd + >>> from opengrid.library import plotting + >>> plt = plotting.plot_style() + >>> index = pd.date_range('2015-1-1','2015-12-31',freq='h') + >>> ser = pd.Series(np.random.normal(size=len(index)), index=index, name='abc') + >>> im = plotting.carpet(ser) + """ + + # define optional input parameters + cmap = kwargs.pop('cmap', cm.coolwarm) + norm = kwargs.pop('norm', LogNorm()) + interpolation = kwargs.pop('interpolation', 'nearest') + cblabel = kwargs.pop('zlabel', timeseries.name if timeseries.name else '') + title = kwargs.pop('title', 'carpet plot: ' + timeseries.name if timeseries.name else '') + + # data preparation + if timeseries.dropna().empty: + print('skipped {} - no data'.format(title)) + return + ts = timeseries.resample('15min').interpolate() + vmin = max(0.1, kwargs.pop('vmin', ts[ts > 0].min())) + vmax = max(vmin, kwargs.pop('vmax', ts.quantile(.999))) + + # convert to dataframe with date as index and time as columns by + # first replacing the index by a MultiIndex + mpldatetimes = date2num(ts.index.to_pydatetime()) + ts.index = pd.MultiIndex.from_arrays( + [np.floor(mpldatetimes), 2 + mpldatetimes % 1]) # '2 +': matplotlib bug workaround. + # and then unstacking the second index level to columns + df = ts.unstack() + + # data plotting + + fig, ax = plt.subplots() + # define the extent of the axes (remark the +- 0.5 for the y axis in order to obtain aligned date ticks) + extent = [df.columns[0], df.columns[-1], df.index[-1] + 0.5, df.index[0] - 0.5] + im = plt.imshow(df, vmin=vmin, vmax=vmax, extent=extent, cmap=cmap, aspect='auto', norm=norm, + interpolation=interpolation, **kwargs) + + # figure formatting + + # x axis + ax.xaxis_date() + ax.xaxis.set_major_locator(HourLocator(interval=2)) + ax.xaxis.set_major_formatter(DateFormatter('%H:%M')) + ax.xaxis.grid(True) + plt.xlabel('UTC Time') + + # y axis + ax.yaxis_date() + dmin, dmax = ax.yaxis.get_data_interval() + number_of_days = (num2date(dmax) - num2date(dmin)).days + # AutoDateLocator is not suited in case few data is available + if abs(number_of_days) <= 35: + ax.yaxis.set_major_locator(DayLocator()) + else: + ax.yaxis.set_major_locator(AutoDateLocator()) + ax.yaxis.set_major_formatter(DateFormatter("%a, %d %b %Y")) + + # plot colorbar + cbticks = np.logspace(np.log10(vmin), np.log10(vmax), 11, endpoint=True) + cb = plt.colorbar(format='%.0f', ticks=cbticks) + cb.set_label(cblabel) + + # plot title + plt.title(title) + + return im diff --git a/opengrid/tests/test_plotting.py b/opengrid/tests/test_plotting.py index 0b14a0d..73dc2b4 100644 --- a/opengrid/tests/test_plotting.py +++ b/opengrid/tests/test_plotting.py @@ -8,11 +8,21 @@ import unittest -class AnalysisTest(unittest.TestCase): - def test_plotting(self): +class PlotStyleTest(unittest.TestCase): + def test_default(self): from opengrid.library.plotting import plot_style plt = plot_style() +class CarpetTest(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-12-31', freq='h') + ser = pd.Series(np.random.normal(size=len(index)), index=index, name='abc') + plotting.carpet(ser) + + if __name__ == '__main__': unittest.main()