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:
ForecastSchemaassigns time, target, entity, and covariate roles.ForecastFramevalidates the data and establishes stable entity/time order.ForecastFeatureBuildercreates leakage-conscious supervised or future design rows.TabularForecasterfits 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 |
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
|
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)→PeriodInferencePeriodInference(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)→GridSplitPanelGridExpandingWindowSplitter(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 |
'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: |
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.