mlr3forecast

Extending mlr3 to time series forecasting


Maximilian Mücke · Marc Becker · Bernd Bischl

useR! 2026 · Warsaw · 2026-07-07

Why mlr3forecast?

R has two strong, separate worlds for forecasting:

Classical ML
Tools forecast, fable, smooth mlr3, …
Models ARIMA, ETS, Theta boosting, forests

Crossing the divide means glue code: hand-rolled temporal CV, lag features, no shared interface.

mlr3forecast bridges the two — bringing time series forecasting to the mlr3 ecosystem, built on:

  • mlr3: a unified machine learning framework in R (successor to mlr)
  • mlr3pipelines: a dataflow programming toolkit
  • forecast, smooth, …: established forecasting packages

A forecaster becomes just another mlr3 Learner — ready to tune, pipeline, benchmark and ensemble.

The mlr3 ecosystem

mlr3forecast adds forecasting tasks, learners, resamplings, and measures to the mlr3 ecosystem.

The mlr3 ecosystem: a central mlr3 core surrounded by groups of extension packages — Data, Learners, Feature Selection, Tuning & Optimization, Pipelines, Analysis, and Special Tasks & Targets. mlr3forecast sits in Special Tasks & Targets, marked as maturing.

mlr3 in five verbs — now for forecasting

The same mlr3 workflow you already know:

  • tsk(): a forecasting task (target + time order)
  • lrn(): a forecaster (here, auto-ARIMA)
  • rsmp(): a temporal resampling
  • resample(): run the experiment
  • msr(): score it

Everything downstream is unchanged.

task = tsk("airpassengers")
learner = lrn("fcst.auto_arima")
resampling = rsmp("fcst.cv", folds = 3, horizon = 12)

rr = resample(
  task = task,
  learner = learner,
  resampling = resampling
)

rr$aggregate(msr("fcst.mase"))
#> fcst.mase 
#> 0.5807121

The forecasting task

A TaskFcst is a regression task plus a temporal order column (and an optional key for many series).

  • tsk() ships ready-made tasks
  • as_task_fcst() wraps your own data.frame
  • autoplot() for a quick look

Built-in: airpassengers, electricity, lynx, …

task = tsk("airpassengers")
task
#> 
#> ── <TaskFcst> (144x1): Monthly Airline Passenger Numbers 1949-1960 ─────────────
#> • Target: passengers
#> • Properties: ordered
#> • Order by: month
#> • Frequency: month
autoplot(task)

Monthly airline passenger counts from 1949 to 1960, showing a rising trend with growing yearly seasonal swings.

mlr3forecast in a nutshell

Learners

  • Classical: ARIMA, ETS, Theta, TBATS, prophet, … (30+)
  • ML: any regr learner → forecaster
    • recursive (one model, iterated)
    • direct (one model per horizon)

Pipelines (mlr3pipelines)

  • lag / rolling / Fourier / calendar PipeOps
  • target transforms: diff, log, Box-Cox

Tasks & resamplings

  • TaskFcst, global (many-series) forecasting
  • fcst.holdout, fcst.cv (respect time)

Measures

  • MASE, RMSSE, Pinball, Winkler, coverage, MSIS

Full mlr3 integration: tuning, benchmarking, ensembling, quantile forecasts

30+ classical forecasters

Wrapping forecast, smooth, prophet, tscount — all fcst.*:

meanarimaetsthetatbatsnnetarprophettscount+…

Quantile predict type → prediction intervals, scored with probabilistic measures (Pinball, Winkler).

learner = lrn("fcst.auto_arima",
  predict_type = "quantiles",
  quantiles = c(0.1, 0.5, 0.9),
  quantile_response = 0.5
)
learner$train(tsk("airpassengers"))
forecast(learner, tsk("airpassengers"), 24)
#> 
#> ── <PredictionRegr> for 24 observations: ───────────────────────────────────────
#>  row_ids truth     q0.1     q0.5     q0.9 response      month
#>        1    NA 430.8905 445.6351 460.3796 445.6351 1961-01-01
#>        2    NA 403.0907 420.3953 437.6999 420.3953 1961-02-01
#>        3    NA 429.7726 449.1988 468.6249 449.1988 1961-03-01
#>      ---   ---      ---      ---      ---      ---        ---
#>       22    NA 488.8134 528.4261 568.0389 528.4261 1962-10-01
#>       23    NA 417.7295 457.6609 497.5924 457.6609 1962-11-01
#>       24    NA 459.6531 499.8602 540.0673 499.8602 1962-12-01

Any regression learner becomes a forecaster

recursive_forecaster() adds lag features and forecasts step by step.

  • recursive: one model, fed its own predictions
  • direct: direct_forecaster(), one model per horizon, no error accumulation

Any mlr3 regr learner works: ranger, xgboost, glmnet, …

flrn = recursive_forecaster(lrn("regr.ranger"), lags = 1:12)
flrn$train(tsk("airpassengers"))
forecast(flrn, tsk("airpassengers"), 24)
#> 
#> ── <PredictionRegr> for 24 observations: ───────────────────────────────────────
#>  row_ids truth response      month
#>        1    NA 444.7887 1961-01-01
#>        2    NA 446.6875 1961-02-01
#>        3    NA 463.6715 1961-03-01
#>      ---   ---      ---        ---
#>       22    NA 526.8590 1962-10-01
#>       23    NA 512.9035 1962-11-01
#>       24    NA 506.3307 1962-12-01

Dataflow programming with mlr3pipelines

A pipeline diagram: data flows into an encode PipeOp, then a pca PipeOp (preprocessing), then a log_reg learner — the graph produces processed data and a fitted model.

Sequential Preprocessing

Assemble new Learners by connecting PipeOps into a Graph1 — preprocessing, feature engineering and the model in one object.

  • PipeOps are created with po()
  • chained with the %>>% operator
  • the whole Graph behaves like any other learner
library(mlr3pipelines)
library(mlr3learners)

graph =
  po("encode", method = "one-hot") %>>%
  po("pca") %>>%
  lrn("classif.log_reg")

learner = as_learner(graph)

Feature engineering with mlr3pipelines

Vertical dataflow diagram in the mlr3pipelines style: a TaskFcst feeds into po fcst.lags, then po datefeatures, then a ranger learner, producing a forecast.

The same idea, applied to forecasting — the Graph is the forecaster:

task = tsk("airpassengers")
task$set_col_roles("month", add = "feature")

graph =
  po("fcst.lags", lags = 1:12) %>>% # lag features
  po("datefeatures") %>>% # calendar features
  lrn("regr.ranger")

flrn = recursive_forecaster(graph)

Forecasting PipeOps:

  • lags & windows: fcst.lags, fcst.rolling, fcst.fourier
  • feature sets: fcst.feasts, fcst.tsfeats
  • target transforms: fcst.targetdiff, fcst.targetboxcox

Resampling that respects time

  • fcst.holdout: a single temporal split
  • fcst.cv: rolling-origin / expanding window

Then the standard resample() and benchmark() machinery just works.

resampling = rsmp("fcst.cv",
  folds = 4, horizon = 12, step_size = 12
)
resampling$instantiate(tsk("airpassengers"))

Horizontal bar chart with one row per fold: blue training windows that all start in 1949 and grow one year longer each fold, each followed immediately by an orange twelve-month test window.

Benchmark classical and ML together

Classical and ML forecasters, compared in one benchmark() call across rolling-origin folds.

learners = list(
  lrn("fcst.auto_arima", id = "auto_arima"),
  lrn("fcst.ets", id = "ets"),
  lrn("fcst.theta", id = "theta"),
  recursive_forecaster(lrn("regr.ranger"), lags = 1:12, id = "ranger")
)
bmr = benchmark(benchmark_grid(
  tasks = tsk("airpassengers"),
  learners = learners,
  resamplings = rsmp("fcst.cv", folds = 10, horizon = 6)
))

autoplot(bmr, measure = msr("regr.rmse")) +
  labs(title = NULL, x = NULL) +
  theme_minimal(base_size = 18)

Boxplots of RMSE across rolling-origin folds for four forecasters on the airline data: auto_arima lowest and tightest, ets and theta in the middle, recursive ranger highest with the widest spread.

Load the Monash forecasting archive

The Monash repository (M1–M4, tourism, energy, …) loads directly:

  • read_tsf(): parse any .tsf file, frequency & horizon included
  • download_zenodo_record(): fetch it straight from Zenodo
  • as_task_fcst(): auto-detects target, time and series key
dt = download_zenodo_record(
  record_id = 4656222,
  dataset_name = "m3_yearly_dataset"
)

# one task per series, or one global task
tasks = as_tasks_fcst(split(dt, by = "series_name", keep.by = FALSE))

learners = lrns(c("fcst.auto_arima", "fcst.ets", "fcst.theta"))
design = benchmark_grid(
  tasks, learners, rsmp("fcst.holdout", ratio = 0.8)
)
bmr = benchmark(design)
bmr$aggregate(msr("fcst.mase"))

Compose the whole mlr3 stack

Forecasters are ordinary learners, so the rest of the ecosystem plugs straight in:

Tuning: temporal CV inside auto_tuner()

flrn$param_set$set_values(
  regr.ranger.mtry.ratio = to_tune(0.1, 1)
)
at = auto_tuner(
  tuner = tnr("random_search"),
  learner = flrn,
  resampling = rsmp("fcst.cv", folds = 3, horizon = 12),
  measure = msr("regr.rmse"),
  term_evals = 20
)

Ensembling: average several forecasters

graph = gunion(list(
  po("learner", lrn("fcst.auto_arima")),
  po("learner", lrn("fcst.ets")),
  po("learner", lrn("fcst.theta"))
)) %>>%
  po("regravg")

flrn = as_learner(graph)

Out of the box:

  • target transforms: ppl("targettrafo"), fcst.targetdiff, fcst.targetboxcox
  • probabilistic measures: MASE, RMSSE, Pinball, Winkler, MSIS
  • quantile forecasts

Global forecasting: one model, many series

A key column turns the task into many series.
A single learner is trained jointly across all of them.

  • learns cross-series patterns
  • one model instead of hundreds
  • compare global vs local with the same API
task = as_task_fcst(
  retail[, .(month, industry, turnover)],
  target = "turnover",
  order = "month",
  key = "industry",
  freq = "month"
)
flrn = recursive_forecaster(lrn("regr.ranger"), lags = 1:12)
flrn$train(task) # one model, fitted jointly on all four series
autoplot(task, facets = TRUE)

Four small line charts, one per Australian retail industry, each showing monthly turnover trending upward over time.

Learn more

Forecasting as a first-class mlr3 task.

tune · pipeline · benchmark · ensemble — classical and ML forecasters, one interface.

pak::pak("mlr-org/mlr3forecast")

Experimental & fast-moving — feedback and feature requests welcome!

QR code linking to the mlr3forecast website at mlr3forecast.mlr-org.com

Scan for the docs