Skip to content

Commit

Permalink
Merge branch 'model_esthetics'
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinecarme committed Feb 4, 2023
2 parents b5c2717 + 76837e2 commit 1177454
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 26 deletions.
16 changes: 10 additions & 6 deletions pyaf/TS/Plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ def decomp_plot_as_png_base64(df, time, signal, estimator, residue, name = None,
plt.close(fig)
return png_b64

def prediction_interval_plot_internal(df, time, signal, estimator, lower, upper, name = None, format='png', max_length = 1000, horizon =
1) :
def prediction_interval_plot_internal(df, time, signal, estimator, lower, upper, name = None, format='png', max_length = 1000, horizon = 1, title = None) :
assert(df.shape[0] > 0)
assert(df.shape[1] > 0)
assert(time in df.columns)
Expand Down Expand Up @@ -119,6 +118,11 @@ def prediction_interval_plot_internal(df, time, signal, estimator, lower, upper,
if(name is not None):
plt.switch_backend('Agg')
fig, axs = plt.subplots(ncols=1, figsize=(16, 8))
if(title is not None):
axs.set_title(title + "\n")
else:
axs.set_title("Prediction Intervals\n")

df1.plot.line(time, [signal, estimator, lower, upper],
color=[SIGNAL_COLOR, FORECAST_COLOR, LOWER_COLOR, UPPER_COLOR],
ax=axs, grid = True, legend=False)
Expand All @@ -128,17 +132,17 @@ def prediction_interval_plot_internal(df, time, signal, estimator, lower, upper,

return fig

def prediction_interval_plot(df, time, signal, estimator, lower, upper, name = None, format='png', max_length = 1000, horizon = 1) :
fig = prediction_interval_plot_internal(df, time, signal, estimator, lower, upper, name, format, max_length, horizon)
def prediction_interval_plot(df, time, signal, estimator, lower, upper, name = None, format='png', max_length = 1000, horizon = 1, title = None) :
fig = prediction_interval_plot_internal(df, time, signal, estimator, lower, upper, name, format, max_length, horizon, title)
if(name is not None):
import matplotlib
import matplotlib.pyplot as plt
plt.switch_backend('Agg')
fig.savefig(name + '_prediction_intervals_output.' + format)
plt.close(fig)

def prediction_interval_plot_as_png_base64(df, time, signal, estimator, lower, upper, name = None, max_length = 1000, horizon = 1) :
fig = prediction_interval_plot_internal(df, time, signal, estimator, lower, upper, name, format, max_length, horizon)
def prediction_interval_plot_as_png_base64(df, time, signal, estimator, lower, upper, name = None, max_length = 1000, horizon = 1, title = None) :
fig = prediction_interval_plot_internal(df, time, signal, estimator, lower, upper, name, format, max_length, horizon, title)

import matplotlib
import matplotlib.pyplot as plt
Expand Down
59 changes: 39 additions & 20 deletions pyaf/TS/TimeSeriesModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,13 +394,17 @@ def standardPlots(self, name = None, format = 'png'):
lOutput.set_index(lTime, inplace=True, drop=False);
# print(lOutput[lTime].dtype);

# Add more informative title for this plot. Investigate Model Esthetics for PyAF #212
lTitle = "Prediction Intervals\n\nModel = " + self.mOutName + " [ "
lTitle = lTitle + "MAPE = " + str(self.mForecastPerf.mMAPE) + " ]"
tsplot.prediction_interval_plot(lOutput,
lTime, self.mOriginalSignal,
lForecastColumn,
lForecastColumn + '_Lower_Bound',
lForecastColumn + '_Upper_Bound',
name = name,
format= format, horizon = self.mTimeInfo.mHorizon);
format= format, horizon = self.mTimeInfo.mHorizon,
title = lTitle);

if(self.mTimeInfo.mOptions.mAddPredictionIntervals):
lQuantiles = self.mPredictionIntervalsEstimator.mForecastPerformances[lForecastColumn + "_1"].mErrorQuantiles.keys()
Expand All @@ -412,6 +416,8 @@ def standardPlots(self, name = None, format = 'png'):
name = name,
format= format, horizon = self.mTimeInfo.mHorizon);
#lOutput.plot()



def getPlotsAsDict(self):
lDict = {};
Expand All @@ -425,30 +431,43 @@ def getPlotsAsDict(self):
lDict["AR"] = tsplot.decomp_plot_as_png_base64(df, lTime, lPrefix + 'Cycle_residue' , lPrefix + 'AR' , lPrefix + 'AR_residue', name = "AR", horizon = self.mTimeInfo.mHorizon);
lDict["Forecast"] = tsplot.decomp_plot_as_png_base64(df, lTime, lSignalColumn, lPrefix2 + 'Forecast' , lPrefix2 + 'Residue', name = "forecast", horizon = self.mTimeInfo.mHorizon);

lOutput = df;
lDict["Prediction_Intervals"] = self.getPredictionIntervalPlot(df)
if(self.mTimeInfo.mOptions.mAddPredictionIntervals):
lDict["Forecast_Quantiles"] = self.getForecastQuantilesPlot(df)
return lDict;

def getPredictionIntervalPlot(self, df = None):
lOutput = df if df is not None else self.getForecastDatasetForPlots();
# print(lOutput.columns)
lPrefix = str(self.mOriginalSignal) + "_";
lForecastColumn = lPrefix + 'Forecast';
lTime = self.mTimeInfo.mTime;
lOutput.set_index(lTime, inplace=True, drop=False);
lDict["Prediction_Intervals"] = tsplot.prediction_interval_plot_as_png_base64(lOutput,
lTime, self.mOriginalSignal,
lForecastColumn ,
lForecastColumn + '_Lower_Bound',
lForecastColumn + '_Upper_Bound',
name = "prediction_intervals",
horizon = self.mTimeInfo.mHorizon);
if(self.mTimeInfo.mOptions.mAddPredictionIntervals):
lQuantiles = self.mPredictionIntervalsEstimator.mForecastPerformances[lForecastColumn + "_1"].mErrorQuantiles.keys()
lQuantiles = sorted(lQuantiles)
lDict["Forecast_Quantiles"] = tsplot.quantiles_plot_as_png_base64(lOutput,
lTime, self.mOriginalSignal,
lForecastColumn ,
lQuantiles,
name = "Forecast_Quantiles",
format= format, horizon = self.mTimeInfo.mHorizon);

return lDict;
# Add more informative title for this plot. Investigate Model Esthetics for PyAF #212
lTitle = "Prediction Intervals\n\nModel = " + self.mOutName + " [ "
lTitle = lTitle + "MAPE = " + str(self.mForecastPerf.mMAPE) + " ]"
return tsplot.prediction_interval_plot_as_png_base64(lOutput,
lTime, self.mOriginalSignal,
lForecastColumn ,
lForecastColumn + '_Lower_Bound',
lForecastColumn + '_Upper_Bound',
name = "prediction_intervals",
horizon = self.mTimeInfo.mHorizon,
title = lTitle);

def getForecastQuantilesPlot(self, df = None):
lOutput = df if df is not None else self.getForecastDatasetForPlots();
lPrefix = self.mOriginalSignal + "_";
lTime = self.mTime;
lForecastColumn = lPrefix + 'Forecast';
lQuantiles = self.mPredictionIntervalsEstimator.mForecastPerformances[lForecastColumn + "_1"].mErrorQuantiles.keys()
lQuantiles = sorted(lQuantiles)
return tsplot.quantiles_plot_as_png_base64(lOutput,
lTime, self.mOriginalSignal,
lForecastColumn ,
lQuantiles,
name = "Forecast_Quantiles [" + self.mOutName + "]",
format= format, horizon = self.mTimeInfo.mHorizon);

def getVersions(self):
lVersionDict = tsutil.getVersions();
Expand Down
103 changes: 103 additions & 0 deletions tests/all_models_plots/model_esthetics_visualizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from __future__ import absolute_import

import pandas as pd
import numpy as np

import pyaf.ForecastEngine as autof


def draw(iEngine, iFormula, iSignal):
print("DRAWING_PREDICTION_INTERVALS", iFormula)
b64_string = iEngine.mSignalDecomposition.mBestModels[iSignal].getPredictionIntervalPlot()
import io, base64, imageio
sourceString = io.BytesIO(base64.b64decode(b64_string))
lArray = imageio.imread(sourceString, pilmode='RGB')
return lArray

def plot_model(arg):
(lDataset , lSpec) = arg
lSignal = lDataset.mSignalVar
print("PLOT_MODEL_START" , lSpec)
lEngine = autof.cForecastEngine()
lEngine
H = lDataset.mHorizon;
# lEngine.mOptions.enable_slow_mode();
# lEngine.mOptions.mDebugPerformance = True;
lEngine.mOptions.mNbCores = 1
lEngine.mOptions.set_active_transformations(lSpec["Transformation"]);
lEngine.mOptions.set_active_trends(lSpec["Trend"]);
lEngine.mOptions.set_active_periodics(lSpec["Periodics"]);
lEngine.mOptions.set_active_autoregressions(lSpec["AutoRegression"]);
lEngine.mOptions.set_active_decomposition_types(lSpec["Decomposition"]);

lPlots = {}
lDict = {}
try:
lEngine.train(lDataset.mPastData , lDataset.mTimeVar , lDataset.mSignalVar, H);
lEngine.getModelInfo();
lDict = lEngine.to_dict()
lMAPE = lDict[lSignal]["Model_Performance"]["MAPE"]
lFormula = lDict[lSignal]["Model"]["Best_Decomposition"]
print("PLOT_MODEL_END" , lSpec ,
lFormula, lMAPE)
img = draw(lEngine, lFormula, lSignal)
except Exception as e:
print("PLOT_MODEL_END_FAILED" , lSpec, str(e))
return (lSpec, None, None, None)

return (lSpec, img, lFormula, lMAPE)


class cModelEstheticsVisualizer:

def __init__(self):
self.mDataset = None
self.mNbThreads = 12
pass


def gen_all(self):
lEngine = autof.cForecastEngine()
lSpecs = []
lSpec = {}
lKnownAutoRegressions = [x for x in lEngine.mOptions.mKnownAutoRegressions if not x.endswith('X')]
lKnownAutoRegressions = [x for x in lKnownAutoRegressions if (x != 'CROSTON')]
lKnownPeriodics = ['NoCycle', 'BestCycle', 'Seasonal_MonthOfYear'];
for tr in lEngine.mOptions.mKnownTransformations:
lSpec["Transformation"] = tr
for tr1 in lEngine.mOptions.mKnownTrends:
lSpec["Trend"] = tr1
for per in lKnownPeriodics:
lSpec["Periodics"] = per
for ar in lKnownAutoRegressions:
lSpec["AutoRegression"] = ar
for dec in lEngine.mOptions.mKnownDecompositionTypes:
lSpec["Decomposition"] = dec
lSpecs = lSpecs + [(self.mDataset, lSpec.copy())]
print("TESTED_MODELS" , len(lSpecs))
lPlots = {}
from multiprocessing import Pool
pool = Pool(self.mNbThreads)
for res in pool.imap(plot_model, lSpecs):
(lSpec, img, lFormula, lMAPE) = res
lPlots[str(lSpec)] = (lSpec, img, lFormula, lMAPE)
pool.close()
pool.join()
return lPlots

def generate_video(self, iDataset):
self.mDataset = iDataset
lSignal = self.mDataset.mSignalVar
plots = self.gen_all()
images = []
for(lSpec , lValue) in plots.items():
(lSpec1, img, lFormula, lMAPE) = lValue
images.append((img , lMAPE))

images = sorted(images, key = lambda x : -x[1])

import imageio as iio
writer = iio.get_writer(lSignal + "_models.mp4", fps=20)
for im in images:
writer.append_data(im[0])
writer.close()
10 changes: 10 additions & 0 deletions tests/all_models_plots/visualize_air_passengers_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@


import pyaf.Bench.TS_datasets as tsds
import model_esthetics_visualizer as viz

lDataset = tsds.load_airline_passengers()

lVisualizer = viz.cModelEstheticsVisualizer()

lVisualizer.generate_video(lDataset)
10 changes: 10 additions & 0 deletions tests/all_models_plots/visualize_ozone_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@


import pyaf.Bench.TS_datasets as tsds
import model_esthetics_visualizer as viz

lDataset = tsds.load_ozone()

lVisualizer = viz.cModelEstheticsVisualizer()

lVisualizer.generate_video(lDataset)

0 comments on commit 1177454

Please sign in to comment.