Docs
Open Studio

US examples

Factor crowding analysis

On this page5

Analyze a strategy's factors systematically, so you add, remove and change them based on data rather than intuition. This page runs FinLab's factor analysis workflow on a three-factor US strategy; it suits investors who want to know why a strategy works and when it might stop working.

Raw Data
    |
    |--- Feature Engineering -----> Factor Features
    |
    +--- Label Engineering -------> Excess Returns (Labels)
                |
                |--- calc_factor_return() -----> Factor Return
                |      |
                |      +-- calc_centrality() --> Factor Centrality
                |
                |--- calc_shapley_values() ----> Factor Contribution
                |
                +--- calc_ic() ----------------> Factor IC
  1. Generate features and labels: features are the factors, labels are excess stock returns.
  2. Compute factor returns: calc_factor_return(features, labels)
  3. Compute factor centrality (crowding): calc_centrality(factor_return, window)
  4. Compute factor contribution: calc_shapley_values(features, labels)
  5. Compute factor IC: calc_ic(features, labels)
  6. Compute factor Rank IC: calc_ic(features, labels, rank=True)

Strategy logic

The example strategy holds stocks that pass three factor conditions at once, rebalanced at each month end:

  1. Market capitalization (small-cap tilt): in the smallest 30% of stocks by market cap.
  2. Revenue growth (short-term against long-term revenue trend): 3-period average revenue divided by 12-period average revenue in the top 30%.
  3. Momentum: 20-day price return in the top 30%.

Code

Start with the strategy's performance:

strategy.py
from finlab import data
from finlab.backtest import sim
from finlab.markets.us import USMarket

data.set_market('us')

close = data.get('price:adj_close')
close = close[close.index.dayofweek < 5]  # remove weekends

revenue = data.get('us_income_statement:revenue')
marketcap = data.get('etl:us_market_value')

cond1 = marketcap.rank(pct=True, axis=1) < 0.3
cond2 = (revenue.average(3) / revenue.average(12)).rank(pct=True, axis=1) > 0.7
cond3 = (close / close.shift(20)).rank(pct=True, axis=1) > 0.7

pos = cond1 & cond2 & cond3

report = sim(pos, resample='ME', market=USMarket(), fee_ratio=0.001, tax_ratio=0)
report.creturn.plot()

Analyze the factors

Each step continues from the code above.

Generate features and labels

feature.combine() consolidates the factor conditions into a feature matrix, and label.excess_over_mean() computes the forward one-month excess return as the label.

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 forward 1-month labels
labels = label.excess_over_mean(index=features.index, resample='ME')

features.dropna().head()
labels.dropna().head()

Plot each factor's return

Factor return is the excess return attributable to a factor over a period, commonly used to judge a factor's effectiveness and investment value. Plotting each factor's cumulative return shows its long-term performance and stability, a key input for choosing factors and building portfolios.

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',
)

The chart shows which factors have the stronger cumulative returns and which have been less effective.

Measure crowding with centrality

Factor centrality quantifies how "crowded" a factor has become: it measures the commonality of factor returns.

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 loading on the first principal component, and $k$ is the total number of factors.

It reflects how factor returns move together:

  • Higher centrality: the factor has done well in stock selection recently, but high crowding raises the risk of future drawdowns, so monitor it closely.
  • Lower centrality: the factor has underperformed recently, and lower crowding means less risk of a sharp reversal. Watch for its centrality to rise as a signal to re-enter.
from finlab.tools.factor_analysis import calc_centrality
from finlab.plot import plot_line

plot_line(calc_centrality(calc_factor_return(features, labels), 12), title='Centrality')

When a factor's centrality is low, consider rotating to other factors for steadier performance, or keep watching it and re-enter when its crowding starts to rise.

See each factor's contribution

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 do) 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.

Despite the name, this is not the game-theoretic Shapley value: it does not measure the change in return from adding a factor, and the contributions do not sum to the return when all factors hold together. A higher value means the combinations containing the factor performed better.

from finlab.tools.factor_analysis import calc_shapley_values
from finlab.plot import plot_line

plot_line(calc_shapley_values(features, labels))

Check predictive power with IC

The Information Coefficient (IC) measures a factor's predictive power as the correlation between factor scores and subsequent returns:

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

where corr\text{corr} is the correlation coefficient (typically Pearson or Spearman rank), Factor Score\text{Factor Score} is the factor value, and Future Return\text{Future Return} is the asset's return over the next period.

IC ranges from -1 to 1:

  • Close to 1: strong positive predictive power for future returns.
  • Close to -1: strong negative (contrarian) predictive power.
  • Close to 0: little or no predictive power.

IC is a primary metric for evaluating factors in quantitative stock selection; factors with a higher IC are generally more valuable to include in a portfolio.

IC needs continuous numerical features rather than boolean conditions, so redefine the features first:

from finlab.tools.factor_analysis import calc_ic

features = feature.combine(
    {
        'marketcap': -marketcap,  # small-cap tilt (negative sign)
        'revenue': revenue.average(3) / revenue.average(12),
        'momentum': close / close.shift(20),
    },
    resample='ME',
)

plot_line(calc_ic(features, labels, rank=True))

How to adjust it

  • resample='ME': month-end rebalancing, for both the strategy and the factor analysis.
  • calc_factor_return(features, labels): the return attributable to each factor, from the performance of stocks in the top against the bottom quantile of that factor.
  • calc_centrality(factor_return, 12): a 12-period rolling window for the PCA-based centrality, which reflects the recent crowding level.
  • calc_shapley_values(features, labels): enumerates all 2n2^n factor subsets and splits each subset's return evenly among its factors. Keep $n$ small (under 8–10 factors).
  • calc_ic(features, labels, rank=True): the Spearman rank IC when rank=True, more robust to outliers than the Pearson IC.

Used together, these tools support data-driven factor rotation: factor returns show long-term efficacy, centrality warns of crowding, contribution scores show which factors appear in the best-performing combinations, and IC measures raw predictive power. Reduce exposure to crowded factors and increase it to factors with an improving IC and low centrality. In the US market, common factors such as size, value and momentum go through regime shifts, which makes this kind of monitoring essential for adaptive strategy management.

↑↓ Move Enter Open Esc Close