Guides
Factor analysis
On this page8
A strategy is usually built from several screening conditions, and each condition is a "factor". This page shows how to measure the link between each factor and the strategy's returns, so you add, drop or change factors based on data rather than intuition.
How the analysis works
First turn the strategy into two tables, then pass them to the functions in finlab.tools.factor_analysis:
- Features: for every month and stock, whether each factor holds.
- Labels: each stock's return over the next month minus the average return of all stocks (the excess return).
| Function | Input | Output | Question it answers |
|---|---|---|---|
calc_factor_return() | Features, labels | Factor return | How much excess return does this factor bring? |
calc_centrality() | Factor returns | Factor centrality | Is this factor crowded? |
calc_shapley_values() | Features, labels | Factor contribution | Do the combinations containing this factor do well? |
calc_ic() | Raw factor values, labels | Factor correlation (IC) | Do the factor's values predict future returns? |
calc_regression_stats() | Any of the above over time | Trend statistics | Is this measure getting stronger or weaker? |
Turn a strategy into features and labels
Write the example strategy
This strategy trades S&P 500 stocks and is built from three basic factors: market capitalization, revenue growth (quarterly, year over year) and momentum. It buys when all three hold and rebalances monthly.
from finlab import data
from finlab.backtest import sim
data.set_market('us')
with data.us_universe(index='SP500'):
marketcap = data.get('etl:us_market_value')
revenue = data.get('us_income_statement:revenue')
price = data.get('price:adj_close')
cond1 = marketcap.rank(pct=True, axis=1) < 0.3
cond2 = (revenue / revenue.shift(4)).rank(pct=True, axis=1) > 0.7
cond3 = (price / price.shift(20)).rank(pct=True, axis=1) > 0.7
pos = cond1 & cond2 & cond3
report = sim(pos, resample='ME', upload=False)
report.creturn.plot()rank(pct=True, axis=1) ranks all stocks each day as a percentile from 0 to 1: cond1 keeps the smallest 30% by market value; cond2 and cond3 keep the top 30% by revenue growth and by 20-day return.
Build features and labels
feature.combine() merges the three conditions into one feature table, and label.excess_over_mean() computes each stock's excess return over the next month. Both use resample='ME' so their dates line up:
from finlab import data
import finlab.ml.feature as feature
import finlab.ml.label as label
features = feature.combine(
{'marketcap': cond1, 'revenue': cond2, 'momentum': cond3}, resample='ME'
)
# Compute labels for the next 1 month
labels = label.excess_over_mean(index=features.index, resample='ME')
features.dropna().head()features is indexed by (datetime, instrument), one row per month-end and ticker (for example AAPL), with one boolean column per factor: marketcap, revenue and momentum.
labels.dropna().head()labels is a Series with the same (datetime, instrument) index: each stock's return over the next month minus the average return of all stocks.
See how much return each factor brings
Factor return measures the excess return a factor brings over a period, and is commonly used to judge whether a factor works and is worth investing in. Adding up each factor's returns over time shows its long-term performance and stability, a basis for choosing and combining factors.
from finlab.tools.factor_analysis import calc_factor_return
from finlab.plot import plot_line
plot_line(
calc_factor_return(features, labels).cumsum(),
unit='.0%',
title='Factor Cumulative Return',
)Compare the curves: a factor whose cumulative return keeps rising has been adding excess return, while a flat or falling curve means the factor has not helped over that period.
Check whether a factor is crowded
Factor centrality quantifies the "commonality" of factor returns, that is, how crowded a factor is:
where is the factor's contribution to the first principal component, and $k$ is the total number of factors.
| Centrality | What it means | What to do |
|---|---|---|
| Higher | Selecting stocks with this factor has performed better recently, but higher centrality raises the risk of a future reversal | Monitor it closely |
| Lower | Selecting stocks with this factor has performed worse recently; the risk of a future reversal is lower | Watch the factor while crowding is low, and use it when crowding starts to rise |
The second argument of calc_centrality() is the rolling window length in rows; 12 here means 12 months:
from finlab.tools.factor_analysis import calc_centrality
plot_line(calc_centrality(calc_factor_return(features, labels), 12), title='Centrality')If a factor currently has low crowding, you might switch to a different factor to make the strategy more stable, or monitor it closely and use it when crowding starts to rise (factor reversion effect).
See each factor's contribution to combinations
calc_shapley_values() enumerates every factor combination. At each point in time, it takes the equal-weight mean excess return of the stocks that satisfy every factor in the combination (0 when none) and splits it evenly among the combination's factors; a factor's contribution is the sum of its shares over all combinations that include it.
from finlab.tools.factor_analysis import calc_shapley_values
plot_line(calc_shapley_values(features, labels))A higher value means the combinations containing the factor performed better.
Check whether a factor predicts returns
Factor IC (information coefficient) is a key measure of a factor's predictive ability: the correlation between factor scores and future returns.
where is the correlation coefficient (usually Pearson), is the factor score, and is the asset return over a future period. IC values typically range between -1 and 1:
| IC | What it means |
|---|---|
| Close to 1 | Strong positive predictive ability for future returns |
| Close to -1 | Strong negative (contrarian) predictive ability for future returns |
| Close to 0 | Almost no predictive ability for future returns |
In quantitative stock selection, IC is a key basis for judging whether a factor works; factors with higher IC are generally more worth including in a portfolio.
IC needs continuous factor values rather than True/False, so build a second feature table from the raw values. rank=True ranks both the factor values and the labels on each date before correlating them (Spearman rank IC); without it, the Pearson correlation is used.
from finlab.tools.factor_analysis import calc_ic
features_raw = feature.combine(
{
'marketcap': -marketcap, # Small cap (negated)
'revenue': revenue / revenue.shift(4),
'momentum': price / price.shift(20),
},
resample='ME',
)
plot_line(calc_ic(features_raw, labels, rank=True))Tell whether a factor is getting stronger or weaker
Centrality, contribution and IC all change over time. calc_regression_stats() fits a linear regression to each factor's series and tells you whether it is trending up, down or flat:
from finlab.tools.factor_analysis import calc_regression_stats
centrality_df = calc_centrality(calc_factor_return(features, labels), 12)
centrality_trend = calc_regression_stats(centrality_df)The result is a DataFrame with one row per factor (marketcap, revenue, momentum) and the columns slope, p_value, r_squared, tail_estimate and trend.
| Field | Description | Value range | Meaning |
|---|---|---|---|
| slope | Linear regression slope | (-inf, +inf) | Trend direction and strength: positive is an upward trend (crowding increasing), negative a downward one (crowding decreasing); a larger absolute value is a stronger trend |
| p_value | Statistical significance | [0, 1] | Trend credibility |
| r_squared | Coefficient of determination | [0, 1] | Explanatory power of the linear model |
| tail_estimate | Tail estimate value | (-inf, +inf) | Predicted value at the end of the time series |
| trend | Trend classification | "up"/"down"/"flat" | Simplified trend judgment |
A trend counts as up or down only when p_value < 0.05 and r_squared > 0.1 (the p_value_threshold and r_squared_threshold defaults); otherwise it is flat.
| p_value | r_squared | slope | trend | Meaning |
|---|---|---|---|---|
| small | high | positive | up | Strong and stable upward trend |
| small | high | negative | down | Strong and stable downward trend |
| small | low | any | flat | Trend exists but effect is small / high noise |
| large | high | any | flat | Small sample, high noise, cannot determine |
| large | low | any | flat | Essentially no trend and model has no explanatory power |
Reading the result
- trend = up (small p_value, high r_squared, positive slope): the factor's centrality is rising steadily. The factor is becoming popular and more capital is chasing it, which also raises the risk of a reversal.
- trend = down (small p_value, high r_squared, negative slope): the factor's centrality is falling steadily; crowding is decreasing.
- trend = flat: either the slope is not significant, or it is significant but explains too little of the variation (low r_squared), so no clear trend can be claimed.
Grading p_value and r_squared
p_value (statistical significance)
- [0, 0.01]: Highly significant (***)
- [0.05, 0.1]: Marginally significant (*)
- [0.1, 1]: Not significant
r_squared (coefficient of determination)
- [0.8, 1.0]: Very strong explanatory power
- [0.6, 0.8): Strong explanatory power
- [0.4, 0.6): Moderate explanatory power
- [0.2, 0.4): Weak explanatory power
- [0, 0.2): Very weak explanatory power
Next steps
- Factor crowding example Track how crowded US factors are with centrality.
- Machine learning Train a stock-selection model on the same features and labels.
- Strategy optimization Backtest every combination of conditions and pick the best.
- finlab.tools Full parameters of the factor analysis functions.
Was this page helpful?