Concepts / Preparing Feature Matrices for Time Series Models

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.

  • Programming

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.

Key point

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.

features[0]earliest observationfeatures[1]features[n_train - 1]last training rowfeatures[n_train]first test rowfeatures[-1]most recent observation...split point...
Which rows fall into the training segment and which into the test segment when the split point n_train is applied along the time index? The test set holds the latest observations.

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?

  • 0

  • 4

  • 5

  • 8

Reveal answer
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.

python
Output (expected)
[10, 12, 14, 13, 15]
[17, 19, 21]
Notes on the code
  • 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.

features, targetfull input arraysSlice at n_trainsame boundary for botharraysTraining pairX_train, y_trainregressor.fitreceives only the trainingpairregressor.scorereceives only the test pairPrinted R^2 two decimal placesTest pairX_test, y_testcut at n_trainearlier rowslater rowstraining datafitted modelheld-out dataR^2
How does the data move from the full feature matrix through the slicing step into the model fit, the test prediction, and the final R2 score?
Worked example

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.

  1. 1
    Slice the features

    features[:5] gives rows 0 to 4, which become X_train. features[5:] gives rows 5 to 7, which become X_test.

  2. 2
    Slice the targets

    The same cut is applied to the target array, so y_train holds the targets for rows 0 to 4 and y_test holds the targets for rows 5 to 7.

  3. 3
    Fit

    regressor.fit receives only X_train and y_train. The test rows are not used at this stage.

  4. 4
    Score

    regressor.score receives X_test and y_test. The model is checked only on rows it was not fitted on.

  5. 5
    Report

    The function prints the R2 value rounded to two decimal places.

Result

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.

Global scopemodule leveleval_on_featuresfunction scopefeatures, target,regressorparametersX_train, X_test,y_train, y_test,y_pred, y_pred_trainlocal assignmentsXnot defined in the functioncontainsreceivesassignsuses, looks up
Which variables are defined inside the function, which come from the enclosing module, and which name is missing from the function's own scope?

Common Mistakes in Time Series Splits

  • Common mistake

    Shuffling the rows before splitting

    Why it fails

    Shuffling 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 instead

    Split 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 like
    features[:n_train] paired with y[:n_train + 1]
    Why it fails

    The rows stop lining up, so each feature row is paired with the wrong target.

    Do this instead

    Apply the same n_train boundary to both arrays, as the source does.

  • Common mistake

    Scoring on the training rows

    Why it fails

    A score on rows the model was fitted on measures fit to known data, which is a weaker check of prediction.

    Do this instead
    Call regressor.score on X_test and y_test only.
  • Common mistake

    Assuming a name used in a function is defined there

    What it looks like

    len(X) inside eval_on_features when X is only a global variable

    Why it fails

    If X is not defined in the function or the enclosing scope when the call runs, Python raises an error.

    Do this instead

    List the parameters and local assignments first, then check the enclosing scope for any remaining names.

Practice: Checking Your Split

EASY

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.

Related concepts

More concepts starting with P