From c3d2280af8928000288ff864a271835a533448f4 Mon Sep 17 00:00:00 2001 From: Ben Letham Date: Wed, 11 Oct 2017 09:38:15 -0700 Subject: [PATCH 1/7] Add layer_changepoints from #273 --- R/NAMESPACE | 1 + R/R/utils.R | 33 +++++++++++++++++++++++++++++++++ R/man/layer_changepoints.Rd | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 R/R/utils.R create mode 100644 R/man/layer_changepoints.Rd diff --git a/R/NAMESPACE b/R/NAMESPACE index 415e5a9..6a5312a 100644 --- a/R/NAMESPACE +++ b/R/NAMESPACE @@ -6,6 +6,7 @@ export(add_regressor) export(add_seasonality) export(cross_validation) export(fit.prophet) +export(layer_changepoints) export(make_future_dataframe) export(plot_forecast_component) export(predictive_samples) diff --git a/R/R/utils.R b/R/R/utils.R new file mode 100644 index 0000000..2116e91 --- /dev/null +++ b/R/R/utils.R @@ -0,0 +1,33 @@ +#' Get layers to overlay significant changepoints on prophet forecast plot. +#' +#' @param m Prophet model object. +#' @param threshold Numeric, changepoints where abs(delta) >= threshold are +#' significant. (Default 0.01) +#' @param cp_color Character, line color. (Default "red") +#' @param cp_linetype Character or integer, line type. (Default "dashed") +#' @param trend Logical, if FALSE, do not draw trend line. (Default TRUE) +#' @param ... Other arguments passed on to layers. +#' +#' @return A list of ggplot2 layers. +#' +#' @examples +#' \dontrun{ +#' plot(m, fcst) + layer_changepoints(m) +#' } +#' +#' @export +layer_changepoints <- function(m, threshold = 0.01, cp_color = "red", + cp_linetype = "dashed", trend = TRUE, ...) { + layers <- list() + if (trend) { + trend_layer <- ggplot2::geom_line( + ggplot2::aes_string("ds", "trend"), color = cp_color, ...) + layers <- append(layers, trend_layer) + } + signif_changepoints <- m$changepoints[abs(m$params$delta) >= threshold] + cp_layer <- ggplot2::geom_vline( + xintercept = as.integer(signif_changepoints), color = cp_color, + linetype = cp_linetype, ...) + layers <- append(layers, cp_layer) + return(layers) +} diff --git a/R/man/layer_changepoints.Rd b/R/man/layer_changepoints.Rd new file mode 100644 index 0000000..674c717 --- /dev/null +++ b/R/man/layer_changepoints.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{layer_changepoints} +\alias{layer_changepoints} +\title{Get layers to overlay significant changepoints on prophet forecast plot.} +\usage{ +layer_changepoints(m, threshold = 0.01, cp_color = "red", + cp_linetype = "dashed", trend = TRUE, ...) +} +\arguments{ +\item{m}{Prophet model object.} + +\item{threshold}{Numeric, changepoints where abs(delta) >= threshold are +significant. (Default 0.01)} + +\item{cp_color}{Character, line color. (Default "red")} + +\item{cp_linetype}{Character or integer, line type. (Default "dashed")} + +\item{trend}{Logical, if FALSE, do not draw trend line. (Default TRUE)} + +\item{...}{Other arguments passed on to layers.} +} +\value{ +A list of ggplot2 layers. +} +\description{ +Get layers to overlay significant changepoints on prophet forecast plot. +} +\examples{ +\dontrun{ +plot(m, fcst) + layer_changepoints(m) +} + +} From 1e3046277925314be59d539f55203a534cbc7e35 Mon Sep 17 00:00:00 2001 From: Bernie Gray Date: Tue, 17 Oct 2017 19:33:11 -0400 Subject: [PATCH 2/7] efficiency and robustness improvements for R package (#308) * efficiency improvements in r package * add drop = FALSE to df subsetting * adding two more drop = FALSE * add back in global var vector * revert to previous style of piecewise_linear, piecewise_logistic * more reversion * even more reversion * revert get_changepoint_matrix * trying to pinpoint issues * more debugginh * tests finally pass * last commit of pr * last commit of pr * add utils to imports --- R/DESCRIPTION | 3 ++- R/R/diagnostics.R | 6 ++--- R/R/prophet.R | 59 ++++++++++++++++++++++------------------------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/R/DESCRIPTION b/R/DESCRIPTION index f89ec25..89c2640 100644 --- a/R/DESCRIPTION +++ b/R/DESCRIPTION @@ -22,7 +22,8 @@ Imports: rstan (>= 2.14.0), scales, stats, - tidyr (>= 0.6.1) + tidyr (>= 0.6.1), + utils Suggests: knitr, testthat, diff --git a/R/R/diagnostics.R b/R/R/diagnostics.R index c35db40..58c9816 100644 --- a/R/R/diagnostics.R +++ b/R/R/diagnostics.R @@ -26,7 +26,7 @@ generate_cutoffs <- function(df, horizon, k, period) { stop('Less data than horizon.') } tzone <- attr(cutoff, "tzone") # Timezone is wiped by putting in array - result <- c(cutoff) + result <- cutoff if (k > 1) { for (i in 2:k) { cutoff <- cutoff - period @@ -74,7 +74,7 @@ simulated_historical_forecasts <- function(model, horizon, units, k, } cutoffs <- generate_cutoffs(df, horizon, k, period) predicts <- data.frame() - for (i in 1:length(cutoffs)) { + for (i in seq_along(cutoffs)) { cutoff <- cutoffs[i] # Copy the model m <- prophet_copy(model, cutoff) @@ -83,7 +83,7 @@ simulated_historical_forecasts <- function(model, horizon, units, k, m <- fit.prophet(m, history.c) # Calculate yhat df.predict <- dplyr::filter(df, ds > cutoff, ds <= cutoff + horizon) - columns <- c('ds') + columns <- 'ds' if (m$growth == 'logistic') { columns <- c(columns, 'cap') if (m$logistic.floor) { diff --git a/R/R/prophet.R b/R/R/prophet.R index a1daaa1..a5e51bc 100644 --- a/R/R/prophet.R +++ b/R/R/prophet.R @@ -6,7 +6,7 @@ ## of patent rights can be found in the PATENTS file in the same directory. ## Makes R CMD CHECK happy due to dplyr syntax below -globalVariables(c( +utils::globalVariables(c( "ds", "y", "cap", ".", "component", "dow", "doy", "holiday", "holidays", "holidays_lower", "holidays_upper", "ix", "lower", "n", "stat", "trend", "row_number", "extra_regressors", "col", @@ -190,21 +190,21 @@ validate_column_name <- function( 'trend', 'seasonal', 'seasonalities', 'daily', 'weekly', 'yearly', 'holidays', 'zeros', 'extra_regressors', 'yhat' ) - rn_l = paste(reserved_names,"_lower",sep="") - rn_u = paste(reserved_names,"_upper",sep="") + rn_l = paste0(reserved_names,"_lower") + rn_u = paste0(reserved_names,"_upper") reserved_names = c(reserved_names, rn_l, rn_u, c("ds", "y", "cap", "floor", "y_scaled", "cap_scaled")) if(name %in% reserved_names){ stop("Name ", name, " is reserved.") } - if(check_holidays & !is.null(m$holidays) & + if(check_holidays && !is.null(m$holidays) && (name %in% unique(m$holidays$holiday))){ stop("Name ", name, " already used for a holiday.") } - if(check_seasonalities & (!is.null(m$seasonalities[[name]]))){ + if(check_seasonalities && (!is.null(m$seasonalities[[name]]))){ stop("Name ", name, " already used for a seasonality.") } - if(check_regressors & (!is.null(m$seasonalities[[name]]))){ + if(check_regressors && (!is.null(m$seasonalities[[name]]))){ stop("Name ", name, " already used for an added regressor.") } } @@ -274,11 +274,8 @@ set_date <- function(ds = NULL, tz = "GMT") { ds <- as.character(ds) } - if (min(nchar(ds)) < 12) { - ds <- as.POSIXct(ds, format = "%Y-%m-%d", tz = tz) - } else { - ds <- as.POSIXct(ds, format = "%Y-%m-%d %H:%M:%S", tz = tz) - } + fmt <- if (min(nchar(ds)) < 12) "%Y-%m-%d" else "%Y-%m-%d %H:%M:%S" + ds <- as.POSIXct(ds, format = fmt, tz = tz) attr(ds, "tzone") <- tz return(ds) } @@ -461,7 +458,7 @@ set_changepoints <- function(m) { m$changepoints.t <- sort( time_diff(m$changepoints, m$start, "secs")) / m$t.scale } else { - m$changepoints.t <- c(0) # dummy changepoint + m$changepoints.t <- 0 # dummy changepoint } return(m) } @@ -475,7 +472,7 @@ set_changepoints <- function(m) { #' @keywords internal get_changepoint_matrix <- function(m) { A <- matrix(0, nrow(m$history), length(m$changepoints.t)) - for (i in 1:length(m$changepoints.t)) { + for (i in seq_along(m$changepoints.t)) { A[m$history$t >= m$changepoints.t[i], i] <- 1 } return(A) @@ -493,7 +490,7 @@ get_changepoint_matrix <- function(m) { fourier_series <- function(dates, period, series.order) { t <- time_diff(dates, set_date('1970-01-01 00:00:00')) features <- matrix(0, length(t), 2 * series.order) - for (i in 1:series.order) { + for (i in seq_len(series.order)) { x <- as.numeric(2 * i * pi * t / period) features[, i * 2 - 1] <- sin(x) features[, i * 2] <- cos(x) @@ -513,7 +510,7 @@ fourier_series <- function(dates, period, series.order) { #' @keywords internal make_seasonality_features <- function(dates, period, series.order, prefix) { features <- fourier_series(dates, period, series.order) - colnames(features) <- paste(prefix, 1:ncol(features), sep = '_delim_') + colnames(features) <- paste(prefix, seq_len(ncol(features)), sep = '_delim_') return(data.frame(features)) } @@ -540,13 +537,13 @@ make_holiday_features <- function(m, dates) { && !is.na(.$upper_window)) { offsets <- seq(.$lower_window, .$upper_window) } else { - offsets <- c(0) + offsets <- 0 } names <- paste(.$holiday, '_delim_', ifelse(offsets < 0, '-', '+'), abs(offsets), sep = '') dplyr::data_frame(ds = .$ds + offsets * 24 * 3600, holiday = names) }) %>% - dplyr::mutate(x = 1.) %>% + dplyr::mutate(x = 1) %>% tidyr::spread(holiday, x, fill = 0) holiday.features <- data.frame(ds = set_date(dates)) %>% @@ -684,7 +681,7 @@ add_seasonality <- function(m, name, period, fourier.order, prior.scale = NULL) #' #' @keywords internal make_all_seasonality_features <- function(m, df) { - seasonal.features <- data.frame(row.names = 1:nrow(df)) + seasonal.features <- data.frame(row.names = seq_len(nrow(df))) prior.scales <- c() # Seasonality features @@ -712,7 +709,7 @@ make_all_seasonality_features <- function(m, df) { if (ncol(seasonal.features) == 0) { seasonal.features <- data.frame(zeros = rep(0, nrow(df))) - prior.scales <- c(1.) + prior.scales <- 1 } return(list(seasonal.features = seasonal.features, prior.scales = prior.scales)) @@ -1036,9 +1033,7 @@ predict.prophet <- function(object, df = NULL, ...) { cols <- c(cols, 'floor') } df <- df[cols] - df <- df %>% - dplyr::bind_cols(seasonal.components) %>% - dplyr::bind_cols(intervals) + df <- dplyr::bind_cols(df, seasonal.components, intervals) df$yhat <- df$trend + df$seasonal return(df) } @@ -1060,7 +1055,7 @@ piecewise_linear <- function(t, deltas, k, m, changepoint.ts) { # Get cumulative slope and intercept at each t k_t <- rep(k, length(t)) m_t <- rep(m, length(t)) - for (s in 1:length(changepoint.ts)) { + for (s in seq_along(changepoint.ts)) { indx <- t >= changepoint.ts[s] k_t[indx] <- k_t[indx] + deltas[s] m_t[indx] <- m_t[indx] + gammas[s] @@ -1085,14 +1080,14 @@ piecewise_logistic <- function(t, cap, deltas, k, m, changepoint.ts) { # Compute offset changes k.cum <- c(k, cumsum(deltas) + k) gammas <- rep(0, length(changepoint.ts)) - for (i in 1:length(changepoint.ts)) { + for (i in seq_along(changepoint.ts)) { gammas[i] <- ((changepoint.ts[i] - m - sum(gammas)) * (1 - k.cum[i] / k.cum[i + 1])) } # Get cumulative rate and offset at each t k_t <- rep(k, length(t)) m_t <- rep(m, length(t)) - for (s in 1:length(changepoint.ts)) { + for (s in seq_along(changepoint.ts)) { indx <- t >= changepoint.ts[s] k_t[indx] <- k_t[indx] + deltas[s] m_t[indx] <- m_t[indx] + gammas[s] @@ -1139,14 +1134,14 @@ predict_seasonal_components <- function(m, df) { upper.p <- (1 + m$interval.width)/2 components <- dplyr::data_frame(component = colnames(seasonal.features)) %>% - dplyr::mutate(col = 1:n()) %>% + dplyr::mutate(col = seq_len(n())) %>% tidyr::separate(component, c('component', 'part'), sep = "_delim_", extra = "merge", fill = "right") %>% dplyr::select(col, component) # Add total for all regression components components <- rbind( components, - data.frame(col = 1:ncol(seasonal.features), component = 'seasonal')) + data.frame(col = seq_len(ncol(seasonal.features)), component = 'seasonal')) # Add totals for seasonality, holiday, and extra regressors components <- add_group_component( components, 'seasonalities', names(m$seasonalities)) @@ -1163,7 +1158,7 @@ predict_seasonal_components <- function(m, df) { dplyr::group_by(component) %>% dplyr::do({ comp <- (as.matrix(seasonal.features[, .$col]) %*% t(m$params$beta[, .$col, drop = FALSE])) * m$y.scale - dplyr::data_frame(ix = 1:nrow(seasonal.features), + dplyr::data_frame(ix = seq_len(nrow(seasonal.features)), mean = rowMeans(comp, na.rm = TRUE), lower = apply(comp, 1, stats::quantile, lower.p, na.rm = TRUE), @@ -1217,9 +1212,9 @@ sample_posterior_predictive <- function(m, df) { "seasonal" = matrix(, nrow = nrow(df), ncol = nsamp), "yhat" = matrix(, nrow = nrow(df), ncol = nsamp)) - for (i in 1:n.iterations) { + for (i in seq_len(n.iterations)) { # For each set of parameters from MCMC (or just 1 set for MAP), - for (j in 1:samp.per.iter) { + for (j in seq_len(samp.per.iter)) { # Do a simulation with this set of parameters, sim <- sample_model(m, df, seasonal.features, i) # Store the results @@ -1485,7 +1480,7 @@ prophet_plot_components <- function( # Plot the trend panels <- list(plot_forecast_component(fcst, 'trend', uncertainty, plot_cap)) # Plot holiday components, if present. - if (!is.null(m$holidays) & ('holidays' %in% colnames(fcst))) { + if (!is.null(m$holidays) && ('holidays' %in% colnames(fcst))) { panels[[length(panels) + 1]] <- plot_forecast_component( fcst, 'holidays', uncertainty, FALSE) } @@ -1515,7 +1510,7 @@ prophet_plot_components <- function( grid::grid.newpage() grid::pushViewport(grid::viewport(layout = grid::grid.layout(length(panels), 1))) - for (i in 1:length(panels)) { + for (i in seq_along(panels)) { print(panels[[i]], vp = grid::viewport(layout.pos.row = i, layout.pos.col = 1)) } From e78f583f902f25ca9568f02518c775de01b50e87 Mon Sep 17 00:00:00 2001 From: Ben Letham Date: Wed, 8 Nov 2017 10:09:08 -0800 Subject: [PATCH 3/7] Merge in bugfixes from master (#349) * Update memory requirement description per #326 * Fix R warning with extra regressor; disallow constant extra regressors. * Fix unit test broken in new pandas * Fix diagnostics unit tests for new pandas * Fix copy with extra seasonalities / regressors Py * Fix copy with extra seasonalities / regressors R * Fix weekly_start and yearly_start in R plot_components * Fix plotting in pandas 0.21 by using pydatetime instead of numpy --- R/R/prophet.R | 36 +++++++---- R/tests/testthat/test_prophet.R | 61 +++++++++++------- README.md | 2 +- docs/_docs/installation.md | 2 +- docs/_docs/seasonality_and_holiday_effects.md | 2 +- .../seasonality_and_holiday_effects.ipynb | 2 +- python/fbprophet/forecaster.py | 63 +++++++++++-------- python/fbprophet/tests/test_diagnostics.py | 8 ++- python/fbprophet/tests/test_prophet.py | 33 ++++++++-- 9 files changed, 139 insertions(+), 70 deletions(-) diff --git a/R/R/prophet.R b/R/R/prophet.R index a5e51bc..36adf23 100644 --- a/R/R/prophet.R +++ b/R/R/prophet.R @@ -392,9 +392,13 @@ initialize_scales_fn <- function(m, initialize_scales, df) { m$start <- min(df$ds) m$t.scale <- time_diff(max(df$ds), m$start, "secs") for (name in names(m$extra_regressors)) { + n.vals <- length(unique(df[[name]])) + if (n.vals < 2) { + stop('Regressor ', name, ' is constant.') + } standardize <- m$extra_regressors[[name]]$standardize if (standardize == 'auto') { - if (all(sort(unique(df[[name]])) == c(0, 1))) { + if (n.vals == 2 && all(sort(unique(df[[name]])) == c(0, 1))) { # Don't standardize binary variables standardize <- FALSE } else { @@ -404,9 +408,6 @@ initialize_scales_fn <- function(m, initialize_scales, df) { if (standardize) { mu <- mean(df[[name]]) std <- stats::sd(df[[name]]) - if (std == 0) { - std <- mu - } m$extra_regressors[[name]]$mu <- mu m$extra_regressors[[name]]$std <- std } @@ -1586,7 +1587,8 @@ seasonality_plot_df <- function(m, ds) { #' @keywords internal plot_weekly <- function(m, uncertainty = TRUE, weekly_start = 0) { # Compute weekly seasonality for a Sun-Sat sequence of dates. - days <- seq(set_date('2017-01-01'), by='d', length.out=7) + weekly_start + days <- seq(set_date('2017-01-01'), by='d', length.out=7) + as.difftime( + weekly_start, units = "days") df.w <- seasonality_plot_df(m, days) seas <- predict_seasonal_components(m, df.w) seas$dow <- factor(weekdays(df.w$ds), levels=weekdays(df.w$ds)) @@ -1619,7 +1621,8 @@ plot_weekly <- function(m, uncertainty = TRUE, weekly_start = 0) { #' @keywords internal plot_yearly <- function(m, uncertainty = TRUE, yearly_start = 0) { # Compute yearly seasonality for a Jan 1 - Dec 31 sequence of dates. - days <- seq(set_date('2017-01-01'), by='d', length.out=365) + yearly_start + days <- seq(set_date('2017-01-01'), by='d', length.out=365) + as.difftime( + yearly_start, units = "days") df.y <- seasonality_plot_df(m, days) seas <- predict_seasonal_components(m, df.y) seas$ds <- df.y$ds @@ -1695,6 +1698,10 @@ plot_seasonality <- function(m, name, uncertainty = TRUE) { #' #' @keywords internal prophet_copy <- function(m, cutoff = NULL) { + if (is.null(m$history)) { + stop("This is for copying a fitted Prophet object.") + } + if (m$specified.changepoints) { changepoints <- m$changepoints if (!is.null(cutoff)) { @@ -1704,13 +1711,15 @@ prophet_copy <- function(m, cutoff = NULL) { } else { changepoints <- NULL } - return(prophet( + # Auto seasonalities are set to FALSE because they are already set in + # m$seasonalities. + m2 <- prophet( growth = m$growth, changepoints = changepoints, n.changepoints = m$n.changepoints, - yearly.seasonality = m$yearly.seasonality, - weekly.seasonality = m$weekly.seasonality, - daily.seasonality = m$daily.seasonality, + yearly.seasonality = FALSE, + weekly.seasonality = FALSE, + daily.seasonality = FALSE, holidays = m$holidays, seasonality.prior.scale = m$seasonality.prior.scale, changepoint.prior.scale = m$changepoint.prior.scale, @@ -1718,8 +1727,11 @@ prophet_copy <- function(m, cutoff = NULL) { mcmc.samples = m$mcmc.samples, interval.width = m$interval.width, uncertainty.samples = m$uncertainty.samples, - fit = FALSE, - )) + fit = FALSE + ) + m2$extra_regressors <- m$extra_regressors + m2$seasonalities <- m$seasonalities + return(m2) } # fb-block 3 diff --git a/R/tests/testthat/test_prophet.R b/R/tests/testthat/test_prophet.R index 313b1ef..351082f 100644 --- a/R/tests/testthat/test_prophet.R +++ b/R/tests/testthat/test_prophet.R @@ -511,24 +511,24 @@ test_that("added_regressors", { expect_equal(fcst$seasonal[1], fcst$seasonalities[1] + fcst$extra_regressors[1]) expect_equal(fcst$yhat[1], fcst$trend[1] + fcst$seasonal[1]) + # Check fails if constant extra regressor + df$constant_feature <- 5 + m <- prophet() + m <- add_regressor(m, 'constant_feature') + expect_error(fit.prophet(m, df)) }) test_that("copy", { skip_if_not(Sys.getenv('R_ARCH') != '/i386') + df <- DATA + df$cap <- 200. + df$binary_feature <- c(rep(0, 255), rep(1, 255)) inputs <- list( growth = c('linear', 'logistic'), - changepoints = c(NULL, c('2016-12-25')), - n.changepoints = c(3), yearly.seasonality = c(TRUE, FALSE), weekly.seasonality = c(TRUE, FALSE), daily.seasonality = c(TRUE, FALSE), - holidays = c(NULL, 'insert_dataframe'), - seasonality.prior.scale = c(1.1), - holidays.prior.scale = c(1.1), - changepoints.prior.scale = c(0.1), - mcmc.samples = c(100), - interval.width = c(0.9), - uncertainty.samples = c(200) + holidays = c('null', 'insert_dataframe') ) products <- expand.grid(inputs) for (i in 1:length(products)) { @@ -538,32 +538,51 @@ test_that("copy", { holidays <- NULL } m1 <- prophet( - growth = products$growth[i], - changepoints = products$changepoints[i], - n.changepoints = products$n.changepoints[i], + growth = as.character(products$growth[i]), + changepoints = NULL, + n.changepoints = 3, yearly.seasonality = products$yearly.seasonality[i], weekly.seasonality = products$weekly.seasonality[i], daily.seasonality = products$daily.seasonality[i], holidays = holidays, - seasonality.prior.scale = products$seasonality.prior.scale[i], - holidays.prior.scale = products$holidays.prior.scale[i], - changepoints.prior.scale = products$changepoints.prior.scale[i], - mcmc.samples = products$mcmc.samples[i], - interval.width = products$interval.width[i], - uncertainty.samples = products$uncertainty.samples[i], + seasonality.prior.scale = 1.1, + holidays.prior.scale = 1.1, + changepoints.prior.scale = 0.1, + mcmc.samples = 100, + interval.width = 0.9, + uncertainty.samples = 200, fit = FALSE ) + out <- prophet:::setup_dataframe(m1, df, initialize_scales = TRUE) + m1 <- out$m + m1$history <- out$df + m1 <- prophet:::set_auto_seasonalities(m1) m2 <- prophet:::prophet_copy(m1) # Values should be copied correctly - for (arg in names(inputs)) { + args <- c('growth', 'changepoints', 'n.changepoints', 'holidays', + 'seasonality.prior.scale', 'holidays.prior.scale', + 'changepoints.prior.scale', 'mcmc.samples', 'interval.width', + 'uncertainty.samples') + for (arg in args) { expect_equal(m1[[arg]], m2[[arg]]) } + expect_equal(FALSE, m2$yearly.seasonality) + expect_equal(FALSE, m2$weekly.seasonality) + expect_equal(FALSE, m2$daily.seasonality) + expect_equal(m1$yearly.seasonality, 'yearly' %in% names(m2$seasonalities)) + expect_equal(m1$weekly.seasonality, 'weekly' %in% names(m2$seasonalities)) + expect_equal(m1$daily.seasonality, 'daily' %in% names(m2$seasonalities)) } - # Check for cutoff + # Check for cutoff and custom seasonality and extra regressors changepoints <- seq.Date(as.Date('2012-06-15'), as.Date('2012-09-15'), by='d') cutoff <- as.Date('2012-07-25') - m1 <- prophet(DATA, changepoints = changepoints) + m1 <- prophet(changepoints = changepoints) + m1 <- add_seasonality(m1, 'custom', 10, 5) + m1 <- add_regressor(m1, 'binary_feature') + m1 <- fit.prophet(m1, df) m2 <- prophet:::prophet_copy(m1, cutoff) changepoints <- changepoints[changepoints <= cutoff] expect_equal(prophet:::set_date(changepoints), m2$changepoints) + expect_true('custom' %in% names(m2$seasonalities)) + expect_true('binary_feature' %in% names(m2$extra_regressors)) }) diff --git a/README.md b/README.md index 2a79577..5c6123d 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ On Windows, PyStan requires a compiler so you'll need to [follow the instruction ### Linux -Make sure compilers (gcc, g++) and Python development tools (python-dev) are installed. If you are using a VM, be aware that you will need at least 2GB of memory to run PyStan. +Make sure compilers (gcc, g++) and Python development tools (python-dev) are installed. If you are using a VM, be aware that you will need at least 4GB of memory to install fbprophet, and at least 2GB of memory to use fbprophet. ### Anaconda diff --git a/docs/_docs/installation.md b/docs/_docs/installation.md index f8363eb..6a3fef5 100644 --- a/docs/_docs/installation.md +++ b/docs/_docs/installation.md @@ -43,7 +43,7 @@ On Windows, PyStan requires a compiler so you'll need to [follow the instruction ### Linux -Make sure compilers (gcc, g++) and Python development tools (python-dev) are installed. If you are using a VM, be aware that you will need at least 2GB of memory to run PyStan. +Make sure compilers (gcc, g++) and Python development tools (python-dev) are installed. If you are using a VM, be aware that you will need at least 4GB of memory to install fbprophet, and at least 2GB of memory to use fbprophet. ### Anaconda diff --git a/docs/_docs/seasonality_and_holiday_effects.md b/docs/_docs/seasonality_and_holiday_effects.md index 6c49caf..ad3d757 100644 --- a/docs/_docs/seasonality_and_holiday_effects.md +++ b/docs/_docs/seasonality_and_holiday_effects.md @@ -362,6 +362,6 @@ m.plot_components(forecast); ![png](/prophet/static/seasonality_and_holiday_effects_files/seasonality_and_holiday_effects_26_0.png) -NFL Sundays could also have been handled using the "holidays" interface described above, by creating a list of past and future NFL Sundays. The `add_regressor` function provides a more general interface for defining extra linear regressors, and in particular does not require that the regressor be a binary indicator. Another time series could be used as a regressor, although its future values would have to be known. +NFL Sundays could also have been handled using the "holidays" interface described above, by creating a list of past and future NFL Sundays. The `add_regressor` function provides a more general interface for defining extra linear regressors, and in particular does not require that the regressor be a binary indicator. Another time series could be used as a regressor, although its future values would have to be known. The regressor cannot be constant in the training data; fitting will exit with an error if it is. The `add_regressor` function has optional arguments for specifying the prior scale (holiday prior scale is used by default) and whether or not the regressor is standardized - see the docstring with `help(Prophet.add_regressor)` in Python and `?add_regressor` in R. diff --git a/notebooks/seasonality_and_holiday_effects.ipynb b/notebooks/seasonality_and_holiday_effects.ipynb index 9e4d17a..160d07c 100644 --- a/notebooks/seasonality_and_holiday_effects.ipynb +++ b/notebooks/seasonality_and_holiday_effects.ipynb @@ -728,7 +728,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "NFL Sundays could also have been handled using the \"holidays\" interface described above, by creating a list of past and future NFL Sundays. The `add_regressor` function provides a more general interface for defining extra linear regressors, and in particular does not require that the regressor be a binary indicator. Another time series could be used as a regressor, although its future values would have to be known.\n", + "NFL Sundays could also have been handled using the \"holidays\" interface described above, by creating a list of past and future NFL Sundays. The `add_regressor` function provides a more general interface for defining extra linear regressors, and in particular does not require that the regressor be a binary indicator. Another time series could be used as a regressor, although its future values would have to be known. The regressor cannot be constant in the training data; fitting will exit with an error if it is.\n", "\n", "The `add_regressor` function has optional arguments for specifying the prior scale (holiday prior scale is used by default) and whether or not the regressor is standardized - see the docstring with `help(Prophet.add_regressor)` in Python and `?add_regressor` in R." ] diff --git a/python/fbprophet/forecaster.py b/python/fbprophet/forecaster.py index 3f0c323..4c361df 100644 --- a/python/fbprophet/forecaster.py +++ b/python/fbprophet/forecaster.py @@ -11,6 +11,7 @@ from __future__ import print_function from __future__ import unicode_literals from collections import defaultdict +from copy import deepcopy from datetime import timedelta import logging @@ -278,6 +279,9 @@ class Prophet(object): self.t_scale = df['ds'].max() - self.start for name, props in self.extra_regressors.items(): standardize = props['standardize'] + n_vals = len(df[name].unique()) + if n_vals < 2: + raise ValueError('Regressor {} is constant.'.format(name)) if standardize == 'auto': if set(df[name].unique()) == set([1, 0]): # Don't standardize binary variables. @@ -287,8 +291,6 @@ class Prophet(object): if standardize: mu = df[name].mean() std = df[name].std() - if std == 0: - std = mu self.extra_regressors[name]['mu'] = mu self.extra_regressors[name]['std'] = std @@ -1248,16 +1250,16 @@ class Prophet(object): ax = fig.add_subplot(111) else: fig = ax.get_figure() - ax.plot(self.history['ds'].values, self.history['y'], 'k.') - ax.plot(fcst['ds'].values, fcst['yhat'], ls='-', c='#0072B2') + fcst_t = fcst['ds'].dt.to_pydatetime() + ax.plot(self.history['ds'].dt.to_pydatetime(), self.history['y'], 'k.') + ax.plot(fcst_t, fcst['yhat'], ls='-', c='#0072B2') if 'cap' in fcst and plot_cap: - ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') + ax.plot(fcst_t, fcst['cap'], ls='--', c='k') if self.logistic_floor and 'floor' in fcst and plot_cap: - ax.plot(fcst['ds'].values, fcst['floor'], ls='--', c='k') + ax.plot(fcst_t, fcst['floor'], ls='--', c='k') if uncertainty: - ax.fill_between(fcst['ds'].values, fcst['yhat_lower'], - fcst['yhat_upper'], color='#0072B2', - alpha=0.2) + ax.fill_between(fcst_t, fcst['yhat_lower'], fcst['yhat_upper'], + color='#0072B2', alpha=0.2) ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) @@ -1345,15 +1347,16 @@ class Prophet(object): if not ax: fig = plt.figure(facecolor='w', figsize=(10, 6)) ax = fig.add_subplot(111) - artists += ax.plot(fcst['ds'].values, fcst[name], ls='-', c='#0072B2') + fcst_t = fcst['ds'].dt.to_pydatetime() + artists += ax.plot(fcst_t, fcst[name], ls='-', c='#0072B2') if 'cap' in fcst and plot_cap: - artists += ax.plot(fcst['ds'].values, fcst['cap'], ls='--', c='k') + artists += ax.plot(fcst_t, fcst['cap'], ls='--', c='k') if self.logistic_floor and 'floor' in fcst and plot_cap: - ax.plot(fcst['ds'].values, fcst['floor'], ls='--', c='k') + ax.plot(fcst_t, fcst['floor'], ls='--', c='k') if uncertainty: artists += [ax.fill_between( - fcst['ds'].values, fcst[name + '_lower'], - fcst[name + '_upper'], color='#0072B2', alpha=0.2)] + fcst_t, fcst[name + '_lower'], fcst[name + '_upper'], + color='#0072B2', alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) ax.set_xlabel('ds') ax.set_ylabel(name) @@ -1441,11 +1444,11 @@ class Prophet(object): pd.Timedelta(days=yearly_start)) df_y = self.seasonality_plot_df(days) seas = self.predict_seasonal_components(df_y) - artists += ax.plot(df_y['ds'], seas['yearly'], ls='-', - c='#0072B2') + artists += ax.plot( + df_y['ds'].dt.to_pydatetime(), seas['yearly'], ls='-', c='#0072B2') if uncertainty: artists += [ax.fill_between( - df_y['ds'].values, seas['yearly_lower'], + df_y['ds'].dt.to_pydatetime(), seas['yearly_lower'], seas['yearly_upper'], color='#0072B2', alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) months = MonthLocator(range(1, 13), bymonthday=1, interval=2) @@ -1481,14 +1484,16 @@ class Prophet(object): days = pd.to_datetime(np.linspace(start.value, end.value, plot_points)) df_y = self.seasonality_plot_df(days) seas = self.predict_seasonal_components(df_y) - artists += ax.plot(df_y['ds'], seas[name], ls='-', + artists += ax.plot(df_y['ds'].dt.to_pydatetime(), seas[name], ls='-', c='#0072B2') if uncertainty: artists += [ax.fill_between( - df_y['ds'].values, seas[name + '_lower'], + df_y['ds'].dt.to_pydatetime(), seas[name + '_lower'], seas[name + '_upper'], color='#0072B2', alpha=0.2)] ax.grid(True, which='major', c='gray', ls='-', lw=1, alpha=0.2) - ax.set_xticks(pd.to_datetime(np.linspace(start.value, end.value, 7))) + xticks = pd.to_datetime(np.linspace(start.value, end.value, 7) + ).to_pydatetime() + ax.set_xticks(xticks) if period <= 2: fmt_str = '{dt:%T}' elif period < 14: @@ -1514,6 +1519,9 @@ class Prophet(object): ------- Prophet class object with the same parameter with model variable """ + if self.history is None: + raise Exception('This is for copying a fitted Prophet object.') + if self.specified_changepoints: changepoints = self.changepoints if cutoff is not None: @@ -1522,18 +1530,23 @@ class Prophet(object): else: changepoints = None - return Prophet( + # Auto seasonalities are set to False because they are already set in + # self.seasonalities. + m = Prophet( growth=self.growth, n_changepoints=self.n_changepoints, changepoints=changepoints, - yearly_seasonality=self.yearly_seasonality, - weekly_seasonality=self.weekly_seasonality, - daily_seasonality=self.daily_seasonality, + yearly_seasonality=False, + weekly_seasonality=False, + daily_seasonality=False, holidays=self.holidays, seasonality_prior_scale=self.seasonality_prior_scale, changepoint_prior_scale=self.changepoint_prior_scale, holidays_prior_scale=self.holidays_prior_scale, mcmc_samples=self.mcmc_samples, interval_width=self.interval_width, - uncertainty_samples=self.uncertainty_samples + uncertainty_samples=self.uncertainty_samples, ) + m.extra_regressors = deepcopy(self.extra_regressors) + m.seasonalities = deepcopy(self.seasonalities) + return m diff --git a/python/fbprophet/tests/test_diagnostics.py b/python/fbprophet/tests/test_diagnostics.py index 43907e6..3dee544 100644 --- a/python/fbprophet/tests/test_diagnostics.py +++ b/python/fbprophet/tests/test_diagnostics.py @@ -84,7 +84,9 @@ class TestDiagnostics(TestCase): df_shf2 = diagnostics.simulated_historical_forecasts( m, horizon='10 days', k=1, period='5 days') self.assertAlmostEqual( - ((df_shf1 - df_shf2)**2)[['y', 'yhat']].sum().sum(), 0.0) + ((df_shf1['y'] - df_shf2['y']) ** 2).sum(), 0.0) + self.assertAlmostEqual( + ((df_shf1['yhat'] - df_shf2['yhat']) ** 2).sum(), 0.0) def test_cross_validation(self): m = Prophet() @@ -111,4 +113,6 @@ class TestDiagnostics(TestCase): df_cv2 = diagnostics.cross_validation( m, horizon='32 days', period='10 days', initial='96 days') self.assertAlmostEqual( - ((df_cv1 - df_cv2)**2)[['y', 'yhat']].sum().sum(), 0.0) + ((df_cv1['y'] - df_cv2['y']) ** 2).sum(), 0.0) + self.assertAlmostEqual( + ((df_cv1['yhat'] - df_cv2['yhat']) ** 2).sum(), 0.0) diff --git a/python/fbprophet/tests/test_prophet.py b/python/fbprophet/tests/test_prophet.py index ac5b3e9..254701a 100644 --- a/python/fbprophet/tests/test_prophet.py +++ b/python/fbprophet/tests/test_prophet.py @@ -62,7 +62,6 @@ class TestProphet(TestCase): def test_fit_changepoint_not_in_history(self): train = DATA[(DATA['ds'] < '2013-01-01') | (DATA['ds'] > '2014-01-01')] - train[(train['ds'] > '2014-01-01')] += 20 future = pd.DataFrame({'ds': DATA['ds']}) forecaster = Prophet(changepoints=['2013-06-06']) forecaster.fit(train) @@ -548,8 +547,17 @@ class TestProphet(TestCase): fcst['yhat'][0], fcst['trend'][0] + fcst['seasonal'][0], ) + # Check fails if constant extra regressor + df['constant_feature'] = 5 + m = Prophet() + m.add_regressor('constant_feature') + with self.assertRaises(ValueError): + m.fit(df.copy()) def test_copy(self): + df = DATA.copy() + df['cap'] = 200. + df['binary_feature'] = [0] * 255 + [1] * 255 # These values are created except for its default values holiday = pd.DataFrame( {'ds': pd.to_datetime(['2016-12-25']), 'holiday': ['x']}) @@ -571,13 +579,22 @@ class TestProphet(TestCase): # Values should be copied correctly for product in products: m1 = Prophet(*product) + m1.history = m1.setup_dataframe( + df.copy(), initialize_scales=True) + m1.set_auto_seasonalities() m2 = m1.copy() self.assertEqual(m1.growth, m2.growth) self.assertEqual(m1.n_changepoints, m2.n_changepoints) self.assertEqual(m1.changepoints, m2.changepoints) - self.assertEqual(m1.yearly_seasonality, m2.yearly_seasonality) - self.assertEqual(m1.weekly_seasonality, m2.weekly_seasonality) - self.assertEqual(m1.daily_seasonality, m2.daily_seasonality) + self.assertEqual(False, m2.yearly_seasonality) + self.assertEqual(False, m2.weekly_seasonality) + self.assertEqual(False, m2.daily_seasonality) + self.assertEqual( + m1.yearly_seasonality, 'yearly' in m2.seasonalities) + self.assertEqual( + m1.weekly_seasonality, 'weekly' in m2.seasonalities) + self.assertEqual( + m1.daily_seasonality, 'daily' in m2.seasonalities) if m1.holidays is None: self.assertEqual(m1.holidays, m2.holidays) else: @@ -589,11 +606,15 @@ class TestProphet(TestCase): self.assertEqual(m1.interval_width, m2.interval_width) self.assertEqual(m1.uncertainty_samples, m2.uncertainty_samples) - # Check for cutoff + # Check for cutoff and custom seasonality and extra regressors changepoints = pd.date_range('2012-06-15', '2012-09-15') cutoff = pd.Timestamp('2012-07-25') m1 = Prophet(changepoints=changepoints) - m1.fit(DATA) + m1.add_seasonality('custom', 10, 5) + m1.add_regressor('binary_feature') + m1.fit(df) m2 = m1.copy(cutoff=cutoff) changepoints = changepoints[changepoints <= cutoff] self.assertTrue((changepoints == m2.changepoints).all()) + self.assertTrue('custom' in m2.seasonalities) + self.assertTrue('binary_feature' in m2.extra_regressors) From 451c886c734e1acc9cf2f89c5a364c0858613c93 Mon Sep 17 00:00:00 2001 From: Willy Hardy Date: Fri, 22 Dec 2017 15:09:11 -0500 Subject: [PATCH 4/7] Added dygraphs functionality. (#320) * Added dygraphs functionality. * reversed the unintentional inclusion of prophet_copy * namespaced functions and corrected %>% typo --- R/DESCRIPTION | 4 +- R/R/plot.R | 373 ++++++++++++++++++++++++++++++++++++++++++++++++++ R/R/prophet.R | 304 ---------------------------------------- 3 files changed, 376 insertions(+), 305 deletions(-) create mode 100644 R/R/plot.R diff --git a/R/DESCRIPTION b/R/DESCRIPTION index 89c2640..abfe2f6 100644 --- a/R/DESCRIPTION +++ b/R/DESCRIPTION @@ -16,6 +16,7 @@ Depends: Rcpp (>= 0.12.0) Imports: dplyr (>= 0.5.0), + dygraphs, extraDistr, ggplot2, grid, @@ -23,7 +24,8 @@ Imports: scales, stats, tidyr (>= 0.6.1), - utils + utils, + xts Suggests: knitr, testthat, diff --git a/R/R/plot.R b/R/R/plot.R new file mode 100644 index 0000000..9797ee4 --- /dev/null +++ b/R/R/plot.R @@ -0,0 +1,373 @@ +# +#' Merge history and forecast for plotting. +#' +#' @param m Prophet object. +#' @param fcst Data frame returned by prophet predict. +#' +#' @importFrom dplyr "%>%" +#' @keywords internal +df_for_plotting <- function(m, fcst) { + # Make sure there is no y in fcst + fcst$y <- NULL + df <- m$history %>% + dplyr::select(ds, y) %>% + dplyr::full_join(fcst, by = "ds") %>% + dplyr::arrange(ds) + return(df) +} + +#' Plot the prophet forecast. +#' +#' @param x Prophet object. +#' @param fcst Data frame returned by predict(m, df). +#' @param uncertainty Boolean indicating if the uncertainty interval for yhat +#' should be plotted. Must be present in fcst as yhat_lower and yhat_upper. +#' @param plot_cap Boolean indicating if the capacity should be shown in the +#' figure, if available. +#' @param xlabel Optional label for x-axis +#' @param ylabel Optional label for y-axis +#' @param ... additional arguments +#' +#' @return A ggplot2 plot. +#' +#' @examples +#' \dontrun{ +#' history <- data.frame(ds = seq(as.Date('2015-01-01'), as.Date('2016-01-01'), by = 'd'), +#' y = sin(1:366/200) + rnorm(366)/10) +#' m <- prophet(history) +#' future <- make_future_dataframe(m, periods = 365) +#' forecast <- predict(m, future) +#' plot(m, forecast) +#' } +#' +#' @export +plot.prophet <- function(x, fcst, uncertainty = TRUE, plot_cap = TRUE, + xlabel = 'ds', ylabel = 'y', ...) { + df <- df_for_plotting(x, fcst) + gg <- ggplot2::ggplot(df, ggplot2::aes(x = ds, y = y)) + + ggplot2::labs(x = xlabel, y = ylabel) + if (exists('cap', where = df) && plot_cap) { + gg <- gg + ggplot2::geom_line( + ggplot2::aes(y = cap), linetype = 'dashed', na.rm = TRUE) + } + if (x$logistic.floor && exists('floor', where = df) && plot_cap) { + gg <- gg + ggplot2::geom_line( + ggplot2::aes(y = floor), linetype = 'dashed', na.rm = TRUE) + } + if (uncertainty && exists('yhat_lower', where = df)) { + gg <- gg + + ggplot2::geom_ribbon(ggplot2::aes(ymin = yhat_lower, ymax = yhat_upper), + alpha = 0.2, + fill = "#0072B2", + na.rm = TRUE) + } + gg <- gg + + ggplot2::geom_point(na.rm=TRUE) + + ggplot2::geom_line(ggplot2::aes(y = yhat), color = "#0072B2", + na.rm = TRUE) + + ggplot2::theme(aspect.ratio = 3 / 5) + return(gg) +} + +#' Plot the components of a prophet forecast. +#' Prints a ggplot2 with panels for trend, weekly and yearly seasonalities if +#' present, and holidays if present. +#' +#' @param m Prophet object. +#' @param fcst Data frame returned by predict(m, df). +#' @param uncertainty Boolean indicating if the uncertainty interval should be +#' plotted for the trend, from fcst columns trend_lower and trend_upper. +#' @param plot_cap Boolean indicating if the capacity should be shown in the +#' figure, if available. +#' @param weekly_start Integer specifying the start day of the weekly +#' seasonality plot. 0 (default) starts the week on Sunday. 1 shifts by 1 day +#' to Monday, and so on. +#' @param yearly_start Integer specifying the start day of the yearly +#' seasonality plot. 0 (default) starts the year on Jan 1. 1 shifts by 1 day +#' to Jan 2, and so on. +#' +#' @return Invisibly return a list containing the plotted ggplot objects +#' +#' @export +#' @importFrom dplyr "%>%" +prophet_plot_components <- function( + m, fcst, uncertainty = TRUE, plot_cap = TRUE, weekly_start = 0, + yearly_start = 0 +) { + # Plot the trend + panels <- list(plot_forecast_component(fcst, 'trend', uncertainty, plot_cap)) + # Plot holiday components, if present. + if (!is.null(m$holidays) & ('holidays' %in% colnames(fcst))) { + panels[[length(panels) + 1]] <- plot_forecast_component( + fcst, 'holidays', uncertainty, FALSE) + } + # Plot weekly seasonality, if present + if ("weekly" %in% colnames(fcst)) { + panels[[length(panels) + 1]] <- plot_weekly(m, uncertainty, weekly_start) + } + # Plot yearly seasonality, if present + if ("yearly" %in% colnames(fcst)) { + panels[[length(panels) + 1]] <- plot_yearly(m, uncertainty, yearly_start) + } + # Plot other seasonalities + for (name in names(m$seasonalities)) { + if (!(name %in% c('weekly', 'yearly')) && + (name %in% colnames(fcst))) { + panels[[length(panels) + 1]] <- plot_seasonality(m, name, uncertainty) + } + } + # Plot extra regressors + if ((length(m$extra_regressors) > 0) + & ('extra_regressors' %in% colnames(fcst))) { + panels[[length(panels) + 1]] <- plot_forecast_component( + fcst, 'extra_regressors', uncertainty, FALSE) + } + + # Make the plot. + grid::grid.newpage() + grid::pushViewport(grid::viewport(layout = grid::grid.layout(length(panels), + 1))) + for (i in 1:length(panels)) { + print(panels[[i]], vp = grid::viewport(layout.pos.row = i, + layout.pos.col = 1)) + } + return(invisible(panels)) +} + +#' Plot a particular component of the forecast. +#' +#' @param fcst Dataframe output of `predict`. +#' @param name String name of the component to plot (column of fcst). +#' @param uncertainty Boolean to plot uncertainty intervals. +#' @param plot_cap Boolean indicating if the capacity should be shown in the +#' figure, if available. +#' +#' @return A ggplot2 plot. +#' +#' @export +plot_forecast_component <- function( + fcst, name, uncertainty = TRUE, plot_cap = FALSE +) { + gg.comp <- ggplot2::ggplot( + fcst, ggplot2::aes_string(x = 'ds', y = name, group = 1)) + + ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) + if (exists('cap', where = fcst) && plot_cap) { + gg.comp <- gg.comp + ggplot2::geom_line( + ggplot2::aes(y = cap), linetype = 'dashed', na.rm = TRUE) + } + if (exists('floor', where = fcst) && plot_cap) { + gg.comp <- gg.comp + ggplot2::geom_line( + ggplot2::aes(y = floor), linetype = 'dashed', na.rm = TRUE) + } + if (uncertainty) { + gg.comp <- gg.comp + + ggplot2::geom_ribbon( + ggplot2::aes_string( + ymin = paste0(name, '_lower'), ymax = paste0(name, '_upper') + ), + alpha = 0.2, + fill = "#0072B2", + na.rm = TRUE) + } + return(gg.comp) +} + +#' Prepare dataframe for plotting seasonal components. +#' +#' @param m Prophet object. +#' @param ds Array of dates for column ds. +#' +#' @return A dataframe with seasonal components on ds. +#' +#' @keywords internal +seasonality_plot_df <- function(m, ds) { + df_list <- list(ds = ds, cap = 1) + for (name in names(m$extra_regressors)) { + df_list[[name]] <- 0 + } + df <- as.data.frame(df_list) + df <- setup_dataframe(m, df)$df + return(df) +} + +#' Plot the weekly component of the forecast. +#' +#' @param m Prophet model object +#' @param uncertainty Boolean to plot uncertainty intervals. +#' @param weekly_start Integer specifying the start day of the weekly +#' seasonality plot. 0 (default) starts the week on Sunday. 1 shifts by 1 day +#' to Monday, and so on. +#' +#' @return A ggplot2 plot. +#' +#' @keywords internal +plot_weekly <- function(m, uncertainty = TRUE, weekly_start = 0) { + # Compute weekly seasonality for a Sun-Sat sequence of dates. + days <- seq(set_date('2017-01-01'), by='d', length.out=7) + weekly_start + df.w <- seasonality_plot_df(m, days) + seas <- predict_seasonal_components(m, df.w) + seas$dow <- factor(weekdays(df.w$ds), levels=weekdays(df.w$ds)) + + gg.weekly <- ggplot2::ggplot(seas, ggplot2::aes(x = dow, y = weekly, + group = 1)) + + ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) + + ggplot2::labs(x = "Day of week") + if (uncertainty) { + gg.weekly <- gg.weekly + + ggplot2::geom_ribbon(ggplot2::aes(ymin = weekly_lower, + ymax = weekly_upper), + alpha = 0.2, + fill = "#0072B2", + na.rm = TRUE) + } + return(gg.weekly) +} + +#' Plot the yearly component of the forecast. +#' +#' @param m Prophet model object. +#' @param uncertainty Boolean to plot uncertainty intervals. +#' @param yearly_start Integer specifying the start day of the yearly +#' seasonality plot. 0 (default) starts the year on Jan 1. 1 shifts by 1 day +#' to Jan 2, and so on. +#' +#' @return A ggplot2 plot. +#' +#' @keywords internal +plot_yearly <- function(m, uncertainty = TRUE, yearly_start = 0) { + # Compute yearly seasonality for a Jan 1 - Dec 31 sequence of dates. + days <- seq(set_date('2017-01-01'), by='d', length.out=365) + yearly_start + df.y <- seasonality_plot_df(m, days) + seas <- predict_seasonal_components(m, df.y) + seas$ds <- df.y$ds + + gg.yearly <- ggplot2::ggplot(seas, ggplot2::aes(x = ds, y = yearly, + group = 1)) + + ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) + + ggplot2::labs(x = "Day of year") + + ggplot2::scale_x_datetime(labels = scales::date_format('%B %d')) + if (uncertainty) { + gg.yearly <- gg.yearly + + ggplot2::geom_ribbon(ggplot2::aes(ymin = yearly_lower, + ymax = yearly_upper), + alpha = 0.2, + fill = "#0072B2", + na.rm = TRUE) + } + return(gg.yearly) +} + +#' Plot a custom seasonal component. +#' +#' @param m Prophet model object. +#' @param name String name of the seasonality. +#' @param uncertainty Boolean to plot uncertainty intervals. +#' +#' @return A ggplot2 plot. +#' +#' @keywords internal +plot_seasonality <- function(m, name, uncertainty = TRUE) { + # Compute seasonality from Jan 1 through a single period. + start <- set_date('2017-01-01') + period <- m$seasonalities[[name]]$period + end <- start + period * 24 * 3600 + plot.points <- 200 + days <- seq(from=start, to=end, length.out=plot.points) + df.y <- seasonality_plot_df(m, days) + seas <- predict_seasonal_components(m, df.y) + seas$ds <- df.y$ds + gg.s <- ggplot2::ggplot( + seas, ggplot2::aes_string(x = 'ds', y = name, group = 1)) + + ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) + if (period <= 2) { + fmt.str <- '%T' + } else if (period < 14) { + fmt.str <- '%m/%d %R' + } else { + fmt.str <- '%m/%d' + } + gg.s <- gg.s + + ggplot2::scale_x_datetime(labels = scales::date_format(fmt.str)) + if (uncertainty) { + gg.s <- gg.s + + ggplot2::geom_ribbon( + ggplot2::aes_string( + ymin = paste0(name, '_lower'), ymax = paste0(name, '_upper') + ), + alpha = 0.2, + fill = "#0072B2", + na.rm = TRUE) + } + return(gg.s) +} + +#' Plot the prophet forecast. +#' +#' @param x Prophet object. +#' @param fcst Data frame returned by predict(m, df). +#' @param uncertainty Boolean indicating if the uncertainty interval for yhat +#' should be plotted. Must be present in fcst as yhat_lower and yhat_upper. +#' @param ... additional arguments +#' @importFrom magrittr "%>%" +#' @return A dygraph plot. +#' +#' @examples +#' \dontrun{ +#' history <- data.frame(ds = seq(as.Date('2015-01-01'), as.Date('2016-01-01'), by = 'd'), +#' y = sin(1:366/200) + rnorm(366)/10) +#' m <- prophet(history) +#' future <- make_future_dataframe(m, periods = 365) +#' forecast <- predict(m, future) +#' dyplot.prophet(m, forecast) +#' } +#' +#' @export +dyplot.prophet <- function(x, fcst, uncertainty=TRUE, + ...) +{ + forecast.label='Predicted' + actual.label='Actual' + # create data.frame for plotting + df <- df_for_plotting(x, fcst) + + # build variables to include, or not, the uncertainty data + if(uncertainty && exists("yhat_lower", where = df)) + { + colsToKeep <- c('y', 'yhat', 'yhat_lower', 'yhat_upper') + forecastCols <- c('yhat_lower', 'yhat', 'yhat_upper') + } else + { + colsToKeep <- c('y', 'yhat') + forecastCols <- c('yhat') + } + # convert to xts for easier date handling by dygraph + dfTS <- xts::xts(df %>% dplyr::select_(.dots=colsToKeep), order.by = df$ds) + + # base plot + dyBase <- dygraphs::dygraph(dfTS) + + presAnnotation <- function(dygraph, x, text) { + dygraph %>% + dygraphs::dyAnnotation(x, text, text, attachAtBottom = TRUE) + } + + dyBase <- dyBase %>% + dygraphs::dyOptions(colors = RColorBrewer::brewer.pal(3, "Set1")) %>% + # plot actual values + dygraphs::dySeries('y', label=actual.label) %>% + # plot forecast and ribbon + dygraphs::dySeries(forecastCols, label=forecast.label) %>% + # allow zooming + dygraphs::dyRangeSelector() %>% + # make unzoom button + dygraphs::dyUnzoom() + if (!is.null(x$holidays)) { + for (i in 1:nrow(x$holidays)) { + # make a gray line + dyBase <- dyBase %>% dygraphs::dyEvent(x$holidays$ds[i],color = "rgb(200,200,200)", strokePattern = "solid") + dyBase <- dyBase %>% dygraphs::dyAnnotation(x$holidays$ds[i], x$holidays$holiday[i], x$holidays$holiday[i], attachAtBottom = TRUE) + } + } + return(dyBase) +} + diff --git a/R/R/prophet.R b/R/R/prophet.R index 36adf23..401e24e 100644 --- a/R/R/prophet.R +++ b/R/R/prophet.R @@ -1383,310 +1383,6 @@ make_future_dataframe <- function(m, periods, freq = 'day', return(data.frame(ds = dates)) } -#' Merge history and forecast for plotting. -#' -#' @param m Prophet object. -#' @param fcst Data frame returned by prophet predict. -#' -#' @importFrom dplyr "%>%" -#' @keywords internal -df_for_plotting <- function(m, fcst) { - # Make sure there is no y in fcst - fcst$y <- NULL - df <- m$history %>% - dplyr::select(ds, y) %>% - dplyr::full_join(fcst, by = "ds") %>% - dplyr::arrange(ds) - return(df) -} - -#' Plot the prophet forecast. -#' -#' @param x Prophet object. -#' @param fcst Data frame returned by predict(m, df). -#' @param uncertainty Boolean indicating if the uncertainty interval for yhat -#' should be plotted. Must be present in fcst as yhat_lower and yhat_upper. -#' @param plot_cap Boolean indicating if the capacity should be shown in the -#' figure, if available. -#' @param xlabel Optional label for x-axis -#' @param ylabel Optional label for y-axis -#' @param ... additional arguments -#' -#' @return A ggplot2 plot. -#' -#' @examples -#' \dontrun{ -#' history <- data.frame(ds = seq(as.Date('2015-01-01'), as.Date('2016-01-01'), by = 'd'), -#' y = sin(1:366/200) + rnorm(366)/10) -#' m <- prophet(history) -#' future <- make_future_dataframe(m, periods = 365) -#' forecast <- predict(m, future) -#' plot(m, forecast) -#' } -#' -#' @export -plot.prophet <- function(x, fcst, uncertainty = TRUE, plot_cap = TRUE, - xlabel = 'ds', ylabel = 'y', ...) { - df <- df_for_plotting(x, fcst) - gg <- ggplot2::ggplot(df, ggplot2::aes(x = ds, y = y)) + - ggplot2::labs(x = xlabel, y = ylabel) - if (exists('cap', where = df) && plot_cap) { - gg <- gg + ggplot2::geom_line( - ggplot2::aes(y = cap), linetype = 'dashed', na.rm = TRUE) - } - if (x$logistic.floor && exists('floor', where = df) && plot_cap) { - gg <- gg + ggplot2::geom_line( - ggplot2::aes(y = floor), linetype = 'dashed', na.rm = TRUE) - } - if (uncertainty && exists('yhat_lower', where = df)) { - gg <- gg + - ggplot2::geom_ribbon(ggplot2::aes(ymin = yhat_lower, ymax = yhat_upper), - alpha = 0.2, - fill = "#0072B2", - na.rm = TRUE) - } - gg <- gg + - ggplot2::geom_point(na.rm=TRUE) + - ggplot2::geom_line(ggplot2::aes(y = yhat), color = "#0072B2", - na.rm = TRUE) + - ggplot2::theme(aspect.ratio = 3 / 5) - return(gg) -} - -#' Plot the components of a prophet forecast. -#' Prints a ggplot2 with panels for trend, weekly and yearly seasonalities if -#' present, and holidays if present. -#' -#' @param m Prophet object. -#' @param fcst Data frame returned by predict(m, df). -#' @param uncertainty Boolean indicating if the uncertainty interval should be -#' plotted for the trend, from fcst columns trend_lower and trend_upper. -#' @param plot_cap Boolean indicating if the capacity should be shown in the -#' figure, if available. -#' @param weekly_start Integer specifying the start day of the weekly -#' seasonality plot. 0 (default) starts the week on Sunday. 1 shifts by 1 day -#' to Monday, and so on. -#' @param yearly_start Integer specifying the start day of the yearly -#' seasonality plot. 0 (default) starts the year on Jan 1. 1 shifts by 1 day -#' to Jan 2, and so on. -#' -#' @return Invisibly return a list containing the plotted ggplot objects -#' -#' @export -#' @importFrom dplyr "%>%" -prophet_plot_components <- function( - m, fcst, uncertainty = TRUE, plot_cap = TRUE, weekly_start = 0, - yearly_start = 0 -) { - # Plot the trend - panels <- list(plot_forecast_component(fcst, 'trend', uncertainty, plot_cap)) - # Plot holiday components, if present. - if (!is.null(m$holidays) && ('holidays' %in% colnames(fcst))) { - panels[[length(panels) + 1]] <- plot_forecast_component( - fcst, 'holidays', uncertainty, FALSE) - } - # Plot weekly seasonality, if present - if ("weekly" %in% colnames(fcst)) { - panels[[length(panels) + 1]] <- plot_weekly(m, uncertainty, weekly_start) - } - # Plot yearly seasonality, if present - if ("yearly" %in% colnames(fcst)) { - panels[[length(panels) + 1]] <- plot_yearly(m, uncertainty, yearly_start) - } - # Plot other seasonalities - for (name in names(m$seasonalities)) { - if (!(name %in% c('weekly', 'yearly')) && - (name %in% colnames(fcst))) { - panels[[length(panels) + 1]] <- plot_seasonality(m, name, uncertainty) - } - } - # Plot extra regressors - if ((length(m$extra_regressors) > 0) - & ('extra_regressors' %in% colnames(fcst))) { - panels[[length(panels) + 1]] <- plot_forecast_component( - fcst, 'extra_regressors', uncertainty, FALSE) - } - - # Make the plot. - grid::grid.newpage() - grid::pushViewport(grid::viewport(layout = grid::grid.layout(length(panels), - 1))) - for (i in seq_along(panels)) { - print(panels[[i]], vp = grid::viewport(layout.pos.row = i, - layout.pos.col = 1)) - } - return(invisible(panels)) -} - -#' Plot a particular component of the forecast. -#' -#' @param fcst Dataframe output of `predict`. -#' @param name String name of the component to plot (column of fcst). -#' @param uncertainty Boolean to plot uncertainty intervals. -#' @param plot_cap Boolean indicating if the capacity should be shown in the -#' figure, if available. -#' -#' @return A ggplot2 plot. -#' -#' @export -plot_forecast_component <- function( - fcst, name, uncertainty = TRUE, plot_cap = FALSE -) { - gg.comp <- ggplot2::ggplot( - fcst, ggplot2::aes_string(x = 'ds', y = name, group = 1)) + - ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) - if (exists('cap', where = fcst) && plot_cap) { - gg.comp <- gg.comp + ggplot2::geom_line( - ggplot2::aes(y = cap), linetype = 'dashed', na.rm = TRUE) - } - if (exists('floor', where = fcst) && plot_cap) { - gg.comp <- gg.comp + ggplot2::geom_line( - ggplot2::aes(y = floor), linetype = 'dashed', na.rm = TRUE) - } - if (uncertainty) { - gg.comp <- gg.comp + - ggplot2::geom_ribbon( - ggplot2::aes_string( - ymin = paste0(name, '_lower'), ymax = paste0(name, '_upper') - ), - alpha = 0.2, - fill = "#0072B2", - na.rm = TRUE) - } - return(gg.comp) -} - -#' Prepare dataframe for plotting seasonal components. -#' -#' @param m Prophet object. -#' @param ds Array of dates for column ds. -#' -#' @return A dataframe with seasonal components on ds. -#' -#' @keywords internal -seasonality_plot_df <- function(m, ds) { - df_list <- list(ds = ds, cap = 1, floor = 0) - for (name in names(m$extra_regressors)) { - df_list[[name]] <- 0 - } - df <- as.data.frame(df_list) - df <- setup_dataframe(m, df)$df - return(df) -} - -#' Plot the weekly component of the forecast. -#' -#' @param m Prophet model object -#' @param uncertainty Boolean to plot uncertainty intervals. -#' @param weekly_start Integer specifying the start day of the weekly -#' seasonality plot. 0 (default) starts the week on Sunday. 1 shifts by 1 day -#' to Monday, and so on. -#' -#' @return A ggplot2 plot. -#' -#' @keywords internal -plot_weekly <- function(m, uncertainty = TRUE, weekly_start = 0) { - # Compute weekly seasonality for a Sun-Sat sequence of dates. - days <- seq(set_date('2017-01-01'), by='d', length.out=7) + as.difftime( - weekly_start, units = "days") - df.w <- seasonality_plot_df(m, days) - seas <- predict_seasonal_components(m, df.w) - seas$dow <- factor(weekdays(df.w$ds), levels=weekdays(df.w$ds)) - - gg.weekly <- ggplot2::ggplot(seas, ggplot2::aes(x = dow, y = weekly, - group = 1)) + - ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) + - ggplot2::labs(x = "Day of week") - if (uncertainty) { - gg.weekly <- gg.weekly + - ggplot2::geom_ribbon(ggplot2::aes(ymin = weekly_lower, - ymax = weekly_upper), - alpha = 0.2, - fill = "#0072B2", - na.rm = TRUE) - } - return(gg.weekly) -} - -#' Plot the yearly component of the forecast. -#' -#' @param m Prophet model object. -#' @param uncertainty Boolean to plot uncertainty intervals. -#' @param yearly_start Integer specifying the start day of the yearly -#' seasonality plot. 0 (default) starts the year on Jan 1. 1 shifts by 1 day -#' to Jan 2, and so on. -#' -#' @return A ggplot2 plot. -#' -#' @keywords internal -plot_yearly <- function(m, uncertainty = TRUE, yearly_start = 0) { - # Compute yearly seasonality for a Jan 1 - Dec 31 sequence of dates. - days <- seq(set_date('2017-01-01'), by='d', length.out=365) + as.difftime( - yearly_start, units = "days") - df.y <- seasonality_plot_df(m, days) - seas <- predict_seasonal_components(m, df.y) - seas$ds <- df.y$ds - - gg.yearly <- ggplot2::ggplot(seas, ggplot2::aes(x = ds, y = yearly, - group = 1)) + - ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) + - ggplot2::labs(x = "Day of year") + - ggplot2::scale_x_datetime(labels = scales::date_format('%B %d')) - if (uncertainty) { - gg.yearly <- gg.yearly + - ggplot2::geom_ribbon(ggplot2::aes(ymin = yearly_lower, - ymax = yearly_upper), - alpha = 0.2, - fill = "#0072B2", - na.rm = TRUE) - } - return(gg.yearly) -} - -#' Plot a custom seasonal component. -#' -#' @param m Prophet model object. -#' @param name String name of the seasonality. -#' @param uncertainty Boolean to plot uncertainty intervals. -#' -#' @return A ggplot2 plot. -#' -#' @keywords internal -plot_seasonality <- function(m, name, uncertainty = TRUE) { - # Compute seasonality from Jan 1 through a single period. - start <- set_date('2017-01-01') - period <- m$seasonalities[[name]]$period - end <- start + period * 24 * 3600 - plot.points <- 200 - days <- seq(from=start, to=end, length.out=plot.points) - df.y <- seasonality_plot_df(m, days) - seas <- predict_seasonal_components(m, df.y) - seas$ds <- df.y$ds - gg.s <- ggplot2::ggplot( - seas, ggplot2::aes_string(x = 'ds', y = name, group = 1)) + - ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) - if (period <= 2) { - fmt.str <- '%T' - } else if (period < 14) { - fmt.str <- '%m/%d %R' - } else { - fmt.str <- '%m/%d' - } - gg.s <- gg.s + - ggplot2::scale_x_datetime(labels = scales::date_format(fmt.str)) - if (uncertainty) { - gg.s <- gg.s + - ggplot2::geom_ribbon( - ggplot2::aes_string( - ymin = paste0(name, '_lower'), ymax = paste0(name, '_upper') - ), - alpha = 0.2, - fill = "#0072B2", - na.rm = TRUE) - } - return(gg.s) -} - #' Copy Prophet object. #' #' @param m Prophet model object. From a19589a6620b29a3c97281f9b8ef195a2901f7ed Mon Sep 17 00:00:00 2001 From: Ben Letham Date: Fri, 22 Dec 2017 12:19:09 -0800 Subject: [PATCH 5/7] Fix merge issues --- R/R/plot.R | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/R/R/plot.R b/R/R/plot.R index 9797ee4..3718869 100644 --- a/R/R/plot.R +++ b/R/R/plot.R @@ -1,4 +1,3 @@ -# #' Merge history and forecast for plotting. #' #' @param m Prophet object. @@ -97,7 +96,7 @@ prophet_plot_components <- function( # Plot the trend panels <- list(plot_forecast_component(fcst, 'trend', uncertainty, plot_cap)) # Plot holiday components, if present. - if (!is.null(m$holidays) & ('holidays' %in% colnames(fcst))) { + if (!is.null(m$holidays) && ('holidays' %in% colnames(fcst))) { panels[[length(panels) + 1]] <- plot_forecast_component( fcst, 'holidays', uncertainty, FALSE) } @@ -127,7 +126,7 @@ prophet_plot_components <- function( grid::grid.newpage() grid::pushViewport(grid::viewport(layout = grid::grid.layout(length(panels), 1))) - for (i in 1:length(panels)) { + for (i in seq_along(panels)) { print(panels[[i]], vp = grid::viewport(layout.pos.row = i, layout.pos.col = 1)) } @@ -181,7 +180,7 @@ plot_forecast_component <- function( #' #' @keywords internal seasonality_plot_df <- function(m, ds) { - df_list <- list(ds = ds, cap = 1) + df_list <- list(ds = ds, cap = 1, floor = 0) for (name in names(m$extra_regressors)) { df_list[[name]] <- 0 } @@ -203,7 +202,8 @@ seasonality_plot_df <- function(m, ds) { #' @keywords internal plot_weekly <- function(m, uncertainty = TRUE, weekly_start = 0) { # Compute weekly seasonality for a Sun-Sat sequence of dates. - days <- seq(set_date('2017-01-01'), by='d', length.out=7) + weekly_start + days <- seq(set_date('2017-01-01'), by='d', length.out=7) + as.difftime( + weekly_start, units = "days") df.w <- seasonality_plot_df(m, days) seas <- predict_seasonal_components(m, df.w) seas$dow <- factor(weekdays(df.w$ds), levels=weekdays(df.w$ds)) @@ -236,7 +236,8 @@ plot_weekly <- function(m, uncertainty = TRUE, weekly_start = 0) { #' @keywords internal plot_yearly <- function(m, uncertainty = TRUE, yearly_start = 0) { # Compute yearly seasonality for a Jan 1 - Dec 31 sequence of dates. - days <- seq(set_date('2017-01-01'), by='d', length.out=365) + yearly_start + days <- seq(set_date('2017-01-01'), by='d', length.out=365) + as.difftime( + yearly_start, units = "days") df.y <- seasonality_plot_df(m, days) seas <- predict_seasonal_components(m, df.y) seas$ds <- df.y$ds From 07138f7deb2f07c42f0d491d1a1663aa579f88f3 Mon Sep 17 00:00:00 2001 From: Ben Letham Date: Fri, 22 Dec 2017 13:36:46 -0800 Subject: [PATCH 6/7] R package doc update for dygraph addition --- R/NAMESPACE | 1 + R/R/plot.R | 2 +- R/man/df_for_plotting.Rd | 2 +- R/man/dyplot.prophet.Rd | 35 ++++++++++++++++++++++++++++++++ R/man/plot.prophet.Rd | 2 +- R/man/plot_forecast_component.Rd | 2 +- R/man/plot_seasonality.Rd | 2 +- R/man/plot_weekly.Rd | 2 +- R/man/plot_yearly.Rd | 2 +- R/man/prophet_plot_components.Rd | 2 +- R/man/seasonality_plot_df.Rd | 2 +- 11 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 R/man/dyplot.prophet.Rd diff --git a/R/NAMESPACE b/R/NAMESPACE index 6a5312a..1e460d9 100644 --- a/R/NAMESPACE +++ b/R/NAMESPACE @@ -5,6 +5,7 @@ S3method(predict,prophet) export(add_regressor) export(add_seasonality) export(cross_validation) +export(dyplot.prophet) export(fit.prophet) export(layer_changepoints) export(make_future_dataframe) diff --git a/R/R/plot.R b/R/R/plot.R index 3718869..1264b2b 100644 --- a/R/R/plot.R +++ b/R/R/plot.R @@ -309,7 +309,7 @@ plot_seasonality <- function(m, name, uncertainty = TRUE) { #' @param uncertainty Boolean indicating if the uncertainty interval for yhat #' should be plotted. Must be present in fcst as yhat_lower and yhat_upper. #' @param ... additional arguments -#' @importFrom magrittr "%>%" +#' @importFrom dplyr "%>%" #' @return A dygraph plot. #' #' @examples diff --git a/R/man/df_for_plotting.Rd b/R/man/df_for_plotting.Rd index f8d630d..3bbda91 100644 --- a/R/man/df_for_plotting.Rd +++ b/R/man/df_for_plotting.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R +% Please edit documentation in R/plot.R \name{df_for_plotting} \alias{df_for_plotting} \title{Merge history and forecast for plotting.} diff --git a/R/man/dyplot.prophet.Rd b/R/man/dyplot.prophet.Rd new file mode 100644 index 0000000..002a7db --- /dev/null +++ b/R/man/dyplot.prophet.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/plot.R +\name{dyplot.prophet} +\alias{dyplot.prophet} +\title{Plot the prophet forecast.} +\usage{ +dyplot.prophet(x, fcst, uncertainty = TRUE, ...) +} +\arguments{ +\item{x}{Prophet object.} + +\item{fcst}{Data frame returned by predict(m, df).} + +\item{uncertainty}{Boolean indicating if the uncertainty interval for yhat +should be plotted. Must be present in fcst as yhat_lower and yhat_upper.} + +\item{...}{additional arguments} +} +\value{ +A dygraph plot. +} +\description{ +Plot the prophet forecast. +} +\examples{ +\dontrun{ +history <- data.frame(ds = seq(as.Date('2015-01-01'), as.Date('2016-01-01'), by = 'd'), + y = sin(1:366/200) + rnorm(366)/10) +m <- prophet(history) +future <- make_future_dataframe(m, periods = 365) +forecast <- predict(m, future) +dyplot.prophet(m, forecast) +} + +} diff --git a/R/man/plot.prophet.Rd b/R/man/plot.prophet.Rd index 25c520e..38cd604 100644 --- a/R/man/plot.prophet.Rd +++ b/R/man/plot.prophet.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R +% Please edit documentation in R/plot.R \name{plot.prophet} \alias{plot.prophet} \title{Plot the prophet forecast.} diff --git a/R/man/plot_forecast_component.Rd b/R/man/plot_forecast_component.Rd index f85f896..06c1fb1 100644 --- a/R/man/plot_forecast_component.Rd +++ b/R/man/plot_forecast_component.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R +% Please edit documentation in R/plot.R \name{plot_forecast_component} \alias{plot_forecast_component} \title{Plot a particular component of the forecast.} diff --git a/R/man/plot_seasonality.Rd b/R/man/plot_seasonality.Rd index a92e8b5..e5f2df4 100644 --- a/R/man/plot_seasonality.Rd +++ b/R/man/plot_seasonality.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R +% Please edit documentation in R/plot.R \name{plot_seasonality} \alias{plot_seasonality} \title{Plot a custom seasonal component.} diff --git a/R/man/plot_weekly.Rd b/R/man/plot_weekly.Rd index 6476b56..6092345 100644 --- a/R/man/plot_weekly.Rd +++ b/R/man/plot_weekly.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R +% Please edit documentation in R/plot.R \name{plot_weekly} \alias{plot_weekly} \title{Plot the weekly component of the forecast.} diff --git a/R/man/plot_yearly.Rd b/R/man/plot_yearly.Rd index 9a7c534..7a227ed 100644 --- a/R/man/plot_yearly.Rd +++ b/R/man/plot_yearly.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R +% Please edit documentation in R/plot.R \name{plot_yearly} \alias{plot_yearly} \title{Plot the yearly component of the forecast.} diff --git a/R/man/prophet_plot_components.Rd b/R/man/prophet_plot_components.Rd index eeeea29..4bf0f13 100644 --- a/R/man/prophet_plot_components.Rd +++ b/R/man/prophet_plot_components.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R +% Please edit documentation in R/plot.R \name{prophet_plot_components} \alias{prophet_plot_components} \title{Plot the components of a prophet forecast. diff --git a/R/man/seasonality_plot_df.Rd b/R/man/seasonality_plot_df.Rd index 12d2da9..11def86 100644 --- a/R/man/seasonality_plot_df.Rd +++ b/R/man/seasonality_plot_df.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/prophet.R +% Please edit documentation in R/plot.R \name{seasonality_plot_df} \alias{seasonality_plot_df} \title{Prepare dataframe for plotting seasonal components.} From 29275ac6e95fa705b71973067c8de6557eefccd2 Mon Sep 17 00:00:00 2001 From: Nagi Teramo Date: Sat, 23 Dec 2017 09:13:11 +0900 Subject: [PATCH 7/7] Add R setting for Travis CI (#385) * Update .travis.yml * Add status badge to README.md * Update .travis.yml to change directory before install R --- .travis.yml | 29 ++++++++++++++++++----------- README.md | 2 ++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6692785..3c6694e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,18 @@ -language: python -python: - - "2.7" - - "3.6" - -install: - - pip install --upgrade pip - - pip install -U -r python/requirements.txt - -script: - - cd python && python setup.py develop test +matrix: + include: + - language: python + python: + - "2.7" + - "3.6" + + install: + - pip install --upgrade pip + - pip install -U -r python/requirements.txt + + script: + - cd python && python setup.py develop test + - language: R + sudo: false + cache: packages + before_install: + - cd R diff --git a/README.md b/README.md index 5c6123d..64471b3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Prophet: Automatic Forecasting Procedure +[![Build Status](https://travis-ci.org/facebook/prophet.svg?branch=master)](https://travis-ci.org/facebook/prophet) + Prophet is a procedure for forecasting time series data. It is based on an additive model where non-linear trends are fit with yearly and weekly seasonality, plus holidays. It works best with daily periodicity data with at least one year of historical data. Prophet is robust to missing data, shifts in the trend, and large outliers. Prophet is [open source software](https://code.facebook.com/projects/) released by Facebook's [Core Data Science team](https://research.fb.com/category/data-science/). It is available for download on [CRAN](https://cran.r-project.org/package=prophet) and [PyPI](https://pypi.python.org/pypi/fbprophet/).