Regression vs classification: predicting a number or a label
On this page
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):
| Question | Type of answer | ML type |
|---|---|---|
| Will next month be a stress month (volatility above 30% a year)? | A category: yes or no | Classification |
| 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%): 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%): . 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 . Sounds decent. But a “model” that simply says every month is calm is right 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
where is 1 when the prediction is wrong and 0 when it’s right. Here it’s , 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: for the calm July 2024 day (actual 19.7%) and 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):
| Symbol | Meaning |
|---|---|
| number of test rows | |
| the real next-month volatility for row | |
| the model’s prediction for row (read “y-hat”) | |
| the miss, squared: positive either way, and big misses count much more | |
| add up over all the test rows |
Here the test MSE is . 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:
on months that average 25.6% volatility. A simpler alternative is the mean absolute error, , the plain average size of a miss: 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
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):
| Name | What it holds |
|---|---|
X_train | the features of past rows: last month’s volatility and return |
vol_train | the number to predict for each row: next month’s volatility |
stress_train | the category to predict for each row: 1 if that number is above 30, else 0 |
X_new | rows 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):
| Ask yourself | Regression | Classification |
|---|---|---|
| What does the answer look like? | A number | A 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 examples | Next month’s volatility, loss on a loan, tomorrow’s return | Stress 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
- James, Witten, Hastie, Tibshirani and Taylor, An Introduction to Statistical Learning, with Applications in Python (Springer, 2023), section 2.1.5: regression versus classification; section 2.2: mean squared error (equation 2.5) and error rate (equation 2.8)
- scikit-learn: linear models