Preparing Feature Matrices for Time Series Models
A time series is split into training and test sets by position, using one index n_train: features[:n_train] trains and features[n_train:] tests, so the test set holds the latest observations.
Why Order Matters in Time Series
A time series is a sequence of values recorded in order over time, such as the number of bicycle rentals on each day. Unlike a table of unrelated customers, the position of each observation carries meaning: Tuesday's count comes after Monday's and before Wednesday's. That ordering changes how we should judge a model.
When you evaluate a model on this kind of data, the model should be judged on how well it predicts values it has not seen. In a time series, the unseen values are usually the most recent ones. The practical setup is to split the data into a training part and a test part, fit a model on the first part, score it on the second, and plot both parts together so the result can be inspected.
The rest of this article follows one reusable evaluation function. Before reading its code line by line, ask what it has to do: receive the features, the target values, and a regressor; decide which rows train and which rows test; fit the regressor; report a score; and draw a figure. Each step leaves a trace you can check.
Splitting by Position, Not by Shuffling
The split is controlled by one index, n_train. The rows before that index form the training set, and the rows from that index onward form the test set. In the source, n_train is fixed at 184, so the first 184 data points train the model and the remaining points test it.
The split is positional, which means it depends only on where each row sits in the sequence. Nothing is shuffled. Because of that, every training row comes earlier in time than every test row. The test set therefore holds the latest observations, and the model is asked to predict the period that follows what it learned from.
What do you think happens?
If the feature array has 8 rows and n_train is 5, which index is the first row of the test set?
Reveal answer
5
The test slice starts at n_train, so index 5 is the first test row. Indices 0 through 4 are the five training rows.
Reading the Slice Boundary
The split is done with slicing. A slice written as features[:n_train] takes every element from the start up to, but not including, index n_train. A slice written as features[n_train:] takes every element from index n_train to the end. The two slices together cover the whole array exactly once, with no overlap and no gap.
The same boundary is applied to the target array. That is what keeps each training row paired with its target value and each test row paired with its target value. If the features were cut at one point and the targets at another, the rows would no longer line up.
[10, 12, 14, 13, 15]
[17, 19, 21]- data[:
n_train] starts at index 0 and stops before index 5 - data[
n_train:] starts at index 5 and runs to the end - The two slices cover all 8 elements with no overlap
Tracing the Evaluation Function
The function receives three inputs: a feature array, a target array, and a regressor. It uses n_train to slice both arrays, fits the regressor on the training pair, makes predictions, scores the model on the test pair, and draws the figure. The data moves in one direction, which the diagram below traces from the full inputs to the printed score.
Tracing one call with a small array
Using the slicing rule from the previous section, follow the data for an 8-row feature array and n_train = 5.
- 1Slice the features
features[:5] gives rows 0 to 4, which become X_train. features[5:] gives rows 5 to 7, which become X_test.
- 2Slice the targets
The same cut is applied to the target array, so
y_trainholds the targets for rows 0 to 4 andy_testholds the targets for rows 5 to 7. - 3Fit
regressor.fit receives only X_train and
y_train. The test rows are not used at this stage. - 4Score
regressor.score receives X_test and
y_test. The model is checked only on rows it was not fitted on. - 5Report
The function prints the R2 value rounded to two decimal places.
The model is trained on rows 0 to 4 and its printed R2 describes how well it does on rows 5 to 7.
Scoring Only on Unseen Rows
After fitting, the function calls regressor.score on the test pair, X_test and y_test, and prints the result with two decimal places. The R2 score is the number that summarises how well the model predicts the test set.
The important detail is which rows the score sees. It sees only the test rows, which the model was not fitted on. A score computed on the training rows would tell you how well the model fits data it has already learned from, which is a weaker check. A model can look good on its own training rows and still fail on the period that follows.
The printed value is a single number. Use it together with the plot rather than on its own. The plot shows where predictions follow the real values and where they drift, while the score compresses that comparison into one figure.
Reading the Prediction Plot
The plotting code draws four series on one set of axes. The training actual values and the training predictions run over the index range from 0 up to n_train. The test actual values and the test predictions start at n_train and continue for as many points as there are in y_test. Each series has a label, so the legend can tell them apart.
Read the plot from left to right. The left segment, ending at n_train, shows training behaviour. The right segment, beginning at n_train, shows test behaviour. The two halves sit side by side on the same horizontal axis, so you can compare how the model behaves in each period.
The two solid lines show the real values, and the two dashed lines show predictions. Start by checking that the test prediction begins where the test actual values begin, at the boundary n_train. Then check whether it follows their shape. Including the training prediction lets you see whether the model does well on data it learned from as well as on data it has not seen.
Finding a Missing Name in a Scope
Debugging an evaluation function usually means finding the line where a value stops being what you expected. The source's plotting line that sets the tick positions uses len(X). Inside eval_on_features, the names defined locally are X_train, X_test, y_train, y_test, y_pred, and y_pred_train. The parameters are features, target, and regressor.
The name X does not appear in either list. Python therefore looks for it in the surrounding global scope. Whether the function works depends on whether a global variable named X exists when the function is called. The scope diagram separates the names the function defines from the name it borrows.
Common Mistakes in Time Series Splits
Common mistake Shuffling the rows before splitting
Why it failsShuffling mixes past and future observations across the training and test sets, so the test set no longer represents values the model must predict after its training period. The positional split keeps every training row before every test row.
Do this insteadSplit by position using one index n_train, and keep the original time order.
Common mistake Cutting features and targets at different points
What it looks likefeatures[:n_train] paired with y[:n_train + 1]Why it failsThe rows stop lining up, so each feature row is paired with the wrong target.
Do this insteadApply the same n_train boundary to both arrays, as the source does.
Common mistake Scoring on the training rows
Why it failsA score on rows the model was fitted on measures fit to known data, which is a weaker check of prediction.
Do this insteadCall regressor.score on X_test and y_test only.Common mistake Assuming a name used in a function is defined there
What it looks likelen(X) inside eval_on_features when X is only a global variable
Why it failsIf X is not defined in the function or the enclosing scope when the call runs, Python raises an error.
Do this insteadList the parameters and local assignments first, then check the enclosing scope for any remaining names.
Practice: Checking Your Split
A feature array has 12 rows and n_train is 9. Which rows are in the training set and which are in the test set? Then say which rows the R2 score should be computed on, and why a score on rows 0 to 8 would be a weaker check.
Hints
- features[:9] includes indices 0 through 8.
- features[9:] starts at index 9 and runs to the end of the array.
- The score should use only rows the model was not fitted on.
Key Takeaways
- A time series is split by position using one index
n_train: features[:n_train] trains the model and features[n_train:] tests it, so the test set holds the latest observations. - The same boundary cuts the feature and target arrays, keeping each row paired with its target value.
- regressor.score is computed on the test pair only, giving a held-out check that a training-row score cannot provide.
- The prediction plot places training and test segments side by side, with solid lines for actual values and dashed lines for predictions, so the two periods can be compared.
- When an evaluation function fails, list its parameters and local names first, then check the enclosing scope for any name it uses but does not define.