import qrt as qTemporal split
q.dataset.TemporalSplit creates one fixed chronological split. Partitions can be defined either by row counts or proportions, or by inclusive index boundaries. This notebook starts with a proportional train/validation/test split, then demonstrates exact boundaries, a bounded test period, and a custom split name.
Create the smallest useful dataset
A temporal split needs only an ordered Dataset. Targets, sample weights, and row metadata are optional. This example creates one-session-ahead labels and keeps the latest 12 labelable SPY sessions so the split sizes remain easy to inspect.
metadata is reserved for row-aligned context that is not a model input, such as a label’s end time, symbol, or event identifier. TemporalSplit does not read it; it partitions rows from the dataset index. Metadata becomes necessary for operations such as PurgedTimeSeriesSplit, which uses label end times to remove overlapping training labels.
spy = q.data.datasets.load("spy")
labels = q.label.fixed_horizon(spy["close"], 1)
dataset = q.dataset.Dataset(
X=spy.loc[labels.index, ["close"]],
y=labels,
)
assert dataset.index.is_monotonic_increasing # We ensure that the index is sorted in increasing order, which is important for temporal datasets.
labels| end_time | return | threshold | label | |
|---|---|---|---|---|
| event_time | ||||
| 2000-01-03 | 2000-01-04 | -0.039106 | 0.0 | -1 |
| 2000-01-04 | 2000-01-05 | 0.001789 | 0.0 | 1 |
| 2000-01-05 | 2000-01-06 | -0.016071 | 0.0 | -1 |
| 2000-01-06 | 2000-01-07 | 0.058076 | 0.0 | 1 |
| 2000-01-07 | 2000-01-10 | 0.003430 | 0.0 | 1 |
| ... | ... | ... | ... | ... |
| 2026-07-16 | 2026-07-17 | -0.009897 | 0.0 | -1 |
| 2026-07-17 | 2026-07-20 | -0.001614 | 0.0 | -1 |
| 2026-07-20 | 2026-07-21 | 0.008341 | 0.0 | 1 |
| 2026-07-21 | 2026-07-22 | -0.001163 | 0.0 | -1 |
| 2026-07-22 | 2026-07-23 | -0.012349 | 0.0 | -1 |
6677 rows × 4 columns
1. Split by proportion or row count
Use size arguments when the desired partition lengths matter more than exact dates. Floats represent proportions of the full dataset; integers represent exact row counts. Partitions are contiguous, preserve chronological order, and consume every row.
| Parameter | Default | Effect |
|---|---|---|
train_size |
None |
Training proportion or row count |
validation_size |
None |
Validation proportion or row count; None omits validation |
test_size |
None |
Test proportion or row count |
Float sizes must sum to 1, and integer sizes must sum to the dataset length. One of train_size or test_size may be omitted to receive the remainder. Size arguments cannot be combined with boundary arguments.
Proportional boundaries are rounded to whole rows while preserving the requested cumulative allocation. For this 12-row dataset, 70/15/15 produces 8 train, 2 validation, and 2 test rows.
proportional = dataset.split(
q.dataset.TemporalSplit(
train_size=0.70,
validation_size=0.15,
test_size=0.15,
)
)
q.dataset.split_diagnostics(proportional, "default")| role | rows | proportion | start | end | |
|---|---|---|---|---|---|
| partition | |||||
| train | fit | 4673 | 0.699865 | 2000-01-03 | 2018-07-30 |
| validation | evaluate | 1002 | 0.150067 | 2018-07-31 | 2022-07-22 |
| test | holdout | 1002 | 0.150067 | 2022-07-25 | 2026-07-22 |
| excluded | excluded | 0 | 0.000000 | NaT | NaT |
2. Split with train_end
Use inclusive index boundaries when partition cutoffs must align with specific dates or index values. In boundary mode, train_end is required and the row at that index remains in train. The defaults provide the simplest train/test holdout:
| Parameter | Default | Effect |
|---|---|---|
train_end |
required | Last row assigned to train |
validation_end |
None |
Omits the validation partition |
test_end |
None |
Extends test through the final dataset row |
name |
"default" |
Names the split inside the default scheme |
basic = dataset.split(q.dataset.TemporalSplit(train_end="2000-01-05"))
membership = basic.splits["default"].split().membership
basic.X.assign(partition=membership)| close | partition | |
|---|---|---|
| event_time | ||
| 2000-01-03 | 91.132759 | train |
| 2000-01-04 | 87.568886 | train |
| 2000-01-05 | 87.725533 | train |
| 2000-01-06 | 86.315674 | test |
| 2000-01-07 | 91.328545 | test |
| ... | ... | ... |
| 2026-07-16 | 750.719971 | test |
| 2026-07-17 | 743.289978 | test |
| 2026-07-20 | 742.090027 | test |
| 2026-07-21 | 748.280029 | test |
| 2026-07-22 | 747.409973 | test |
6677 rows × 2 columns
Inspect partition views
The minimum call creates one fixed train/test assignment. train_end belongs to train; because validation_end=None, the next row begins test; because test_end=None, test continues through the final row.
flowchart LR
TRAIN["train<br/>rows 1-6<br/>includes train_end"] -->|next row| TEST["test<br/>rows 7-12<br/>through dataset end"]
TEST -.-> EXCLUDED["excluded<br/>0 rows"]
classDef train fill:#d8f3dc,stroke:#2d6a4f,color:#16382a
classDef test fill:#ffefc1,stroke:#9c6b00,color:#4d3500
classDef excluded fill:#eceff1,stroke:#6b7280,color:#374151
class TRAIN train
class TEST test
class EXCLUDED excluded
Splitting returns a new dataset and leaves the original unsplit. Conventional attributes and named indexing return lazy partition views over the split dataset. Diagnostics summarize every partition, including the empty excluded partition.
q.dataset.split_diagnostics(basic, "default")| role | rows | proportion | start | end | |
|---|---|---|---|---|---|
| partition | |||||
| train | fit | 3 | 0.000449 | 2000-01-03 | 2000-01-05 |
| test | holdout | 6674 | 0.999551 | 2000-01-06 | 2026-07-22 |
| excluded | excluded | 0 | 0.000000 | NaT | NaT |
3. Add validation_end
Set validation_end to create a three-way boundary split. This boundary is also inclusive: validation begins immediately after train_end and ends at validation_end; test begins on the next row.
validation_end = dataset.index[7]
three_way = dataset.split(
q.dataset.TemporalSplit(
train_end=dataset.index[4],
validation_end=validation_end,
)
)
q.dataset.split_diagnostics(three_way, "default")| role | rows | proportion | start | end | |
|---|---|---|---|---|---|
| partition | |||||
| train | fit | 5 | 0.000749 | 2000-01-03 | 2000-01-07 |
| validation | evaluate | 3 | 0.000449 | 2000-01-10 | 2000-01-12 |
| test | holdout | 6669 | 0.998802 | 2000-01-13 | 2026-07-22 |
| excluded | excluded | 0 | 0.000000 | NaT | NaT |
4. Bound the test period with test_end
By default, test consumes every row after the preceding boundary. Set test_end when later observations must remain outside this experiment. Rows after that inclusive boundary are retained in the dataset and assigned to excluded.
test_end = dataset.index[9]
bounded = dataset.split(
q.dataset.TemporalSplit(
train_end=dataset.index[3],
validation_end=dataset.index[6],
test_end=test_end,
)
)
q.dataset.split_diagnostics(bounded, "default")| role | rows | proportion | start | end | |
|---|---|---|---|---|---|
| partition | |||||
| train | fit | 4 | 0.000599 | 2000-01-03 | 2000-01-06 |
| validation | evaluate | 3 | 0.000449 | 2000-01-07 | 2000-01-11 |
| test | holdout | 3 | 0.000449 | 2000-01-12 | 2000-01-14 |
| excluded | excluded | 6667 | 0.998502 | 2000-01-18 | 2026-07-22 |
5. Give the split a descriptive name
A TemporalSplit is attached as the dataset’s default scheme because it represents one fixed holdout design. Changing name identifies the split within that scheme, which is useful when recording the purpose or date of a holdout.
named = dataset.split(
q.dataset.TemporalSplit(
train_end=dataset.index[7],
name="final_holdout",
)
)
assert tuple(named.splits) == ("default",)
assert named.splits["default"].split().name == "final_holdout"
q.dataset.split_diagnostics(named, "default", "final_holdout")| role | rows | proportion | start | end | |
|---|---|---|---|---|---|
| partition | |||||
| train | fit | 8 | 0.001198 | 2000-01-03 | 2000-01-12 |
| test | holdout | 6669 | 0.998802 | 2000-01-13 | 2026-07-22 |
| excluded | excluded | 0 | 0.000000 | NaT | NaT |
Split rules
Choose TemporalSplit when one fixed chronological holdout is sufficient. The dataset index must be monotonic increasing, and size arguments cannot be combined with boundary arguments.
In size mode, use either all proportions or all row counts. Requested partitions must be non-empty and consume the full dataset. In boundary mode, validation_end must be later than train_end, and test_end must be later than the preceding boundary.
Use TimeSeriesSplit when evaluation must repeat across multiple walk-forward folds, or PurgedTimeSeriesSplit when overlapping label horizons require leakage controls.