Create an autoregressive forecasting model and publish it to Huggingface

Build and backtest an autoregressive SE3 consumption forecaster, then deploy it to Rebase and publish the model version to Hugging Face.

This example starts with model development. It fetches daily SE3 consumption from the public eSett Open Data API, builds a simple autoregressive model, and compares it against a persistence baseline. Only after the local backtest looks reasonable do we deploy the model and publish the resulting Rebase model version to Hugging Face.

The target variable is SE3 total consumption in MWh from eSett's consumption endpoint. eSett exposes consumption by Metering Balance Area (MBA); SE3's MBA code is 10Y1001A1001A46L.

1. Install

Create a local Python environment:

uv venv .venv
source .venv/bin/activate
uv pip install "rebase-toolkit[huggingface]" numpy pandas requests scikit-learn

Configure Rebase now if you plan to deploy later:

rebase setup

Hugging Face authentication is not needed while developing the model. You only need it when you publish:

rebase connect huggingface

2. Fetch SE3 Consumption

Create se3_ar_forecast.py:

from __future__ import annotations

import math
from typing import Any

import numpy as np
import pandas as pd
import requests
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error, mean_squared_error

import rebase


ESETT_API = "https://api.opendata.esett.com"
SE3_MBA = "10Y1001A1001A46L"
DEFAULT_START = "2025-01-01"
DEFAULT_END = "2026-05-31"
DEFAULT_LAGS = [1, 2, 3, 7, 14, 28]


def fetch_se3_consumption(start: str = DEFAULT_START, end: str = DEFAULT_END) -> pd.DataFrame:
    response = requests.get(
        f"{ESETT_API}/EXP15/Aggregate",
        params={
            "start": f"{start}T00:00:00.000Z",
            "end": f"{end}T00:00:00.000Z",
            "mba": SE3_MBA,
            "resolution": "Day",
        },
        headers={"Accept": "application/json"},
        timeout=30,
    )
    response.raise_for_status()

    rows = response.json()
    frame = pd.DataFrame(rows)
    if frame.empty:
        raise ValueError("eSett returned no SE3 consumption rows")

    frame["timestamp"] = pd.to_datetime(frame["timestampUTC"], utc=True)
    frame["consumption_mwh"] = -frame["total"].astype(float)
    return frame[["timestamp", "consumption_mwh"]].sort_values("timestamp").reset_index(drop=True)

The eSett API returns consumption values as negative MWh values. The example flips the sign so the model sees positive consumption.

3. Build Autoregressive Features

Append feature-building helpers to se3_ar_forecast.py:

def _feature_row(history: list[float], timestamp: pd.Timestamp, lags: list[int]) -> dict[str, float]:
    day = float(timestamp.dayofweek)
    row = {
        "dow_sin": math.sin(2.0 * math.pi * day / 7.0),
        "dow_cos": math.cos(2.0 * math.pi * day / 7.0),
        "trend": float(len(history)),
    }
    for lag in lags:
        row[f"lag_{lag}"] = float(history[-lag])
    return row


def make_training_matrix(
    frame: pd.DataFrame,
    lags: list[int] | None = None,
) -> tuple[pd.DataFrame, pd.Series]:
    resolved_lags = lags or DEFAULT_LAGS
    max_lag = max(resolved_lags)
    values = frame["consumption_mwh"].astype(float).tolist()
    timestamps = frame["timestamp"].tolist()

    rows: list[dict[str, float]] = []
    targets: list[float] = []
    for index in range(max_lag, len(values)):
        rows.append(_feature_row(values[:index], timestamps[index], resolved_lags))
        targets.append(values[index])

    return pd.DataFrame(rows), pd.Series(targets, name="consumption_mwh")


def fit_autoregressive_model(frame: pd.DataFrame, lags: list[int] | None = None) -> Ridge:
    features, target = make_training_matrix(frame, lags=lags)
    return Ridge(alpha=1.0).fit(features, target)

This is intentionally modest: lagged consumption explains most of the daily baseline, while day-of-week features capture a simple weekly shape.

4. Backtest Against Persistence

Before deploying anything, compare the autoregressive model to a persistence baseline. Persistence predicts that tomorrow will equal today.

Add the backtest:

def backtest(
    frame: pd.DataFrame,
    holdout_days: int = 60,
    lags: list[int] | None = None,
) -> dict[str, Any]:
    resolved_lags = lags or DEFAULT_LAGS
    max_lag = max(resolved_lags)
    if len(frame) <= holdout_days + max_lag:
        raise ValueError("not enough data for the requested backtest")

    train = frame.iloc[:-holdout_days].reset_index(drop=True)
    test = frame.iloc[-holdout_days:].reset_index(drop=True)
    model = fit_autoregressive_model(train, lags=resolved_lags)

    history = train["consumption_mwh"].astype(float).tolist()
    ar_predictions: list[float] = []
    persistence_predictions: list[float] = []

    for _, row in test.iterrows():
        features = pd.DataFrame([_feature_row(history, row["timestamp"], resolved_lags)])
        ar_prediction = float(model.predict(features)[0])
        persistence_prediction = float(history[-1])

        ar_predictions.append(ar_prediction)
        persistence_predictions.append(persistence_prediction)
        history.append(float(row["consumption_mwh"]))

    actual = test["consumption_mwh"].astype(float).to_numpy()
    ar_mae = mean_absolute_error(actual, ar_predictions)
    persistence_mae = mean_absolute_error(actual, persistence_predictions)

    return {
        "holdout_days": holdout_days,
        "ar_mae_mwh": float(ar_mae),
        "persistence_mae_mwh": float(persistence_mae),
        "ar_rmse_mwh": float(mean_squared_error(actual, ar_predictions) ** 0.5),
        "persistence_rmse_mwh": float(mean_squared_error(actual, persistence_predictions) ** 0.5),
        "mae_improvement_vs_persistence_pct": float(100.0 * (persistence_mae - ar_mae) / persistence_mae),
    }

Run a local backtest without changing the model module:

python - <<'PY'
import json

from se3_ar_forecast import backtest, fetch_se3_consumption

data = fetch_se3_consumption()
metrics = backtest(data)
print(json.dumps(metrics, indent=2))
PY

At this point you are still just developing the model. If persistence wins, change the lags, add calendar features, use a longer history window, or test a different model class before deploying.

5. Wrap It as a Rebase Model

Once the local backtest is acceptable, append a deployable predictor to the same file:

def forecast_next_days(
    frame: pd.DataFrame,
    horizon_days: int = 7,
    lags: list[int] | None = None,
) -> list[dict[str, Any]]:
    resolved_lags = lags or DEFAULT_LAGS
    model = fit_autoregressive_model(frame, lags=resolved_lags)

    history = frame["consumption_mwh"].astype(float).tolist()
    timestamp = frame["timestamp"].iloc[-1]
    forecast: list[dict[str, Any]] = []

    for _ in range(horizon_days):
        timestamp = timestamp + pd.Timedelta(days=1)
        features = pd.DataFrame([_feature_row(history, timestamp, resolved_lags)])
        value = float(model.predict(features)[0])
        history.append(value)
        forecast.append(
            {
                "timestamp": timestamp.isoformat(),
                "consumption_mwh": value,
            }
        )

    return forecast


class SE3AutoregressiveConsumption(rebase.Predictor):
    project = "forecasting"
    name = "se3-autoregressive-consumption"
    description = "Daily SE3 consumption forecast using eSett Open Data and autoregressive lags."
    dependencies = [
        "numpy",
        "pandas",
        "requests",
        "scikit-learn",
    ]

    def predict(
        self,
        start: str = DEFAULT_START,
        end: str = DEFAULT_END,
        horizon_days: int = 7,
        holdout_days: int = 60,
    ) -> dict[str, Any]:
        frame = fetch_se3_consumption(start=start, end=end)
        return {
            "target": "SE3 daily total consumption",
            "unit": "MWh",
            "training_start": frame["timestamp"].iloc[0].isoformat(),
            "training_end": frame["timestamp"].iloc[-1].isoformat(),
            "backtest": backtest(frame, holdout_days=holdout_days),
            "forecast": forecast_next_days(frame, horizon_days=horizon_days),
        }


model = SE3AutoregressiveConsumption()

Run it locally:

python - <<'PY'
from se3_ar_forecast import model

result = model.predict(horizon_days=3)
print(result["backtest"])
print(result["forecast"])
PY

6. Deploy to Rebase

Deploy the model to dev without involving Hugging Face yet:

rebase deploy se3_ar_forecast.py --name model

rebase deploy creates or updates the Rebase model and deploys the current version to dev by default. Inspect the model version and environment pointer:

rebase model versions se3-autoregressive-consumption --project forecasting
rebase model deployments se3-autoregressive-consumption --project forecasting

Validate the deployed model:

rebase model run se3-autoregressive-consumption \
  --project forecasting \
  --env dev \
  --param horizon_days=3

7. Publish to Hugging Face

Publishing is a separate step after the model and backtest flow are working. First, create a Hub README as README.hf.md:

---
library_name: scikit-learn
tags:
  - forecasting
  - time-series
  - electricity-consumption
  - se3
  - esett
---

# SE3 Autoregressive Consumption Forecast

Daily SE3 total consumption forecast trained on eSett Open Data.

The model uses lagged daily consumption, day-of-week features, and a linear Ridge regressor. The accompanying `backtest.json` compares the autoregressive forecast against a persistence baseline.

The Rebase SDK also publishes `rebase_model.json` with model-version provenance, including the Rebase model version and the Hugging Face commit recorded for this publication.

Then create a small artifact folder with the data window, backtest summary, and Hub README:

import json
from pathlib import Path

from se3_ar_forecast import DEFAULT_END, DEFAULT_START, backtest, fetch_se3_consumption, model

import rebase


HUGGINGFACE_README = Path("README.hf.md")


def write_huggingface_artifacts() -> Path:
    artifact_dir = Path("artifacts/se3-autoregressive-consumption")
    artifact_dir.mkdir(parents=True, exist_ok=True)

    frame = fetch_se3_consumption(DEFAULT_START, DEFAULT_END)
    frame.to_csv(artifact_dir / "training_data.csv", index=False)

    metrics = backtest(frame)
    (artifact_dir / "backtest.json").write_text(json.dumps(metrics, indent=2) + "\n", encoding="utf-8")
    (artifact_dir / "README.md").write_text(HUGGINGFACE_README.read_text(encoding="utf-8"), encoding="utf-8")
    return artifact_dir


deployed = model.deploy(
    environment="dev",
    huggingface=rebase.HuggingFacePublishConfig(
        repo_id="your-hf-org/se3-autoregressive-consumption",
        repo_type="model",
        private=False,
        artifact_path=write_huggingface_artifacts(),
        sync_source_git=True,
    ),
)

print("huggingface_publication:", deployed.data["huggingface_publication"])

Save that as publish_huggingface.py and run:

python publish_huggingface.py

The Hub repo receives the artifact folder plus Rebase provenance. Rebase records the Hub repo, revision, commit SHA, visibility, and the model version that produced the publication.

Set sync_source_git=False if this example is running outside a clean Git checkout:

rebase.HuggingFacePublishConfig(
    repo_id="your-hf-org/se3-autoregressive-consumption",
    artifact_path=write_huggingface_artifacts(),
    sync_source_git=False,
)

8. Promote to Production

Promote the same immutable model version to staging:

rebase model promote se3-autoregressive-consumption \
  --project forecasting \
  --from dev \
  --to staging

Request production approval:

rebase model request-promotion se3-autoregressive-consumption \
  --project forecasting \
  --from staging \
  --to prod \
  --reason "Backtest passed against persistence and Hugging Face publication is recorded"

Approve the request:

rebase model approve-promotion <promotion-request-id> \
  --reason "Approved for production"

Promote to production:

rebase model promote se3-autoregressive-consumption \
  --project forecasting \
  --from staging \
  --to prod \
  --promotion-request-id <promotion-request-id>

Run production:

rebase model run se3-autoregressive-consumption \
  --project forecasting \
  --env prod \
  --param horizon_days=7

9. Inspect Lineage

List model versions and environment pointers:

rebase model versions se3-autoregressive-consumption --project forecasting
rebase model deployments se3-autoregressive-consumption --project forecasting

The Rebase model publication links the immutable model version to the Hugging Face repo and Hub commit. The Hub repo contains rebase_model.json, training_data.csv, and backtest.json.

On this page