| Title: | Multi-Model Time Series Forecasting Toolkit and Local App |
|---|---|
| Description: | Fits, compares and visualizes univariate time series forecasts through an extensible model registry built largely on the 'forecast' package: AutoRegressive Integrated Moving Average (ARIMA) and Seasonal ARIMA (SARIMA) models (Hyndman and Khandakar (2008) <doi:10.18637/jss.v027.i03>), Error, Trend, Seasonal (ETS) exponential smoothing (Hyndman et al. (2002) <doi:10.1016/S0169-2070(01)00110-8>), Trigonometric seasonality, Box-Cox transformation, AutoRegressive Moving Average (ARMA) errors, Trend and Seasonal components (TBATS) models (De Livera, Hyndman and Snyder (2011) <doi:10.1198/jasa.2011.tm09771>), Neural Network AutoRegression (NNETAR) and Holt-Winters exponential smoothing (Winters (1960) <doi:10.1287/mnsc.6.3.324>), plus optional 'Prophet' models via the 'prophet' package (Taylor and Letham (2018) <doi:10.1080/00031305.2017.1380080>) and Long Short-Term Memory (LSTM) networks via the 'torch' package (Hochreiter and Schmidhuber (1997) <doi:10.1162/neco.1997.9.8.1735>). Also provides grouped series with bottom-up and top-down reconciliation, rolling-origin cross-validation, ensembles and residual diagnostics, following Hyndman and Athanasopoulos (2021) <https://otexts.com/fpp3/>, and a bundled local 'shiny' application exposing every model. |
| Authors: | Mukul Maheshwari [aut, cre, cph] (ORCID: <https://orcid.org/0000-0002-1918-5100>) |
| Maintainer: | Mukul Maheshwari <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 1.0.2 |
| Built: | 2026-10-18 18:25:17 UTC |
| Source: | https://github.com/cran/forecastsuite |
Calendar dates (month-day) that are zero or missing in every year present – these look like de facto holidays/closures that were never declared, so a holiday-aware model can't account for them.
always_zero_dates(data, holidays = NULL)always_zero_dates(data, holidays = NULL)
data |
A tibble with |
holidays |
Optional tibble with a |
A tibble with columns ds, flag ("Zero every year" or
"Missing every year"), and declared_holiday (logical) – the
undeclared rows are the actual gap.
data <- tibble::tibble( ds = c(as.Date("2023-01-01"), as.Date("2024-01-01"), as.Date("2023-06-01")), y = c(0, 0, 5) ) always_zero_dates(data)data <- tibble::tibble( ds = c(as.Date("2023-01-01"), as.Date("2024-01-01"), as.Date("2023-06-01")), y = c(0, 0, 5) ) always_zero_dates(data)
A lightweight, no-model-fitting heuristic: measures trend/seasonal
strength (via STL decomposition), differencing needed
(forecast::ndiffs/nsdiffs), missing-data ratio, and a Box-Jenkins
ACF/PACF-based (p,q)(P,Q) order suggestion. Feeds recommend_model()'s
scoring and the app's suggested-hyperparameters table.
analyze_series(df, date_agg = "day", robust = FALSE)analyze_series(df, date_agg = "day", robust = FALSE)
df |
A tibble with |
date_agg |
Aggregation frequency: one of |
robust |
Logical; use robust (outlier-downweighted) STL when a
seasonal decomposition is attempted internally. Default |
A list with n_obs, detected_freq, n_cycles,
trend_strength, seasonal_strength, ndiffs_needed,
nsdiffs_needed, missing_ratio, arima_p, arima_q, arima_P,
arima_Q.
df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:29, y = 1:30 + rnorm(30)) analyze_series(df, date_agg = "day")df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:29, y = 1:30 + rnorm(30)) analyze_series(df, date_agg = "day")
Builds up to k_requested chronological, non-overlapping walk-forward
folds from one ds-ordered series. Each fold's test window is exactly
horizon_periods rows long. Degrades gracefully: if the series can't
support k_requested folds, fewer are returned instead of erroring –
possibly zero, for a very short series.
build_cv_folds( df, horizon_periods, k_requested, min_train = 2, window = c("expanding", "rolling"), train_window = NULL )build_cv_folds( df, horizon_periods, k_requested, min_train = 2, window = c("expanding", "rolling"), train_window = NULL )
df |
A data frame with a |
horizon_periods |
Integer, the length of each fold's test window,
in the same units as |
k_requested |
Integer, the number of folds requested (may return fewer). |
min_train |
Integer, the minimum number of training rows a fold
must have to be included. Default |
window |
|
train_window |
Integer, the fixed training-window width used when
|
A list of folds, oldest-first (fold 1 has the smallest training
window under "expanding", or the same fixed width under
"rolling"), each a list with train and test data frames.
df <- data.frame(ds = as.Date("2024-01-01") + 0:29, y = 1:30) folds <- build_cv_folds(df, horizon_periods = 5, k_requested = 3) length(folds) rolling_folds <- build_cv_folds(df, horizon_periods = 5, k_requested = 3, window = "rolling", train_window = 10) length(rolling_folds)df <- data.frame(ds = as.Date("2024-01-01") + 0:29, y = 1:30) folds <- build_cv_folds(df, horizon_periods = 5, k_requested = 3) length(folds) rolling_folds <- build_cv_folds(df, horizon_periods = 5, k_requested = 3, window = "rolling", train_window = 10) length(rolling_folds)
Snaps each row's ds to the start of its period (per date_agg) and
combines rows that land on the same period (and, if group_col is
supplied, the same group) – e.g. case data split by district/sex/age
with many rows per Year+Quarter.
collapse_to_period( df, date_agg = "day", fun = c("sum", "mean", "median", "none"), group_col = NULL )collapse_to_period( df, date_agg = "day", fun = c("sum", "mean", "median", "none"), group_col = NULL )
df |
A data frame with |
date_agg |
Aggregation frequency: one of |
fun |
How to combine rows sharing a period: |
group_col |
Optional character scalar; rows only collapse together if they share both a period and this column's value. |
A tibble with one row per period (and group, if group_col is
supplied), columns ds/y(/group_col).
df <- data.frame(ds = as.Date(c("2024-01-01", "2024-01-02")), y = c(3, 5)) collapse_to_period(df, date_agg = "month")df <- data.frame(ds = as.Date(c("2024-01-01", "2024-01-02")), y = c(3, 5)) collapse_to_period(df, date_agg = "month")
Real-world tables often split the period across columns (a Year column
plus a Quarter or Month column, sometimes plus a Day column) rather than
carrying one parseable date. This assembles those into a single Date
and reports the finest granularity present, so the caller can aggregate
at that level (Year+Quarter -> quarterly, Year+Month -> monthly, and so
on) instead of guessing.
compose_date_parts( df, year_col, quarter_col = NULL, month_col = NULL, day_col = NULL )compose_date_parts( df, year_col, quarter_col = NULL, month_col = NULL, day_col = NULL )
df |
A data frame containing the part columns. |
year_col |
Character scalar, the Year column's name. Required. |
quarter_col |
Optional character scalar, a Quarter column (accepts
|
month_col |
Optional character scalar, a Month column (accepts
|
day_col |
Optional character scalar, a Day-of-month column. Only
meaningful alongside |
A list with ds (a Date vector) and granularity (one of
"year", "quarter", "month", "day", the finest part supplied).
df <- data.frame(Year = c(2023, 2023, 2024), Month = c("Jan", "Feb", "Jan")) compose_date_parts(df, year_col = "Year", month_col = "Month")df <- data.frame(Year = c(2023, 2023, 2024), Month = c("Jan", "Feb", "Jan")) compose_date_parts(df, year_col = "Year", month_col = "Month")
MASE/sMAPE/RMSE are all magnitude-only, so a model that's consistently
over- or under-forecasting can still look "fine" on those alone. This
surfaces the signed error directly. Generic across every registered
model – only needs fc_tib's ds/yhat and test_df's ds/y.
compute_forecast_bias(fc_tib, test_df)compute_forecast_bias(fc_tib, test_df)
fc_tib |
A tibble with |
test_df |
A tibble with |
A list with bias (mean signed error, y - yhat), pct_bias
(bias as a percentage of mean |y|), and n. All NA/0 if there's
no overlap.
ds <- as.Date("2024-01-01") + 0:4 fc_tib <- tibble::tibble(ds = ds, yhat = rep(8, 5)) test_df <- tibble::tibble(ds = ds, y = rep(10, 5)) compute_forecast_bias(fc_tib, test_df)ds <- as.Date("2024-01-01") + 0:4 fc_tib <- tibble::tibble(ds = ds, yhat = rep(8, 5)) test_df <- tibble::tibble(ds = ds, y = rep(10, 5)) compute_forecast_bias(fc_tib, test_df)
Compute MASE, sMAPE, and RMSE for a forecast
compute_forecast_metrics(actual_df, forecast_df, label = "Test")compute_forecast_metrics(actual_df, forecast_df, label = "Test")
actual_df |
A tibble with |
forecast_df |
A tibble with |
label |
Character scalar identifying this evaluation (shown as the
|
A tibble with columns Set, Metric ("MASE", "sMAPE (%)",
"RMSE"), and Value – NA for all three if fewer than 2 rows
overlap.
ds <- as.Date("2024-01-01") + 0:9 actual_df <- tibble::tibble(ds = ds, y = 1:10) forecast_df <- tibble::tibble(ds = ds, yhat = 1:10 + 0.5) compute_forecast_metrics(actual_df, forecast_df, label = "Test")ds <- as.Date("2024-01-01") + 0:9 actual_df <- tibble::tibble(ds = ds, y = 1:10) forecast_df <- tibble::tibble(ds = ds, yhat = 1:10 + 0.5) compute_forecast_metrics(actual_df, forecast_df, label = "Test")
Real-world grouping columns are often messy: "female"/"Female"/
"FEMALE" in the same column, meant to be one group, not three. Builds a
raw-value -> canonical-label mapping (one row per distinct raw value)
that the Import tab's "Merge / relabel values" table starts from and the
user can further edit by hand.
compute_group_value_map(raw_values, merge_case = TRUE)compute_group_value_map(raw_values, merge_case = TRUE)
raw_values |
The full grouping column, with repeats – frequency matters, since the most common casing/spacing variant becomes each cluster's canonical label (ties broken by whichever appears first). |
merge_case |
Logical; if |
A tibble with columns raw (every distinct input value) and
label (its canonical label).
compute_group_value_map(c("female", "Female", "male"))compute_group_value_map(c("female", "Female", "male"))
Whether a model's uncertainty bounds are honest, not just its point
forecast: what fraction of actuals actually fell inside
yhat_lower/yhat_upper, versus the interval's nominal confidence
level (Prophet and the forecast-package adapters default to an 80%
interval).
compute_interval_coverage(fc_tib, test_df, nominal_level = 0.8)compute_interval_coverage(fc_tib, test_df, nominal_level = 0.8)
fc_tib |
A tibble with |
test_df |
A tibble with |
nominal_level |
Numeric, the interval's nominal confidence level.
Default |
A list with empirical_coverage, nominal_level, gap
(empirical_coverage - nominal_level), and n.
ds <- as.Date("2024-01-01") + 0:9 fc_tib <- tibble::tibble(ds = ds, yhat = 10, yhat_lower = 9, yhat_upper = 11) test_df <- tibble::tibble(ds = ds, y = c(rep(10, 8), 20, 20)) compute_interval_coverage(fc_tib, test_df)ds <- as.Date("2024-01-01") + 0:9 fc_tib <- tibble::tibble(ds = ds, yhat = 10, yhat_lower = 9, yhat_upper = 11) test_df <- tibble::tibble(ds = ds, y = c(rep(10, 8), 20, 20)) compute_interval_coverage(fc_tib, test_df)
A single aggregate score hides where a model actually fails – it can
look strong on the full training range while drifting badly over the
most recent months. Scores Train / Last 6 Months / Last 2 Years / Test
(held out) using safe_compute_metrics(), skipping windows the series
is too short to contain rather than reporting them as NA.
compute_multi_window_metrics(forecast_df, train_df, test_df, date_agg = "day")compute_multi_window_metrics(forecast_df, train_df, test_df, date_agg = "day")
forecast_df |
A tibble with |
train_df |
A tibble with |
test_df |
A tibble with |
date_agg |
Aggregation frequency (currently unused directly, kept for interface consistency with the rest of the package). |
A tibble with columns Set, Metric, Value – one block per
window that applied.
ds <- as.Date("2024-01-01") + 0:29 train_df <- tibble::tibble(ds = ds[1:20], y = 1:20) test_df <- tibble::tibble(ds = ds[21:30], y = 21:30) forecast_df <- tibble::tibble(ds = ds, yhat = 1:30 + 0.5) compute_multi_window_metrics(forecast_df, train_df, test_df)ds <- as.Date("2024-01-01") + 0:29 train_df <- tibble::tibble(ds = ds[1:20], y = 1:20) test_df <- tibble::tibble(ds = ds[21:30], y = 21:30) forecast_df <- tibble::tibble(ds = ds, yhat = 1:30 + 0.5) compute_multi_window_metrics(forecast_df, train_df, test_df)
Whether a fitted model's error looks like unpredictable noise, or still
has structure left in it: a Ljung-Box test for residual autocorrelation,
a Shapiro-Wilk normality test, and the residual ACF with its Box-Jenkins
confidence band. Generic across every registered model – only needs
fc_tib's ds/yhat and test_df's ds/y.
compute_residual_diagnostics(fc_tib, test_df)compute_residual_diagnostics(fc_tib, test_df)
fc_tib |
A tibble with |
test_df |
A tibble with |
A list with residuals (tibble of ds/resid), ljung_box_p,
shapiro_p (each NA if there isn't enough overlapping data),
acf_df (tibble of lag/acf), ci (the confidence bound), and n.
ds <- as.Date("2024-01-01") + 0:29 fc_tib <- tibble::tibble(ds = ds, yhat = 100) test_df <- tibble::tibble(ds = ds, y = 100 + rnorm(30)) compute_residual_diagnostics(fc_tib, test_df)ds <- as.Date("2024-01-01") + 0:29 fc_tib <- tibble::tibble(ds = ds, yhat = 100) test_df <- tibble::tibble(ds = ds, y = 100 + rnorm(30)) compute_residual_diagnostics(fc_tib, test_df)
Convert a forecast horizon in months to periods
convert_months_to_horizon(months, aggregation)convert_months_to_horizon(months, aggregation)
months |
Numeric, the horizon in months (as set by the app's "Forecast Horizon (months)" slider). |
aggregation |
One of |
A numeric horizon in the same units as aggregation (e.g. days,
for aggregation = "day"), suitable for a model's forecast(model, h)
call.
convert_months_to_horizon(12, "day") convert_months_to_horizon(12, "month")convert_months_to_horizon(12, "day") convert_months_to_horizon(12, "month")
Used to tell the user what collapse_to_period() would do, before doing
it. With group_col supplied, rows only count as duplicates if they
share both a period and a group – two different districts on the same
date are not a collision.
count_duplicate_periods(df, date_agg = "day", group_col = NULL)count_duplicate_periods(df, date_agg = "day", group_col = NULL)
df |
A data frame with a |
date_agg |
Aggregation frequency: one of |
group_col |
Optional character scalar, a grouping column. |
A single integer: how many rows would be removed by collapsing
(0 if none).
df <- data.frame(ds = as.Date(c("2024-01-01", "2024-01-02")), y = c(3, 5)) count_duplicate_periods(df, date_agg = "month")df <- data.frame(ds = as.Date(c("2024-01-01", "2024-01-02")), y = c(3, 5)) count_duplicate_periods(df, date_agg = "month")
Surfaces the STL trend/seasonal/remainder split that analyze_series()
already computes internally to drive the recommendation heuristic, as a
tidy tibble a caller can plot directly.
decompose_series(df, date_agg = "day", robust = FALSE)decompose_series(df, date_agg = "day", robust = FALSE)
df |
A tibble with |
date_agg |
Aggregation frequency: one of |
robust |
Logical; use robust (outlier-downweighted) STL –
|
A tibble with columns ds, observed, trend, seasonal,
remainder, or NULL (not an error) when no candidate frequency has
enough cycles of data, or date_agg has no seasonal candidates at all
(e.g. "year").
n <- 60 df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:(n - 1), y = 100 + 10 * sin(2 * pi * seq_len(n) / 7) + rnorm(n)) decompose_series(df, date_agg = "day")n <- 60 df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:(n - 1), y = 100 + 10 * sin(2 * pi * seq_len(n) / 7) + rnorm(n)) decompose_series(df, date_agg = "day")
A statistical check on the values themselves – distinct from the
Holidays tab's consistency check (calendar-vs-declared-holiday
mismatch). Uses the STL remainder when decompose_series() finds one
(isolates unusual points from normal trend/seasonal movement, which a
raw-value check can't); falls back to deviation from a running median
when no seasonal decomposition applies (short series, yearly
aggregation, etc.), so it still works on every series shape.
detect_anomalies( df, date_agg = "day", method = c("iqr", "zscore"), threshold = 1.5 )detect_anomalies( df, date_agg = "day", method = c("iqr", "zscore"), threshold = 1.5 )
df |
A tibble with |
date_agg |
Aggregation frequency: one of |
method |
|
threshold |
Numeric sensitivity threshold. Default |
A tibble with columns ds, y, score (the value the
threshold was applied to), and is_anomaly (logical).
df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:29, y = c(rep(10, 29), 100)) detect_anomalies(df, date_agg = "day", method = "iqr")df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:29, y = c(rep(10, 29), 100)) detect_anomalies(df, date_agg = "day", method = "iqr")
Splits the test window into n_splits sequential, roughly-equal chunks
(oldest first) and computes compute_forecast_bias() in each – a
simple, explainable drift signal (is |bias| growing chunk over chunk),
not a full statistical trend test.
detect_bias_drift(fc_tib, test_df, n_splits = 2)detect_bias_drift(fc_tib, test_df, n_splits = 2)
fc_tib |
A tibble with |
test_df |
A tibble with |
n_splits |
Integer, how many sequential chunks to split the window
into. Default |
A list with chunks (a tibble of chunk/bias/pct_bias/n,
one row per chunk) and drifting (TRUE if |bias| strictly
increases chunk over chunk, FALSE if not, NA if there isn't enough
data for a read).
ds <- as.Date("2024-01-01") + 0:9 fc_tib <- tibble::tibble(ds = ds, yhat = c(rep(10, 5), rep(6, 5))) test_df <- tibble::tibble(ds = ds, y = rep(10, 10)) detect_bias_drift(fc_tib, test_df, n_splits = 2)ds <- as.Date("2024-01-01") + 0:9 fc_tib <- tibble::tibble(ds = ds, yhat = c(rep(10, 5), rep(6, 5))) test_df <- tibble::tibble(ds = ds, y = rep(10, 10)) detect_bias_drift(fc_tib, test_df, n_splits = 2)
Averages (simple or inverse-error-weighted) several already-fit models'
forecasts – no re-fitting. Models that don't share every ds are
handled by renormalizing weights per-row over whichever models actually
have a value there.
ensemble_forecasts( forecasts, method = c("mean", "inverse_error"), errors = NULL )ensemble_forecasts( forecasts, method = c("mean", "inverse_error"), errors = NULL )
forecasts |
A named list of |
method |
|
errors |
Required for |
A tibble with columns ds and yhat – the combined forecast.
ds <- as.Date("2024-01-01") + 0:2 forecasts <- list( A = tibble::tibble(ds = ds, yhat = c(10, 10, 10)), B = tibble::tibble(ds = ds, yhat = c(20, 20, 20)) ) ensemble_forecasts(forecasts, method = "mean") ensemble_forecasts(forecasts, method = "inverse_error", errors = list(A = 1, B = 5))ds <- as.Date("2024-01-01") + 0:2 forecasts <- list( A = tibble::tibble(ds = ds, yhat = c(10, 10, 10)), B = tibble::tibble(ds = ds, yhat = c(20, 20, 20)) ) ensemble_forecasts(forecasts, method = "mean") ensemble_forecasts(forecasts, method = "inverse_error", errors = list(A = 1, B = 5))
The Holidays tab's checklist of recurring, fixed-date holidays. Matches the hosted Forecast Dashboard app's catalog exactly.
fixed_holiday_catalog()fixed_holiday_catalog()
A named character vector: names are "MM-DD" dates, values are
the holiday label.
fixed_holiday_catalog()fixed_holiday_catalog()
Look up a registered model
get_model(key)get_model(key)
key |
Character scalar, the registry key (e.g. |
The registered model's list (key, label, fit, forecast,
to_tibble, supports_holidays, requires, annotate).
model <- get_model("arima") model$labelmodel <- get_model("arima") model$label
Turns a link-shared Google Sheets URL (or a bare spreadsheet ID) into its CSV export endpoint, which works for any sheet shared as "Anyone with the link can view" – no OAuth, no credential storage.
googlesheet_csv_url(url, gid = NULL)googlesheet_csv_url(url, gid = NULL)
url |
The pasted Google Sheets link (several shapes accepted, e.g.
|
gid |
Optional worksheet gid. If |
A character scalar, the CSV export URL.
googlesheet_csv_url("https://docs.google.com/spreadsheets/d/abc123/edit#gid=456")googlesheet_csv_url("https://docs.google.com/spreadsheets/d/abc123/edit#gid=456")
Correlation between every pair of configured groups' series, aligned on
ds (pairwise-complete, so groups that don't share every period still
compare on their overlap).
group_correlation_matrix(grouped_series_list)group_correlation_matrix(grouped_series_list)
grouped_series_list |
A named list of |
A tibble with columns group_a, group_b, correlation –
upper triangle only, no self-pairs, one row per distinct pair. Empty
if fewer than 2 groups are supplied.
ds <- as.Date("2024-01-01") + 0:9 groups <- list( A = tibble::tibble(ds = ds, y = 1:10), B = tibble::tibble(ds = ds, y = 10:1) ) group_correlation_matrix(groups)ds <- as.Date("2024-01-01") + 0:9 groups <- list( A = tibble::tibble(ds = ds, y = 1:10), B = tibble::tibble(ds = ds, y = 10:1) ) group_correlation_matrix(groups)
Bundles holidays_with_data() and always_zero_dates() – the app's
full "consistency check."
holiday_contingency(holidays, data)holiday_contingency(holidays, data)
holidays |
A tibble with a |
data |
A tibble with |
A list with with_data (see holidays_with_data()) and
always_zero (see always_zero_dates()).
holidays <- tibble::tibble(ds = as.Date("2024-01-01"), holiday = "New Year") data <- tibble::tibble(ds = as.Date("2024-01-01"), y = 5) holiday_contingency(holidays, data)holidays <- tibble::tibble(ds = as.Date("2024-01-01"), holiday = "New Year") data <- tibble::tibble(ds = as.Date("2024-01-01"), y = 5) holiday_contingency(holidays, data)
Shown in the UI whenever the selected model's supports_holidays is
FALSE, mirroring the wording/style already used in the hosted app's
ARIMA tab.
holiday_limitation_note(model_label)holiday_limitation_note(model_label)
model_label |
Character scalar, the model's display label. |
A one-line character string.
holiday_limitation_note("ARIMA")holiday_limitation_note("ARIMA")
Days declared as holidays that nonetheless carry non-zero values – usually means the holiday wasn't actually observed at that site, or data entry continued anyway.
holidays_with_data(holidays, data)holidays_with_data(holidays, data)
holidays |
A tibble with a |
data |
A tibble with |
A tibble with columns ds, holiday, y, sorted by y
descending; empty if holidays/data are NULL/empty.
holidays <- tibble::tibble(ds = as.Date("2024-01-01"), holiday = "New Year") data <- tibble::tibble(ds = as.Date("2024-01-01"), y = 5) holidays_with_data(holidays, data)holidays <- tibble::tibble(ds = as.Date("2024-01-01"), holiday = "New Year") data <- tibble::tibble(ds = as.Date("2024-01-01"), y = 5) holidays_with_data(holidays, data)
Runs detect_anomalies(), replaces flagged points with NA, and
re-interpolates with the same fill idiom used throughout this package
(zoo::na.approx(), remaining edge NAs filled with the series mean) –
a cleaned series to fit on instead of the raw one, so a handful of
spikes/dips don't distort the fitted model the way they would
untouched. Distinct from decompose_series()'s robust argument, which
downweights outliers during decomposition rather than removing them
before fitting; the two can be used independently or together.
impute_anomalies( df, date_agg = "day", method = c("iqr", "zscore"), threshold = 1.5 )impute_anomalies( df, date_agg = "day", method = c("iqr", "zscore"), threshold = 1.5 )
df |
A tibble with |
date_agg |
Aggregation frequency: one of |
method |
|
threshold |
Numeric sensitivity threshold, passed through to
|
A tibble with columns ds, y (the cleaned series). The count
of points that were flagged and replaced is attached as
attr(result, "n_imputed").
df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:29, y = c(rep(10, 29), 100)) impute_anomalies(df, date_agg = "day", method = "iqr")df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:29, y = c(rep(10, 29), 100)) impute_anomalies(df, date_agg = "day", method = "iqr")
List every registered model
list_models(available_only = TRUE)list_models(available_only = TRUE)
available_only |
Logical; if |
A list of registered model entries (see get_model() for the
shape of each one).
vapply(list_models(available_only = TRUE), function(m) m$key, character(1))vapply(list_models(available_only = TRUE), function(m) m$key, character(1))
The "lstm" model requires the optional torch package. This checks
whether it's installed, without loading it.
lstm_available()lstm_available()
TRUE if torch is installed, FALSE otherwise.
lstm_available()lstm_available()
Model-agnostic plotly plotting: works off just ds/yhat(+intervals)
for models that only produce that, and additionally draws Prophet's
trend/holiday/changepoint decomposition when forecast_df still has
those columns (pass the adapter's raw forecast() output for that; pass
the standardized to_tibble() output to metrics functions instead).
plot_forecast_generic( forecast_df, train_df = NULL, model_obj = NULL, subtitle = NULL, title = "Forecast", show_trend = TRUE, show_uncertainty = TRUE, show_holidays = FALSE, show_changepoints = FALSE, max_marker_lines = 60, color_actual = "#1b9e77", color_forecast = "#d95f02", color_trend = "#7570b3", color_ci = "#1b9e77" )plot_forecast_generic( forecast_df, train_df = NULL, model_obj = NULL, subtitle = NULL, title = "Forecast", show_trend = TRUE, show_uncertainty = TRUE, show_holidays = FALSE, show_changepoints = FALSE, max_marker_lines = 60, color_actual = "#1b9e77", color_forecast = "#d95f02", color_trend = "#7570b3", color_ci = "#1b9e77" )
forecast_df |
A tibble with |
train_df |
Optional tibble with |
model_obj |
Optional fitted model object; only used to read
|
subtitle |
Optional character scalar, e.g. an ARIMA/SARIMA
|
title |
Character scalar, the plot title. Default |
show_trend, show_uncertainty, show_holidays, show_changepoints
|
Logical toggles for each optional layer. |
max_marker_lines |
Integer cap on how many holiday/changepoint marker lines are drawn (evenly subsampled beyond this), so a long horizon with recurring holidays doesn't render as unreadable clutter. |
color_actual, color_forecast, color_trend, color_ci
|
Hex color strings for each line/ribbon. |
A plotly htmlwidget.
ds <- as.Date("2024-01-01") + 0:9 forecast_df <- tibble::tibble(ds = ds, yhat = 1:10) plot_forecast_generic(forecast_df)ds <- as.Date("2024-01-01") + 0:9 forecast_df <- tibble::tibble(ds = ds, yhat = 1:10) plot_forecast_generic(forecast_df)
Used for the "Compare Selected Models" flow (and forecast ensembling, which adds one more named entry).
plot_model_comparison(forecasts, train_df = NULL, title = "Model Comparison")plot_model_comparison(forecasts, train_df = NULL, title = "Model Comparison")
forecasts |
A named list of |
train_df |
Optional tibble with |
title |
Character scalar, the plot title. Default
|
A plotly htmlwidget.
ds <- as.Date("2024-01-01") + 0:9 forecasts <- list( ARIMA = tibble::tibble(ds = ds, yhat = 1:10), ETS = tibble::tibble(ds = ds, yhat = 1:10 + 1) ) plot_model_comparison(forecasts)ds <- as.Date("2024-01-01") + 0:9 forecasts <- list( ARIMA = tibble::tibble(ds = ds, yhat = 1:10), ETS = tibble::tibble(ds = ds, yhat = 1:10 + 1) ) plot_model_comparison(forecasts)
The Import tab's core transform: renames the user's chosen date/value
columns to ds/y, aggregates individual-observation data into periods,
and optionally normalizes by a population table to produce incidence
rather than absolute counts.
process_uploaded_data( data, type = c("agg", "individual"), date_col, value_col = NULL, group_col = NULL, pop_df = NULL, pop_date_col = NULL, pop_value_col = NULL, pop_group_col = NULL, unit_divisor = 1, pop_multiplier = 1, date_agg = "day", pop_freq = "year" )process_uploaded_data( data, type = c("agg", "individual"), date_col, value_col = NULL, group_col = NULL, pop_df = NULL, pop_date_col = NULL, pop_value_col = NULL, pop_group_col = NULL, unit_divisor = 1, pop_multiplier = 1, date_agg = "day", pop_freq = "year" )
data |
A data frame – the uploaded main dataset. |
type |
|
date_col |
Character scalar, the name of |
value_col |
Character scalar, the name of |
group_col |
Optional character scalar, a grouping column (e.g.
District) to carry through untouched alongside |
pop_df |
Optional population data frame for normalization. |
pop_date_col, pop_value_col
|
Character scalars naming |
pop_group_col |
Optional character scalar, |
unit_divisor, pop_multiplier
|
Numeric scaling applied to the
population figure (e.g. |
date_agg |
Aggregation frequency: one of |
pop_freq |
Whether |
A tibble with columns ds, y, and group_col if supplied.
df <- data.frame(when = as.Date("2024-01-01") + 0:9, count = 1:10) process_uploaded_data(df, type = "agg", date_col = "when", value_col = "count")df <- data.frame(when = as.Date("2024-01-01") + 0:9, count = 1:10) process_uploaded_data(df, type = "agg", date_col = "when", value_col = "count")
The "prophet" model requires the optional prophet package. This
checks whether it's installed, without loading it.
prophet_available()prophet_available()
TRUE if prophet is installed, FALSE otherwise.
prophet_available()prophet_available()
Requires the sheet be shared as "Anyone with the link can view." Makes a
real network request via googlesheet_csv_url() – not runnable on
CRAN's offline check machines.
read_google_sheet(url, gid = NULL)read_google_sheet(url, gid = NULL)
url |
The pasted Google Sheets link, or a bare spreadsheet ID. |
gid |
Optional worksheet gid (see |
A data frame, as read by utils::read.csv().
## Not run: read_google_sheet("https://docs.google.com/spreadsheets/d/abc123/edit") ## End(Not run)## Not run: read_google_sheet("https://docs.google.com/spreadsheets/d/abc123/edit") ## End(Not run)
Scores every candidate model against analyze_series()'s measured
trend/seasonality/regularity, with short human-readable reasons – no
model is actually fit, so this is fast enough to run right after a
dataset is finalized.
recommend_model(analysis, holidays_configured = FALSE, candidates = NULL)recommend_model(analysis, holidays_configured = FALSE, candidates = NULL)
analysis |
The list returned by |
holidays_configured |
Logical; whether holidays are configured for
this dataset. Every model except Prophet is scored down when |
candidates |
Character vector of registry keys to score (see
|
A data frame with columns model (label), key, score, and
reason, sorted by score descending.
df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:59, y = 1:60 + rnorm(60)) recommend_model(analyze_series(df, "day"))df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:59, y = 1:60 + rnorm(60)) recommend_model(analyze_series(df, "day"))
Sums already-fit per-group forecasts (and their actuals) instead of fitting the aggregate separately – the sum of individually-coherent group forecasts is itself always coherent by construction. Point forecasts sum exactly; prediction intervals are summed too, under the standard (if approximate) bottom-up assumption that group forecast errors are perfectly correlated – flagged as a known simplification, not a bug.
reconcile_bottom_up(fits)reconcile_bottom_up(fits)
fits |
A named list of fit-result lists (one per group), each with
|
A list with fc_tib, train, test (each summed by ds across
groups, or NULL if no group supplied that field), and components
(the names of the groups that were actually combined).
ds <- as.Date("2024-01-01") + 0:2 fits <- list( A = list(fc_tib = tibble::tibble(ds = ds, yhat = c(10, 11, 12)), train = tibble::tibble(ds = ds, y = c(9, 10, 11))), B = list(fc_tib = tibble::tibble(ds = ds, yhat = c(1, 2, 3)), train = tibble::tibble(ds = ds, y = c(1, 1, 1))) ) reconcile_bottom_up(fits)ds <- as.Date("2024-01-01") + 0:2 fits <- list( A = list(fc_tib = tibble::tibble(ds = ds, yhat = c(10, 11, 12)), train = tibble::tibble(ds = ds, y = c(9, 10, 11))), B = list(fc_tib = tibble::tibble(ds = ds, yhat = c(1, 2, 3)), train = tibble::tibble(ds = ds, y = c(1, 1, 1))) ) reconcile_bottom_up(fits)
Disaggregates one already-fit aggregate-level forecast down to each
group by that group's fixed historical share of the total, instead of
fitting (or summing) each group's own forecast shape the way
reconcile_bottom_up() does. Coherent by construction (the shares sum
to 1), and a meaningfully different result from bottom-up whenever the
aggregate-level model captures dynamics the per-group models miss (or
vice versa) – not just the same total recomputed a different way.
reconcile_top_down(fits, aggregate_forecast)reconcile_top_down(fits, aggregate_forecast)
fits |
A named list of fit-result lists (one per group), each with
|
aggregate_forecast |
A tibble with |
A list with fc_tib (aggregate_forecast's own columns,
unchanged – the top-down total is the aggregate-level forecast),
train/test (NULL; the caller already has the aggregate-level
train/test that produced aggregate_forecast and should use those
directly for display), components (the names of the groups whose
historical shares were used), and proportions (a named list, one
fixed share per group, summing to 1).
ds <- as.Date("2024-01-01") + 0:2 fits <- list( A = list(train = tibble::tibble(ds = ds, y = c(9, 10, 11))), B = list(train = tibble::tibble(ds = ds, y = c(1, 1, 1))) ) aggregate_forecast <- tibble::tibble(ds = ds, yhat = c(10, 11, 12)) reconcile_top_down(fits, aggregate_forecast)ds <- as.Date("2024-01-01") + 0:2 fits <- list( A = list(train = tibble::tibble(ds = ds, y = c(9, 10, 11))), B = list(train = tibble::tibble(ds = ds, y = c(1, 1, 1))) ) aggregate_forecast <- tibble::tibble(ds = ds, yhat = c(10, 11, 12)) reconcile_top_down(fits, aggregate_forecast)
Adds a model to the package's pluggable registry, keyed by key. Every
model implements the same three-function contract (fit/forecast/
to_tibble), so once registered it works everywhere in the bundled app
(Model tab, Compare Selected Models, Cross-Validation, the Backtest
Leaderboard, the Show Code panel) with no other code changes.
register_model( key, label, fit, forecast, to_tibble, supports_holidays = FALSE, requires = NULL, annotate = NULL )register_model( key, label, fit, forecast, to_tibble, supports_holidays = FALSE, requires = NULL, annotate = NULL )
key |
Character scalar, a unique registry key (e.g. |
label |
Character scalar, the human-readable name shown in the UI. |
fit |
Function with signature |
forecast |
Function with signature |
to_tibble |
Function with signature
|
supports_holidays |
Logical; whether |
requires |
Character scalar naming an optional package this model
needs (e.g. |
annotate |
Optional function with signature |
TRUE, invisibly.
register_model( key = "naive_last", label = "Naive (last value)", fit = function(train_df, date_agg = "day", ...) utils::tail(train_df$y, 1), forecast = function(model_obj, h, ...) list(mean = rep(model_obj, h)), to_tibble = function(fc_obj, test_df) { tibble::tibble(ds = test_df$ds[seq_along(fc_obj$mean)], yhat = fc_obj$mean) } )register_model( key = "naive_last", label = "Naive (last value)", fit = function(train_df, date_agg = "day", ...) utils::tail(train_df$y, 1), forecast = function(model_obj, h, ...) list(mean = rep(model_obj, h)), to_tibble = function(fc_obj, test_df) { tibble::tibble(ds = test_df$ds[seq_along(fc_obj$mean)], yhat = fc_obj$mean) } )
Unlike the hosted Forecast Dashboard app, this exposes every registered,
available model (Prophet, ARIMA, SARIMA, ETS, TBATS, NNETAR, Holt-Winters,
and LSTM if torch is installed) with Plotly always on and no
memory-conserving toggles, since it targets local/offline use rather than
a free hosting tier.
run_app(...)run_app(...)
... |
Passed through to |
The Data Import tab's "Global environment" source only reads data frames;
it never writes. By default it lists them from the global environment.
Set options(forecastsuite.data_env = <environment>) to point it at a
different environment instead.
A shiny.appobj, as returned by shiny::shinyApp(). Printing it
(e.g. calling run_app() at the console, or letting it auto-print)
launches the app; it does not block on its own.
if (interactive()) { run_app() }if (interactive()) { run_app() }
Runs build_cv_folds() once and refits every model in model_keys
across every fold, ranking them by mean test MASE – a real multi-model
extension of single-model cross-validation. A fold a model fails on is
dropped for that model rather than failing the whole row; a model with
zero successful folds (including an invalid model_keys entry) gets an
NA row instead of being dropped, so it's still visible in the result as
"failed."
run_backtest_leaderboard( model_keys, df, date_agg, horizon_periods, k_requested, build_args = function(model_key, train_df) { list(train_df = train_df, date_agg = date_agg) }, window = c("expanding", "rolling"), train_window = NULL )run_backtest_leaderboard( model_keys, df, date_agg, horizon_periods, k_requested, build_args = function(model_key, train_df) { list(train_df = train_df, date_agg = date_agg) }, window = c("expanding", "rolling"), train_window = NULL )
model_keys |
Character vector of registry keys (see
|
df |
A data frame with |
date_agg |
Aggregation frequency (e.g. |
horizon_periods |
Integer, each fold's test window length (see
|
k_requested |
Integer, the number of folds requested (may return
fewer; see |
build_args |
A function |
window |
|
train_window |
Integer or |
A tibble with one row per model in model_keys, columns model
(label), key, n_folds, mean_mase, sd_mase, mean_smape,
mean_rmse, ranked ascending by mean_mase (NA last).
df <- data.frame(ds = as.Date("2024-01-01") + 0:59, y = 100 + seq_len(60) * 0.2 + stats::rnorm(60, sd = 2)) run_backtest_leaderboard(c("arima", "ets"), df, date_agg = "day", horizon_periods = 5, k_requested = 2)df <- data.frame(ds = as.Date("2024-01-01") + 0:59, y = 100 + seq_len(60) * 0.2 + stats::rnorm(60, sd = 2)) run_backtest_leaderboard(c("arima", "ets"), df, date_agg = "day", horizon_periods = 5, k_requested = 2)
Same as compute_forecast_metrics(), but checks actual_df's row count
first so callers don't need their own length guard before calling it.
safe_compute_metrics(forecast_df, actual_df, label)safe_compute_metrics(forecast_df, actual_df, label)
forecast_df |
A tibble with |
actual_df |
A tibble with |
label |
Character scalar identifying this evaluation. |
A tibble with columns Set, Metric, Value (see
compute_forecast_metrics()).
ds <- as.Date("2024-01-01") + 0:9 forecast_df <- tibble::tibble(ds = ds, yhat = 1:10 + 0.5) actual_df <- tibble::tibble(ds = ds, y = 1:10) safe_compute_metrics(forecast_df, actual_df, label = "Test")ds <- as.Date("2024-01-01") + 0:9 forecast_df <- tibble::tibble(ds = ds, yhat = 1:10 + 0.5) actual_df <- tibble::tibble(ds = ds, y = 1:10) safe_compute_metrics(forecast_df, actual_df, label = "Test")
Split a ds/y/group tibble into one tibble per group
split_by_group(df, group_col)split_by_group(df, group_col)
df |
A data frame with |
group_col |
Character scalar, the grouping column's name, or |
A named list of ds/y tibbles, one per distinct value of
group_col (names sorted), or NULL if group_col is NULL or not
present in df.
df <- data.frame(ds = as.Date("2024-01-01") + 0:3, y = 1:4, District = c("A", "A", "B", "B")) split_by_group(df, "District")df <- data.frame(ds = as.Date("2024-01-01") + 0:3, y = 1:4, District = c("A", "A", "B", "B")) split_by_group(df, "District")
Derives starting-point hyperparameters from analyze_series()'s
diagnostics rather than fitting a grid of candidates. These are informed
starting points, not optimal values – for ARIMA/SARIMA, auto.arima()
still searches the order space better than this heuristic; the
suggested p/q matter mainly for hand-tuning, while d/D come
straight from ndiffs()/nsdiffs().
suggest_parameters(analysis, model_key)suggest_parameters(analysis, model_key)
analysis |
The list returned by |
model_key |
A registry key, e.g. |
A list with params (a named list of suggested argument
values, matching that model's fit() parameters), text (a one-line
human-readable summary), and reasons (a character vector explaining
each suggestion).
df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:59, y = 1:60 + rnorm(60)) suggest_parameters(analyze_series(df, "day"), "arima")df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:59, y = 1:60 + rnorm(60)) suggest_parameters(analyze_series(df, "day"), "arima")
Vectorized suggest_parameters(), for the recommendation table's
"suggested settings" column.
suggest_parameters_for(analysis, model_keys)suggest_parameters_for(analysis, model_keys)
analysis |
The list returned by |
model_keys |
Character vector of registry keys. |
A character vector, the same length as model_keys, of each
model's suggestion text.
df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:59, y = 1:60 + rnorm(60)) suggest_parameters_for(analyze_series(df, "day"), c("arima", "prophet"))df <- tibble::tibble(ds = as.Date("2024-01-01") + 0:59, y = 1:60 + rnorm(60)) suggest_parameters_for(analyze_series(df, "day"), c("arima", "prophet"))
Map an aggregation frequency to a ts() seasonal frequency
ts_frequency_for(date_agg)ts_frequency_for(date_agg)
date_agg |
One of |
A single integer, suitable for stats::ts(y, frequency = ...).
ts_frequency_for("month")ts_frequency_for("month")