Tutorial 05 — Adaptive Intelligence

Open in Colab

These features are unique to Vectrix — not available in statsforecast, Prophet, Darts, or any other forecasting library. Adaptive intelligence means your forecasts respond to changing conditions in real time: detecting regime shifts, self-correcting as new data arrives, respecting business constraints, and profiling data DNA for intelligent model selection.

Regime Detection

Real-world data rarely follows a single pattern throughout its history. Markets alternate between bull and bear phases, retail demand shifts between peak and off-season, and business metrics change after product launches or policy changes. Vectrix detects these regimes automatically using Hidden Markov Models

from vectrix import RegimeDetector
import numpy as np

np.random.seed(42)
regime1 = np.random.randn(50) * 2 + 100
regime2 = np.random.randn(50) * 5 + 130
regime3 = np.random.randn(50) * 2 + 110
data = np.concatenate([regime1, regime2, regime3])

detector = RegimeDetector(nRegimes=3)
result = detector.detect(data)

print(f"Current regime: {result.currentRegime}")
print(f"Number of regimes: {len(result.regimeStats)}")
for label, stats in result.regimeStats.items():
    print(f"  Regime {label}: mean={stats['mean']:.2f}, std={stats['std']:.2f}")

Expected output:

Current regime: 2
Number of regimes: 3
  Regime 0: mean=100.12, std=2.05
  Regime 1: mean=130.45, std=4.87
  Regime 2: mean=110.23, std=1.98

RegimeDetector Parameters

ParameterDefaultDescription
nRegimes2Number of regimes to detect

RegimeResult Attributes

AttributeTypeDescription
currentRegimeintIndex of the current (most recent) regime
regimeStatsdictPer-regime statistics (mean, std, etc.)
regimeSequencenp.ndarrayRegime label for each observation

Regime-Aware Forecasting

Traditional forecasters use one model for all data — even if the data’s behavior changed dramatically midway. RegimeAwareForecaster does something smarter: it identifies which regime the series is currently in and uses the model that performed best during similar past regimes

from vectrix import RegimeAwareForecaster
import numpy as np

data = np.random.randn(200).cumsum() + 100

raf = RegimeAwareForecaster()
result = raf.forecast(data, steps=30, period=7)

print(f"Current regime: {result.currentRegime}")
print(f"Model per regime: {result.modelPerRegime}")
print(f"Predictions: {result.predictions[:5].round(2)}")

Expected output:

Current regime: 1
Model per regime: {0: 'AutoETS', 1: 'DOT', 2: 'Theta'}
Predictions: [102.34 103.12 103.89 104.67 105.44]

The forecaster detects which regime the series is currently in and uses the model that performed best during similar past regimes.

Forecast DNA

Every time series has a unique statistical signature. DNA profiling extracts 65+ features — autocorrelation structure, Hurst exponent, entropy, volatility clustering, seasonal strength, and more — to create a deterministic fingerprint. This fingerprint drives intelligent model selection and difficulty estimation

from vectrix import ForecastDNA
import numpy as np

dna = ForecastDNA()

t = np.arange(100)
data = 50 + 0.3 * t + 15 * np.sin(2 * np.pi * t / 12) + np.random.randn(100) * 3

profile = dna.analyze(data, period=12)

print(f"Fingerprint: {profile.fingerprint}")
print(f"Difficulty: {profile.difficulty} ({profile.difficultyScore:.0f}/100)")
print(f"Category: {profile.category}")
print(f"Recommended models: {profile.recommendedModels[:5]}")

Expected output:

Fingerprint: b7e2f4a1
Difficulty: easy (28/100)
Category: seasonal
Recommended models: ['AutoETS', 'MSTL', 'Theta', 'DOT', 'AutoCES']

DNA Profile Attributes

AttributeTypeDescription
fingerprintstrDeterministic hash of the data’s statistical properties
difficultystreasy, medium, hard, very_hard
difficultyScorefloat0-100, higher means harder to forecast
categorystrseasonal, trending, volatile, intermittent, stationary
recommendedModelslist[str]Models ranked by expected performance

Using DNA for Decision Making

from vectrix import ForecastDNA, forecast

dna = ForecastDNA()
profile = dna.analyze(data, period=7)

if profile.difficultyScore > 70:
    print("Hard series -- consider using ensemble or longer history.")
elif profile.category == "intermittent":
    print("Sparse demand -- Croston variants recommended.")
else:
    result = forecast(data, steps=14)
    print(f"Forecast with {result.model}")

Self-Healing Forecast

Forecasts degrade over time — the further out you predict, the less accurate the results. Self-healing solves this by monitoring errors as actual values arrive and automatically adjusting the remaining predictions. If your forecast was too optimistic for the first 3 days, the healer compensates for the remaining days

from vectrix import SelfHealingForecast, forecast
import numpy as np

data = np.random.randn(100).cumsum() + 200
result = forecast(data, steps=14)

healer = SelfHealingForecast(
    result.predictions,
    result.lower,
    result.upper,
    data,
)

As actual values arrive, feed them to the healer

actual_day1_to_5 = np.array([198.5, 201.2, 195.8, 203.4, 199.1])
healer.observe(actual_day1_to_5)

report = healer.getReport()
print(f"Health: {report.overallHealth} ({report.healthScore:.1f}/100)")
print(f"Observed: {report.totalObserved}")
print(f"Corrected: {report.totalCorrected}")
print(f"Improvement: {report.improvementPct:.1f}%")

Expected output:

Health: good (82.5/100)
Observed: 5
Corrected: 3
Improvement: 12.4%

Get the updated forecast with corrections applied

updated_preds, updated_lower, updated_upper = healer.getUpdatedForecast()
print(f"Original remaining: {result.predictions[5:].round(1)}")
print(f"Updated remaining:  {updated_preds[5:].round(1)}")

HealingReport Attributes

AttributeTypeDescription
overallHealthstrexcellent, good, fair, poor
healthScorefloat0-100, higher is better
totalObservedintNumber of actual values received
totalCorrectedintNumber of corrections applied
improvementPctfloatError reduction percentage

Constraint-Aware Forecasting

Statistical models don’t know about your business rules. Predictions can go negative (impossible for unit sales), exceed warehouse capacity, or show unrealistic year-over-year swings. Constraint-aware forecasting applies domain knowledge as post-processing rules — without modifying the underlying model

from vectrix import ConstraintAwareForecaster, Constraint, forecast
import numpy as np

data = np.random.randn(100).cumsum() + 500
result = forecast(data, steps=14)

caf = ConstraintAwareForecaster()
constrained = caf.apply(
    result.predictions,
    result.lower,
    result.upper,
    constraints=[
        Constraint('non_negative', {}),
        Constraint('range', {'min': 100, 'max': 5000}),
        Constraint('capacity', {'capacity': 3000}),
    ]
)

print(f"Original: {result.predictions[:5].round(1)}")
print(f"Constrained: {constrained.predictions[:5].round(1)}")

Year-over-Year Constraint

Limit how much the forecast can change compared to last year

past_year = data[-365:]

constrained = caf.apply(
    result.predictions,
    result.lower,
    result.upper,
    constraints=[
        Constraint('non_negative', {}),
        Constraint('yoy_change', {'maxPct': 30, 'historicalData': past_year}),
    ]
)

All Constraint Types

TypeParametersDescription
non_negative{}Ensures all predictions >= 0
range{'min': N, 'max': M}Clips to [min, max]
capacity{'capacity': N}Caps at capacity ceiling
yoy_change{'maxPct': N, 'historicalData': arr}Limits year-over-year change
sum_constraint{'window': N, 'maxSum': M}Limits sum within rolling windows
monotone{'direction': 'increasing'}Forces monotonic increase or decrease
ratio{'minRatio': N, 'maxRatio': M}Limits consecutive value ratio
custom{'fn': callable}Applies a custom constraint function

Custom Constraint Example

import numpy as np

def weekend_boost(predictions, lower, upper):
    boosted = predictions.copy()
    lo = lower.copy()
    hi = upper.copy()
    for i in range(len(boosted)):
        if i % 7 in [5, 6]:
            boosted[i] *= 1.2
            lo[i] *= 1.2
            hi[i] *= 1.2
    return boosted, lo, hi

constrained = caf.apply(
    result.predictions,
    result.lower,
    result.upper,
    constraints=[
        Constraint('custom', {'fn': weekend_boost}),
    ]
)

Complete Example

A full adaptive forecasting workflow — profile the data, detect regimes, generate a forecast, and apply business constraints

import numpy as np
from vectrix import (
    forecast, ForecastDNA, RegimeDetector,
    SelfHealingForecast, ConstraintAwareForecaster, Constraint
)

np.random.seed(42)
data = np.random.randn(200).cumsum() + 500

dna = ForecastDNA()
profile = dna.analyze(data, period=7)
print(f"Difficulty: {profile.difficulty}")
print(f"Recommended: {profile.recommendedModels[:3]}")

detector = RegimeDetector(nRegimes=2)
regimes = detector.detect(data)
print(f"Current regime: {regimes.currentRegime}")

result = forecast(data, steps=14)
print(f"Model: {result.model}, MAPE: {result.mape:.2f}%")

caf = ConstraintAwareForecaster()
constrained = caf.apply(
    result.predictions, result.lower, result.upper,
    constraints=[
        Constraint('non_negative', {}),
        Constraint('range', {'min': 300, 'max': 800}),
    ]
)
print(f"Constrained predictions: {constrained.predictions[:5].round(1)}")