Skip to content

Commit 6220193

Browse files
committed
2026-03-03 DOT-Hybrid engine + Rust dot_hybrid_objective (M4 OWA 0.885, 9.8x faster)
1 parent 3f1168f commit 6220193

File tree

14 files changed

+2632
-81
lines changed

14 files changed

+2632
-81
lines changed

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ All notable changes to Vectrix will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.0.9] - 2026-03-03
9+
10+
Accuracy & speed release — DOT-Hybrid model achieves M4 Competition OWA 0.885 (beats #18 Theta 0.897), with 9.8x benchmark speedup from Rust `dot_hybrid_objective`.
11+
12+
### Added
13+
14+
**DOT-Hybrid Mode (M4 OWA: 0.905 → 0.885)**
15+
- Period < 24: 8-way auto-select (2 trend × 2 model × 2 season types) with exponential trend and multiplicative theta line
16+
- Period ≥ 24: Original 3-parameter DOT optimization (preserves Hourly OWA 0.722)
17+
- Automatic mode switching at `_HYBRID_THRESHOLD = 24`
18+
- Yearly OWA 0.797 (world-class, near M4 #1 ES-RNN 0.821)
19+
- NumPy-vectorized combination functions replacing Python for-loops
20+
21+
**Rust Acceleration: `dot_hybrid_objective` (26th function)**
22+
- Full DOT-Hybrid objective: buildThetaLine + golden section alpha optimization + SES filter + combine + MAE — all in Rust
23+
- Golden section search (50 iterations) replaces scipy `minimize_scalar` inside Rust for alpha optimization
24+
- M4 100K benchmark: 16.6 min → 1.7 min (9.8x faster)
25+
- Per-group speedups: Yearly 3x, Quarterly 3.7x, Monthly 4.8x, Weekly 7.7x, Daily 9.8x, Hourly 53x
26+
27+
### Changed
28+
29+
- `engine/dot.py`: `DynamicOptimizedTheta` now includes hybrid mode with backward-compatible API
30+
- `rust/src/lib.rs`: 25 → 26 Rust-accelerated functions
31+
- Version sync: pyproject.toml, Cargo.toml, __init__.py all at 0.0.9
32+
33+
[0.0.9]: https://github.com/eddmpython/vectrix/compare/v0.0.8...v0.0.9
34+
835
## [0.0.8] - 2026-03-03
936

1037
Built-in Rust engine release — Rust acceleration expanded to all engines and compiled into every wheel. No `[turbo]` extra, no flags — `pip install vectrix` includes the Rust engine like Polars.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "vectrix"
3-
version = "0.0.8"
3+
version = "0.0.9"
44
edition = "2021"
55
license-file = "LICENSE"
66

README.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -336,27 +336,28 @@ result = caf.apply(predictions, lower95, upper95, constraints=[
336336

337337
## ◈ Benchmarks
338338

339-
Evaluated on M3 and M4 competition datasets (first 100 series per category). OWA < 1.0 means better than Naive2.
340-
341-
**M3 Competition** — 4/4 categories beat Naive2:
342-
343-
| Category | OWA |
344-
|:---------|:---:|
345-
| Yearly | **0.848** |
346-
| Quarterly | **0.825** |
347-
| Monthly | **0.758** |
348-
| Other | **0.819** |
349-
350-
**M4 Competition** — 4/6 frequencies beat Naive2:
351-
352-
| Frequency | OWA |
353-
|:----------|:---:|
354-
| Yearly | **0.974** |
355-
| Quarterly | **0.797** |
356-
| Monthly | **0.987** |
357-
| Weekly | **0.737** |
358-
| Daily | 1.207 |
359-
| Hourly | 1.006 |
339+
Evaluated on **M4 Competition 100,000 time series** (2,000 sample per frequency, seed=42). OWA < 1.0 means better than Naive2.
340+
341+
**DOT-Hybrid** (single model, OWA 0.885 — beats M4 #18 Theta 0.897):
342+
343+
| Frequency | OWA | vs Naive2 |
344+
|:----------|:---:|:---------:|
345+
| Yearly | **0.797** | -20.3% |
346+
| Quarterly | **0.905** | -9.5% |
347+
| Monthly | **0.933** | -6.7% |
348+
| Weekly | **0.959** | -4.1% |
349+
| Daily | **0.996** | -0.4% |
350+
| Hourly | **0.722** | -27.8% |
351+
352+
**M4 Competition Leaderboard Context:**
353+
354+
| Rank | Method | OWA |
355+
|:-----|:-------|:---:|
356+
| #1 | ES-RNN (Smyl) | 0.821 |
357+
| #2 | FFORMA | 0.838 |
358+
| #11 | 4Theta | 0.874 |
359+
|| **Vectrix DOT-Hybrid** | **0.885** |
360+
| #18 | Theta | 0.897 |
360361

361362
Full results with sMAPE/MASE breakdown: [benchmarks](https://eddmpython.github.io/vectrix/docs/benchmarks/)
362363

README_KR.md

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -333,27 +333,28 @@ result = caf.apply(predictions, lower95, upper95, constraints=[
333333

334334
## ◈ 벤치마크
335335

336-
M3, M4 대회 데이터셋으로 평가 (카테고리별 100개 시계열). OWA < 1.0이면 Naive2보다 우수.
337-
338-
**M3 Competition** — 4/4 카테고리에서 Naive2 능가:
339-
340-
| 카테고리 | OWA |
341-
|:---------|:---:|
342-
| Yearly | **0.848** |
343-
| Quarterly | **0.825** |
344-
| Monthly | **0.758** |
345-
| Other | **0.819** |
346-
347-
**M4 Competition** — 4/6 빈도에서 Naive2 능가:
348-
349-
| 빈도 | OWA |
350-
|:-----|:---:|
351-
| Yearly | **0.974** |
352-
| Quarterly | **0.797** |
353-
| Monthly | **0.987** |
354-
| Weekly | **0.737** |
355-
| Daily | 1.207 |
356-
| Hourly | 1.006 |
336+
**M4 Competition 100,000 시계열** 벤치마크 (빈도별 2,000 샘플, seed=42). OWA < 1.0이면 Naive2보다 우수.
337+
338+
**DOT-Hybrid** (단일 모델, OWA 0.885 — M4 #18 Theta 0.897 초과):
339+
340+
| 빈도 | OWA | vs Naive2 |
341+
|:-----|:---:|:---------:|
342+
| Yearly | **0.797** | -20.3% |
343+
| Quarterly | **0.905** | -9.5% |
344+
| Monthly | **0.933** | -6.7% |
345+
| Weekly | **0.959** | -4.1% |
346+
| Daily | **0.996** | -0.4% |
347+
| Hourly | **0.722** | -27.8% |
348+
349+
**M4 Competition 리더보드 위치:**
350+
351+
| 순위 | 방법 | OWA |
352+
|:-----|:-----|:---:|
353+
| #1 | ES-RNN (Smyl) | 0.821 |
354+
| #2 | FFORMA | 0.838 |
355+
| #11 | 4Theta | 0.874 |
356+
|| **Vectrix DOT-Hybrid** | **0.885** |
357+
| #18 | Theta | 0.897 |
357358

358359
sMAPE/MASE 상세 결과: [벤치마크 상세](https://eddmpython.github.io/vectrix/docs/benchmarks/)
359360

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vectrix"
3-
version = "0.0.8"
3+
version = "0.0.9"
44
description = "Zero-config time series forecasting & analysis library. 30+ models with built-in Rust engine for blazing-fast performance."
55
readme = "README.md"
66
license = {file = "LICENSE"}

rust/src/lib.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,99 @@ fn four_theta_deseasonalize(
12051205
}
12061206
}
12071207

1208+
// ── DOT Hybrid objective (buildThetaLine + SES optimize + combine + MAE) ──
1209+
#[pyfunction]
1210+
fn dot_hybrid_objective(
1211+
y: PyReadonlyArray1<f64>,
1212+
theta_line0: PyReadonlyArray1<f64>,
1213+
theta: f64,
1214+
is_additive: bool,
1215+
) -> PyResult<f64> {
1216+
let y = y.as_array();
1217+
let t0 = theta_line0.as_array();
1218+
let n = y.len();
1219+
if n < 3 {
1220+
return Ok(f64::MAX);
1221+
}
1222+
1223+
let mut theta_line = vec![0.0f64; n];
1224+
if is_additive {
1225+
for i in 0..n {
1226+
theta_line[i] = theta * y[i] + (1.0 - theta) * t0[i];
1227+
}
1228+
} else {
1229+
for i in 0..n {
1230+
let yv = if y[i] > 1e-10 { y[i] } else { 1e-10 };
1231+
let tv = if t0[i] > 1e-10 { t0[i] } else { 1e-10 };
1232+
theta_line[i] = yv.powf(theta) * tv.powf(1.0 - theta);
1233+
}
1234+
}
1235+
1236+
let alpha = optimize_alpha_bounded(&theta_line);
1237+
1238+
let mut filtered = vec![0.0f64; n];
1239+
filtered[0] = theta_line[0];
1240+
for t in 1..n {
1241+
filtered[t] = alpha * theta_line[t] + (1.0 - alpha) * filtered[t - 1];
1242+
}
1243+
1244+
let mut mae = 0.0f64;
1245+
if is_additive {
1246+
let w = 1.0 / theta.max(1.0);
1247+
let w2 = 1.0 - w;
1248+
for i in 0..n {
1249+
let fitted = w * filtered[i] + w2 * t0[i];
1250+
mae += (y[i] - fitted).abs();
1251+
}
1252+
} else {
1253+
let inv = 1.0 / theta.max(1.0);
1254+
let inv2 = 1.0 - inv;
1255+
for i in 0..n {
1256+
let fv = if filtered[i] > 1e-10 { filtered[i] } else { 1e-10 };
1257+
let tv = if t0[i] > 1e-10 { t0[i] } else { 1e-10 };
1258+
let fitted = fv.powf(inv) * tv.powf(inv2);
1259+
mae += (y[i] - fitted).abs();
1260+
}
1261+
}
1262+
1263+
Ok(mae / n as f64)
1264+
}
1265+
1266+
fn ses_sse_inner(y: &[f64], alpha: f64) -> f64 {
1267+
let mut level = y[0];
1268+
let mut sse = 0.0f64;
1269+
for t in 1..y.len() {
1270+
let error = y[t] - level;
1271+
sse += error * error;
1272+
level = alpha * y[t] + (1.0 - alpha) * level;
1273+
}
1274+
sse
1275+
}
1276+
1277+
fn optimize_alpha_bounded(y: &[f64]) -> f64 {
1278+
let n = y.len();
1279+
if n < 3 {
1280+
return 0.3;
1281+
}
1282+
1283+
let (mut lo, mut hi) = (0.001f64, 0.999f64);
1284+
let gr = (5.0f64.sqrt() - 1.0) / 2.0;
1285+
1286+
for _ in 0..50 {
1287+
let x1 = hi - gr * (hi - lo);
1288+
let x2 = lo + gr * (hi - lo);
1289+
let f1 = ses_sse_inner(y, x1);
1290+
let f2 = ses_sse_inner(y, x2);
1291+
if f1 < f2 {
1292+
hi = x2;
1293+
} else {
1294+
lo = x1;
1295+
}
1296+
}
1297+
1298+
(lo + hi) / 2.0
1299+
}
1300+
12081301
#[pymodule]
12091302
fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
12101303
m.add_function(wrap_pyfunction!(ets_filter, m)?)?;
@@ -1232,5 +1325,6 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
12321325
m.add_function(wrap_pyfunction!(esn_reservoir_update, m)?)?;
12331326
m.add_function(wrap_pyfunction!(four_theta_fitted, m)?)?;
12341327
m.add_function(wrap_pyfunction!(four_theta_deseasonalize, m)?)?;
1328+
m.add_function(wrap_pyfunction!(dot_hybrid_objective, m)?)?;
12351329
Ok(())
12361330
}

src/vectrix/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
)
8080
from .vectrix import Vectrix
8181

82-
__version__ = "0.0.8"
82+
__version__ = "0.0.9"
8383
__all__ = [
8484
"Vectrix",
8585
"ForecastResult",

0 commit comments

Comments
 (0)