Tutorial 06 — Business Intelligence

Open in Colab

Forecasting is only the first step. Real-world decision-making requires anomaly detection to clean your data, what-if scenarios for planning, backtesting to validate your approach, and business-specific accuracy metrics that go beyond MAPE.

Vectrix’s Business Intelligence module provides all four — designed for operations managers, analysts, and data scientists who need production-ready forecasting workflows.

Anomaly Detection

Before forecasting, identify and understand unusual observations in your historical data. Anomalies can distort model training and lead to biased predictions

from vectrix.business import AnomalyDetector
import numpy as np

np.random.seed(42)
data = np.random.randn(200) * 10 + 100
data[50] = 200
data[120] = 30
data[175] = 250

detector = AnomalyDetector()
result = detector.detect(data, method="auto")

print(f"Method used: {result.method}")
print(f"Anomalies found: {result.nAnomalies}")
print(f"Anomaly ratio: {result.anomalyRatio:.1%}")
print(f"Anomaly indices: {result.indices}")

Expected output:

Method used: zscore
Anomalies found: 3
Anomaly ratio: 1.5%
Anomaly indices: [50, 120, 175]

Detection Methods

MethodHow It WorksBest For
autoAutomatically selects the best methodGeneral use (recommended)
zscoreFlags points > 3 standard deviations from meanNormally distributed data
iqrFlags points outside 1.5x interquartile rangeSkewed distributions
rollingFlags points outside rolling window statisticsNon-stationary data

Example with Specific Method

result_iqr = detector.detect(data, method="iqr")
print(f"IQR method found: {result_iqr.nAnomalies} anomalies")

result_rolling = detector.detect(data, method="rolling")
print(f"Rolling method found: {result_rolling.nAnomalies} anomalies")

AnomalyResult Attributes

AttributeTypeDescription
methodstrDetection method used
nAnomaliesintNumber of anomalies detected
anomalyRatiofloatFraction of data points flagged
indicesnp.ndarrayIndices of anomalous observations

What-If Analysis

What-if analysis lets you explore hypothetical scenarios against your baseline forecast — essential for budget planning, risk assessment, and stakeholder presentations. Define optimistic, pessimistic, and shock scenarios, then compare their impact

from vectrix.business import WhatIfAnalyzer
from vectrix import forecast
import numpy as np

data = np.random.randn(200).cumsum() + 500
result = forecast(data, steps=30)

analyzer = WhatIfAnalyzer()
scenarios = analyzer.analyze(
    result.predictions,
    data,
    [
        {"name": "Optimistic", "trend_change": 0.1},
        {"name": "Pessimistic", "trend_change": -0.15},
        {"name": "Supply Shock", "shock_at": 10, "shock_magnitude": -0.3, "shock_duration": 5},
        {"name": "Level Shift", "level_shift": 0.05},
    ]
)

for sr in scenarios:
    print(f"{sr.name}: mean={sr.predictions.mean():.2f}, impact={sr.impact:+.1f}%")

Expected output:

Optimistic: mean=535.42, impact=+10.0%
Pessimistic: mean=425.18, impact=-15.0%
Supply Shock: mean=480.67, impact=-5.8%
Level Shift: mean=525.00, impact=+5.0%

Scenario Parameters

ParameterTypeDescription
namestrScenario label
trend_changefloatPercentage trend adjustment (0.1 = +10%)
shock_atintStep index where shock begins
shock_magnitudefloatShock size (-0.3 = -30% drop)
shock_durationintNumber of steps the shock lasts
level_shiftfloatPermanent level adjustment (0.05 = +5%)

ScenarioResult Attributes

AttributeTypeDescription
namestrScenario name
predictionsnp.ndarrayModified predictions
impactfloatOverall impact vs. baseline

Tip: Use what-if analysis for budget planning — create optimistic, baseline, and pessimistic scenarios and present all three to stakeholders.

Backtesting

How do you know if your forecasting approach actually works? Backtesting (walk-forward validation) simulates how well your model would have performed on historical data by repeatedly training on past data and predicting the next window

from vectrix.business import Backtester
from vectrix.engine.ets import AutoETS
import numpy as np

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

bt = Backtester(nFolds=5, horizon=14, strategy='expanding')
result = bt.run(data, lambda: AutoETS())

print(f"Average MAPE: {result.avgMAPE:.2f}%")
print(f"Average RMSE: {result.avgRMSE:.2f}")
print(f"Best fold: #{result.bestFold}")
print(f"Worst fold: #{result.worstFold}")

Expected output:

Average MAPE: 4.56%
Average RMSE: 12.34
Best fold: #3
Worst fold: #1

Per-Fold Results

print()
print("Per-fold breakdown:")
for f in result.folds:
    print(f"  Fold {f.fold}: MAPE={f.mape:.2f}%, RMSE={f.rmse:.2f}")

Expected output:

Per-fold breakdown:
  Fold 1: MAPE=6.12%, RMSE=16.45
  Fold 2: MAPE=4.23%, RMSE=11.89
  Fold 3: MAPE=3.45%, RMSE=9.67
  Fold 4: MAPE=4.89%, RMSE=13.12
  Fold 5: MAPE=4.12%, RMSE=10.58

Backtester Parameters

ParameterDefaultDescription
nFolds5Number of validation folds
horizon14Forecast horizon per fold
strategy'expanding''expanding' (growing window) or 'sliding' (fixed window)

Strategy Comparison

Expanding window — Each fold uses all data up to the cutoff point. Earlier folds train on less data, later folds train on more. Recommended for most cases.

Fold 1: [====TRAIN====][TEST]
Fold 2: [======TRAIN======][TEST]
Fold 3: [========TRAIN========][TEST]

Sliding window — Each fold uses a fixed-size training window. Useful when older data is no longer relevant (e.g., regime changes).

Fold 1: [====TRAIN====][TEST]
Fold 2:    [====TRAIN====][TEST]
Fold 3:       [====TRAIN====][TEST]

BacktestResult Attributes

AttributeTypeDescription
avgMAPEfloatAverage MAPE across all folds
avgRMSEfloatAverage RMSE across all folds
bestFoldintFold number with lowest MAPE
worstFoldintFold number with highest MAPE
foldslistPer-fold results (mape, rmse, fold index)

Business Metrics

MAPE and RMSE tell you about statistical accuracy, but businesses care about different things: Are we systematically over- or under-forecasting? What’s our volume-weighted error? Does our model beat a naive baseline? BusinessMetrics answers these questions

from vectrix.business import BusinessMetrics
import numpy as np

actual = np.array([100, 120, 110, 130, 140, 125, 135, 150, 145, 155])
predicted = np.array([105, 115, 112, 128, 145, 120, 138, 148, 140, 160])

metrics = BusinessMetrics()
result = metrics.calculate(actual, predicted)

print(f"Bias: {result['bias']:+.2f}")
print(f"Bias %: {result['biasPercent']:+.2f}%")
print(f"WAPE: {result['wape']:.2f}%")
print(f"MASE: {result['mase']:.2f}")
print(f"Accuracy: {result['forecastAccuracy']:.1f}%")
print(f"Over-forecast ratio: {result['overForecastRatio']:.1%}")
print(f"Under-forecast ratio: {result['underForecastRatio']:.1%}")

Expected output:

Bias: -0.10
Bias %: -0.08%
WAPE: 3.42%
MASE: 0.45
Accuracy: 96.6%
Over-forecast ratio: 50.0%
Under-forecast ratio: 50.0%

Metrics Reference

MetricKeyWhat It Means
BiasbiasPositive = systematic over-forecasting
Bias %biasPercentBias as percentage of actual
WAPEwapeWeighted Absolute Percentage Error (volume-weighted)
MASEmaseBelow 1 means better than Naive forecast
AccuracyforecastAccuracy100% - WAPE, higher is better
Over-forecastoverForecastRatioFraction of periods where predicted exceeds actual
Under-forecastunderForecastRatioFraction of periods where predicted is below actual

Note: WAPE is preferred over MAPE in business contexts because it handles near-zero values gracefully and weights errors by volume. A WAPE of 5% means your total absolute error is 5% of total actual volume.

Interpreting MASE

MASE (Mean Absolute Scaled Error) compares your model to a Naive baseline

  • MASE below 1.0 — Your model beats Naive. Good.
  • MASE = 1.0 — Your model equals Naive. No value added.
  • MASE above 1.0 — Naive would have been better. Investigate.

Combining Business Tools

In practice, these tools work together as a complete business forecasting workflow — detect anomalies, backtest your approach, generate the forecast, then explore scenarios

import numpy as np
from vectrix import forecast
from vectrix.business import AnomalyDetector, Backtester, BusinessMetrics, WhatIfAnalyzer
from vectrix.engine.ets import AutoETS

np.random.seed(42)
data = np.random.randn(365).cumsum() + 1000

detector = AnomalyDetector()
anomalies = detector.detect(data, method="auto")
print(f"Anomalies in history: {anomalies.nAnomalies}")

bt = Backtester(nFolds=4, horizon=30, strategy='expanding')
bt_result = bt.run(data, lambda: AutoETS())
print(f"Backtest MAPE: {bt_result.avgMAPE:.2f}%")

result = forecast(data, steps=30)
print(f"Model: {result.model}")

analyzer = WhatIfAnalyzer()
scenarios = analyzer.analyze(result.predictions, data, [
    {"name": "Growth +10%", "trend_change": 0.10},
    {"name": "Decline -10%", "trend_change": -0.10},
])
for s in scenarios:
    print(f"  {s.name}: mean={s.predictions.mean():.0f}")

Complete Example: Monthly Sales Review

A realistic example — evaluating last month’s forecast accuracy using business metrics to decide if the model needs recalibration

import numpy as np
from vectrix import forecast
from vectrix.business import BusinessMetrics

actual_last_month = np.array([
    320, 345, 310, 380, 400, 420, 350,
    330, 360, 325, 390, 410, 430, 365,
    340, 370, 335, 395, 415, 440, 375,
    345, 375, 340, 400, 425, 445, 380,
    350, 385
])

predicted_last_month = np.array([
    315, 340, 320, 370, 395, 415, 345,
    325, 355, 330, 385, 405, 425, 360,
    335, 365, 340, 390, 410, 435, 370,
    340, 370, 345, 395, 420, 440, 375,
    345, 380
])

metrics = BusinessMetrics()
result = metrics.calculate(actual_last_month, predicted_last_month)

print("=== Monthly Performance Review ===")
print(f"Forecast Accuracy: {result['forecastAccuracy']:.1f}%")
print(f"Bias: {result['bias']:+.1f} units/day")
print(f"WAPE: {result['wape']:.1f}%")
print(f"MASE: {result['mase']:.2f}")

if result['mase'] < 1.0:
    print("Model outperforms Naive baseline.")
if abs(result['biasPercent']) > 5:
    print(f"Warning: Systematic {'over' if result['bias'] > 0 else 'under'}-forecasting detected.")