Skip to content

Forecast

Forecasting building blocks for ordered single-series, panel, multi-horizon, and multi-target workflows. The core module uses pandas, NumPy, scikit-learn, and the standard BitBullet artifact format; specialised statistical models are available through an optional adapter.

Install

The reduction-based tools, temporal feature builders, baselines, evaluation, intervals, and reconciliation classes are included in the core installation:

pip install bitbullet

Install the optional statistical adapter only when you need native StatsForecast models:

pip install "bitbullet[forecast-statistical]"

Mental Model

A forecasting workflow has four explicit layers:

  1. ForecastSchema assigns time, target, entity, and covariate roles.
  2. ForecastFrame validates the data and establishes stable entity/time order.
  3. ForecastFeatureBuilder creates leakage-conscious supervised or future design rows.
  4. TabularForecaster fits ordinary regressors using direct, recursive, multi-output, or shared-horizon reduction.

Evaluation uses one long-form row per prediction. The canonical identity is fold, origin, time, horizon, entity, and target; observed and predicted values are y_true and y_pred.

Quick Start

from sklearn.linear_model import Ridge

from bitbullet.forecast import (
    ForecastConfig,
    ForecastFeatureBuilder,
    ForecastFrame,
    ForecastSchema,
    GroupedRollingFeature,
    RollingFeature,
    TabularForecaster,
)

schema = ForecastSchema(
    time="date",
    targets="sales",
    entities="store",
    static_covariates="store_size",
    historic_covariates="inventory",
    future_covariates=("promotion", "temperature"),
    cadence="D",
)
history = ForecastFrame(history_df, schema)

features = ForecastFeatureBuilder(
    target_lags=(1, 7, 14),
    exogenous_lags={"inventory": (1, 7)},
    rolling_features=(
        RollingFeature("sales", window=7, statistic="mean", lag=1),
    ),
    grouped_rolling_features=(
        # Trailing mean over the store's last eight observations sharing the
        # row's promotion state, read as of each row.
        GroupedRollingFeature("sales", by="promotion", window=8),
    ),
    calendar_features=("day_of_week_sin", "day_of_week_cos"),
)

forecaster = TabularForecaster(
    Ridge(alpha=1.0),
    config=ForecastConfig(horizons=(1, 2, 3), strategy="direct"),
    feature_builder=features,
).fit(history)

# future_df contains store, date, promotion, and temperature. Target values are
# neither required nor expected.
future_predictions = forecaster.forecast(future_df)

Schema and Data Roles

ForecastSchema makes availability assumptions visible:

Role Meaning Used from
time Ordering and forecast axis Every row
targets Values to forecast History and supervised labels
entities One or more panel identifiers Every row
static_covariates Constant within an entity Forecast origin
historic_covariates Observed no later than the origin Forecast origin
future_covariates Genuinely known for the forecasted time Future row
cadence Expected interval, such as "D" or integer step size Validation

Targets, entities, and all covariate roles may contain multiple columns. Role columns cannot overlap. Entity and covariate names also cannot reuse canonical forecast identity names such as origin, horizon, target, or y_true. Static covariates must remain constant within each entity.

ForecastFrame copies input data, performs a stable ascending entity/time sort, and preserves the original row labels in source_index. Duplicate entity/time rows are rejected by default; duplicate_policy="keep_first" and "keep_last" are explicit alternatives.

frame = ForecastFrame(data, schema, duplicate_policy="raise")

print(frame.groups)
print(frame.cadence_diagnostics.to_dict())
print(frame.diagnostics["missing_targets"])

Cadence diagnostics report inferred intervals, irregular series, missing periods, and duplicate counts. Set strict_cadence=True when an irregular series should fail validation. A declared cadence does not fill missing rows.

Horizons and Feature Framing

Shared-horizon reduction

ForecastConfig(horizons=(1, 7, 14), strategy="shared") fits one horizon-conditioned regressor per target and panel scope. With one target and panel_strategy="global", that is one fitted model across all entities and horizons. In contrast, strategy="direct" retains one model for each horizon. Existing strategies and defaults are unchanged.

Shared reduction uses every eligible long-form training example. It does not require the same label availability at every horizon or intersect complete output blocks. A collision-safe numerical horizon input is injected before preprocessing and recorded in state.training_metadata["horizon_feature"]; the external feature columns remain unchanged. Saved forecasters use the same input contract on replay. Entity identifiers are still identity-only unless explicitly requested as features. Sharing is an inductive assumption, not a guarantee of better forecasting accuracy.

Externally prepared calendar features

For already engineered tables, make_horizon_design aligns source rows without creating lags, rewriting data, filling dates or imputing targets:

import numpy as np
from bitbullet.forecast import (
    infer_period, place_panel_on_grid, make_horizon_design,
    horizon_holdout_split, FixedOriginExpandingWindowSplitter,
)

period = infer_period(history_df["date"]).period
grid = place_panel_on_grid(
    history_df["date"], period, entities=history_df[["store"]],
)
design = make_horizon_design(
    grid, horizons=(1, 7, 14),
    label_available=np.isfinite(history_df["sales"].to_numpy(dtype=float)),
)
X = design.features(
    history_df, origin_columns=("inventory",),
    known_ahead_columns=("promotion",),
)
y = history_df["sales"].iloc[design.forecast_rows].reset_index(drop=True)
outer = horizon_holdout_split(design, test_size=0.2)
training_design = design.subset(outer.train_indices)
cv = FixedOriginExpandingWindowSplitter(
    training_design, n_splits=3, end_position=outer.cutoff,
)

Every feature role is explicit. Origin features stay at the cutoff; known-ahead features come from the forecast date. Declaring a column known-ahead asserts that those values were genuinely knowable at the origin; historical availability alone does not prove that. Static features may use the origin role. Select a different horizon_column if its default name collides with a source column. Grouping columns are not automatically added as predictors.

The outer split reserves ceil(number_of_calendar_periods * test_size) periods. It evaluates only the configured lead times from the single cutoff immediately before that window; other reserved periods are unused, not fed back as observed history. A window shorter than the largest horizon is rejected. Training includes only examples whose labels occur by the cutoff. CV constructs earlier, non-overlapping fixed-origin windows and applies the same label boundary. All transforms and model selection must be fitted within each fold's training membership; the splitter cannot make preprocessing fitted outside a fold safe.

Eligibility counts distinguish out-of-history dates, absent entity-periods and unavailable labels. Partial series support remains explicit; a completely missing configured training/evaluation horizon is rejected. These new development APIs are not available in the published 0.6.0 release yet.

Horizons are positive one-based steps. An integer is exact: ForecastHorizon(7) means step 7, not steps 1 through 7. Use ForecastHorizon.up_to(7) for (1, 2, ..., 7).

from bitbullet.forecast import ForecastHorizon

one_week_ahead = ForecastHorizon(7)
next_week = ForecastHorizon.up_to(7)
selected_steps = ForecastHorizon((1, 3, 7))

Lags never cross entity boundaries and use one-based recency at an inclusive forecast origin: lag 1 is the latest value observed at that origin, lag 2 is the preceding value, and so on. Rolling features use the same convention, so a lag-1 window ends at the latest observed value. The label always occurs after the origin. Calendar features are derived from the forecasted time in direct-horizon rows.

single = features.make_supervised(frame, 1)
multiple = features.make_supervised(frame, (1, 3, 7))

print(multiple.X.columns)
print(multiple.identity.head())

SupervisedForecastFrame keeps X, optional y, and identity aligned. Its identity includes raw entity columns plus origin_time, forecast_time, horizon, target, origin_index, and forecast_index. Use select() to take one horizon, target, or entity without breaking alignment.

For inference, make_future() uses the final history row of each entity as its origin. Future target values are not required:

future_design = features.make_future(frame, future_df, (1, 2, 3))
assert future_design.y is None

Every future-known feature must be supplied for every requested future time. An identifier or timestamp can be an ordering column without being a predictive feature; the schema and design matrix are separate concerns.

Reduction Strategies

TabularForecaster accepts a cloneable estimator with familiar fit(X, y) and predict(X) methods.

Strategy Models Main trade-off
direct One model per target and horizon No recursive error propagation; model count grows with outputs
recursive One-step model per target Compact; later steps depend on earlier predictions
multioutput One estimator for the complete target/horizon matrix Learns outputs jointly; estimator must support multi-output regression
shared One horizon-conditioned model per target and panel scope Pools examples across lead times; sharing is not guaranteed to improve accuracy
direct = ForecastConfig(horizons=(1, 7, 14), strategy="direct")
recursive = ForecastConfig(horizons=(1, 2, 3), strategy="recursive")
joint = ForecastConfig(horizons=(1, 7), strategy="multioutput")
shared = ForecastConfig(horizons=(1, 7, 14), strategy="shared")

Recursive configurations must include horizon 1. During recursive inference, predicted targets are appended to history before the next step is built. Any other feature required at later origins must also be available or derived.

Panel strategy is independent of horizon strategy:

  • panel_strategy="global" shares models across entities.
  • panel_strategy="local" fits independent models for each entity.

Global models can pool signal across related series. Local models can preserve entity-specific dynamics when each series has enough history. Entity columns are identity-only by default. Set include_entity_columns=True on ForecastFeatureBuilder when the estimator should learn entity effects, and apply suitable categorical preprocessing when raw identifiers are not numeric. Static descriptors remain another useful way to share entity information.

Multiple target columns are a genuine output axis. Direct reduction fits each target/horizon pair separately. Multi-output reduction fits the full target × horizon matrix jointly.

Baselines

Always compare a learned forecaster with a simple ordered benchmark:

from bitbullet.forecast import SeasonalNaiveForecaster

baseline = SeasonalNaiveForecaster(seasonal_period=7).fit(
    frame.data,
    target_columns=frame.schema.target_columns,
    time_column=frame.schema.time,
    entity_columns=frame.schema.entity_columns,
)
baseline_predictions = baseline.predict((1, 2, 3))

The module includes naive, seasonal-naive, drift, and trailing-window-average forecasters. They are deterministic, panel-aware, and support multiple targets.

Rolling-Origin Backtesting

A backtest repeatedly fits a fresh forecaster on earlier origins and evaluates later origins. It is not an ordinary shuffled cross-validation loop.

from bitbullet.forecast import ForecastBacktester
from bitbullet.model_selection import ExpandingWindowSplitter

splitter = ExpandingWindowSplitter(
    n_splits=3,
    test_size=7,
    gap=2,
    min_train_size=28,
)
report = ForecastBacktester(splitter).run(frame, forecaster)

print(report.metrics.metrics)
print(report.folds[0].split.to_dict())
print(report.folds[0].purged_label_samples)

The splitter's gap removes origins immediately before each validation window. The backtester also removes any training example whose target would not yet be available at the validation boundary. Panel splits operate on synchronized unique origins, so entities are evaluated at the same times.

Each BacktestFold records temporal boundaries, training sample count, removed label count, validation-origin count, and prediction count. The complete report is JSON-oriented through to_dict().

Forecast Metrics

from bitbullet.forecast import evaluate_forecasts

metrics = evaluate_forecasts(report.predictions)
print(metrics.metrics)
print(metrics.grouped_metrics["horizon"])
print(metrics.grouped_metrics["series_horizon"])

Point metrics include RMSE, MAE, bias, WAPE, MAPE, and sMAPE. Percentage metrics use percentage points. Undefined denominators produce None, and each metric has a valid-observation count.

MASE and RMSSE require explicit in-sample history. Their scale denominators are calculated independently for each entity-target series and are never inferred from evaluation targets.

Conformal Prediction Intervals

Conformal intervals turn held-out residuals into distribution-free symmetric bounds. Calibration rows must be separate from model fitting rows.

import numpy as np

from bitbullet.forecast import (
    ConformalIntervalCalibrator,
    evaluate_interval_coverage,
)

calibrator = ConformalIntervalCalibrator(
    alpha=0.1,
    group_by=("horizon", "target"),
    min_group_size=20,
).fit(calibration_predictions)

future_with_intervals = calibrator.transform(future_predictions)
validation_with_intervals = calibrator.transform(validation_predictions)
coverage = evaluate_interval_coverage(validation_with_intervals)

Groups may use horizon, entity, and target. Sparse or unseen groups use the pooled residual quantile by default. Coverage reports include empirical coverage, miss direction, width, and interval score when alpha is known.

Conformal validity depends on the calibration design and stability assumptions; the class supplies the mechanics rather than claiming those assumptions hold.

Hierarchical Reconciliation

Hierarchical forecasts should add up. Define relationships, then reconcile bottom-level or all-node forecasts:

from bitbullet.forecast import HierarchicalReconciler, Hierarchy

hierarchy = Hierarchy.from_parent_map(
    {
        "store_north": "all_stores",
        "store_south": "all_stores",
        "all_stores": None,
    }
)
reconciler = HierarchicalReconciler(hierarchy, method="bottom_up")
coherent = reconciler.reconcile_frame(store_forecasts, actual_col=None)

bottom_up trusts bottom-node forecasts and aggregates them. ols projects an all-node base forecast onto the coherent subspace. Reconciliation is performed independently for each available fold/origin/time/horizon/target key.

Period Inference and the Calendar Grid

Horizons, withheld rows, holdouts and validation folds are all counted in periods, and a row count only equals a period count when consecutive rows are exactly one period apart. infer_period scores candidate calendars — daily, business-daily, weekly, month start/end, quarterly, yearly, hourly and minute multiples, plus the modal fixed interval, or integer steps for numeric axes — by how much of each candidate's grid the observed values cover, keeping only candidates on which every value sits. A Monday–Friday series with a few holidays missing therefore reads as business-daily with holes, not as daily with 30% of its days missing.

from bitbullet.forecast import (
    GridExpandingWindowSplitter,
    grid_holdout_split,
    infer_period,
    naive_forecasts_on_grid,
    place_on_grid,
    seasonal_period_for,
    shift_on_grid,
)

inference = infer_period(sales["date"], min_coverage=0.9)
if not inference.is_usable:
    raise ValueError(inference.reason)          # ambiguous or unsupported axis

grid = place_on_grid(sales["date"], inference.period)
y_h3 = shift_on_grid(sales["y"], grid, periods=3)   # label 3 periods ahead; NaN over holes
outer = grid_holdout_split(grid, test_size=0.2, gap=2)   # last 20% of periods
X_train, X_gap, X_test = outer.rows.partition(X)
folds = GridExpandingWindowSplitter(grid, n_splits=4, gap=2)   # inject as cv_splitter

place_on_grid maps each row to a slot on the complete grid of periods; empty slots are never rows. shift_on_grid and lag_on_grid look values up by slot, so a row whose target slot is empty receives NaN instead of a value from a later period. grid_holdout_split and GridExpandingWindowSplitter decide boundaries in slots and return ordinary positional TemporalSplit memberships over the sorted rows, so aligned objects are partitioned exactly as with the row splitters; on a complete grid the two agree exactly. Both refuse rows that share a slot: resolve duplicate periods first.

naive_forecasts_on_grid and naive_scale_on_grid give the naive and seasonal-naive forecasts for the value horizon periods ahead of each row and the MASE denominator, using seasonal_period_for for the conventional cycle (daily → 7, business-daily → 5, hourly → 24, monthly → 12).

Nothing here resamples, fills or reorders data. Aggregate irregular event data to a period yourself before treating it as a calendar.

For panel data, provide one entity column or a DataFrame of composite identity columns. Shifts remain within an entity, while holdout and validation cutoffs are synchronized on one shared calendar even when rows are entity-major and histories are unequal:

from bitbullet.forecast import (
    PanelGridExpandingWindowSplitter,
    panel_grid_holdout_split,
    place_panel_on_grid,
    shift_on_panel_grid,
)

panel = place_panel_on_grid(
    sales["date"],
    inference.period,
    entities=sales[["store_id", "product_id"]],
)
y_h3 = shift_on_panel_grid(sales["y"], panel, periods=3)
labelled = panel.subset_rows(np.isfinite(y_h3))  # full calendar tail remains available
model_grid = labelled.subset_rows(np.arange(labelled.n_rows), n_slots=panel.n_slots - 3)
outer = panel_grid_holdout_split(model_grid, test_size=0.2, gap=2)
training_grid = model_grid.subset_rows(
    outer.rows.train_indices,
    n_slots=outer.slot_metadata.train_end,
)
folds = PanelGridExpandingWindowSplitter(training_grid, n_splits=4, gap=2)

subset_rows never re-bases slot positions. Its default retains the original calendar tail; n_slots=N explicitly restricts a training view to the first N global slots. Panel entity codes and their original identity universe stay stable across subsets. An omitted entities argument is an implicit singleton and delegates shift and split behaviour to the original single-series path. Apply the same row selections to targets and features. Inner validation uses only the outer training grid; the final holdout stays outside model selection.

Artifact Replay

Fitted forecasters retain their schema-derived state, feature recipe, estimator collection, and optional bounded history:

forecaster.save("artifacts/sales_forecast.pkl")
restored = TabularForecaster.load("artifacts/sales_forecast.pkl")

replayed = restored.forecast(future_df)

Replay still requires the same future-known covariate contract. Set store_history=False only when inference callers will always supply an explicit history= frame.

Optional Statistical Models

StatsForecastAdapter is a lazy optional integration for native statistical models. It maps each entity-target pair to a native series, supports future-known exogenous variables, retains native interval columns through predict_raw(), and can emit the canonical long-form evaluation schema.

from statsforecast.models import AutoARIMA
from bitbullet.forecast.adapters import StatsForecastAdapter

adapter = StatsForecastAdapter([AutoARIMA(season_length=7)], freq="D")
adapter.fit(frame)
native_predictions = adapter.predict(h=7, future=future_df)

The core reduction workflow does not require this extra. Neural forecasting libraries are intentionally not core dependencies; custom adapters can implement NativeForecasterProtocol without changing the tabular contracts.

API Reference

Schema, Frames, and Features

bitbullet.forecast.schema.ForecastSchema dataclass

Column roles for one time series or a long-form collection of series.

Parameters:

Name Type Description Default
time str

Column containing timestamps or ordered integer time values.

required
targets ColumnInput

One target column or a sequence of target columns.

required
entities ColumnInput | None

Optional columns whose combined values identify a series.

None
static_covariates ColumnInput | None

Features that must remain constant within each entity. For a single unkeyed series they must remain constant over the complete frame.

None
historic_covariates ColumnInput | None

Features known only up to a forecast origin.

None
future_covariates ColumnInput | None

Features known for the times being forecast.

None
cadence Cadence | None

Optional declared date offset (for example "D" or "MS") or a positive integer step.

None

All role sets are disjoint. Unlisted columns are retained by :class:~bitbullet.forecast.frame.ForecastFrame but are not assigned a forecasting role.

covariate_columns property

All assigned covariates in role order.

entity_columns property

Normalized entity column names.

required_columns property

Columns required to validate a historical frame.

target_columns property

Normalized target column names.

time_column property

Name of the time column.

to_dict()

Return a JSON-friendly representation.

bitbullet.forecast.schema.ForecastHorizon dataclass

An explicit, canonical collection of positive forecast steps.

An integer represents that exact step: ForecastHorizon(7) means seven steps ahead. Use :meth:up_to for all steps from one through a maximum.

is_contiguous property

Whether every step from one through max_step is included.

max_step property

Largest requested step.

to_dict()

Return a JSON-friendly representation.

up_to(max_step) classmethod

Create the contiguous horizon 1..max_step.

bitbullet.forecast.frame.ForecastFrame

A validated and stably ordered historical forecasting frame.

The input is copied. Rows are ordered by entity columns and then time using a stable sort, while :attr:source_index retains every row's original index label. Duplicate entity/time keys fail by default and can only be resolved through an explicit policy.

data property

A defensive copy of the ordered frame.

diagnostics property

JSON-friendly structural diagnostics.

groups property

Entity keys in stable first-appearance order.

is_panel property

Whether entity columns identify multiple series.

source_index property

Original input index labels aligned with :attr:data.

time_values property

Ordered time values, including repeated times across entities.

copy()

Create an independent equivalent frame.

iter_series(*, copy=True)

Iterate over entity keys and their time-ordered rows.

to_long_targets(*, target_name='target', value_name='y')

Return one row per entity/time/target observation.

Covariates and unassigned columns are retained. This is primarily useful for multiple-target evaluation and visualization.

bitbullet.forecast.features.ForecastFeatureBuilder

Construct forecasting features and direct-horizon supervised frames.

Target and exogenous lags are grouped by entity and use one-based recency: lag one is the latest value observed at the inclusive forecast origin. Rolling features follow the same convention. During direct-horizon framing, static and historic features come from the forecast origin, while future-known covariates and calendar features come from the forecasted time.

Entity identifiers remain identity-only unless include_entity_columns=True. Raw identifiers may require categorical preprocessing before an estimator can consume them.

make_future(history, future, horizon, *, drop_incomplete=True)

Create features for future rows without requiring target values.

Each entity's final historical row is the forecast origin. future must supply entity/time keys and future-known covariates for every requested step. Missing static values are inherited from history.

make_supervised(frame, horizon, *, drop_incomplete=True)

Create a long direct-horizon training frame.

For each origin, target, and requested step, y is taken from the future row within the same entity. No values cross entity boundaries.

transform(frame)

Return the ordered frame with engineered features appended.

bitbullet.forecast.features.RollingFeature dataclass

A rolling statistic calculated from observations known at an origin.

Lags use one-based recency: lag=1 ends the window at the forecast origin's latest observed value, lag=2 ends it one row earlier, and so on. The forecast label always occurs after the origin, so the origin value is available without exposing the future label.

bitbullet.forecast.features.GroupedRollingFeature dataclass

A rolling statistic over one group's history, read as of each row.

The statistic is computed independently over each group's own ordered observations — groups are named by the by column — and carried forward, so at any row it reads "as of" the group's most recent observation at or before that row. A weekday by column, for example, yields the trailing same-weekday level, spread, or extreme: the seasonal profile features that lag and plain rolling statistics cannot express.

Which group a row reads defaults to its own by value but can be redirected with key: a column naming another group whose statistic the row should carry. A key describing a future period's group — the weekday of the period being forecast, which the calendar fixes in advance — stays leakage-free, because the statistic itself never uses observations after the row; only the choice of which past to summarise changes.

Lags follow the one-based recency convention of :class:RollingFeature, counted in group observations: lag=1 ends the window at the group's latest observation at or before the row, lag=2 one group observation earlier.

bitbullet.forecast.features.SupervisedForecastFrame dataclass

Features, optional observations, and identity for forecast samples.

Rows use a long representation: each row identifies exactly one entity, forecast origin, forecast time, horizon, and target. y is present for supervised historical framing and None for future design frames.

select(*, horizon=None, target=None, entity=None)

Select rows while preserving aligned features and identity.

to_frame()

Combine identity, observations, and features into one frame.

to_prediction_frame(y_pred=None, *, fold=None, retain_entity_columns=True)

Return canonical keys for evaluation, plotting, or persistence.

Forecasters and Backtesting

bitbullet.forecast.config.ForecastConfig dataclass

Controls reduction strategy and panel model sharing.

direct trains one model for each target/horizon pair. recursive trains one-step models and feeds predictions back through the feature recipe. multioutput fits one estimator to the complete output matrix and therefore requires an estimator with multi-output support. shared fits one horizon-conditioned estimator per target and panel scope, using every eligible long-form example rather than intersecting complete horizon blocks. A global, single-target recipe has one model.

to_dict()

Return a JSON-friendly representation.

bitbullet.forecast.forecaster.TabularForecaster

Turn a regressor into direct, recursive, multi-output, or shared forecasts.

The class owns model collections and forecasting identity. Estimators keep their familiar fit(X, y)/predict(X) contract, while :class:ForecastFeatureBuilder owns temporal framing and inference history.

Parameters:

Name Type Description Default
estimator Optional[RegressorProtocol]

Cloneable estimator prototype.

None
estimator_factory Optional[EstimatorFactory]

Factory used when an estimator cannot be cloned.

None
config Optional[ForecastConfig]

Reduction and panel strategy configuration.

None
feature_builder Optional[ForecastFeatureBuilder]

Feature recipe used for historical and future framing.

None
preprocessor Any

Optional sklearn-compatible transformer or BitBullet TransformPipeline prototype. A fresh copy is fitted inside every target/horizon/scope model, and therefore inside every backtest fold.

None

state property

Fitted forecasting state.

__getstate__()

Return pickle state with inference-only transform pipelines detached.

TransformPipeline keeps a construction registry containing local factory callables. Fitted forecasting artifacts need only its concrete transformer chain, so the registry is removed from serialized copies. Likewise, an unpickleable estimator factory is not needed for inference and is detached; the loaded artifact then reports a clear error if a caller attempts to refit it. The live forecaster and caller prototypes remain untouched.

fit(data, *, sample_weight=None)

Fit from an ordered historical frame or pre-built supervised frame.

fit_supervised(supervised, *, sample_weight=None)

Fit reduction models from an explicitly framed training set.

forecast(future, *, history=None, horizons=None)

Generate future forecasts from stored or explicitly supplied history.

load(path) classmethod

Load a fitted forecaster saved by :meth:save.

predict(data)

Scikit-style alias for :meth:predict_supervised.

predict_supervised(supervised)

Predict rows from a historical or future design frame.

save(path, *, name=None)

Persist the fitted forecaster through :class:ModelSerializer.

to_model_metadata(*, name='forecast_model')

Build generic artifact metadata for this fitted forecaster.

bitbullet.forecast.backtesting.ForecastBacktester

Refit a forecaster at ordered origins and evaluate future observations.

Splits operate on synchronized unique origin values rather than pooled panel rows. Training examples whose labels occur after the first validation origin are purged automatically, in addition to the splitter's explicit gap.

run(frame, forecaster=None, *, forecaster_factory=None, include_prediction_values=True)

Run a complete ordered backtest.

Exactly one forecaster prototype or factory is required. A fresh forecaster is created for every fold, so estimator state never crosses a validation boundary.

bitbullet.forecast.backtesting.BacktestReport dataclass

Long-form forecasts, metrics, and fold provenance.

Period Inference and Calendar Grid

  • infer_period(values, *, min_coverage=0.9, candidates=None, min_unique_values=3)PeriodInference
  • PeriodInference (axis_kind, status, period, alternatives, duplicate_rows, is_usable, to_dict())
  • PeriodCandidate (alias, kind, freq, coverage, expected_slots, observed_slots, off_grid_values, missing_slots)
  • place_on_grid(values, period)CalendarGrid (positions, n_slots, slot_values, occupied, missing_slots, duplicate_rows)
  • shift_on_grid(values, grid, periods), lag_on_grid(values, grid, periods)
  • grid_holdout_split(grid, *, test_size, gap=0, min_train_rows=1, min_test_rows=1)GridSplit (rows: TemporalSplit, slot_metadata)
  • GridExpandingWindowSplitter(grid, *, n_splits=5, test_size=None, gap=0, min_train_size=1, step_size=None)
  • place_panel_on_grid(values, period, *, entities=None)PanelCalendarGrid (entity_keys, entity_codes, entity_values, series_count)
  • shift_on_panel_grid(values, grid, periods), lag_on_panel_grid(values, grid, periods)
  • panel_grid_holdout_split(grid, *, test_size, gap=0, min_train_rows=1, min_test_rows=1)GridSplit
  • PanelGridExpandingWindowSplitter(grid, *, n_splits=5, test_size=None, gap=0, min_train_size=1, step_size=None)
  • naive_forecasts_on_grid(values, grid, *, horizon, seasonal_period=None), naive_scale_on_grid(values, grid, *, seasonal_period=1), seasonal_period_for(period)

Baselines

bitbullet.forecast.baselines.NaiveForecaster

Bases: BaseBaselineForecaster

Repeat the most recently observed value.

bitbullet.forecast.baselines.SeasonalNaiveForecaster

Bases: BaseBaselineForecaster

Repeat the observation from the matching seasonal position.

bitbullet.forecast.baselines.DriftForecaster

Bases: BaseBaselineForecaster

Extrapolate the average change from the first to last observation.

bitbullet.forecast.baselines.WindowAverageForecaster

Bases: BaseBaselineForecaster

Repeat the mean of the most recent fixed-size window.

Evaluation and Intervals

bitbullet.forecast.evaluation.evaluate_forecasts(predictions, *, insample=None, insample_value_col='y', seasonal_period=1, groupings=None, include_values=True)

Evaluate long-form point forecasts.

MASE and RMSSE require an explicit in-sample history. That history must be long-form with time and insample_value_col plus matching entity and target columns when relevant. Scale denominators are calculated independently for every entity-target series using the requested seasonal lag; they are never inferred from evaluation targets.

Parameters:

Name Type Description Default
predictions DataFrame

Canonical long-form forecast predictions.

required
insample Optional[DataFrame]

Optional history used only for MASE/RMSSE denominators.

None
insample_value_col str

Numeric value column in insample.

'y'
seasonal_period int

Positive seasonal lag used by scaled errors.

1
groupings Optional[Mapping[str, Sequence[str]]]

Mapping from report section name to canonical key columns. Defaults to horizon, entity, target, fold, and series-horizon views.

None
include_values bool

Include canonical row-level predictions in the report.

True

Returns:

Type Description
ForecastMetricsReport

class:ForecastMetricsReport containing overall and grouped metrics.

bitbullet.forecast.evaluation.ForecastMetricsReport dataclass

JSON-friendly overall and grouped forecast metrics.

bias is defined as mean(y_pred - y_true). Positive values indicate over-forecasting. MASE and RMSSE are only populated when an explicit in-sample history is supplied to :func:evaluate_forecasts.

to_dict()

Return a strict-JSON-ready dictionary.

bitbullet.forecast.intervals.ConformalIntervalCalibrator

Calibrate symmetric forecast intervals from absolute residuals.

The default calibrates a separate residual quantile for every forecast horizon. Add entity and/or target to group_by for more local intervals, or pass an empty sequence (or "pooled") for one global interval width. Sparse and unseen groups fall back to the pooled quantile by default.

Calibration rows must be held out from model fitting. This class does not fit or inspect the forecasting model itself.

is_fitted property

Whether residual quantiles have been learned.

fit(calibration)

Learn finite-sample conformal residual quantiles.

fit_transform(calibration, *, lower_col='lower', upper_col='upper')

Fit residual quantiles and add intervals to the same rows.

transform(predictions, *, lower_col='lower', upper_col='upper')

Add calibrated lower and upper bounds to point predictions.

y_true is not required for future prediction rows. All canonical identity columns and caller-supplied extra columns are retained.

bitbullet.forecast.intervals.evaluate_interval_coverage(intervals, *, lower_col='lower', upper_col='upper', alpha=None, groupings=None, include_values=True)

Evaluate empirical interval coverage and sharpness.

When alpha is supplied, the report also includes the standard interval score, which penalizes misses in proportion to their distance outside the interval. A frame returned by :class:ConformalIntervalCalibrator carries its alpha in DataFrame.attrs and can therefore omit it here.

Hierarchies

bitbullet.forecast.hierarchy.Hierarchy dataclass

A summing-matrix representation of a hierarchy.

Rows of summing_matrix follow nodes and columns follow bottom_nodes. Bottom-node rows must form an identity matrix. This representation also supports forests with more than one root.

bottom_indices property

Positions of bottom nodes in the all-node vector.

aggregate(bottom_values)

Aggregate bottom-level values into all hierarchy nodes.

The final array dimension must follow bottom_nodes. Any leading dimensions are retained.

coherence_error(values)

Maximum absolute hierarchy constraint violation.

from_edges(edges, *, node_order=None) classmethod

Build a hierarchy from (parent, child) edges.

from_parent_map(parent_map, *, node_order=None) classmethod

Build a hierarchy from node -> parent relationships.

Roots may map to None or may appear only as a referenced parent. Leaves become bottom nodes. Every non-root node has exactly one parent by construction.

is_coherent(values, *, atol=1e-10)

Whether all-node values agree with their bottom-level aggregation.

to_dict()

Return a JSON-friendly hierarchy definition.

bitbullet.forecast.hierarchy.HierarchicalReconciler

Reconcile forecasts so every hierarchy aggregation constraint holds.

bottom_up ignores base forecasts above the bottom level and aggregates the supplied bottom forecasts. ols projects an all-node base forecast onto the coherent subspace using ordinary least squares.

projection_matrix property

Return a copy of the all-node reconciliation projection.

reconcile(base_forecasts)

Reconcile an array whose final dimension contains hierarchy nodes.

reconcile_frame(forecasts, *, node_col='entity', value_col='y_pred', actual_col='y_true', group_columns=None)

Reconcile long-form forecasts independently for every forecast key.

By default, available fold, origin, time, horizon, and target columns define independent forecast vectors. The returned frame contains those keys, node_col, and the reconciled value. When actual_col exists in the input, bottom-level actuals are also aggregated and retained for direct evaluation. Bottom-up input may contain bottom nodes only; OLS requires every node.

Optional Adapters

bitbullet.forecast.adapters.statsforecast.StatsForecastAdapter

Expose StatsForecast models through multi-target panel contracts.

Each (entity, target) pair becomes one StatsForecast unique_id. :meth:predict_raw retains StatsForecast's wide model and interval columns, :meth:predict returns one tidy row per model forecast, and :meth:to_prediction_frame selects one model into BitBullet's canonical long-form forecast schema.

Static and future-known covariates are supplied to StatsForecast as exogenous variables. Historic-only covariates are intentionally excluded: StatsForecast requires every fitted exogenous column at prediction time.

is_fitted property

Whether native models and adapter metadata have been fitted.

fit(data, *, prediction_intervals=None)

Fit native statistical models from a ForecastFrame.

prediction_intervals is passed to StatsForecast 2.x's fit method. Interval levels themselves are requested when predicting.

load(path) classmethod

Load an adapter saved by :meth:save.

predict(h, *, future=None, levels=None)

Return point forecasts in tidy multi-model form.

Prediction interval columns are available from :meth:predict_raw. Use :meth:to_prediction_frame when one selected model must feed the canonical forecast evaluation contract.

predict_raw(h, *, future=None, levels=None)

Return native wide predictions with entity and target metadata.

The returned frame always has explicit unique_id and ds columns, regardless of whether StatsForecast returned identifiers as columns or index levels. Point and interval columns retain their native StatsForecast names.

save(path)

Persist the adapter, fitted native engine, and schema metadata.

to_prediction_frame(h, *, model=None, future=None, levels=None)

Return one selected model in the canonical forecast-row schema.

model may be omitted when StatsForecast returned exactly one point model. With multiple models it is required so canonical prediction keys cannot silently be duplicated.