Regression vs classification: predicting a number or a label

On this page
  1. Two questions about the same month
  2. Classification: “which one?”
  3. Regression: “how much?”
  4. Both, side by side
  5. The code
  6. How to tell which one you need
  7. References

In the first post we saw that machine learning means learning rules from past examples. The next thing to decide is what kind of answer you want. In trading, for example, predicting a stock’s future return (“how much?”) is a different job from deciding buy, sell or hold (“which one?”).

Two questions about the same month

From here on, this series uses one example: what will the market do next month, in risk terms? The data is daily prices from 2020 to 2025 for five tickers (SPY, NVDA, GS, XOM and GLD), the same prices as the Quantitative Finance Series 1. For every trading day and ticker I compute seven facts that a trader would know at that close (the features): the last 1-, 5- and 20-day returns, the volatility of the last 5 and 20 days, the price against its 20-day average, and its Bollinger position (where the price sits between its Bollinger bands, lines drawn two standard deviations above and below the 20-day average: 0 at the average, +1 or −1 at a band). The thing to predict is the volatility of the next 20 trading days, about a month, annualised (scaled up to a yearly figure) the way the standard deviation post does.

A risk manager might ask two different questions (Table 1):

Table 1. Two questions about next month, and the kind of answer each needs
QuestionType of answerML type
Will next month be a stress month (volatility above 30% a year)?A category: yes or noClassification
How volatile will next month be?A number: 11%, 23%, 48%…Regression

Same data, same features. Different questions, so different models.

Why volatility and not the return itself? Because the return is close to unpredictable from price history alone (why prices are hard to beat explains why), while volatility clusters: a wild month tends to follow a wild month. Predicting risk is something these models can do, and it is what risk teams need.

Classification: “which one?”

The answer is a category (also called a class). Stress or calm. Fraud or normal. Approve, review or reject.

The table has 7,135 rows, one per ticker per trading day. I trained on the 4,930 rows from 2020 to 2023 and tested on the 2,205 rows from 2024 to 2025, in that order: in finance you always test on dates after the training data, never mixed in. The answer (the label) for each row is whether the month that followed was a stress month; that was true for 36.8% of the training rows.

I trained a classifier called logistic regression. (Yes, the name says “regression”, but it’s a classification model. The name is historical, and it confuses everyone.)

A good classifier doesn’t just say “stress” or “calm”. It gives a probability:

  • SPY in early July 2024, after a calm month (5.4% volatility, up 4.4%): 2%\key{2\%} chance of a stress month ahead. The next month came in at 20%.
  • SPY in late April 2025, after a wild month (54% volatility, down 3%): 88%\key{88\%}. The next month came in at 16%, calm. The April 2025 crash reversed within weeks, and a model that only sees the past month had no way to know that. A high probability is still only a probability.

A risk team can size positions off those probabilities.

The accuracy trap

On the test rows, the model’s accuracy (share of correct answers) was 86.58%\key{86.58\%}. Sounds decent. But a “model” that simply says every month is calm is right 75.87%\key{75.87\%} of the time, because only 24.1% of test rows had a stress month ahead. It never catches a single one.

An Introduction to Statistical Learning (ISLP) measures a classifier by its error rate, the share of wrong answers (equation 2.8):

ŷ, the predictiony, the actual valuethe error

error rate=1n∑i=1nI(yi≠y^i)\risk{\text{error rate}} = \frac{1}{n} \sum_{i=1}^{n} I(\good{y_i} \neq \key{\hat{y}_i})

where I(⋅)I(\cdot) is 1 when the prediction is wrong and 0 when it’s right. Here it’s 13.42%\risk{13.42\%}, which is just 100% minus the accuracy.

There is a catch. The model only says “stress” when it’s more than 50% sure. At that cut-off it catches 343 of the 532 test rows that really had a stress month ahead and misses the other 189. When one answer is rarer than the other, accuracy and error rate do not tell you that on their own. This is the same lesson as the backtest in the probability post: always compare with how common the event is. For rare events, look at the predicted probabilities, and at how many of the real cases the model catches.

Regression: “how much?”

The answer is a number on a scale. How volatile next month will be. How much a loan will lose. Tomorrow’s return.

Same rows, same seven features, but now the label is the number itself. I trained a linear regression model, which finds the best straight-line relationship between the inputs and the answer.

Its forecasts for the two days above: 13.6%\key{13.6\%} for the calm July 2024 day (actual 19.7%) and 38.5%\key{38.5\%} for the wild April 2025 day (actual 15.8%). The calm forecast is 6 points under. The wild one is 23 points over, for the same reason the classifier was wrong: the crash ended faster than any past month predicted.

How you measure a regression model

A regression answer is never exactly right, so you measure how far off it is on average. The standard measure, and the one ISLP uses (equation 2.5), is the mean squared error (MSE). ISLP writes it for the training rows; here it is computed the same way on the test rows, the ones the model has not seen (symbols in Table 2):

MSE=1n∑i=1n(yi−y^i)2\risk{\text{MSE}} = \frac{1}{n} \sum_{i=1}^{n} \left( \good{y_i} - \key{\hat{y}_i} \right)^2
Table 2. Symbols in the mean squared error
SymbolMeaning
nnnumber of test rows
yi\good{y_i}the real next-month volatility for row ii
y^i\key{\hat{y}_i}the model’s prediction for row ii (read “y-hat”)
(yi−y^i)2(\good{y_i} - \key{\hat{y}_i})^2the miss, squared: positive either way, and big misses count much more
∑\sumadd up over all the test rows

Here the test MSE is 163.4\risk{163.4}. The unit is “percentage points squared”, which is hard to picture, so people usually take the square root to get back to points. That’s the root mean squared error:

RMSE=MSE=163.4=12.8 points\risk{\text{RMSE}} = \sqrt{\risk{\text{MSE}}} = \sqrt{163.4} = 12.8 \text{ points}

on months that average 25.6% volatility. A simpler alternative is the mean absolute error, ∑∣yi−y^i∣/n\sum |\good{y_i} - \key{\hat{y}_i}| / n, the plain average size of a miss: 8.8\risk{8.8} points here. Squaring makes MSE and RMSE punish one big miss more than several small ones, which suits problems where the big miss is the expensive one, like being set up for a 15% month that turns out to be a 45% one.

Both, side by side

2026-10-31T01:56:04.438675 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/ 0 500 1000 1500 2000 Test ticker-days, sorted by predicted probability (red = a stress month followed) 0.0 0.2 0.4 0.6 0.8 1.0 Predicted probability of a stress month above the line: predict 'stress' 1. Classification: will next month be a stress month? 0 10 20 30 40 50 60 70 Actual volatility of the next 20 days (% a year) 0 10 20 30 40 50 60 70 Predicted volatility (% a year) on the line = perfect guess 2. Regression: how volatile will next month be?
Figure 1. Stress probabilities and predicted against actual volatility on the test rows

In the top panel of Figure 1, each dot is a test row, sorted by predicted risk. Red dots are months that really were stressed, and most of them sit at the high end. In the bottom panel, each dot is a test row: the closer to the dashed line, the better the forecast.

The code

Both models are a few lines in scikit-learn. The pattern is always the same: create, fit on past data, predict on new data. The names (Table 3):

Table 3. Names in the code and what each holds
NameWhat it holds
X_trainthe features of past rows: last month’s volatility and return
vol_trainthe number to predict for each row: next month’s volatility
stress_trainthe category to predict for each row: 1 if that number is above 30, else 0
X_newrows the models have not seen

Twelve made-up rows typed in by hand, so it runs as pasted (pip install scikit-learn):

from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

# Each row is one stock on one day: [volatility of the last 20 days (% a year),
#                                    return over the last 20 days (%)]
X_train = [[ 8,  3], [10,  2], [12, -1], [14,  4], [15, -2], [18,  1],
           [22, -5], [26, -8], [30, -3], [35, -12], [40, -9], [45, -15]]

# Two different answers for the same rows:
vol_train    = [9, 11, 12, 13, 17, 16, 24, 29, 27, 36, 38, 47]  # next month's volatility (% a year)
stress_train = [1 if v > 30 else 0 for v in vol_train]          # 1 = stress month (above 30%)

# Classification: "which one?" The answer is a category, stress or not.
clf = make_pipeline(StandardScaler(), LogisticRegression())
clf.fit(X_train, stress_train)

# Regression: "how much?" The answer is a number.
reg = LinearRegression()
reg.fit(X_train, vol_train)

X_new = [[9, 4], [33, -10]]               # a calm day and a wild day the models have not seen
print(clf.predict_proba(X_new)[:, 1])     # chance of a stress month, one per row
print(reg.predict(X_new))                 # next month's volatility, one per row

It prints about [0.01 0.52]: a 1% chance of a stress month after the calm day, 52% after the wild one. Then about [ 8.5 34.3]: the forecast volatility for each. predict_proba returns two columns per row, the chance of 0 and the chance of 1; [:, 1] takes the second column for every row.

The scaler (StandardScaler) puts every feature on the same footing before the logistic model sees them; on the real data the model fits badly without it, because daily returns are tiny numbers and volatilities are not.

The script behind the real numbers

The numbers above come from examples/regression-vs-classification/two_questions.py, which runs the same two models on all seven features of the blog’s five tickers. To run it yourself, clone the examples repo: its README has a three-command Docker setup, or run uv run regression-vs-classification/two_questions.py.

How to tell which one you need

Four questions settle it (Table 4):

Table 4. Four questions that tell regression from classification
Ask yourselfRegressionClassification
What does the answer look like?A numberA category
Does “a bit off” make sense?Yes (23% instead of 25%)No (it’s a stress month or it isn’t)
How do you measure errors?MSE, RMSE (and MAE)Error rate, but careful with rare events
Finance examplesNext month’s volatility, loss on a loan, tomorrow’s returnStress month or not, will the loan default, buy / sell / hold

You can often turn one into the other. A trading model can predict tomorrow’s return (regression), and then you buy if the prediction is above some level (classification). Many trading strategies work exactly this way; the stress-month label above is the regression target with a line drawn at 30%.

References

Tagged machine-learning, finance, volatility← Home