Saturating minimum R

This commit is contained in:
Ben Letham 2017-09-01 11:16:59 -07:00
parent 66ea9444eb
commit 2ddcf54930
4 changed files with 87 additions and 21 deletions

View file

@ -80,11 +80,14 @@ simulated_historical_forecasts <- function(model, horizon, units, k,
m <- fit.prophet(m, history.c) m <- fit.prophet(m, history.c)
# Calculate yhat # Calculate yhat
df.predict <- dplyr::filter(df, ds > cutoff, ds <= cutoff + horizon) df.predict <- dplyr::filter(df, ds > cutoff, ds <= cutoff + horizon)
columns <- c('ds')
if (m$growth == 'logistic') { if (m$growth == 'logistic') {
future <- dplyr::select(df.predict, ds, cap) columns <- c(columns, 'cap')
} else{ if (m$logistic.floor) {
future <- dplyr::select(df.predict, ds) columns <- c(columns, 'floor')
}
} }
future <- df[columns]
yhat <- stats::predict(m, future) yhat <- stats::predict(m, future)
# Merge yhat, y, and cutoff. # Merge yhat, y, and cutoff.
df.c <- dplyr::inner_join(df.predict, yhat, by = "ds") df.c <- dplyr::inner_join(df.predict, yhat, by = "ds")

View file

@ -114,6 +114,7 @@ prophet <- function(df = NULL,
specified.changepoints = !is.null(changepoints), specified.changepoints = !is.null(changepoints),
start = NULL, # This and following attributes are set during fitting start = NULL, # This and following attributes are set during fitting
y.scale = NULL, y.scale = NULL,
logistic.floor = FALSE,
t.scale = NULL, t.scale = NULL,
changepoints.t = NULL, changepoints.t = NULL,
seasonalities = list(), seasonalities = list(),
@ -191,7 +192,8 @@ validate_column_name <- function(
) )
rn_l = paste(reserved_names,"_lower",sep="") rn_l = paste(reserved_names,"_lower",sep="")
rn_u = paste(reserved_names,"_upper",sep="") rn_u = paste(reserved_names,"_upper",sep="")
reserved_names = c(reserved_names, rn_l, rn_u, c("ds","y")) reserved_names = c(reserved_names, rn_l, rn_u,
c("ds", "y", "cap", "floor", "y_scaled", "cap_scaled"))
if(name %in% reserved_names){ if(name %in% reserved_names){
stop("Name ", name, " is reserved.") stop("Name ", name, " is reserved.")
} }
@ -332,7 +334,13 @@ setup_dataframe <- function(m, df, initialize_scales = FALSE) {
dplyr::arrange(ds) dplyr::arrange(ds)
if (initialize_scales) { if (initialize_scales) {
m$y.scale <- max(abs(df$y)) if ((m$growth == 'logistic') && ('floor' %in% colnames(df))) {
m$logistic.floor <- TRUE
floor <- df$floor
} else {
floor <- 0
}
m$y.scale <- max(abs(df$y - floor))
if (m$y.scale == 0) { if (m$y.scale == 0) {
m$y.scale <- 1 m$y.scale <- 1
} }
@ -360,9 +368,12 @@ setup_dataframe <- function(m, df, initialize_scales = FALSE) {
} }
} }
df$t <- time_diff(df$ds, m$start, "secs") / m$t.scale if (m$logistic.floor) {
if (exists('y', where=df)) { if (!('floor' %in% colnames(df))) {
df$y_scaled <- df$y / m$y.scale stop("Expected column 'floor'.")
}
} else {
df$floor <- 0
} }
if (m$growth == 'logistic') { if (m$growth == 'logistic') {
@ -370,7 +381,12 @@ setup_dataframe <- function(m, df, initialize_scales = FALSE) {
stop('Capacities must be supplied for logistic growth.') stop('Capacities must be supplied for logistic growth.')
} }
df <- df %>% df <- df %>%
dplyr::mutate(cap_scaled = cap / m$y.scale) dplyr::mutate(cap_scaled = (cap - floor) / m$y.scale)
}
df$t <- time_diff(df$ds, m$start, "secs") / m$t.scale
if (exists('y', where=df)) {
df$y_scaled <- (df$y - df$floor) / m$y.scale
} }
for (name in names(m$extra_regressors)) { for (name in names(m$extra_regressors)) {
@ -848,9 +864,16 @@ logistic_growth_init <- function(df) {
i0 <- which.min(df$ds) i0 <- which.min(df$ds)
i1 <- which.max(df$ds) i1 <- which.max(df$ds)
T <- df$t[i1] - df$t[i0] T <- df$t[i1] - df$t[i0]
# Force valid values, in case y > cap.
r0 <- max(1.01, df$cap_scaled[i0] / df$y_scaled[i0]) # Force valid values, in case y > cap or y < 0
r1 <- max(1.01, df$cap_scaled[i1] / df$y_scaled[i1]) C0 <- df$cap_scaled[i0]
C1 <- df$cap_scaled[i1]
y0 <- max(0.01 * C0, min(0.99 * C0, df$y_scaled[i0]))
y1 <- max(0.01 * C1, min(0.99 * C1, df$y_scaled[i1]))
r0 <- C0 / y0
r1 <- C1 / y1
if (abs(r0 - r1) <= 0.01) { if (abs(r0 - r1) <= 0.01) {
r0 <- 1.05 * r0 r0 <- 1.05 * r0
} }
@ -1015,11 +1038,13 @@ predict.prophet <- function(object, df = NULL, ...) {
seasonal.components <- predict_seasonal_components(object, df) seasonal.components <- predict_seasonal_components(object, df)
intervals <- predict_uncertainty(object, df) intervals <- predict_uncertainty(object, df)
# Drop columns except ds, cap, and trend # Drop columns except ds, cap, floor, and trend
cols <- c('ds', 'trend')
if ('cap' %in% colnames(df)) { if ('cap' %in% colnames(df)) {
cols <- c('ds', 'cap', 'trend') cols <- c(cols, 'cap')
} else { }
cols <- c('ds', 'trend') if (object$logistic.floor) {
cols <- c(cols, 'floor')
} }
df <- df[cols] df <- df[cols]
df <- df %>% df <- df %>%
@ -1108,7 +1133,7 @@ predict_trend <- function(model, df) {
trend <- piecewise_logistic( trend <- piecewise_logistic(
t, cap, deltas, k, param.m, model$changepoints.t) t, cap, deltas, k, param.m, model$changepoints.t)
} }
return(trend * model$y.scale) return(trend * model$y.scale + df$floor)
} }
#' Predict seasonality components, holidays, and added regressors. #' Predict seasonality components, holidays, and added regressors.
@ -1343,7 +1368,7 @@ sample_predictive_trend <- function(model, df, iteration) {
cap <- df$cap_scaled cap <- df$cap_scaled
trend <- piecewise_logistic(t, cap, deltas, k, param.m, changepoint.ts) trend <- piecewise_logistic(t, cap, deltas, k, param.m, changepoint.ts)
} }
return(trend * model$y.scale) return(trend * model$y.scale + df$floor)
} }
#' Make dataframe with future dates for forecasting. #' Make dataframe with future dates for forecasting.
@ -1424,6 +1449,10 @@ plot.prophet <- function(x, fcst, uncertainty = TRUE, plot_cap = TRUE,
gg <- gg + ggplot2::geom_line( gg <- gg + ggplot2::geom_line(
ggplot2::aes(y = cap), linetype = 'dashed', na.rm = TRUE) 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)) { if (uncertainty && exists('yhat_lower', where = df)) {
gg <- gg + gg <- gg +
ggplot2::geom_ribbon(ggplot2::aes(ymin = yhat_lower, ymax = yhat_upper), ggplot2::geom_ribbon(ggplot2::aes(ymin = yhat_lower, ymax = yhat_upper),
@ -1525,6 +1554,10 @@ plot_forecast_component <- function(
gg.comp <- gg.comp + ggplot2::geom_line( gg.comp <- gg.comp + ggplot2::geom_line(
ggplot2::aes(y = cap), linetype = 'dashed', na.rm = TRUE) 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) { if (uncertainty) {
gg.comp <- gg.comp + gg.comp <- gg.comp +
ggplot2::geom_ribbon( ggplot2::geom_ribbon(
@ -1647,8 +1680,7 @@ plot_seasonality <- function(m, name, uncertainty = TRUE) {
ggplot2::geom_line(color = "#0072B2", na.rm = TRUE) ggplot2::geom_line(color = "#0072B2", na.rm = TRUE)
if (period <= 2) { if (period <= 2) {
fmt.str <- '%T' fmt.str <- '%T'
} } else if (period < 14) {
else if (period < 14) {
fmt.str <- '%m/%d %R' fmt.str <- '%m/%d %R'
} else { } else {
fmt.str <- '%m/%d' fmt.str <- '%m/%d'

View file

@ -74,6 +74,36 @@ test_that("setup_dataframe", {
expect_equal(max(history$y_scaled), 1) expect_equal(max(history$y_scaled), 1)
}) })
test_that("logistic_floor", {
skip_if_not(Sys.getenv('R_ARCH') != '/i386')
m <- prophet(growth = 'logistic')
history <- train
history$floor <- 10.
history$cap <- 80.
future1 <- future
future1$cap <- 80.
future1$floor <- 10.
m <- fit.prophet(m, history)
expect_true(m$logistic.floor)
expect_true('floor' %in% colnames(m$history))
expect_equal(m$history$y_scaled[1], 1., tolerance = 1e-6)
fcst1 <- predict(m, future1)
m2 <- prophet(growth = 'logistic')
history2 <- history
history2$y <- history2$y + 10.
history2$floor <- history2$floor + 10.
history2$cap <- history2$cap + 10.
future1$cap <- future1$cap + 10.
future1$floor <- future1$floor + 10.
m2 <- fit.prophet(m2, history2)
expect_equal(m2$history$y_scaled[1], 1., tolerance = 1e-6)
fcst2 <- predict(m, future1)
fcst2$yhat <- fcst2$yhat - 10.
# Check for approximate shift invariance
expect_true(all(abs(fcst1$yhat - fcst2$yhat) < 1))
})
test_that("get_changepoints", { test_that("get_changepoints", {
history <- train history <- train
m <- prophet(history, fit = FALSE) m <- prophet(history, fit = FALSE)
@ -481,6 +511,7 @@ test_that("added_regressors", {
}) })
test_that("copy", { test_that("copy", {
skip_if_not(Sys.getenv('R_ARCH') != '/i386')
inputs <- list( inputs <- list(
growth = c('linear', 'logistic'), growth = c('linear', 'logistic'),
changepoints = c(NULL, c('2016-12-25')), changepoints = c(NULL, c('2016-12-25')),

View file

@ -1312,7 +1312,7 @@ class Prophet(object):
return fig return fig
def plot_forecast_component( def plot_forecast_component(
self, fcst, name, ax=None, uncertainty=True, plot_cap=True): self, fcst, name, ax=None, uncertainty=True, plot_cap=False):
"""Plot a particular component of the forecast. """Plot a particular component of the forecast.
Parameters Parameters