From e030a6e31232332b73187eda25870e843152c174 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 16 Feb 2017 01:55:51 +0100
Subject: [PATCH] new version, persistence -7 days

---
 DESCRIPTION                            | 27 +++++++------
 NOTES                                  |  3 ++
 R/D_Zero.R                             |  9 -----
 R/Data.R                               | 13 ++++++
 R/{S_Average.R => F_Average.R}         | 14 +++----
 R/F_Level.R                            | 52 ++++++++++++++++++++++++
 R/{S_Neighbors.R => F_Neighbors.R}     | 32 +++++++--------
 R/{S_Persistence.R => F_Persistence.R} | 14 +++----
 R/F_Zero.R                             | 20 ++++++++++
 R/Forecaster.R                         | 48 ++++++++++++++++++++++
 R/{D_Neighbors.R => J_Neighbors.R}     | 13 +++---
 R/{D_Persistence.R => J_Persistence.R} |  6 +--
 R/J_Zero.R                             |  9 +++++
 R/ShapeForecaster.R                    | 37 -----------------
 R/getError.R                           |  2 +-
 R/getForecast.R                        | 55 +++++++++++---------------
 R/plot.R                               | 30 +++++++++++++-
 reports/report_2017-03-01.ipynb        | 53 ++++++++++++-------------
 18 files changed, 277 insertions(+), 160 deletions(-)
 delete mode 100644 R/D_Zero.R
 rename R/{S_Average.R => F_Average.R} (68%)
 create mode 100644 R/F_Level.R
 rename R/{S_Neighbors.R => F_Neighbors.R} (88%)
 rename R/{S_Persistence.R => F_Persistence.R} (66%)
 create mode 100644 R/F_Zero.R
 create mode 100644 R/Forecaster.R
 rename R/{D_Neighbors.R => J_Neighbors.R} (53%)
 rename R/{D_Persistence.R => J_Persistence.R} (75%)
 create mode 100644 R/J_Zero.R
 delete mode 100644 R/ShapeForecaster.R

diff --git a/DESCRIPTION b/DESCRIPTION
index 82f3e84..f3ee58a 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -1,9 +1,10 @@
 Package: talweg
 Title: talweg : Time-series sAmpLes forecasted With ExoGenous variables
 Version: 0.1-0
-Description: Forecast a curve sampled within the day (seconds, minutes, hours...),
-    using past measured curves, history of some exogenous variables measurements
-		and the exogenous prediction for tomorrow. Main method is getForecast()
+Description: Forecast a curve sampled within the day (seconds, minutes,
+    hours...), using past measured curves, history of some exogenous variables
+    measurements and the exogenous prediction for tomorrow. Main method is
+    getForecast()
 Authors: Benjamin Auder <Benjamin.Auder@math.u-psud.fr> [aut,cre],
     Jean-Michel Poggi <Jean-Michel.Poggi@parisdescartes.fr> [ctb],
     Bruno Portier <Bruno.Portier@insa-rouen.fr>, [ctb]
@@ -13,22 +14,24 @@ Depends:
 Suggests:
     roxygen2,
     testthat,
-		rmarkdown,
-		rainbow
+    rmarkdown,
+    rainbow
 LazyData: yes
 URL: http://git.auder.net/?p=talweg.git
 License: MIT + file LICENSE
 RoxygenNote: 5.0.1
 Collate:
-    'D_Neighbors.R'
-    'D_Persistence.R'
-    'D_Zero.R'
     'Data.R'
+    'Forecaster.R'
+    'F_Average.R'
+    'F_Level.R'
+    'F_Neighbors.R'
+    'F_Persistence.R'
+    'F_Zero.R'
     'Forecast.R'
-    'ShapeForecaster.R'
-    'S_Average.R'
-    'S_Neighbors.R'
-    'S_Persistence.R'
+    'J_Neighbors.R'
+    'J_Persistence.R'
+    'J_Zero.R'
     'getData.R'
     'getError.R'
     'getForecast.R'
diff --git a/NOTES b/NOTES
index 14a3132..8bab5f1 100644
--- a/NOTES
+++ b/NOTES
@@ -27,3 +27,6 @@ Sur exo on doit prédire moyenne du jour au lieu de courbe --------> analyser (?
 
 --> javascript visualisation.................. + tard
 --> soft predict: ne pas virer les séries à NA
+
+--> use R6 class: https://cran.r-project.org/web/packages/R6/vignettes/Introduction.html
+    https://cran.r-project.org/web/packages/R6/vignettes/Performance.html
diff --git a/R/D_Zero.R b/R/D_Zero.R
deleted file mode 100644
index e9e5f6d..0000000
--- a/R/D_Zero.R
+++ /dev/null
@@ -1,9 +0,0 @@
-#' Just predict zero deltas (for reference)
-#'
-#' @inheritParams getForecast
-#' @param today Index of the current day (predict tomorrow)
-#' @param shape_params Optional parameters returned by the shape forecaster
-getZeroDeltaForecast = function(data, today, memory, horizon, shape_params, ...)
-{
-	0
-}
diff --git a/R/Data.R b/R/Data.R
index a17e262..9b7db3f 100644
--- a/R/Data.R
+++ b/R/Data.R
@@ -29,8 +29,21 @@ Data = setRefClass(
 		},
 		getSize = function()
 		{
+			"Number of series in the dataset"
+
 			length(data)
 		},
+		getStdHorizon = function()
+		{
+			"'Standard' horizon, from t+1 to midnight"
+
+			L1 = length(data[[1]]$serie)
+			L2 = length(data[[2]]$serie)
+			if (L1 < L2)
+				L2 - L1
+			else
+				L1
+		},
 		append = function(new_time, new_serie, new_level, new_exo_hat, new_exo_Dm1)
 		{
 			"Acquire a new vector of lists (time, serie, level, exo_hat, exo_Dm1)"
diff --git a/R/S_Average.R b/R/F_Average.R
similarity index 68%
rename from R/S_Average.R
rename to R/F_Average.R
index 819531d..dab156a 100644
--- a/R/S_Average.R
+++ b/R/F_Average.R
@@ -1,19 +1,19 @@
-#' @include ShapeForecaster.R
+#' @include Forecaster.R
 #'
-#' @title Average Shape Forecaster
+#' @title Average Forecaster
 #'
 #' @description Return the (pointwise) average of the all the (similar) centered day curves
-#'   in the past. Inherits \code{\link{ShapeForecaster}}
-AverageShapeForecaster = setRefClass(
-	Class = "AverageShapeForecaster",
-	contains = "ShapeForecaster",
+#'   in the past. Inherits \code{\link{Forecaster}}
+AverageForecaster = setRefClass(
+	Class = "AverageForecaster",
+	contains = "Forecaster",
 
 	methods = list(
 		initialize = function(...)
 		{
 			callSuper(...)
 		},
-		predict = function(today, memory, horizon, ...)
+		predictShape = function(today, memory, horizon, ...)
 		{
 			avg = rep(0., horizon)
 			first_day = max(1, today-memory)
diff --git a/R/F_Level.R b/R/F_Level.R
new file mode 100644
index 0000000..e0132ae
--- /dev/null
+++ b/R/F_Level.R
@@ -0,0 +1,52 @@
+#' @include Forecaster.R
+#'
+#' @title Level Forecaster
+#'
+#' @description Return flat serie of last observed level (on similar day).
+#'   Inherits \code{\link{ShapeForecaster}}
+LevelForecaster = setRefClass(
+	Class = "LevelForecaster",
+	contains = "Forecaster",
+
+	methods = list(
+		initialize = function(...)
+		{
+			callSuper(...)
+		},
+		predict = function(today, memory, horizon, all_memory=TRUE, ...)
+		{
+			#return last (similar) day level, or on all memory if all_memory==TRUE
+			first_day = max(1, today-memory)
+			index = today-7 + 1
+			if (all_memory)
+			{
+				sum_level = 0.
+				nb_series = 0
+			}
+			repeat
+			{
+				{
+					last_similar_serie = data$getSerie(index)[1:horizon]
+					index = index - 7
+				};
+				#TODO: next test is too strict
+				if (!any(is.na(last_similar_serie)))
+				{
+					if (all_memory)
+					{
+						sum_level = sum_level + mean(last_similar_serie)
+						nb_series = nb_series + 1
+					}
+					else
+						return (rep(mean(last_similar_serie), horizon))
+				};
+				if (index < first_day)
+				{
+					if (all_memory)
+						return (rep(sum_level / nb_series, horizon))
+					return (NA)
+				}
+			}
+		}
+	)
+)
diff --git a/R/S_Neighbors.R b/R/F_Neighbors.R
similarity index 88%
rename from R/S_Neighbors.R
rename to R/F_Neighbors.R
index b8e32cc..f1aecb5 100644
--- a/R/S_Neighbors.R
+++ b/R/F_Neighbors.R
@@ -1,19 +1,19 @@
-#' @include ShapeForecaster.R
+#' @include Forecaster.R
 #'
-#' @title Neighbors Shape Forecaster
+#' @title Neighbors Forecaster
 #'
 #' @description Predict tomorrow as a weighted combination of "futures of the past" days.
-#'   Inherits \code{\link{ShapeForecaster}}
-NeighborsShapeForecaster = setRefClass(
-	Class = "NeighborsShapeForecaster",
-	contains = "ShapeForecaster",
+#'   Inherits \code{\link{Forecaster}}
+NeighborsForecaster = setRefClass(
+	Class = "NeighborsForecaster",
+	contains = "Forecaster",
 
 	methods = list(
 		initialize = function(...)
 		{
 			callSuper(...)
 		},
-		predict = function(today, memory, horizon, ...)
+		predictShape = function(today, memory, horizon, ...)
 		{
 			# (re)initialize computed parameters
 			params <<- list("weights"=NA, "indices"=NA, "window"=NA)
@@ -67,8 +67,8 @@ NeighborsShapeForecaster = setRefClass(
 			mix_strategy = ifelse(hasArg("mix_strategy"), list(...)$mix_strategy, "neighb") #or "mult"
 			same_season = ifelse(hasArg("same_season"), list(...)$same_season, TRUE)
 			if (hasArg(h_window))
-				return (.predictAux(fdays_indices, today, horizon, list(...)$h_window, kernel, simtype,
-					simthresh, mix_strategy, FALSE))
+				return (.predictShapeAux(fdays_indices, today, horizon, list(...)$h_window, kernel,
+					simtype, simthresh, mix_strategy, FALSE))
 			#END GET
 
 			# Indices for cross-validation; TODO: 45 = magic number
@@ -87,8 +87,8 @@ NeighborsShapeForecaster = setRefClass(
 					{
 						nb_jours = nb_jours + 1
 						# mix_strategy is never used here (simtype != "mix"), therefore left blank
-						prediction = .predictAux(fdays_indices, i, horizon, h, kernel, simtype, simthresh,
-							"", FALSE)
+						prediction = .predictShapeAux(fdays_indices, i, horizon, h, kernel, simtype,
+							simthresh, "", FALSE)
 						if (!is.na(prediction[1]))
 							error = error + mean((data$getCenteredSerie(i+1)[1:horizon] - prediction)^2)
 					}
@@ -110,22 +110,22 @@ NeighborsShapeForecaster = setRefClass(
 
 			if (simtype == "endo")
 			{
-				return (.predictAux(fdays_indices, today, horizon, h_best_endo, kernel, "endo",
+				return (.predictShapeAux(fdays_indices, today, horizon, h_best_endo, kernel, "endo",
 					simthresh, "", TRUE))
 			}
 			if (simtype == "exo")
 			{
-				return (.predictAux(fdays_indices, today, horizon, h_best_exo, kernel, "exo",
+				return (.predictShapeAux(fdays_indices, today, horizon, h_best_exo, kernel, "exo",
 					simthresh, "", TRUE))
 			}
 			if (simtype == "mix")
 			{
-				return (.predictAux(fdays_indices, today, horizon, c(h_best_endo,h_best_exo), kernel,
-					"mix", simthresh, mix_strategy, TRUE))
+				return (.predictShapeAux(fdays_indices, today, horizon, c(h_best_endo,h_best_exo),
+					kernel, "mix", simthresh, mix_strategy, TRUE))
 			}
 		},
 		# Precondition: "today" is full (no NAs)
-		.predictAux = function(fdays_indices, today, horizon, h, kernel, simtype, simthresh,
+		.predictShapeAux = function(fdays_indices, today, horizon, h, kernel, simtype, simthresh,
 			mix_strategy, final_call)
 		{
 			dat = data$data #HACK: faster this way...
diff --git a/R/S_Persistence.R b/R/F_Persistence.R
similarity index 66%
rename from R/S_Persistence.R
rename to R/F_Persistence.R
index 08647e3..f078484 100644
--- a/R/S_Persistence.R
+++ b/R/F_Persistence.R
@@ -1,19 +1,19 @@
-#' @include ShapeForecaster.R
+#' @include Forecaster.R
 #'
-#' @title Persistence Shape Forecaster
+#' @title Persistence Forecaster
 #'
 #' @description Return the last centered last (similar) day curve.
-#'   Inherits \code{\link{ShapeForecaster}}
-PersistenceShapeForecaster = setRefClass(
-	Class = "PersistenceShapeForecaster",
-	contains = "ShapeForecaster",
+#'   Inherits \code{\link{Forecaster}}
+PersistenceForecaster = setRefClass(
+	Class = "PersistenceForecaster",
+	contains = "Forecaster",
 
 	methods = list(
 		initialize = function(...)
 		{
 			callSuper(...)
 		},
-		predict = function(today, memory, horizon, ...)
+		predictShape = function(today, memory, horizon, ...)
 		{
 			#return centered last (similar) day curve, avoiding NAs until memory is run
 			first_day = max(1, today-memory)
diff --git a/R/F_Zero.R b/R/F_Zero.R
new file mode 100644
index 0000000..247ab04
--- /dev/null
+++ b/R/F_Zero.R
@@ -0,0 +1,20 @@
+#' @include Forecaster.R
+#'
+#' @title Zero Forecaster
+#'
+#' @description Return 0 (and then adjust). Inherits \code{\link{Forecaster}}
+ZeroForecaster = setRefClass(
+	Class = "ZeroForecaster",
+	contains = "Forecaster",
+
+	methods = list(
+		initialize = function(...)
+		{
+			callSuper(...)
+		},
+		predictShape = function(today, memory, horizon, ...)
+		{
+			rep(0., horizon)
+		}
+	)
+)
diff --git a/R/Forecaster.R b/R/Forecaster.R
new file mode 100644
index 0000000..71cb667
--- /dev/null
+++ b/R/Forecaster.R
@@ -0,0 +1,48 @@
+#' @title Forecaster (abstract class)
+#'
+#' @description Abstract class to represent a forecaster (they all inherit this)
+#'
+#' @field params List of computed parameters, for post-run analysis (dev)
+#' @field data Dataset, object of class Data
+#' @field pjump Function: how to predict the jump at day interface ?
+Forecaster = setRefClass(
+	Class = "Forecaster",
+
+	fields = list(
+		params = "list",
+		data = "Data",
+		pjump = "function"
+	),
+
+	methods = list(
+		initialize = function(...)
+		{
+			"Initialize (generic) Forecaster object"
+
+			callSuper(...)
+			if (!hasArg(data))
+				stop("Forecaster must be initialized with a Data object")
+			params <<- list()
+		},
+		predict = function(today, memory, horizon, ...)
+		{
+			"Obtain a new forecasted time-serie"
+
+			# Parameters (potentially) computed during shape prediction stage
+			predicted_shape = predictShape(today, memory, horizon, ...)
+			predicted_delta = pjump(data, today, memory, horizon, params, ...)
+			# Predicted shape is aligned it on the end of current day + jump
+			predicted_shape + tail(data$getSerie(today),1) - predicted_shape[1] + predicted_delta
+		},
+		predictShape = function(today, memory, horizon, ...)
+		{
+			"Shape prediction (centered curve)"
+
+			#empty default implementation: to implement in inherited classes
+		},
+		getParameters = function()
+		{
+			params
+		}
+	)
+)
diff --git a/R/D_Neighbors.R b/R/J_Neighbors.R
similarity index 53%
rename from R/D_Neighbors.R
rename to R/J_Neighbors.R
index dba5f37..03d3340 100644
--- a/R/D_Neighbors.R
+++ b/R/J_Neighbors.R
@@ -1,20 +1,19 @@
-#' Obtain delta forecast by the Neighbors method
+#' Obtain jump forecast by the Neighbors method
 #'
 #' @inheritParams getForecast
-#' @inheritParams getZeroDeltaForecast
-getNeighborsDeltaForecast = function(data, today, memory, horizon, shape_params, ...)
+#' @inheritParams getZeroJumpPredict
+getNeighborsJumpPredict = function(data, today, memory, horizon, params, ...)
 {
 	first_day = max(1, today-memory)
-	filter = shape_params$indices >= first_day
-	indices = shape_params$indices[filter]
-	weights = shape_params$weights[filter]
+	filter = params$indices >= first_day
+	indices = params$indices[filter]
+	weights = params$weights[filter]
 	if (any(is.na(weights) | is.na(indices)))
 		return (NA)
 
 	gaps = sapply(indices, function(i) {
 		data$getSerie(i+1)[1] - tail(data$getSerie(i), 1)
 	})
-
 	scal_product = weights * gaps
 	norm_fact = sum( weights[!is.na(scal_product)] )
 	sum(scal_product, na.rm=TRUE) / norm_fact
diff --git a/R/D_Persistence.R b/R/J_Persistence.R
similarity index 75%
rename from R/D_Persistence.R
rename to R/J_Persistence.R
index 979bf05..744b42a 100644
--- a/R/D_Persistence.R
+++ b/R/J_Persistence.R
@@ -1,8 +1,8 @@
-#' Obtain delta forecast by the Persistence method
+#' Obtain jump forecast by the Persistence method
 #'
 #' @inheritParams getForecast
-#' @inheritParams getZeroDeltaForecast
-getPersistenceDeltaForecast = function(data, today, memory, horizon, shape_params, ...)
+#' @inheritParams getZeroJumpPredict
+getPersistenceJumpPredict = function(data, today, memory, horizon, params, ...)
 {
 	#return gap between end of similar day curve and first day of tomorrow (in the past)
 	first_day = max(1, today-memory)
diff --git a/R/J_Zero.R b/R/J_Zero.R
new file mode 100644
index 0000000..c227609
--- /dev/null
+++ b/R/J_Zero.R
@@ -0,0 +1,9 @@
+#' Just predict zero "jump" (for reference, benchmarking at least)
+#'
+#' @inheritParams getForecast
+#' @param today Index of the current day (predict tomorrow)
+#' @param params Optional parameters computed by the main forecaster
+getZeroJumpPredict = function(data, today, memory, horizon, params, ...)
+{
+	0
+}
diff --git a/R/ShapeForecaster.R b/R/ShapeForecaster.R
deleted file mode 100644
index 0b448d7..0000000
--- a/R/ShapeForecaster.R
+++ /dev/null
@@ -1,37 +0,0 @@
-#' @title Shape Forecaster
-#'
-#' @description Generic class to represent a shape forecaster
-#'
-#' @field params List of computed parameters, potentially useful for some DeltaForecasters
-#' @field data Dataset, object of class Data
-ShapeForecaster = setRefClass(
-	Class = "ShapeForecaster",
-
-	fields = list(
-		params = "list",
-		data = "Data"
-	),
-
-	methods = list(
-		initialize = function(...)
-		{
-			"Initialize (generic) ShapeForecaster object"
-
-			callSuper(...)
-			if (!hasArg(data))
-				stop("ShapeForecaster must be initialized with a Data object")
-			params <<- list()
-		},
-		predict = function(today, memory, horizon, ...)
-		{
-			"Obtain a new forecasted time-serie (+ side-effect: compute parameters)"
-
-			#empty default implementation: to implement in inherited classes
-		},
-		getParameters = function()
-		{
-			params
-		}
-	)
-)
-
diff --git a/R/getError.R b/R/getError.R
index 61e7910..affe6c3 100644
--- a/R/getError.R
+++ b/R/getError.R
@@ -9,7 +9,7 @@
 #' @return A list (abs,MAPE) of lists (day,indices)
 #'
 #' @export
-getError = function(data, forecast, horizon)
+getError = function(data, forecast, horizon=data$getStdHorizon())
 {
 	L = forecast$getSize()
 	mape_day = rep(0, horizon)
diff --git a/R/getForecast.R b/R/getForecast.R
index e126946..fde8e45 100644
--- a/R/getForecast.R
+++ b/R/getForecast.R
@@ -1,44 +1,46 @@
 #' @title get Forecast
 #'
 #' @description Predict time-series curves for the selected days indices (lines in data).
-#'   Run the forecasting task described by \code{delta_forecaster_name} and
-#'   \code{shape_forecaster_name} on data obtained with \code{getData}
 #'
 #' @param data Dataset, object of type \code{Data} output of \code{getData}
 #' @param indices Days indices where to forecast (the day after)
-#' @param memory Data depth (in days) to be used for prediction
-#' @param horizon Number of time steps to predict
-#' @param shape_forecaster_name Name of the shape forcaster
+#' @param forecaster Name of the main forcaster
 #' \itemize{
 #'   \item Persistence : use values of last (similar, next) day
-#'   \item Neighbors : use PM10 from the k closest neighbors' tomorrows
+#'   \item Neighbors : use values from the k closest neighbors' tomorrows
 #'   \item Average : global average of all the (similar) "tomorrow of past"
+#'   \item Zero : just output 0 (benchmarking purpose)
+#'   \item Level : output a flat serie repeating the last observed level
 #' }
-#' @param delta_forecaster_name Name of the delta forecaster
+#' @param pjump How to predict the jump at the interface between two days ?
 #' \itemize{
 #'   \item Persistence : use last (similar) day values
-#'   \item Neighbors: re-use the weights optimized in corresponding shape forecaster
+#'   \item Neighbors: re-use the weights optimized in corresponding forecaster
 #'   \item Zero: just output 0 (no adjustment)
 #' }
+#' @param memory Data depth (in days) to be used for prediction
+#' @param horizon Number of time steps to predict
 #' @param ... Additional parameters for the forecasting models
 #'
 #' @return An object of class Forecast
 #'
 #' @examples
 #' data = getData(ts_data="data/pm10_mesures_H_loc.csv", exo_data="data/meteo_extra_noNAs.csv",
-#'   input_tz = "Europe/Paris", working_tz="Europe/Paris", predict_at="07")
-#' pred = getForecast(data, 2200:2230, Inf, 12, "Persistence", "Persistence")
+#'   input_tz = "Europe/Paris", working_tz="Europe/Paris", predict_at=7)
+#' pred = getForecast(data, 2200:2230, "Persistence", "Persistence", 500, 12)
 #' \dontrun{#Sketch for real-time mode:
 #' data = new("Data", ...)
+#' forecaster = new(..., data=data)
 #' repeat {
 #'   data$append(some_new_data)
-#'   pred = getForecast(data, ...)
+#'   pred = forecaster$predict(data$getSize(), ...)
 #'   #do_something_with_pred
 #' }}
 #' @export
-getForecast = function(data, indices, memory, horizon,
-	shape_forecaster_name, delta_forecaster_name, ...)
+getForecast = function(data, indices, forecaster, pjump,
+	memory=Inf, horizon=data$getStdHorizon(), ...)
 {
+	# (basic) Arguments sanity checks
 	horizon = as.integer(horizon)[1]
 	if (horizon<=0 || horizon>length(data$getCenteredSerie(2)))
 		stop("Horizon too short or too long")
@@ -46,30 +48,19 @@ getForecast = function(data, indices, memory, horizon,
 	if (any(indices<=0 | indices>data$getSize()))
 		stop("Indices out of range")
 	indices = sapply(indices, dateIndexToInteger, data)
-
-	#NOTE: some assymetry here...
-	shape_forecaster = new(paste(shape_forecaster_name,"ShapeForecaster",sep=""), data=data)
-	#A little bit strange, but match.fun() and get() fail
-	delta_forecaster = getFromNamespace(
-		paste("get",delta_forecaster_name,"DeltaForecast",sep=""), "talweg")
+	if (!is.character(forecaster) || !is.character(pjump))
+		stop("forecaster and pjump should be of class character")
 
 	pred = list()
+	forecaster = new(paste(forecaster,"Forecaster",sep=""), data=data,
+		pjump = getFromNamespace(paste("get",pjump,"JumpPredict",sep=""), "talweg"))
 	for (today in indices)
 	{
-		#shape always predicted first (on centered series, no scaling taken into account),
-		#with side-effect: optimize some parameters (h, weights, ...)
-		predicted_shape = shape_forecaster$predict(today, memory, horizon, ...)
-		#then, delta prediction can re-use some variables optimized previously (like neighbors infos)
-		predicted_delta = delta_forecaster(data, today, memory, horizon,
-			shape_forecaster$getParameters(), ...)
-
-		#TODO: this way is faster than a call to append(); why ?
 		pred[[length(pred)+1]] = list(
-			# Predict shape and align it on end of current day
-			serie = predicted_shape + tail( data$getSerie(today), 1 ) - predicted_shape[1] +
-				predicted_delta, #add predicted jump
-			params = shape_forecaster$getParameters(),
-			index = today )
+			"serie" = forecaster$predict(today, memory, horizon, ...),
+			"params" = forecaster$getParameters(),
+			"index" = today
+		)
 	}
 	new("Forecast",pred=pred)
 }
diff --git a/R/plot.R b/R/plot.R
index f551ef4..9a0dbcd 100644
--- a/R/plot.R
+++ b/R/plot.R
@@ -1,3 +1,29 @@
+#' @title plot curves
+#'
+#' @description Plot a range of curves in data
+#'
+#' @param data Object of class Data
+#' @param indices Range of indices (integers or dates)
+#'
+#' @export
+plotCurves <- function(data, indices)
+{
+	yrange = range( sapply( indices, function(i) {
+		serie = c(data$getCenteredSerie(i))
+		if (!all(is.na(serie)))
+			range(serie, na.rm=TRUE)
+		c()
+	}) )
+	par(mar=c(4.7,5,1,1), cex.axis=1.5, cex.lab=1.5)
+	for (i in seq_along(indices))
+	{
+		plot(data$getSerie(indices[i]), type="l", ylim=yrange,
+			xlab=ifelse(i==1,"Temps (en heures)",""), ylab=ifelse(i==1,"PM10",""))
+		if (ii < length(indices))
+			par(new=TRUE)
+	}
+}
+
 #' @title plot measured / predicted
 #'
 #' @description Plot measured curve (in black) and predicted curve (in red)
@@ -45,8 +71,8 @@ plotFilaments <- function(data, index, limit=60)
 		index = i - first_day + 1
 		serie = c(data$getCenteredSerie(index), data$getCenteredSerie(index+1))
 		if (!all(is.na(serie)))
-			return ( range(serie, na.rm=TRUE) )
-		return (0)
+			range(serie, na.rm=TRUE)
+		c()
 	}) )
 	grays = gray.colors(20, 0.1, 0.9) #TODO: 20 == magic number
 	colors = c(
diff --git a/reports/report_2017-03-01.ipynb b/reports/report_2017-03-01.ipynb
index 22e7dc4..3e58706 100644
--- a/reports/report_2017-03-01.ipynb
+++ b/reports/report_2017-03-01.ipynb
@@ -50,12 +50,11 @@
    },
    "outputs": [],
    "source": [
-    "p_ch_nn = getForecast(data, seq(as.Date(\"2015-01-18\"),as.Date(\"2015-01-24\"),\"days\"), Inf, 17,\n",
-    "                   \"Neighbors\", \"Neighbors\", simtype=\"mix\", same_season=FALSE, mix_strategy=\"mult\")\n",
-    "p_ch_pz = getForecast(data, seq(as.Date(\"2015-01-18\"),as.Date(\"2015-01-24\"),\"days\"), Inf, 17,\n",
-    "                   \"Persistence\", \"Zero\")\n",
-    "p_ch_az = getForecast(data, seq(as.Date(\"2015-01-18\"),as.Date(\"2015-01-24\"),\"days\"), Inf, 17,\n",
-    "                   \"Average\", \"Zero\")"
+    "indices = seq(as.Date(\"2015-01-18\"),as.Date(\"2015-01-24\"),\"days\")\n",
+    "p_ch_nn = getForecast(data,indices,\"Neighbors\",\"Neighbors\",simtype=\"mix\",same_season=FALSE,mix_strategy=\"mult\")\n",
+    "p_ch_pz = getForecast(data, indices, \"Persistence\", \"Zero\")\n",
+    "p_ch_az = getForecast(data, indices, \"Average\", \"Zero\")\n",
+    "p_ch_zz = getForecast(data, indices, \"Zero\", \"Zero\")"
    ]
   },
   {
@@ -66,9 +65,9 @@
    },
    "outputs": [],
    "source": [
-    "e_ch_nn = getError(data, p_ch_nn, 17)\n",
-    "e_ch_pz = getError(data, p_ch_pz, 17)\n",
-    "e_ch_az = getError(data, p_ch_az, 17)\n",
+    "e_ch_nn = getError(data, p_ch_nn)\n",
+    "e_ch_pz = getError(data, p_ch_pz)\n",
+    "e_ch_az = getError(data, p_ch_az)\n",
     "options(repr.plot.width=9, repr.plot.height=6)\n",
     "plotError(list(e_ch_nn, e_ch_pz, e_ch_az), cols=c(1,2,colors()[258]))\n",
     "\n",
@@ -176,12 +175,12 @@
    },
    "outputs": [],
    "source": [
-    "p_ep_nn = getForecast(data, seq(as.Date(\"2015-03-15\"),as.Date(\"2015-03-21\"),\"days\"), Inf, 17,\n",
-    "                   \"Neighbors\", \"Neighbors\", simtype=\"mix\", same_season=FALSE, mix_strategy=\"mult\")\n",
-    "p_ep_pz = getForecast(data, seq(as.Date(\"2015-03-15\"),as.Date(\"2015-03-21\"),\"days\"), Inf, 17,\n",
-    "                   \"Persistence\", \"Zero\")\n",
-    "p_ep_az = getForecast(data, seq(as.Date(\"2015-03-15\"),as.Date(\"2015-03-21\"),\"days\"), Inf, 17,\n",
-    "                   \"Average\", \"Zero\")"
+    "indices = seq(as.Date(\"2015-03-15\"),as.Date(\"2015-03-21\"),\"days\")\n",
+    "p_ep_nn = getForecast(data,indices,\"Neighbors\",\"Neighbors\",simtype=\"mix\",same_season=FALSE,mix_strategy=\"mult\")\n",
+    "p_ep_pz = getForecast(data, indices, \"Persistence\", \"Zero\")\n",
+    "p_ep_az = getForecast(data, indices, \"Average\", \"Zero\")\n",
+    "p_ep_zz = getForecast(data, indices, \"Zero\", \"Zero\")\n",
+    "p_ep_lz = getForecast(data, indices, \"Level\", \"Zero\")"
    ]
   },
   {
@@ -192,9 +191,9 @@
    },
    "outputs": [],
    "source": [
-    "e_ep_nn = getError(data, p_ep_nn, 17)\n",
-    "e_ep_pz = getError(data, p_ep_pz, 17)\n",
-    "e_ep_az = getError(data, p_ep_az, 17)\n",
+    "e_ep_nn = getError(data, p_ep_nn)\n",
+    "e_ep_pz = getError(data, p_ep_pz)\n",
+    "e_ep_az = getError(data, p_ep_az)\n",
     "options(repr.plot.width=9, repr.plot.height=6)\n",
     "plotError(list(e_ep_nn, e_ep_pz, e_ep_az), cols=c(1,2,colors()[258]))\n",
     "\n",
@@ -277,12 +276,12 @@
    },
    "outputs": [],
    "source": [
-    "p_np_nn = getForecast(data, seq(as.Date(\"2015-04-26\"),as.Date(\"2015-05-02\"),\"days\"), Inf, 17,\n",
-    "                   \"Neighbors\", \"Neighbors\", simtype=\"mix\", same_season=FALSE, mix_strategy=\"mult\")\n",
-    "p_np_pz = getForecast(data, seq(as.Date(\"2015-04-26\"),as.Date(\"2015-05-02\"),\"days\"), Inf, 17,\n",
-    "                   \"Persistence\", \"Zero\")\n",
-    "p_np_az = getForecast(data, seq(as.Date(\"2015-04-26\"),as.Date(\"2015-05-02\"),\"days\"), Inf, 17,\n",
-    "                   \"Average\", \"Zero\")"
+    "indices = seq(as.Date(\"2015-04-26\"),as.Date(\"2015-05-02\"),\"days\")\n",
+    "p_np_nn = getForecast(data,indices,\"Neighbors\",\"Neighbors\",simtype=\"mix\",same_season=FALSE,mix_strategy=\"mult\")\n",
+    "p_np_pz = getForecast(data, indices, \"Persistence\", \"Zero\")\n",
+    "p_np_az = getForecast(data, indices, \"Average\", \"Zero\")\n",
+    "p_np_zz = getForecast(data, indices, \"Zero\", \"Zero\")\n",
+    "p_np_lz = getForecast(data, indices, \"Level\", \"Zero\")"
    ]
   },
   {
@@ -293,9 +292,9 @@
    },
    "outputs": [],
    "source": [
-    "e_np_nn = getError(data, p_np_nn, 17)\n",
-    "e_np_pz = getError(data, p_np_pz, 17)\n",
-    "e_np_az = getError(data, p_np_az, 17)\n",
+    "e_np_nn = getError(data, p_np_nn)\n",
+    "e_np_pz = getError(data, p_np_pz)\n",
+    "e_np_az = getError(data, p_np_az)\n",
     "options(repr.plot.width=9, repr.plot.height=6)\n",
     "plotError(list(e_np_nn, e_np_pz, e_np_az), cols=c(1,2,colors()[258]))\n",
     "\n",
-- 
2.44.0