Machine Learning for Time Series Data

Most ML quietly assumes your rows are independent. Time series breaks that. Learn machine learning for time series in Python: feature engineering, rolling windows, lags, and walk-forward validation.

Most machine learning tutorials quietly assume your rows are independent. Shuffle them, split them, cross-validate them in any order you like, and nothing breaks. Time series throws that assumption out. The whole point of the data is that order matters, that what happened yesterday shapes what happens today, and that the future must never be allowed to leak backward into the past. Get that one idea wrong and you will build a model that looks brilliant in testing and falls apart the moment it meets tomorrow.

This article walks through the full lifecycle of a time series machine learning project, from looking at the raw signal to validating it honestly. The two running examples are deliberately different: classifying audio recordings, where features have to be invented from a raw waveform, and predicting financial prices, where the trap is leaking the future into your training set. Together they cover the techniques you will reach for again and again.

The math: https://datalad.co.uk/the-mathematics-behind-time-series-ml/

Start by looking at the signal

Before any modelling, plot the raw data. With time series this is not optional, because the shape of the signal tells you almost everything about what features will work and what cleaning it needs. The only subtlety is the horizontal axis. If you plot a pandas Series without specifying a time column, the x-axis becomes the row number, which is fine for a quick glance but misleading the moment you compare two datasets, since row 500 in one might be a completely different moment than row 500 in another. Anchoring the plot to a real time column keeps the comparison honest.

Loading the data depends on its form. Audio is just a long sequence of numbers, the voltage of a microphone sampled thousands of times a second, and a library like librosa reads the file and hands you that sequence along with the sampling rate. Dividing the sample index by the sampling rate converts those raw positions into real seconds.

import librosa as lr
import numpy as np
waveform, sfreq = lr.load('recording.wav')
time = np.arange(len(waveform)) / sfreq

Financial data usually arrives as a CSV with dates in the first column. The thing people forget is that pandas reads those dates as plain strings, so you have to convert the index to actual datetimes before any time-based slicing or plotting will behave.

import pandas as pd
prices = pd.read_csv('prices.csv', index_col=0)
prices.index = pd.to_datetime(prices.index)

That single to_datetime call is what unlocks everything later: date-string slicing, calendar features, and proper time axes on every chart.

Fitting a model is the easy part

Scikit-learn treats time series models exactly like any other, which is both a blessing and a trap. A LinearSVC for classification or a Ridge for regression trains with the same fit and predict calls you already know. The blessing is that there is nothing new to learn about the API. The trap is that scikit-learn will happily let you do statistically invalid things, like shuffling your time series, without complaint. The discipline has to come from you, not the library.

So the right mental model is this. The modelling is trivial. The real work, and the real risk, lives in two places that surround the model: how you turn a raw signal into features, and how you validate without cheating. The rest of this article is about those two things.

Engineering features from a raw signal

A classifier cannot consume a raw waveform of tens of thousands of samples directly in any meaningful way, and feeding it the raw values is just a baseline to beat. The job of feature engineering is to summarize each recording into a small set of numbers that capture what actually distinguishes the classes. Always fit that raw-data baseline first, because if your clever engineered features cannot beat it, they are not adding information.

The first useful summary is the envelope, which captures the energy of the signal over time. A raw waveform swings rapidly between positive and negative, and a model does not care about those individual oscillations, only the overall loudness at each moment. You get the envelope by rectifying the signal, taking the absolute value so every swing becomes positive, and then smoothing it with a rolling mean.

audio_rectified = waveform.apply(np.abs)
envelope = audio_rectified.rolling(50).mean()

You then collapse each recording’s envelope into a few numbers, typically the mean, the standard deviation, and the maximum, which together describe its average energy, how much that energy fluctuates, and its loudest moment. Stack those across all recordings and you have a feature matrix you can cross-validate.

From there the features get richer. Tempo captures rhythm, the rhythmic pulse of the signal, and asking librosa for the tempo at every frame rather than a single average gives you a track you can summarize the same way. The biggest leap, though, is the spectrogram. A raw waveform tells you how loud the signal is at each moment but says nothing about which frequencies are present. The Short-Time Fourier Transform fixes this by slicing the signal into short overlapping windows and running a Fourier transform on each one, producing a two-dimensional picture of energy across both time and frequency. Converting that to decibels makes the quiet detail visible, because human hearing is logarithmic and small absolute energies still matter.

from librosa.core import stft, amplitude_to_db
spec = stft(waveform, hop_length=2**4, n_fft=2**7)
spec_db = amplitude_to_db(spec)

From the spectrogram you can extract two more features per frame. The spectral centroid is the center of mass of the frequency content, roughly the perceived pitch, high for a bright shrill sound and low for a deep rumble. The spectral bandwidth measures how widely the energy is spread, narrow for a pure tone and wide for noise. Average each over the recording and you have added two more columns. The pattern throughout is the same: take a rich time-varying quantity, summarize it per recording, add it to the matrix, and check whether the cross-validation score improved.

Cleaning the messy parts

Real time series have gaps and spikes. The fastest way to find missing data is to plot it, because your eye spots the breaks in a line instantly, and then confirm with a precise count of nulls per column. The interesting question is often whether all series go missing on the same dates, which points to a market holiday or a feed outage rather than a random error.

Filling those gaps is interpolation, and the method matters. Carrying the last value forward suits data that changes in discrete steps. Linear interpolation, drawing a straight bridge between the known points on either side, is the sensible default for most numeric series. Quadratic interpolation bends a curve through the gap and suits something that varies smoothly. The honest way to choose is to fill the gaps, then highlight exactly which points were invented, and look at whether the inventions are plausible.

Outliers need a different treatment. A common rule flags any value more than three standard deviations from the mean, since that band covers around 99.7 percent of normally distributed data, and replaces the offenders. The detail that trips people up is what to replace them with. Use the median, not the mean, because the mean is itself dragged around by the very outliers you are trying to remove, while the median shrugs them off.

Rolling windows, dates, and lags

Once the data is clean, three families of features do most of the heavy lifting.

Rolling window statistics summarize a moving slice of recent history. Pandas lets you compute several at once by passing a list of functions to aggregate, and a couple of arguments are worth knowing. Setting min_periods lets the calculation start before the window is completely full instead of producing nulls at the very beginning, and closed='right' controls exactly which edge of the window is included. A neat trick is expressing each window’s latest value as a percentage change from the window’s own average, which normalizes away the absolute price level and makes a 200 dollar stock directly comparable to a 20 dollar one.

There is one practical snag with custom rolling functions. The aggregate method calls your function with a single argument, the window of values, which clashes with something like np.percentile that needs to be told which percentile to compute. The fix is partial, which bakes the extra argument into the function ahead of time so it only needs the window when called.

from functools import partial
percentiles = [1, 10, 25, 50, 75, 90, 99]
percentile_funcs = [partial(np.percentile, q=p) for p in percentiles]

Date-time features are almost free and often valuable. The model cannot learn from a raw timestamp, but it can learn from the integers you pull out of one. Day of week captures the real difference between Monday and Friday behaviour in markets, month captures seasonality, and quarter captures earnings cycles, and each is a single line off the datetime index.

Lag features are the heart of autoregressive modelling, the idea that today depends on recent past days. You create them by shifting the series, so that yesterday’s value lands in today’s row, and you make a column for each lag depth you want the model to consider.

lagged = {f"lag_{d}_day": prices_perc.shift(d) for d in range(1, 11)}
prices_perc_shifted = pd.DataFrame(lagged)

Shifting leaves nulls in the first few rows, because row zero has no yesterday, so you fill those with a neutral value like the median before fitting. A Ridge regression on those lagged columns then learns a weight for each lag, and plotting those coefficients tells a story: a tall bar at lag one means recent history dominates, while bars that fade toward longer lags show how quickly the past stops mattering.

The part everyone gets wrong: validation

This is where time series projects live or die. The cardinal sin is letting information from the future reach the model during training, and the most common way to commit it is shuffling.

Picture random cross-validation on a price series. It might place a row from November in the training set while a row from October sits in the test set, so the model effectively studies the future to predict the past. The scores will look spectacular and the model will be worthless in production, where the future is genuinely unknown. Plain K-fold without shuffling is better because it preserves order within each fold, but it still trains on some blocks that come after the block it tests on, so a subtler version of the same leak remains.

The correct tool is walk-forward validation, which scikit-learn provides as TimeSeriesSplit. Each fold trains on everything up to a point in time and tests on the block immediately after, exactly mirroring real deployment where you only ever have the past. The training window grows with each fold, so a diagnostic plot of the splits looks like a staircase of expanding history.

from sklearn.model_selection import TimeSeriesSplit, cross_val_score
cv = TimeSeriesSplit(n_splits=10)
scores = cross_val_score(model, X, y, cv=cv)

The same discipline applies to a simple train-test split. Always pass shuffle=False so the first portion of history trains the model and the later portion tests it. If you remember one thing from this article, make it this: never shuffle a time series.

How sure are you, really?

A single cross-validation score hides how much it would change on slightly different data. The bootstrap answers that. You repeatedly resample your data with replacement, so some rows appear twice and others not at all, compute the statistic on each resample, and look at the spread of results. The 2.5th and 97.5th percentiles of those repeated estimates form a 95 percent confidence interval, a plausible range for the true value rather than a single point that pretends to certainty.

This becomes genuinely useful applied to model coefficients across many walk-forward folds. Because the relationships in a time series are rarely stable, each historical window yields slightly different coefficients. Bootstrapping that collection gives you a confidence interval per feature, and the width is the message. A wide interval means the feature’s importance swings wildly over time and cannot be trusted, while a narrow interval near zero means the feature reliably does not matter. You can take the same idea further and track the model’s score itself as a rolling time series with an uncertainty band, which reveals not just whether the model is good on average but whether it is quietly degrading.

When the world keeps changing

The deepest problem in financial time series, and many others, is non-stationarity: the rules generating the data shift over time. When that happens, old data does not just become useless, it can actively mislead, teaching the model patterns that no longer hold. TimeSeriesSplit has a quiet answer in its max_train_size argument, which caps how far back each fold is allowed to look, in effect forcing the model to forget the distant past.

cv = TimeSeriesSplit(n_splits=100, max_train_size=100)

Comparing performance across several lookback lengths turns this into a diagnostic. If a short window consistently beats a long one, recent history is more informative than ancient history, which is a clear signature that the world has changed and your model should change with it.

The thread that ties it together

Time series machine learning is less about exotic algorithms than about respecting the arrow of time at every step. Look at the signal before you model it. Summarize rich signals into honest features and always keep a raw baseline to beat. Clean gaps and spikes with methods that suit the data rather than defaults. Validate with walk-forward splits and never, ever shuffle. Measure your uncertainty with the bootstrap, and stay alert to a world that does not hold still. Master those habits and the modelling itself becomes the simple part it should be.

See you soon.

View Comments (2)

Leave a Reply

  1. […] Time series removes it. Yesterday’s temperature is not independent of today’s; this quarter’s revenue is not a fresh draw unrelated to last quarter’s. The dependence is not a nuisance to be washed out, it is the entire signal, since a series with no dependence across time is white noise and cannot be forecast at all. So the mathematics of time series is the mathematics of what to do when the independence assumption is both false and load-bearing: how much information a correlated sample really contains, what conditions make estimation possible at all, how to convert a sequence into something a regressor can eat, and why the validation scheme from every other article will lie to you here. […]

Subscribe to My Newsletter

Subscribe to my email newsletter to get the latest posts delivered right to your email. Pure inspiration, zero spam.

Discover more from Discuss Data Science, Machine Learning and Analytics

Subscribe now to keep reading and get access to the full archive.

Continue reading