[ANN] Durbyn.jl — Time Series Forecasting in Julia

In general, on the surface, it is to some extent quite similar in Julia as well, though not as pronounced as in other ecosystems. Depending on how you look at it, one could even argue that there is a lot of C++ in Julia because of LLVM. However, I don’t think that claim is fully justified, since, at least as far as I know, thanks to this high-level assembly, much can be expressed directly in Julia’s syntax. Anyway, for example, the upcoming comp neuro & machine learning framework built on Enzyme.jl, Reactant.jl, and Lux.jl relies at its lower levels on LLVM IR and C++, at the middle level mainly on MLIR, and at the higher level primarily on Julia.

To answer your question: honestly, I think it may be more rewarding to focus on innovation than on supporting other ecosystems, especially given that Julia, like Python, is a general purpose language. That said, although I have recently become again more optimistic about Julia (with developments such as PackageCompiler.jl, JuliaC.jl, BorrowChecker.jl, Enzyme.jl, Reactant.jl, Lux.jl, and ongoing research on the GC), I believe there remains a significant risk, mainly due to the speed at which other ecosystems are advancing.

Personally, I am committed to Julia while also experimenting with q/kdb-x. I believe Julia is particularly well suited for time series analysis. By the way, could you share more information about the Time Series Analysis & Forecasting Society and its activities? And would you consider presenting Durbyn.jl at one of the JuliaDynamics monthly meetings? Thanks again for the package!

@j_u From my perspective, the new compiler packages make Julia a strong candidate to be used as a backend for other languages. At the same time, I feel the bigger risk for Julia doesn’t come so much from R or Python, but rather from Rust, which is advancing quickly in performance-sensitive areas.

In my view, the Julia community could also benefit from engaging more with industry practitioners – people like me who work with large-scale forecasting problems – so that tools stay practical and relevant in production.

On a personal note, I’d be very happy to present in Julia community forums. Since I’ve only recently become involved in open source (most of my development work until now was for my employer), I haven’t had much interaction with the community before. If there’s an opportunity to contribute and share, I’d really welcome it.

7 Likes

There’s a related discussion happening on LinkedIn: LinkedIn Post.

Nice to see this effort.
Re custom tables, I worked on some large scale forecasting problems where we also needed custom table implementations for performance, but we also kept all core functions generic by adopting the Tables.jl interface, and then just added the custom table also using that same interface. We could have supported any number of table types without touching any of the model code.

4 Likes

Well, yeah, I agree with you, it does look like not only performance-sensitive areas but almost the entire internet is being rewritten in Rust. In general, this kind of strategic analysis is closer to my background than programming itself. From my experience, it usually takes a lot of time and effort to identify all the relevant factors, opportunities, and strengths. It’s a fascinating topic, especially since it feels like the programming industry might currently be undergoing some structural changes.

I have to admit I haven’t done any deep thinking on this subject. I sustain, that my general impression is that sometimes other ecosystems seem to be advancing a bit faster than Julia’s. That being said, there’s also the question of quality. What I wanted to convey is that the packages and scientific work I mentioned earlier, as well as many other efforts I probably don’t know about, could put Julia in a much stronger position than it might seem at first glance, hence, our friends working with Rust might end up just spinning their wheels … :- )

I think we actually are engaging. I guess, it’s hard to argue otherwise. Every post gets answered, there are already forty three messages in this topic, over 1.3k views, and high-quality advice on package performance and user interface.

I think the best approach would be to contact the organizer of the JuliaDynamics monthly meetings directly with a message. From what I understand, your package is very much related to the kinds of topics usually discussed there. I am also aware that there are other opportunities, like during JuliaConn which is a conference on the Julia programming language, however, I don’t know any details.

I’m sorry, I wasn’t able to check that discussion. I have “LIn” heavily restricted on my computer, and I also have a long standing habit of not posting under my real name online. That said, I did send you an invite there. I’m always happy to make connections.

2 Likes

That’s exactly the plan — the goal is to build an internal data infrastructure, tentatively called PanelData, that’s lightweight and efficient while still supporting all widely used Julia tabular formats at the user level.

By keeping the internals minimal but exposing a Tables.jl-compatible interface, we can ensure Durbyn remains performant and scalable while integrating smoothly with the broader Julia data ecosystem – all without introducing unnecessary dependencies.

Julia provides excellent capabilities for writing hardware-agnostic code, allowing the same algorithm to run seamlessly on CPUs or GPUs. For example:

using LinearAlgebra
using GPUArrays
using Adapt
using CUDA
using KernelAbstractions

@kernel function phi_kernel(y, phi, n, max_iter)
    i = @index(Global)
    if i <= max_iter
        acc_dot = zero(eltype(y))
        acc_norm = zero(eltype(y))
        for j = 1:(n - i)
            acc_dot += y[i + j] * y[j]
            acc_norm += y[j]^2
        end
        phi[i] = acc_dot / acc_norm
    end
end

function compute_phi(y::Union{AbstractGPUArray{T}, AbstractArray{T}}, max_iter::Int=15) where {T<:AbstractFloat}
    n = length(y)
    phi = similar(y, max_iter)
    backend = get_backend(y)

    kernel_inst = phi_kernel(backend)
    kernel_inst(y, phi, n, max_iter; ndrange=(max_iter,))

    return phi
end

Unfortunately, some standard libraries don’t yet fully support GPU arrays. Otherwise, the same computation could be expressed as simply as:

# Original equation from ARAR model 
function compute_phi(;y::AbstractArray, max_iter::Int=15)
    phi = [dot(y[i+1:end], y[1:end-i]) / sum(y[1:end-i].^2) for i in 1:max_iter]
    return phi
end

If we make better use of these features, Julia has everything needed to become a truly powerful, modern, and cross-hardware language.
And just to be clear, I don’t mean these values don’t already exist, but with even more constructive feedback, community collaboration, and structured code review (similar to the CRAN process in R), Julia’s ecosystem could reach an exceptional level of reliability and performance.

4 Likes

I’m afraid I don’t have much to add. I’ve used KernelAbstractions.jl a few times, but I don’t have much experience with it. More often, I’ve used AcceleratedKernels.jl, which, as far as I understand, is a higher level abstraction built on top of KernelAbstractions.jl. As for support for various hardware architectures, I agree with you. Some of the packages I mentioned earlier can even run on TPUs. I’m not sure if it’s technically possible to program FPGAs using Julia, but that would be really cool. Regarding GPU array support, I’ve often thought it would be great if package maintainers always included a section about multithreading or parallel processing on the front page of their GitHub repositories.This would probably make the package ecosystem more coherent and, overall, be beneficial for the community.

An initial benchmark for R forecast vs R smooth vs Julia Durbyn:

> packageVersion("forecast")
[1] ‘8.24.0’
> library(forecast)
> system.time({fit <- ets(AirPassengers, model = "ZZZ")})
   user  system elapsed 
   0.31    0.00    0.31 
> fc = forecast(fit, h = 12)
> autoplot(fc)
> packageVersion("smooth")
[1] ‘4.3.0’
> library(smooth)
> system.time({fit <- es(AirPassengers, model = "ZZZ")})
   user  system elapsed 
  0.198   0.000   0.198 
> fc = forecast(fit, h = 12)
> plot(fc)

using Durbyn
using Durbyn.ExponentialSmoothing

ap = air_passengers();
fit_ets = ets(ap, 12, "ZZZ")
@time fit_ets = ets(ap, 12, "ZZZ");
# 0.011112 seconds
fc_ets = forecast(fit_ets, h = 12)
plot(fc_ets)
2 Likes

30x faster, it looks like. Very nice! :racing_car:

4 Likes

New features to come next week

Formula interface with Tables.jl support.

I started with Arima.
Thank you for your suggestions, now Table.jl supported.
I introduce some R dplyr like functions to help R users to be able to us the Durbyn.jl

Durbyn ARIMA Grammar

The Durbyn forecasting grammar lets you describe ARIMA and SARIMA models with a concise,
readable syntax. This guide covers each grammar component and shows how to combine them
when fitting models via ArimaSpec and @formula.


1. Formula Basics

Use the @formula macro to define the relationship between a target series and its
ARIMA structure:

@formula(sales = p() + d() + q())

Every formula must provide a target (left-hand side) and one or more terms on the
right-hand side. Terms may specify ARIMA orders, seasonal orders, exogenous variables,
or automatic-exogenous selection (see below).


2. Non-Seasonal Orders

Function Meaning Default or form
p() Non-seasonal AR order Search range 2–5
p(k) Fix AR order Uses k exactly
p(min,max) Search AR order range Searches min through max
d() Differencing order (auto) auto_arima chooses
d(k) Fix differencing order Uses k exactly
q() Non-seasonal MA order Search range 2–5
q(k) Fix MA order Uses k exactly
q(min,max) Search MA order range Searches min through max

Any range (min,max) triggers full auto_arima search. If all orders are fixed,
the formula interface automatically calls the faster arima routine.


3. Seasonal Orders

Seasonal counterparts include P, D, and Q:

@formula(sales = p() + d() + q() + P() + Q())
Function Meaning Default or form
P() Seasonal AR order Search range 1–2
P(k) Fix seasonal AR order Uses k exactly
P(min,max) Search seasonal AR order range Searches min through max
D() Seasonal differencing (auto) auto_arima chooses
D(k) Fix seasonal differencing order Uses k exactly
Q() Seasonal MA order Search range 1–2
Q(k) Fix seasonal MA order Uses k exactly
Q(min,max) Search seasonal MA order range Searches min through max

Remember to provide the seasonal period m when fitting (fit(spec, data, m=12)).


4. Exogenous Regressors

4.1 Explicit Variables

Add predictors by listing column names:

@formula(sales = p() + q() + price + promotion)

These become VarTerms—during fitting Durbyn pulls the matching columns from your data.

4.2 Automatic Selection (auto())

Use auto() to include all numeric columns except the target, group columns, and
optional date column. Examples:

@formula(sales = auto())                    # pure auto ARIMA + automatic xregs
@formula(sales = p() + q() + auto())        # combine with explicit ARIMA orders

Automatic selection is mutually exclusive with explicit exogenous variables or
xreg_formula.


5. Complex Designs (xreg_formula)

For interactions or transformations, supply a secondary formula when constructing
ArimaSpec:

spec = ArimaSpec(
    @formula(sales = p() + q()),
    xreg_formula = Formula("~ temperature * promotion + price^2")
)

The xreg_formula is evaluated via Utils.model_matrix, producing the necessary
design matrix before fitting.


6. Putting It Together

spec = ArimaSpec(
    @formula(sales = p(0,3) + d() + q(0,3) + P() + Q() + auto()),
    m = 12,
    stepwise = false          # passthrough option to auto_arima
)
fitted = fit(spec, panel_data; groupby = [:store], datecol = :date)

Key points:

  • Any range triggers automatic model selection.
  • Fixed orders call fast direct estimation.
  • Exogenous support includes explicit columns, auto(), or complex formulas.
  • Combine with PanelData to store group/date metadata cleanly.

This grammar provides a declarative, composable way to describe ARIMA/SARIMA models
without manual tuning loops.


7. End-to-End Example (Retail Panel)

using CSV
using Downloads
using Tables
using Durbyn
using Durbyn.TableOps

# Download and reshape data
local_path = Downloads.download(
    "https://raw.githubusercontent.com/Akai01/example-time-series-datasets/refs/heads/main/Data/retail.csv"
)
retail = CSV.File(local_path)
tbl = Tables.columntable(retail)

glimpse(tbl)  # Column overview

# Convert to long format
tbl_long = pivot_longer(tbl; id_cols = :date, names_to = :series, values_to = :value)
glimpse(tbl_long)

# Wrap in PanelData with group/date metadata, A simple struc for meta data, still a Table 
panel_tbl = PanelData(tbl_long; groupby = :series, date = :date, m = 12)
glimpse(panel_tbl)

# Auto ARIMA with automatic xreg selection
spec = ArimaSpec(@formula(value = auto()))
# spec = ArimaSpec(@formula(value = p() + d() + q() + Q() + D() + Q() )) # Auto ARIMA
# spec = ArimaSpec(@formula(value = p(1)) # ARIMA(1,0,0)(0,0,0)[m]
fitted = fit(spec, panel_tbl)

# Inspect fitted models
glimpse(fitted)

# Forecast 12 steps ahead for every series
fc = forecast(fitted, h = 12)

# Inspect Forecast result
glimpse(fc)

# Forecast Table
fc_tbl = forecast_table(fc)
glimpse(fc_tbl)

8 Likes

This is great. When do you plan on registering the package? Or, when will it be stable enough to use in production?

I plan to register the package in the first quarter of 2026.

The low-level base models, such as arima(), auto_arima(), and ets() are now stable from an API perspective and will not undergo any breaking changes. I’ll continue to refactor and optimize their internals, but the external interfaces will remain consistent.

At the moment, I’m introducing a formula interface grammar and building parallelization capabilities to support large-scale forecasting problems efficiently.

4 Likes

Most of the time, we fit multiple models simultaneously. For such cases, we add an additional layer that allows specifying and training several model configurations at once:


# Fit multiple model specifications at once
models = model(
    ArimaSpec(@formula(sales = p() + q())),
    EtsSpec(e("A"), t("A"), s("A")),
    ArimaSpec(@formula(sales = p(2) + d(1) + q(2))),
    names = ["auto_arima", "ets_aaa", "arima_212"]
)

# Fit all models
fitted = fit(models, data, m = 12)

# Forecast with all models
fc = forecast(fitted, h = 12)

Hence, the code will not be pushed to GitHub this week, as we are incorporating this additional layer and ensuring proper integration before committing.

2 Likes

Durbyn.jl is ready with grammar of forecasting, parallelization and panel data interface.

Please give a feedback if you encountered any problems.

5 Likes

New model introduced in Durbyn.jl.

BATS:

The BATS framework extends exponential smoothing to accommodate multiple, possibly long seasonal cycles together with Box–Cox variance stabilization and ARMA error correction which was introduced by De Livera, Hyndman & Snyder (2011) as part of the innovation-state-space family and is now in Durbyn.jl

See the Docs: BATS · Durbyn.jl

TBATS will soon be there too

4 Likes

:rocket: Speedruns, Robustness, and the Curious Case of Nelder-Mead (An Analysis Inspired by Durbyn.jl)

Hello Julia Community,

I recently stumbled upon the interesting Durbyn.jl repository, and the discussion there regarding the choice of optimization backend sparked the idea for a little whimsical investigation. We used NonlinearOptimizationTestFunctions.jl to put three different Nelder-Mead (NM) implementations through a speedrun and a robustness check. The result reveals a fascinating conflict between speed and thoroughness that directly illuminates the design decisions behind Durbyn.jl!

Our three competitors were:

  1. Durbyn’s NM: Our simple, rudimentary implementation, which mirrors the core Durbyn.jl solver.
  2. Optim.jl: The standard NelderMead method.
  3. NLopt: The robust LN_NELDERMEAD method.

Our Setup: The Projection-Penalty Trick

To test NM solvers on constrained functions (those with bounds), we wrapped the objective function f(x) with a combined Projection and Penalty Wrapper.

This trick forces the optimizer to remain in the feasible region, while the underlying function is always evaluated at a safe, projected point.

f_{\text{wrapped}}(x) = f(\text{clip}(x, l, u)) + \lambda \cdot \| \text{Violation} \|_2

The Results: Fast Failures vs. Slow Perfection

The data shows a wild rollercoaster ride between speed (function calls) and reliability (robustness).

1. Overall Robustness (Who Solves Problems At All?)

First, the overview: How often did the algorithm find the target minimum (|\text{Result} - \text{Target}| \le 10^{-6})?

Method Best (Count) Worst (Count) Overall Avg. Calls
Durbyn’s NM 27 170 1425.6
Optim.jl 157 13 3809.9
NLopt 180 3 1555.9
  • Conclusion: NLopt is the undisputed robustness champion (180 Best results). Durbyn’s NM is catastrophic for general problems (170 Worst results), showing the limits of its simple stopping criterion on complex functions.

2. The Speedrun Test: Efficiency on Equal Footing

Since Durbyn’s NM fails so often, the average value of 1425 calls is misleading. We must measure speed only for the 19 easy functions where all three solvers successfully converged. This is the fair comparison!

Method Avg. Calls (Only 19 Common Successes) Key Behavior
Durbyn’s NM 6.1 The Speedrun Champion. Stops as soon as it touches the minimum.
Optim.jl 47.0 Professional Standard: Efficient and prudent.
NLopt 4054.5 The Pedant. Finds the solution, then spends 4,000 calls confirming it’s absolutely right.

The Whimsical Observation & Durbyn.jl Validation

This bizarre contrast—a 664x speed difference when solving the exact same easy problems—is almost entirely due to convergence criteria:

  • Durbyn’s NM: Achieves extreme speed by using an extremely loose stop criterion. Great for speedruns, but risky for general use.
  • NLopt: Its robustness comes with an efficiency tax. The algorithm is excessively thorough, consuming thousands of calls to rigorously confirm convergence to the last decimal place.

This validates the Durbyn.jl team’s design decision: For the specific, likely well-behaved objective functions in time series, prioritizing maximum speed (an average of \approx 6 calls!) is a clever, domain-specific optimization, accepting that general robustness is sacrificed.


2 Likes

Thank you for this very nice and insightful analysis, truly appreciated. It was great to see such a thorough comparison, and we’re grateful you took the time to benchmark the different Nelder-Mead implementations.

As we’ve mentioned before, the Durbyn.jl Nelder–Mead solver was never intended to compete with Optim.jl or NLopt. It is not a general-purpose optimizer; it was designed specifically for the particular time-series likelihood problems that Durbyn.jl targets. The package is still under heavy development, and the solver itself is intentionally minimalistic.

I would also encouragge the Julia community to try out Durbyn.jl and share feedback. Benchmarks like yours are extremely valuable to us, and we really appreciate the effort that wentt into this ones.

If possible, I would kindly ask whether you could also test the ETS or ARIMA model estimations tasks, simply by replacing the Durbyn.jl solver with Optim.jl and NLopt, and share your results. That would offer very helpful insights into how these established optimizers perform on the actual use cases Durbyn.jl is built for.

Thanks again for the excellent analysis and for sparking such a constructive discussion!

2 Likes