Docs
Open Studio

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).
FunctionInputOutputQuestion it answers
calc_factor_return()Features, labelsFactor returnHow much excess return does this factor bring?
calc_centrality()Factor returnsFactor centralityIs this factor crowded?
calc_shapley_values()Features, labelsFactor contributionDo the combinations containing this factor do well?
calc_ic()Raw factor values, labelsFactor correlation (IC)Do the factor's values predict future returns?
calc_regression_stats()Any of the above over timeTrend statisticsIs 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:

Centralityi=λi∑j=1kλj\text{Centrality}_i = \frac{\lambda_i}{ \sum_{j=1}^{k}\lambda_j}

where λj\lambda_j is the factor's contribution to the first principal component, and $k$ is the total number of factors.

CentralityWhat it meansWhat to do
HigherSelecting stocks with this factor has performed better recently, but higher centrality raises the risk of a future reversalMonitor it closely
LowerSelecting stocks with this factor has performed worse recently; the risk of a future reversal is lowerWatch 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.

IC=corr(Factor Score,Future Return)IC = \text{corr}(\text{Factor Score}, \text{Future Return})

where corr\text{corr} is the correlation coefficient (usually Pearson), Factor Score\text{Factor Score} is the factor score, and Future Return\text{Future Return} is the asset return over a future period. IC values typically range between -1 and 1:

ICWhat it means
Close to 1Strong positive predictive ability for future returns
Close to -1Strong negative (contrarian) predictive ability for future returns
Close to 0Almost 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.

FieldDescriptionValue rangeMeaning
slopeLinear 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_valueStatistical significance[0, 1]Trend credibility
r_squaredCoefficient of determination[0, 1]Explanatory power of the linear model
tail_estimateTail estimate value(-inf, +inf)Predicted value at the end of the time series
trendTrend 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_valuer_squaredslopetrendMeaning
smallhighpositiveupStrong and stable upward trend
smallhighnegativedownStrong and stable downward trend
smalllowanyflatTrend exists but effect is small / high noise
largehighanyflatSmall sample, high noise, cannot determine
largelowanyflatEssentially 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

↑↓ Move Enter Open Esc Close