Model Selection
Position-based splitting for ordered observations. These utilities work with forecasting, temporal classification, ordered regression, or any workflow where future rows must not train a model evaluated on earlier rows.
Ordering Is Explicit
Temporal splitters never guess a time column and never reorder data. Establish the canonical order first, then use the same positional membership for features, targets, weights, identities, and reporting axes.
ordered = data.sort_values("event_time", kind="mergesort").reset_index(drop=True)
origins = ordered["event_time"].tolist()
For forecasting data, ForecastFrame already establishes stable entity/time
order. ForecastBacktester synchronizes panel rows by unique origin before it
calls a splitter.
Final Ordered Holdout
from bitbullet.model_selection import ordered_holdout_split
split = ordered_holdout_split(
ordered,
test_size=0.2,
gap=7,
min_train_size=30,
origins=origins,
)
X_train, X_gap, X_test = split.partition(X)
y_train, y_gap, y_test = split.partition(y)
print(split.metadata.to_dict())
The holdout is always the final test_size positions. An integer is an exact
row count; a fraction is rounded up. gap positions immediately before the
holdout belong to neither training nor test. max_train_size can retain only
the most recent pre-gap history.
Expanding Windows
from bitbullet.model_selection import ExpandingWindowSplitter
splitter = ExpandingWindowSplitter(
n_splits=4,
test_size=14,
gap=7,
min_train_size=60,
step_size=14,
)
for split in splitter.split_with_metadata(ordered, origins=origins):
X_train, X_gap, X_valid = split.partition(X)
y_train, y_gap, y_valid = split.partition(y)
An expanding window begins at position zero unless max_train_size is set.
Validation windows are anchored so the final split ends at the final row.
step_size controls the distance between validation starts; it defaults to the
validation size.
Rolling Windows
from bitbullet.model_selection import RollingWindowSplitter
splitter = RollingWindowSplitter(
max_train_size=180,
n_splits=4,
test_size=14,
gap=7,
step_size=14,
)
A rolling window caps retained training history. By default its minimum training
size equals max_train_size, producing fixed-width windows. Set a smaller
min_train_size to permit a shorter earliest window.
Gap Semantics
A temporal gap removes positions immediately before each validation boundary. It is useful when labels mature after their feature row, when windows overlap, or when operational latency means recent examples would not yet be trainable.
The splitter treats the gap as a row count. It does not infer cadence, target lead, embargo length, or label availability. Choose the gap from the data construction and prediction contract. Forecast backtesting adds a second safety check by removing framed samples whose target time is later than the validation boundary.
Shared Positional Membership
TemporalSplit.partition_many() applies one immutable membership definition to
multiple aligned objects:
(X_parts, y_parts, weight_parts, id_parts) = split.partition_many(
X,
y,
sample_weight,
row_identity,
)
Each result is (train, gap, test). Pandas inputs retain index and type; NumPy
arrays are taken along axis zero; lists and tuples retain their container type.
For scikit-learn-compatible consumers, split() and as_cv_indices() return
only (train_indices, validation_indices). Gap membership remains available on
the full TemporalSplit.
Auditable Metadata
All boundaries are half-open. For example, train_start=2 and train_end=7
describe positions [2, 7).
SplitMetadata records:
- train, gap, and test boundaries and sizes;
- discarded prefix and unused suffix counts;
- validation origin position;
- optional first and last origin values for every partition.
Origin values enrich metadata only. They never change membership or trigger a sort.
metadata = splitter.get_split_metadata(ordered, origins=origins)
for fold in metadata:
print(fold.split_number, fold.test_origin_start, fold.test_origin_end)
API Reference
bitbullet.model_selection.temporal.ordered_holdout_split(data_or_count, *, test_size, gap=0, min_train_size=1, max_train_size=None, origins=None)
Create one final ordered holdout with an optional preceding gap.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data_or_count
|
Any
|
Any sized object, or its positive integer row count. |
required |
test_size
|
SizeLike
|
Positive row count or fraction in |
required |
gap
|
int
|
Number of positions excluded immediately before the holdout. |
0
|
min_train_size
|
int
|
Minimum retained training positions. |
1
|
max_train_size
|
Optional[int]
|
Optional cap retaining only the most recent training positions before the gap. |
None
|
origins
|
Optional[Sequence[Any]]
|
Optional values aligned one-to-one with rows. They enrich metadata but never reorder data. |
None
|
bitbullet.model_selection.temporal.ExpandingWindowSplitter
Bases: _BaseWindowSplitter
Expanding temporal validation windows with optional history cap.
Validation windows are anchored so the final split ends at the final input
row. Earlier validation starts are separated by step_size (defaulting
to the resolved validation size). With max_train_size=None, every split
begins training at position zero.
bitbullet.model_selection.temporal.RollingWindowSplitter
Bases: _BaseWindowSplitter
Fixed/restricted-history temporal validation windows.
max_train_size is required and caps every training window. By default
min_train_size equals that cap, producing fixed-width training windows.
A smaller explicit minimum permits a shorter earliest window.
bitbullet.model_selection.temporal.TemporalSplit
dataclass
Positional train/gap/test indices plus their exact metadata.
as_cv_indices()
Return the sklearn-style (train, validation) pair.
Gap positions remain excluded from both arrays and are available through
:attr:gap_indices and :attr:metadata.
partition(values)
Apply this split to one positionally aligned object.
Pandas inputs retain their index and type, NumPy arrays are taken along axis zero, and lists/tuples retain their container type.
partition_many(*values)
Apply the same positional membership to multiple aligned objects.
bitbullet.model_selection.temporal.SplitMetadata
dataclass
Exact boundaries and optional origin values for one temporal split.
origin_position is the first validation/test position. When callers
supply origins, origin_value is the value at that same position.
The module records origins for audit and reporting only; origin values never
alter membership or trigger an implicit sort.
prefix_discarded
property
Rows before a capped/rolling training window.
suffix_unused
property
Rows after this split's validation window.
validation_end
property
Alias for the exclusive test/validation end.
validation_start
property
Alias for the first test/validation position.
to_dict()
Return flat, JSON-oriented boundary metadata.
Date-like origin values are intentionally retained as their original objects so callers can choose their own ISO/string serialization policy.